Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UTOPIA-1177] added CPO review columns + deleted reviews by other user fix + tests #1447

Merged
merged 2 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/backend/src/common/interfaces/form-field.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ export interface IFormField<T> {
type?: 'text' | 'boolean'; // add ORs for future support if needed
isRichText?: boolean;
allowedUserTypesEdit: Array<UserTypesEnum>; // null if no role restrictions apply

// @isSystemGeneratedField
// applicable for fields like createdAt, createdBy, or IDIR-derived fields which are added by the server [and FE User cannot change these].
// This is currently used to filter and validate role changes of the only USER specific fields when the user deletes a particular review - MPO/CPO/ProgramArea
isSystemGeneratedField?: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ForbiddenException } from '@nestjs/common';
import { KeycloakUser } from 'src/modules/auth/keycloak-user.model';
import { PiaIntakeStatusEnum } from '../enums/pia-intake-status.enum';
import { Review } from '../jsonb-classes/review';
import { CpoReview } from '../jsonb-classes/review/cpo-review';
import { ProgramAreaSelectedRolesReview } from '../jsonb-classes/review/programArea/programAreaSelectedRoleReviews';

/**
* @method updateReviewSubmissionFields
* @description
* This method updates the dto with the submission related fields, which needs to be filled in by the server based on the user logged in
*/
export const updateReviewSubmissionFields = (
updatedValue:
| Review
| Record<string, ProgramAreaSelectedRolesReview>
| Record<string, CpoReview>, // add more types here
storedValue:
| Review
| Record<string, ProgramAreaSelectedRolesReview>
| Record<string, CpoReview>,
user: KeycloakUser,
key: 'mpo' | string,
allowedInSpecificStatus?: PiaIntakeStatusEnum[] | null,
updatedStatus?: PiaIntakeStatusEnum,
) => {
if (!updatedValue?.[key]) return;

// overwrite the updated values to include the stored fields that may not be passed by the client
if (updatedValue?.[key]) {
updatedValue[key] = {
...(storedValue?.[key] || {}),
...updatedValue?.[key],
};
}

// Scenario 1: User is saving review information for the first time [First time save]
// Scenario 2: User is saving review information for the subsequent times [Editing]

// Either ways, if the value is changed from the stored one, update the submission fields
if (
storedValue?.[key]?.isAcknowledged !==
updatedValue?.[key]?.isAcknowledged ||
storedValue?.[key]?.reviewNote !== updatedValue?.[key]?.reviewNote
) {
// if it is not the same person updating the fields, throw forbidden error
if (
storedValue?.[key]?.reviewedByGuid &&
storedValue?.[key]?.reviewedByGuid !== user.idir_user_guid
) {
throw new ForbiddenException({
message: `You do not have permissions to edit this review.`,
});
}

if (
allowedInSpecificStatus &&
updatedStatus &&
!allowedInSpecificStatus.includes(updatedStatus)
) {
throw new ForbiddenException({
message: 'You do not permissions to update review in this status',
});
}

// if first time reviewed
if (!storedValue?.[key]?.reviewedByGuid) {
updatedValue[key].reviewedAt = new Date();
updatedValue[key].reviewedByGuid = user.idir_user_guid;
updatedValue[key].reviewedByUsername = user.idir_username;
updatedValue[key].reviewedByDisplayName = user.display_name;
}

// update last review updated at
updatedValue[key].reviewLastUpdatedAt = new Date();
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { IsBoolean, IsString } from '@nestjs/class-validator';
import { Transform } from 'class-transformer';
import { UserTypesEnum } from 'src/common/enums/users.enum';
import { IFormField } from 'src/common/interfaces/form-field.interface';
import { validateRoleForFormField } from 'src/common/validators/form-field-role.validator';
import { RoleReview } from './role-review';

export class CpoReview extends RoleReview {
// overriding the mandatory fields
@IsBoolean()
isAcknowledged: boolean;

@IsString()
@Transform(({ value }) => value?.trim())
reviewNote?: string;
}

export const cpoReviewMetadata: Array<IFormField<CpoReview>> = [
{
key: 'isAcknowledged',
type: 'boolean',
allowedUserTypesEdit: [UserTypesEnum.CPO],
isSystemGeneratedField: false,
},
{
key: 'reviewNote',
type: 'text',
allowedUserTypesEdit: [UserTypesEnum.CPO],
isSystemGeneratedField: false,
},
{
key: 'reviewedByDisplayName',
type: 'text',
allowedUserTypesEdit: [], // empty array signifies that the client can't edit these fields
isSystemGeneratedField: true,
},
{
key: 'reviewedByUsername',
type: 'text',
allowedUserTypesEdit: [],
isSystemGeneratedField: true,
},
{
key: 'reviewedByGuid',
type: 'text',
allowedUserTypesEdit: [],
isSystemGeneratedField: true,
},
{
key: 'reviewedAt',
type: 'text',
allowedUserTypesEdit: [],
isSystemGeneratedField: true,
},
{
key: 'reviewLastUpdatedAt',
type: 'text',
allowedUserTypesEdit: [],
isSystemGeneratedField: true,
},
];

export const validateRoleForCpoReview = (
updatedValue: CpoReview,
storedValue: CpoReview,
userType: UserTypesEnum[],
path: string,
isDeleted?: boolean, // when review is deleted, only check roles of NON-system generated values
) => {
if (!updatedValue) return;

let keys = Object.keys(updatedValue) as Array<keyof CpoReview>;

// if review is deleted, only check role for ONLY user generated keys
if (isDeleted) {
keys = keys.filter((key) => {
const metadata = cpoReviewMetadata.find((m) => m.key === key);
return !metadata.isSystemGeneratedField;
});
}

keys.forEach((key) => {
const updatedKeyValue = updatedValue?.[key];
const storedKeyValue = storedValue?.[key];
const metadata = cpoReviewMetadata.find((m) => m.key === key);

validateRoleForFormField(
metadata,
updatedKeyValue,
storedKeyValue,
userType,
`${path}.${key}`,
);
});
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { IsObject, IsOptional, ValidateNested } from '@nestjs/class-validator';
import { ForbiddenException } from '@nestjs/common';
import { Type } from 'class-transformer';
import { UserTypesEnum } from 'src/common/enums/users.enum';
import { KeycloakUser } from 'src/modules/auth/keycloak-user.model';
import { CpoReview, validateRoleForCpoReview } from './cpo-review';
import { MpoReview, validateRoleForMpoReview } from './mpo-review';
import {
ProgramAreaReview,
Expand All @@ -19,6 +22,10 @@ export class Review {
@ValidateNested()
@Type(() => MpoReview)
mpo?: MpoReview;

@IsObject()
@IsOptional()
cpo?: Record<string, CpoReview>;
}

/**
Expand All @@ -29,16 +36,95 @@ export const validateRoleForReview = (
updatedValue: Review,
storedValue: Review,
userType: UserTypesEnum[],
loggedInUser: KeycloakUser,
) => {
if (!updatedValue) return;
if (!updatedValue && !storedValue) return;

//
// program area validations
//
validateRoleForProgramAreaReview(
updatedValue?.programArea,
storedValue?.programArea,
userType,
loggedInUser,
);

//
// mpo validations
validateRoleForMpoReview(updatedValue?.mpo, storedValue?.mpo, userType);
//
let isMpoReviewDeleted = false;

if (!updatedValue?.mpo && storedValue?.mpo) {
isMpoReviewDeleted = true;

if (updatedValue?.mpo?.reviewedByGuid !== loggedInUser.idir_user_guid) {
new ForbiddenException({
message: 'User is not allowed to remove review for the path',
path: `review.mpo`,
});
}
}

validateRoleForMpoReview(
updatedValue?.mpo,
storedValue?.mpo,
userType,
isMpoReviewDeleted,
);

//
// cpo validations
//
const updatedCpoReviews = updatedValue?.cpo;
const storedCpoReviews = storedValue?.cpo;

// no records
if (!updatedCpoReviews && !storedCpoReviews) return;

const updatedReviewers = Object.keys(updatedCpoReviews || {});
const storedReviewers = Object.keys(storedCpoReviews || {});

// check role access for reviews that are cleared / removed reviews
// check role for non-system generated values
const deletedReviewers = storedReviewers.filter((r) => {
return (
!updatedReviewers.includes(r) || // when the role key does not exist at all
!updatedCpoReviews[r] // or when the key exists, but null or undefined
);
});

deletedReviewers.forEach((userId) => {
// check if user only deletes/removes reviews he only left
if (
storedCpoReviews?.[userId]?.reviewedByGuid !==
loggedInUser?.idir_user_guid
) {
new ForbiddenException({
message: 'User is not allowed to remove review for the path',
path: `review.cpo.${userId}`,
});
}

// check for role considerations of the user
validateRoleForCpoReview(
updatedCpoReviews?.[userId],
storedCpoReviews?.[userId],
userType,
`review.cpo.${userId}`,
true,
);
});

// check role access for reviews that are updated or added
updatedReviewers
.filter((r) => !deletedReviewers.includes(r)) // filter deleted / nullified reviews
.forEach((userId) => {
validateRoleForCpoReview(
updatedCpoReviews?.[userId],
storedCpoReviews?.[userId],
userType,
`review.cpo.${userId}`,
);
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,47 +15,63 @@ export const mpoReviewMetadata: Array<IFormField<MpoReview>> = [
key: 'isAcknowledged',
type: 'boolean',
allowedUserTypesEdit: [UserTypesEnum.MPO],
isSystemGeneratedField: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be ok to add a comment on what isSystemGeneratedField does or it's purpose ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, makes sense. Done. Added at the definition file - form-field.interface.ts

},
{
key: 'reviewNote',
type: 'text',
allowedUserTypesEdit: [UserTypesEnum.MPO],
isSystemGeneratedField: false,
},
{
key: 'reviewedByDisplayName',
type: 'text',
allowedUserTypesEdit: [], // empty array signifies that the client can't edit these fields
isSystemGeneratedField: true,
},
{
key: 'reviewedByUsername',
type: 'text',
allowedUserTypesEdit: [],
isSystemGeneratedField: true,
},
{
key: 'reviewedByGuid',
type: 'text',
allowedUserTypesEdit: [],
isSystemGeneratedField: true,
},
{
key: 'reviewedAt',
type: 'text',
allowedUserTypesEdit: [],
isSystemGeneratedField: true,
},
{
key: 'reviewLastUpdatedAt',
type: 'text',
allowedUserTypesEdit: [],
isSystemGeneratedField: true,
},
];

export const validateRoleForMpoReview = (
updatedValue: MpoReview,
storedValue: MpoReview,
userType: UserTypesEnum[],
isDeleted?: boolean,
) => {
if (!updatedValue) return;

const keys = Object.keys(updatedValue) as Array<keyof MpoReview>;
let keys = Object.keys(updatedValue) as Array<keyof MpoReview>;

// if review is deleted, only check role for ONLY user generated keys
if (isDeleted) {
keys = keys.filter((key) => {
const metadata = mpoReviewMetadata.find((m) => m.key === key);
return !metadata.isSystemGeneratedField;
});
}

keys.forEach((key) => {
const updatedKeyValue = updatedValue?.[key];
Expand Down
Loading
Loading