diff --git a/apps/judicial-system/api/src/app/modules/auth/auth.controller.ts b/apps/judicial-system/api/src/app/modules/auth/auth.controller.ts index 4ad6f20aef72..2fa13c4d8969 100644 --- a/apps/judicial-system/api/src/app/modules/auth/auth.controller.ts +++ b/apps/judicial-system/api/src/app/modules/auth/auth.controller.ts @@ -256,16 +256,16 @@ export class AuthController { ? PRISON_CASES_ROUTE : CASES_ROUTE, } - } else { - const defender = await this.authService.findDefender(authUser.nationalId) - - if (defender) { - return { - userId: defender.id, - userNationalId: defender.nationalId, - jwtToken: this.sharedAuthService.signJwt(defender, csrfToken), - redirectRoute: requestedRedirectRoute ?? DEFENDER_CASES_ROUTE, - } + } + + const defender = await this.authService.findDefender(authUser.nationalId) + + if (defender) { + return { + userId: defender.id, + userNationalId: defender.nationalId, + jwtToken: this.sharedAuthService.signJwt(defender, csrfToken), + redirectRoute: requestedRedirectRoute ?? DEFENDER_CASES_ROUTE, } } diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts index 18a20f17b447..36a406f4badb 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts @@ -1,5 +1,4 @@ import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' -import type { User } from '@island.is/judicial-system/types' import { CaseAppealState, CaseDecision, @@ -20,16 +19,18 @@ import { isRequestCase, isRestrictionCase, RequestSharedWithDefender, + type User, UserRole, } from '@island.is/judicial-system/types' +import { CivilClaimant, Defendant } from '../../defendant' import { Case } from '../models/case.model' import { DateLog } from '../models/dateLog.model' const canProsecutionUserAccessCase = ( theCase: Case, user: User, - forUpdate = true, + forUpdate: boolean, ): boolean => { // Check case type access if (user.role !== UserRole.PROSECUTOR && !isIndictmentCase(theCase.type)) { @@ -196,7 +197,7 @@ const canAppealsCourtUserAccessCase = (theCase: Case): boolean => { const canPrisonStaffUserAccessCase = ( theCase: Case, - forUpdate = true, + forUpdate: boolean, ): boolean => { // Prison staff users cannot update cases if (forUpdate) { @@ -234,7 +235,7 @@ const canPrisonStaffUserAccessCase = ( const canPrisonAdminUserAccessCase = ( theCase: Case, - forUpdate = true, + forUpdate: boolean, ): boolean => { // Prison admin users cannot update cases if (forUpdate) { @@ -306,29 +307,27 @@ const canPrisonAdminUserAccessCase = ( return true } -const canDefenceUserAccessCase = (theCase: Case, user: User): boolean => { +const canDefenceUserAccessRequestCase = ( + theCase: Case, + user: User, +): boolean => { // Check case state access if ( ![ CaseState.SUBMITTED, - CaseState.WAITING_FOR_CANCELLATION, CaseState.RECEIVED, CaseState.ACCEPTED, CaseState.REJECTED, CaseState.DISMISSED, - CaseState.COMPLETED, ].includes(theCase.state) ) { return false } - const arraignmentDate = DateLog.arraignmentDate(theCase.dateLogs) - // Check submitted case access const canDefenderAccessSubmittedCase = - isRequestCase(theCase.type) && theCase.requestSharedWithDefender === - RequestSharedWithDefender.READY_FOR_COURT + RequestSharedWithDefender.READY_FOR_COURT if ( theCase.state === CaseState.SUBMITTED && @@ -338,50 +337,94 @@ const canDefenceUserAccessCase = (theCase: Case, user: User): boolean => { } // Check received case access - if (theCase.state === CaseState.RECEIVED) { - const canDefenderAccessReceivedCase = - isIndictmentCase(theCase.type) || - canDefenderAccessSubmittedCase || - Boolean(arraignmentDate) + const canDefenderAccessReceivedCase = + canDefenderAccessSubmittedCase || + Boolean(DateLog.arraignmentDate(theCase.dateLogs)) - if (!canDefenderAccessReceivedCase) { - return false - } + if (theCase.state === CaseState.RECEIVED && !canDefenderAccessReceivedCase) { + return false } const normalizedAndFormattedNationalId = normalizeAndFormatNationalId( user.nationalId, ) - // Check case defender access + // Check case defender assignment + if ( + theCase.defenderNationalId && + normalizedAndFormattedNationalId.includes(theCase.defenderNationalId) + ) { + return true + } + + return false +} + +const canDefenceUserAccessIndictmentCase = ( + theCase: Case, + user: User, + forUpdate: boolean, +): boolean => { + // Check case state access + if ( + ![ + CaseState.WAITING_FOR_CANCELLATION, + CaseState.RECEIVED, + CaseState.COMPLETED, + ].includes(theCase.state) + ) { + return false + } + + // Check received case access + const canDefenderAccessReceivedCase = Boolean( + DateLog.arraignmentDate(theCase.dateLogs), + ) + + if (theCase.state === CaseState.RECEIVED && !canDefenderAccessReceivedCase) { + return false + } + + // Check case defender assignment + if (Defendant.isDefenderOfDefendant(user.nationalId, theCase.defendants)) { + return true + } + + // Check case spokesperson assignment + if ( + CivilClaimant.isSpokespersonOfCivilClaimant( + user.nationalId, + theCase.civilClaimants, + ) && + !forUpdate + ) { + return true + } + + return false +} + +const canDefenceUserAccessCase = ( + theCase: Case, + user: User, + forUpdate: boolean, +): boolean => { + if (isRequestCase(theCase.type)) { + return canDefenceUserAccessRequestCase(theCase, user) + } + if (isIndictmentCase(theCase.type)) { - if ( - !theCase.defendants?.some( - (defendant) => - defendant.defenderNationalId && - normalizedAndFormattedNationalId.includes( - defendant.defenderNationalId, - ), - ) - ) { - return false - } - } else { - if ( - !theCase.defenderNationalId || - !normalizedAndFormattedNationalId.includes(theCase.defenderNationalId) - ) { - return false - } + return canDefenceUserAccessIndictmentCase(theCase, user, forUpdate) } - return true + // Other cases are not accessible to defence users + return false } export const canUserAccessCase = ( theCase: Case, user: User, - forUpdate = true, + forUpdate: boolean, ): boolean => { if (isProsecutionUser(user)) { return canProsecutionUserAccessCase(theCase, user, forUpdate) @@ -404,7 +447,7 @@ export const canUserAccessCase = ( } if (isDefenceUser(user)) { - return canDefenceUserAccessCase(theCase, user) + return canDefenceUserAccessCase(theCase, user, forUpdate) } if (isPublicProsecutorUser(user)) { diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts index c07fe09ae772..847292fbddbe 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts @@ -277,13 +277,26 @@ const getDefenceUserCasesQueryFilter = (user: User): WhereOptions => { ], }, { - id: { - [Op.in]: Sequelize.literal(` - (SELECT case_id - FROM defendant - WHERE defender_national_id in ('${normalizedNationalId}', '${formattedNationalId}')) - `), - }, + [Op.or]: [ + { + id: { + [Op.in]: Sequelize.literal(` + (SELECT case_id + FROM defendant + WHERE defender_national_id in ('${normalizedNationalId}', '${formattedNationalId}')) + `), + }, + }, + { + id: { + [Op.in]: Sequelize.literal(` + (SELECT case_id + FROM civil_claimant + WHERE has_spokesperson = true AND spokesperson_national_id in ('${normalizedNationalId}', '${formattedNationalId}')) + `), + }, + }, + ], }, ], }, diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts index 176476b1ab4f..2a0af1e49d7d 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts @@ -453,13 +453,26 @@ describe('getCasesQueryFilter', () => { ], }, { - id: { - [Op.in]: Sequelize.literal(` - (SELECT case_id - FROM defendant - WHERE defender_national_id in ('${user.nationalId}', '${user.nationalId}')) - `), - }, + [Op.or]: [ + { + id: { + [Op.in]: Sequelize.literal(` + (SELECT case_id + FROM defendant + WHERE defender_national_id in ('${user.nationalId}', '${user.nationalId}')) + `), + }, + }, + { + id: { + [Op.in]: Sequelize.literal(` + (SELECT case_id + FROM civil_claimant + WHERE has_spokesperson = true AND spokesperson_national_id in ('${user.nationalId}', '${user.nationalId}')) + `), + }, + }, + ], }, ], }, diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts b/apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts index 4b71e77db3ba..4ada3f02f7c1 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts @@ -2,7 +2,8 @@ import { uuid } from 'uuidv4' import { CaseState, - completedCaseStates, + completedIndictmentCaseStates, + completedRequestCaseStates, DateType, defenceRoles, indictmentCases, @@ -13,19 +14,21 @@ import { } from '@island.is/judicial-system/types' import { Case } from '../../models/case.model' -import { verifyFullAccess, verifyNoAccess } from './verify' +import { verifyFullAccess, verifyNoAccess, verifyReadAccess } from './verify' + +// TODO: Fix defender indictment tests +// Add spokesperson tests describe.each(defenceRoles)('defence user %s', (role) => { const user = { role, nationalId: uuid() } as User describe.each([...restrictionCases, ...investigationCases])( - `r-case type %s`, + `defender r-case type %s`, (type) => { const accessibleCaseStates = [ - CaseState.WAITING_FOR_CANCELLATION, CaseState.SUBMITTED, CaseState.RECEIVED, - ...completedCaseStates, + ...completedRequestCaseStates, ] describe.each( @@ -169,11 +172,11 @@ describe.each(defenceRoles)('defence user %s', (role) => { }, ) - describe.each(indictmentCases)(`s-case type %s`, (type) => { + describe.each(indictmentCases)(`defender s-case type %s`, (type) => { const accessibleCaseStates = [ CaseState.WAITING_FOR_CANCELLATION, CaseState.RECEIVED, - ...completedCaseStates, + ...completedIndictmentCaseStates, ] describe.each( @@ -201,10 +204,79 @@ describe.each(defenceRoles)('defence user %s', (role) => { type, state, defendants: [{}, { defenderNationalId: user.nationalId }, {}], + dateLogs: [{ dateType: DateType.ARRAIGNMENT_DATE, date: new Date() }], } as Case verifyFullAccess(theCase, user) }) }) }) + + describe.each([...restrictionCases, ...investigationCases])( + 'spokesperson inaccessible r-case type %s', + (type) => { + const theCase = { + type, + } as Case + + verifyNoAccess(theCase, user) + }, + ) + + describe.each(indictmentCases)(`spokesperson s-case type %s`, (type) => { + const accessibleCaseStates = [ + CaseState.WAITING_FOR_CANCELLATION, + CaseState.RECEIVED, + ...completedIndictmentCaseStates, + ] + + describe.each( + Object.values(CaseState).filter( + (state) => !accessibleCaseStates.includes(state), + ), + )('inaccessible case state %s', (state) => { + const theCase = { + type, + state, + civilClaimants: [ + {}, + { hasSpokesperson: true, spokespersonNationalId: user.nationalId }, + {}, + ], + } as Case + + verifyNoAccess(theCase, user) + }) + + describe.each(accessibleCaseStates)('accessible case state %s', (state) => { + describe('spokesperson not assigned to case', () => { + const theCase = { + type, + state, + civilClaimants: [ + {}, + { hasSpokesperson: false, spokespersonNationalId: user.nationalId }, + {}, + ], + } as Case + + verifyNoAccess(theCase, user) + }) + + describe('spokesperson assigned to case', () => { + const theCase = { + type, + state, + civilClaimants: [ + {}, + { hasSpokesperson: true, spokespersonNationalId: user.nationalId }, + {}, + ], + dateLogs: [{ dateType: DateType.ARRAIGNMENT_DATE, date: new Date() }], + } as Case + + verifyReadAccess(theCase, user) + }) + }) + }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/test/publicProsecutionUserFilter.spec.ts b/apps/judicial-system/backend/src/app/modules/case/filters/test/publicProsecutionUserFilter.spec.ts index 7f351559a056..0f7767f13285 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/test/publicProsecutionUserFilter.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/test/publicProsecutionUserFilter.spec.ts @@ -9,40 +9,12 @@ import { publicProsecutorRoles, restrictionCases, User, - UserRole, } from '@island.is/judicial-system/types' import { Case } from '../../models/case.model' import { verifyNoAccess } from './verify' -describe.each([UserRole.PUBLIC_PROSECUTOR_STAFF])( - 'public prosecutor user %s', - (role) => { - const user = { - role, - institution: { id: uuid(), type: InstitutionType.PROSECUTORS_OFFICE }, - } as User - - describe.each(indictmentCases)('accessible case type %s', (type) => { - const accessibleCaseStates = completedCaseStates - - describe.each( - Object.values(CaseState).filter( - (state) => !accessibleCaseStates.includes(state), - ), - )('inaccessible case state %s', (state) => { - const theCase = { - type, - state, - } as Case - - verifyNoAccess(theCase, user) - }) - }) - }, -) - -describe.each(publicProsecutorRoles)('public prosecution user %s', (role) => { +describe.each(publicProsecutorRoles)('public prosecutor user %s', (role) => { const user = { role, institution: { id: uuid(), type: InstitutionType.PROSECUTORS_OFFICE }, @@ -58,4 +30,21 @@ describe.each(publicProsecutorRoles)('public prosecution user %s', (role) => { verifyNoAccess(theCase, user) }, ) + + describe.each(indictmentCases)('accessible case type %s', (type) => { + const accessibleCaseStates = completedCaseStates + + describe.each( + Object.values(CaseState).filter( + (state) => !accessibleCaseStates.includes(state), + ), + )('inaccessible case state %s', (state) => { + const theCase = { + type, + state, + } as Case + + verifyNoAccess(theCase, user) + }) + }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts b/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts index dbe63f9c778a..dbb92e3e7b11 100644 --- a/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts +++ b/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts @@ -6,6 +6,7 @@ import { UserRole, } from '@island.is/judicial-system/types' +import { CivilClaimant, Defendant } from '../../defendant' import { TransitionCaseDto } from '../dto/transitionCase.dto' import { UpdateCaseDto } from '../dto/updateCase.dto' import { Case } from '../models/case.model' @@ -274,6 +275,37 @@ export const defenderTransitionRule: RolesRule = { }, } +// Allows defenders to access generated PDFs +export const defenderGeneratedPdfRule: RolesRule = { + role: UserRole.DEFENDER, + type: RulesType.BASIC, + canActivate: (request) => { + const user: User = request.user + const theCase: Case = request.case + + // Deny if something is missing - shuould never happen + if (!user || !theCase) { + return false + } + + // Allow if the user is a defender of a defendant of the case + if (Defendant.isDefenderOfDefendant(user.nationalId, theCase.defendants)) { + return true + } + + if ( + CivilClaimant.isSpokespersonOfCivilClaimantWithCaseFileAccess( + user.nationalId, + theCase.civilClaimants, + ) + ) { + return true + } + + return false + }, +} + // Allows judges to transition cases export const districtCourtJudgeTransitionRule: RolesRule = { role: UserRole.DISTRICT_COURT_JUDGE, diff --git a/apps/judicial-system/backend/src/app/modules/case/index.ts b/apps/judicial-system/backend/src/app/modules/case/index.ts index 5bcc48aa33d5..feb231e51078 100644 --- a/apps/judicial-system/backend/src/app/modules/case/index.ts +++ b/apps/judicial-system/backend/src/app/modules/case/index.ts @@ -9,6 +9,7 @@ export { CaseNotCompletedGuard } from './guards/caseNotCompleted.guard' export { CaseReceivedGuard } from './guards/caseReceived.guard' export { CaseTypeGuard } from './guards/caseType.guard' export { CaseCompletedGuard } from './guards/caseCompleted.guard' +export { defenderGeneratedPdfRule } from './guards/rolesRules' export { CurrentCase } from './guards/case.decorator' export { CaseOriginalAncestorInterceptor } from './interceptors/caseOriginalAncestor.interceptor' export { CaseService } from './case.service' diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/limitedAccessCaseFile.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/limitedAccessCaseFile.interceptor.ts index 70d735d40e37..62c246937ca8 100644 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/limitedAccessCaseFile.interceptor.ts +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/limitedAccessCaseFile.interceptor.ts @@ -9,7 +9,7 @@ import { import { CaseFileCategory, User } from '@island.is/judicial-system/types' -import { canLimitedAcccessUserViewCaseFile } from '../../file' +import { canLimitedAccessUserViewCaseFile } from '../../file' @Injectable() export class LimitedAccessCaseFileInterceptor implements NestInterceptor { @@ -21,11 +21,13 @@ export class LimitedAccessCaseFileInterceptor implements NestInterceptor { map((theCase) => { const caseFiles = theCase.caseFiles?.filter( ({ category }: { category: CaseFileCategory }) => - canLimitedAcccessUserViewCaseFile( + canLimitedAccessUserViewCaseFile( user, theCase.type, theCase.state, category, + theCase.defendants, + theCase.civilClaimants, ), ) diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts index aa1a56f603ae..f4728247f295 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts @@ -53,7 +53,11 @@ import { CaseWriteGuard } from './guards/caseWrite.guard' import { LimitedAccessCaseExistsGuard } from './guards/limitedAccessCaseExists.guard' import { MergedCaseExistsGuard } from './guards/mergedCaseExists.guard' import { RequestSharedWithDefenderGuard } from './guards/requestSharedWithDefender.guard' -import { defenderTransitionRule, defenderUpdateRule } from './guards/rolesRules' +import { + defenderGeneratedPdfRule, + defenderTransitionRule, + defenderUpdateRule, +} from './guards/rolesRules' import { CaseInterceptor } from './interceptors/case.interceptor' import { CompletedAppealAccessedInterceptor } from './interceptors/completedAppealAccessed.interceptor' import { LimitedAccessCaseFileInterceptor } from './interceptors/limitedAccessCaseFile.interceptor' @@ -240,13 +244,13 @@ export class LimitedAccessCaseController { @UseGuards( JwtAuthGuard, - RolesGuard, CaseExistsGuard, + RolesGuard, new CaseTypeGuard(indictmentCases), CaseReadGuard, MergedCaseExistsGuard, ) - @RolesRules(defenderRule) + @RolesRules(defenderGeneratedPdfRule) @Get([ 'case/:caseId/limitedAccess/caseFilesRecord/:policeCaseNumber', 'case/:caseId/limitedAccess/mergedCase/:mergedCaseId/caseFilesRecord/:policeCaseNumber', @@ -375,13 +379,13 @@ export class LimitedAccessCaseController { @UseGuards( JwtAuthGuard, - RolesGuard, CaseExistsGuard, + RolesGuard, new CaseTypeGuard(indictmentCases), CaseReadGuard, MergedCaseExistsGuard, ) - @RolesRules(defenderRule) + @RolesRules(defenderGeneratedPdfRule) @Get([ 'case/:caseId/limitedAccess/indictment', 'case/:caseId/limitedAccess/mergedCase/:mergedCaseId/indictment', diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts index efeaf459c19a..e31beb4454e5 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts @@ -30,12 +30,14 @@ import { import { nowFactory, uuidFactory } from '../../factories' import { AwsS3Service } from '../aws-s3' -import { CivilClaimant, Defendant, DefendantService } from '../defendant' -import { EventLog } from '../event-log' import { - CaseFile, - defenderCaseFileCategoriesForRestrictionAndInvestigationCases, -} from '../file' + CivilClaimant, + CivilClaimantService, + Defendant, + DefendantService, +} from '../defendant' +import { EventLog } from '../event-log' +import { CaseFile, defenderCaseFileCategoriesForRequestCases } from '../file' import { IndictmentCount } from '../indictment-count' import { Institution } from '../institution' import { Subpoena } from '../subpoena' @@ -281,6 +283,7 @@ export class LimitedAccessCaseService { constructor( private readonly messageService: MessageService, private readonly defendantService: DefendantService, + private readonly civilClaimantService: CivilClaimantService, private readonly pdfService: PdfService, private readonly awsS3Service: AwsS3Service, @InjectModel(Case) private readonly caseModel: typeof Case, @@ -425,6 +428,7 @@ export class LimitedAccessCaseService { }) .then((theCase) => { if (theCase) { + // The national id is associated with a defender in a request case return this.constructDefender( nationalId, theCase.defenderName, @@ -437,6 +441,7 @@ export class LimitedAccessCaseService { .findLatestDefendantByDefenderNationalId(nationalId) .then((defendant) => { if (defendant) { + // The national id is associated with a defender in an indictment case return this.constructDefender( nationalId, defendant.defenderName, @@ -445,7 +450,21 @@ export class LimitedAccessCaseService { ) } - throw new NotFoundException('Defender not found') + return this.civilClaimantService + .findLatestClaimantBySpokespersonNationalId(nationalId) + .then((civilClaimant) => { + if (civilClaimant) { + // The national id is associated with a spokesperson for a civil claimant in an indictment case + return this.constructDefender( + nationalId, + civilClaimant.spokespersonName, + civilClaimant.spokespersonPhoneNumber, + civilClaimant.spokespersonEmail, + ) + } + + throw new NotFoundException('Defender not found') + }) }) }) } @@ -488,9 +507,7 @@ export class LimitedAccessCaseService { (file) => file.key && file.category && - defenderCaseFileCategoriesForRestrictionAndInvestigationCases.includes( - file.category, - ), + defenderCaseFileCategoriesForRequestCases.includes(file.category), ) ?? [] // TODO: speed this up by fetching all files in parallel diff --git a/apps/judicial-system/backend/src/app/modules/case/test/createTestingCaseModule.ts b/apps/judicial-system/backend/src/app/modules/case/test/createTestingCaseModule.ts index 1e8dd996ea45..358e3028963a 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/createTestingCaseModule.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/createTestingCaseModule.ts @@ -18,6 +18,7 @@ import { MessageService } from '@island.is/judicial-system/message' import { AwsS3Service } from '../../aws-s3' import { CourtService } from '../../court' +import { CivilClaimantService } from '../../defendant' import { DefendantService } from '../../defendant' import { EventService } from '../../event' import { FileService } from '../../file' @@ -45,6 +46,7 @@ jest.mock('../../user/user.service') jest.mock('../../file/file.service') jest.mock('../../aws-s3/awsS3.service') jest.mock('../../defendant/defendant.service') +jest.mock('../../defendant/civilClaimant.service') jest.mock('../../indictment-count/indictmentCount.service') export const createTestingCaseModule = async () => { @@ -70,6 +72,7 @@ export const createTestingCaseModule = async () => { EventService, SigningService, DefendantService, + CivilClaimantService, IndictmentCountService, { provide: IntlService, @@ -148,6 +151,9 @@ export const createTestingCaseModule = async () => { const defendantService = caseModule.get(DefendantService) + const civilClaimantService = + caseModule.get(CivilClaimantService) + const indictmentCountService = caseModule.get( IndictmentCountService, ) @@ -197,6 +203,7 @@ export const createTestingCaseModule = async () => { fileService, awsS3Service, defendantService, + civilClaimantService, indictmentCountService, logger, sequelize, diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/findDefenderByNationalId.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/findDefenderByNationalId.spec.ts index d2fb64ec33ff..8822b52de893 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/findDefenderByNationalId.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/findDefenderByNationalId.spec.ts @@ -9,7 +9,7 @@ import { createTestingCaseModule } from '../createTestingCaseModule' import { nowFactory, uuidFactory } from '../../../../factories' import { randomDate } from '../../../../test' -import { DefendantService } from '../../../defendant' +import { CivilClaimantService, DefendantService } from '../../../defendant' import { User } from '../../../user' import { Case } from '../../models/case.model' @@ -32,19 +32,28 @@ describe('LimitedAccessCaseController - Find defender by national id', () => { const defenderEmail = 'dummy@dummy.dy' let mockDefendantService: DefendantService + let mockCivilClaimantService: CivilClaimantService let mockCaseModel: typeof Case let givenWhenThen: GivenWhenThen beforeEach(async () => { - const { defendantService, caseModel, limitedAccessCaseController } = - await createTestingCaseModule() + const { + defendantService, + civilClaimantService, + caseModel, + limitedAccessCaseController, + } = await createTestingCaseModule() mockDefendantService = defendantService + mockCivilClaimantService = civilClaimantService mockCaseModel = caseModel const mockFindLatestDefendantByDefenderNationalId = mockDefendantService.findLatestDefendantByDefenderNationalId as jest.Mock mockFindLatestDefendantByDefenderNationalId.mockResolvedValue(null) + const mockFindLatestClaimantBySpokespersonNationalId = + mockCivilClaimantService.findLatestClaimantBySpokespersonNationalId as jest.Mock + mockFindLatestClaimantBySpokespersonNationalId.mockResolvedValue(null) const mockFindOne = mockCaseModel.findOne as jest.Mock mockFindOne.mockResolvedValue(null) const mockToday = nowFactory as jest.Mock @@ -156,4 +165,37 @@ describe('LimitedAccessCaseController - Find defender by national id', () => { }) }) }) + + describe('defender found in a civil claimant', () => { + let then: Then + + beforeEach(async () => { + const mockFindLatestClaimantBySpokespersonNationalId = + mockCivilClaimantService.findLatestClaimantBySpokespersonNationalId as jest.Mock + mockFindLatestClaimantBySpokespersonNationalId.mockResolvedValueOnce({ + spokespersonNationalId: defenderNationalId, + spokespersonName: defenderName, + spokespersonPhoneNumber: defenderPhoneNumber, + spokespersonEmail: defenderEmail, + }) + + then = await givenWhenThen(defenderNationalId) + }) + + it('should return the user', () => { + expect(then.result).toEqual({ + id: defenderId, + created: date, + modified: date, + nationalId: defenderNationalId, + name: defenderName, + title: 'verjandi', + mobileNumber: defenderPhoneNumber, + email: defenderEmail, + role: UserRole.DEFENDER, + active: true, + canConfirmIndictment: false, + }) + }) + }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts index f9cf460fb962..210ee0af1d13 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts @@ -21,8 +21,8 @@ describe('LimitedAccessCaseController - Get case files record pdf guards', () => it('should have the right guard configuration', () => { expect(guards).toHaveLength(6) expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard) - expect(new guards[1]()).toBeInstanceOf(RolesGuard) - expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard) + expect(new guards[1]()).toBeInstanceOf(CaseExistsGuard) + expect(new guards[2]()).toBeInstanceOf(RolesGuard) expect(guards[3]).toBeInstanceOf(CaseTypeGuard) expect(guards[3]).toEqual({ allowedCaseTypes: indictmentCases, diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfRolesRules.spec.ts index c923799a844c..986897519b70 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfRolesRules.spec.ts @@ -1,4 +1,4 @@ -import { defenderRule } from '../../../../guards' +import { defenderGeneratedPdfRule } from '../../guards/rolesRules' import { LimitedAccessCaseController } from '../../limitedAccessCase.controller' describe('LimitedAccessCaseController - Get case files record pdf rules', () => { @@ -14,6 +14,6 @@ describe('LimitedAccessCaseController - Get case files record pdf rules', () => it('should give permission to one roles', () => { expect(rules).toHaveLength(1) - expect(rules).toContain(defenderRule) + expect(rules).toContain(defenderGeneratedPdfRule) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts index bc2a8f2e1546..c0ec34df4638 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts @@ -21,8 +21,8 @@ describe('LimitedAccessCaseController - Get indictment pdf guards', () => { it('should have the right guard configuration', () => { expect(guards).toHaveLength(6) expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard) - expect(new guards[1]()).toBeInstanceOf(RolesGuard) - expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard) + expect(new guards[1]()).toBeInstanceOf(CaseExistsGuard) + expect(new guards[2]()).toBeInstanceOf(RolesGuard) expect(guards[3]).toBeInstanceOf(CaseTypeGuard) expect(guards[3]).toEqual({ allowedCaseTypes: indictmentCases, diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfRolesRules.spec.ts index 46e7299e2b0e..0f399b066398 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfRolesRules.spec.ts @@ -1,4 +1,4 @@ -import { defenderRule } from '../../../../guards' +import { defenderGeneratedPdfRule } from '../../guards/rolesRules' import { LimitedAccessCaseController } from '../../limitedAccessCase.controller' describe('LimitedAccessCaseController - Get indictment pdf rules', () => { @@ -14,6 +14,6 @@ describe('LimitedAccessCaseController - Get indictment pdf rules', () => { it('should give permission to one roles', () => { expect(rules).toHaveLength(1) - expect(rules).toContain(defenderRule) + expect(rules).toContain(defenderGeneratedPdfRule) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts b/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts index 171c3cc7ee29..950e62bf84da 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts @@ -1,10 +1,15 @@ +import { Op } from 'sequelize' + import { Inject, Injectable } from '@nestjs/common' import { InjectModel } from '@nestjs/sequelize' import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' -import { Case } from '../case' +import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' +import { CaseState } from '@island.is/judicial-system/types' + +import { Case } from '../case/models/case.model' import { UpdateCivilClaimantDto } from './dto/updateCivilClaimant.dto' import { CivilClaimant } from './models/civilClaimant.model' @@ -66,4 +71,26 @@ export class CivilClaimantService { return true } + + findLatestClaimantBySpokespersonNationalId( + nationalId: string, + ): Promise { + return this.civilClaimantModel.findOne({ + include: [ + { + model: Case, + as: 'case', + where: { + state: { [Op.not]: CaseState.DELETED }, + isArchived: false, + }, + }, + ], + where: { + hasSpokesperson: true, + spokespersonNationalId: normalizeAndFormatNationalId(nationalId), + }, + order: [['created', 'DESC']], + }) + } } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts b/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts index a7cb4fca9e0f..64517bda3c3a 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts @@ -26,6 +26,6 @@ import { InternalDefendantController } from './internalDefendant.controller' CivilClaimantController, ], providers: [DefendantService, CivilClaimantService], - exports: [DefendantService], + exports: [DefendantService, CivilClaimantService], }) export class DefendantModule {} diff --git a/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts b/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts index f323632f2476..5436816f44a8 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts @@ -298,7 +298,7 @@ export class DefendantService { }, }, ], - where: { defenderNationalId: nationalId }, + where: { defenderNationalId: normalizeAndFormatNationalId(nationalId) }, order: [['created', 'DESC']], }) } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/index.ts b/apps/judicial-system/backend/src/app/modules/defendant/index.ts index ed6753c1f764..d9914e3dcda4 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/index.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/index.ts @@ -3,3 +3,4 @@ export { DefendantService } from './defendant.service' export { CivilClaimant } from './models/civilClaimant.model' export { DefendantExistsGuard } from './guards/defendantExists.guard' export { CurrentDefendant } from './guards/defendant.decorator' +export { CivilClaimantService } from './civilClaimant.service' diff --git a/apps/judicial-system/backend/src/app/modules/defendant/models/civilClaimant.model.ts b/apps/judicial-system/backend/src/app/modules/defendant/models/civilClaimant.model.ts index 308ba8820ce8..ce59dd77b472 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/models/civilClaimant.model.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/models/civilClaimant.model.ts @@ -6,10 +6,13 @@ import { ForeignKey, Model, Table, + UpdatedAt, } from 'sequelize-typescript' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' +import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' + import { Case } from '../../case/models/case.model' @Table({ @@ -17,6 +20,35 @@ import { Case } from '../../case/models/case.model' timestamps: false, }) export class CivilClaimant extends Model { + static isSpokespersonOfCivilClaimant( + spokespersonNationalId: string, + civilClaimants?: CivilClaimant[], + ) { + return civilClaimants?.some( + (civilClaimant) => + civilClaimant.hasSpokesperson && + civilClaimant.spokespersonNationalId && + normalizeAndFormatNationalId(spokespersonNationalId).includes( + civilClaimant.spokespersonNationalId, + ), + ) + } + + static isSpokespersonOfCivilClaimantWithCaseFileAccess( + spokespersonNationalId: string, + civilClaimants?: CivilClaimant[], + ) { + return civilClaimants?.some( + (civilClaimant) => + civilClaimant.hasSpokesperson && + civilClaimant.spokespersonNationalId && + normalizeAndFormatNationalId(spokespersonNationalId).includes( + civilClaimant.spokespersonNationalId, + ) && + civilClaimant.caseFilesSharedWithSpokesperson, + ) + } + @Column({ type: DataType.UUID, primaryKey: true, @@ -27,14 +59,13 @@ export class CivilClaimant extends Model { id!: string @CreatedAt - @Column({ - type: DataType.DATE, - defaultValue: DataType.NOW, - allowNull: false, - }) @ApiProperty({ type: Date }) created!: Date + @UpdatedAt + @ApiProperty({ type: Date }) + modified!: Date + @ForeignKey(() => Case) @Column({ type: DataType.UUID, diff --git a/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts b/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts index b8c66a3bc14b..d36b4a8ad6d8 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts @@ -12,6 +12,7 @@ import { import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' +import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' import { DefendantPlea, DefenderChoice, @@ -28,6 +29,19 @@ import { Subpoena } from '../../subpoena/models/subpoena.model' timestamps: true, }) export class Defendant extends Model { + static isDefenderOfDefendant( + defenderNationalId: string, + defendants?: Defendant[], + ) { + return defendants?.some( + (defendant) => + defendant.defenderNationalId && + normalizeAndFormatNationalId(defenderNationalId).includes( + defendant.defenderNationalId, + ), + ) + } + @Column({ type: DataType.UUID, primaryKey: true, diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts index 64a57a3ef17c..f9ab8226bf93 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts @@ -11,7 +11,9 @@ import { User, } from '@island.is/judicial-system/types' -export const defenderCaseFileCategoriesForRestrictionAndInvestigationCases = [ +import { CivilClaimant, Defendant } from '../../defendant' + +export const defenderCaseFileCategoriesForRequestCases = [ CaseFileCategory.PROSECUTOR_APPEAL_BRIEF, CaseFileCategory.PROSECUTOR_APPEAL_STATEMENT, CaseFileCategory.DEFENDANT_APPEAL_BRIEF, @@ -23,18 +25,22 @@ export const defenderCaseFileCategoriesForRestrictionAndInvestigationCases = [ CaseFileCategory.APPEAL_COURT_RECORD, ] -const defenderCaseFileCategoriesForIndictmentCases = [ +const defenderDefaultCaseFileCategoriesForIndictmentCases = [ CaseFileCategory.COURT_RECORD, CaseFileCategory.RULING, - CaseFileCategory.INDICTMENT, - CaseFileCategory.CRIMINAL_RECORD, - CaseFileCategory.COST_BREAKDOWN, - CaseFileCategory.CASE_FILE, - CaseFileCategory.PROSECUTOR_CASE_FILE, - CaseFileCategory.DEFENDANT_CASE_FILE, - CaseFileCategory.CIVIL_CLAIM, ] +const defenderCaseFileCategoriesForIndictmentCases = + defenderDefaultCaseFileCategoriesForIndictmentCases.concat( + CaseFileCategory.INDICTMENT, + CaseFileCategory.CRIMINAL_RECORD, + CaseFileCategory.COST_BREAKDOWN, + CaseFileCategory.CASE_FILE, + CaseFileCategory.PROSECUTOR_CASE_FILE, + CaseFileCategory.DEFENDANT_CASE_FILE, + CaseFileCategory.CIVIL_CLAIM, + ) + const prisonAdminCaseFileCategories = [ CaseFileCategory.APPEAL_RULING, CaseFileCategory.RULING, @@ -42,49 +48,117 @@ const prisonAdminCaseFileCategories = [ const prisonStaffCaseFileCategories = [CaseFileCategory.APPEAL_RULING] -export const canLimitedAcccessUserViewCaseFile = ( +const canDefenceUserViewCaseFileOfRequestCase = ( + caseState: CaseState, + caseFileCategory: CaseFileCategory, +) => { + return ( + isCompletedCase(caseState) && + defenderCaseFileCategoriesForRequestCases.includes(caseFileCategory) + ) +} + +const canDefenceUserViewCaseFileOfIndictmentCase = ( + nationalId: string, + caseFileCategory: CaseFileCategory, + defendants?: Defendant[], + civilClaimants?: CivilClaimant[], +) => { + if (Defendant.isDefenderOfDefendant(nationalId, defendants)) { + return defenderCaseFileCategoriesForIndictmentCases.includes( + caseFileCategory, + ) + } + + if ( + CivilClaimant.isSpokespersonOfCivilClaimantWithCaseFileAccess( + nationalId, + civilClaimants, + ) + ) { + return defenderCaseFileCategoriesForIndictmentCases.includes( + caseFileCategory, + ) + } + + return defenderDefaultCaseFileCategoriesForIndictmentCases.includes( + caseFileCategory, + ) +} + +const canDefenceUserViewCaseFile = ( + nationalId: string, + caseType: CaseType, + caseState: CaseState, + caseFileCategory: CaseFileCategory, + defendants?: Defendant[], + civilClaimants?: CivilClaimant[], +) => { + if (isRequestCase(caseType)) { + return canDefenceUserViewCaseFileOfRequestCase(caseState, caseFileCategory) + } + + if (isIndictmentCase(caseType)) { + return canDefenceUserViewCaseFileOfIndictmentCase( + nationalId, + caseFileCategory, + defendants, + civilClaimants, + ) + } + + return false +} + +const canPrisonStaffUserViewCaseFile = ( + caseState: CaseState, + caseFileCategory: CaseFileCategory, +) => { + return ( + isCompletedCase(caseState) && + prisonStaffCaseFileCategories.includes(caseFileCategory) + ) +} + +const canPrisonAdminUserViewCaseFile = ( + caseState: CaseState, + caseFileCategory: CaseFileCategory, +) => { + return ( + isCompletedCase(caseState) && + prisonAdminCaseFileCategories.includes(caseFileCategory) + ) +} + +export const canLimitedAccessUserViewCaseFile = ( user: User, caseType: CaseType, caseState: CaseState, caseFileCategory?: CaseFileCategory, + defendants?: Defendant[], + civilClaimants?: CivilClaimant[], ) => { if (!caseFileCategory) { return false } if (isDefenceUser(user)) { - if ( - isRequestCase(caseType) && - isCompletedCase(caseState) && - defenderCaseFileCategoriesForRestrictionAndInvestigationCases.includes( - caseFileCategory, - ) - ) { - return true - } - - if ( - isIndictmentCase(caseType) && - defenderCaseFileCategoriesForIndictmentCases.includes(caseFileCategory) - ) { - return true - } + return canDefenceUserViewCaseFile( + user.nationalId, + caseType, + caseState, + caseFileCategory, + defendants, + civilClaimants, + ) + } + + if (isPrisonStaffUser(user)) { + return canPrisonStaffUserViewCaseFile(caseState, caseFileCategory) } - if (isCompletedCase(caseState)) { - if ( - isPrisonStaffUser(user) && - prisonStaffCaseFileCategories.includes(caseFileCategory) - ) { - return true - } - - if ( - isPrisonAdminUser(user) && - prisonAdminCaseFileCategories.includes(caseFileCategory) - ) { - return true - } + if (isPrisonAdminUser(user)) { + return canPrisonAdminUserViewCaseFile(caseState, caseFileCategory) } return false diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts b/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts index b9bdf659e23b..f6b0d1c69a3a 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts @@ -10,7 +10,7 @@ import { User } from '@island.is/judicial-system/types' import { Case } from '../../case' import { CaseFile } from '../models/file.model' -import { canLimitedAcccessUserViewCaseFile } from './caseFileCategory' +import { canLimitedAccessUserViewCaseFile } from './caseFileCategory' @Injectable() export class LimitedAccessViewCaseFileGuard implements CanActivate { @@ -36,11 +36,13 @@ export class LimitedAccessViewCaseFileGuard implements CanActivate { } if ( - canLimitedAcccessUserViewCaseFile( + canLimitedAccessUserViewCaseFile( user, theCase.type, theCase.state, caseFile.category, + theCase.defendants, + theCase.civilClaimants, ) ) { return true diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts index d980ddc5024e..998d9a7807d3 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts @@ -1,3 +1,5 @@ +import { uuid } from 'uuidv4' + import { ExecutionContext, ForbiddenException, @@ -164,22 +166,187 @@ describe('Limited Access View Case File Guard', () => { ] describe.each(allowedCaseFileCategories)( - 'a defender can view %s', + 'case file category %s', (category) => { + describe('a defender with case files access can view', () => { + let then: Then + + beforeEach(() => { + const nationalId = uuid() + mockRequest.mockImplementationOnce(() => ({ + user: { role: UserRole.DEFENDER, nationalId }, + case: { + type, + state, + defendants: [{ defenderNationalId: nationalId }], + }, + caseFile: { category }, + })) + + then = givenWhenThen() + }) + + it('should activate', () => { + expect(then.result).toBe(true) + }) + }) + + describe('spokesperson with case files access can view', () => { + let then: Then + + beforeEach(() => { + const nationalId = uuid() + mockRequest.mockImplementationOnce(() => ({ + user: { role: UserRole.DEFENDER, nationalId }, + case: { + type, + state, + civilClaimants: [ + { + hasSpokesperson: true, + spokespersonNationalId: nationalId, + caseFilesSharedWithSpokesperson: true, + }, + ], + }, + caseFile: { category }, + })) + + then = givenWhenThen() + }) + + it('should activate', () => { + expect(then.result).toBe(true) + }) + }) + }, + ) + + describe.each( + Object.keys(CaseFileCategory).filter( + (category) => + !allowedCaseFileCategories.includes(category as CaseFileCategory), + ), + )('case file category %s', (category) => { + describe('a defender with case files access can not view', () => { let then: Then beforeEach(() => { + const nationalId = uuid() mockRequest.mockImplementationOnce(() => ({ - user: { role: UserRole.DEFENDER }, - case: { type, state }, + user: { role: UserRole.DEFENDER, nationalId }, + case: { + type, + state, + defendants: [{ defenderNationalId: nationalId }], + }, caseFile: { category }, })) then = givenWhenThen() }) - it('should activate', () => { - expect(then.result).toBe(true) + it('should throw ForbiddenException', () => { + expect(then.error).toBeInstanceOf(ForbiddenException) + expect(then.error.message).toBe( + `Forbidden for ${UserRole.DEFENDER}`, + ) + }) + }) + + describe('spokesperson with case files access can not view', () => { + let then: Then + + beforeEach(() => { + const nationalId = uuid() + mockRequest.mockImplementationOnce(() => ({ + user: { role: UserRole.DEFENDER, nationalId }, + case: { + type, + state, + civilClaimants: [ + { + hasSpokesperson: true, + spokespersonNationalId: nationalId, + caseFilesSharedWithSpokesperson: true, + }, + ], + }, + caseFile: { category }, + })) + + then = givenWhenThen() + }) + + it('should throw ForbiddenException', () => { + expect(then.error).toBeInstanceOf(ForbiddenException) + expect(then.error.message).toBe( + `Forbidden for ${UserRole.DEFENDER}`, + ) + }) + }) + }) + }) + }) + + describe.each(indictmentCases)('for %s cases', (type) => { + describe.each(Object.values(CaseState))('in state %s', (state) => { + const allowedCaseFileCategories = [ + CaseFileCategory.COURT_RECORD, + CaseFileCategory.RULING, + ] + + describe.each(allowedCaseFileCategories)( + 'case file category %s', + (category) => { + describe('a defender without case files access can view', () => { + let then: Then + + beforeEach(() => { + const nationalId = uuid() + mockRequest.mockImplementationOnce(() => ({ + user: { role: UserRole.DEFENDER, nationalId }, + case: { + type, + state, + }, + caseFile: { category }, + })) + + then = givenWhenThen() + }) + + it('should activate', () => { + expect(then.result).toBe(true) + }) + }) + + describe('spokesperson without case files access can view', () => { + let then: Then + + beforeEach(() => { + const nationalId = uuid() + mockRequest.mockImplementationOnce(() => ({ + user: { role: UserRole.DEFENDER, nationalId }, + case: { + type, + state, + civilClaimants: [ + { + hasSpokesperson: true, + spokespersonNationalId: nationalId, + }, + ], + }, + caseFile: { category }, + })) + + then = givenWhenThen() + }) + + it('should activate', () => { + expect(then.result).toBe(true) + }) }) }, ) @@ -189,24 +356,61 @@ describe('Limited Access View Case File Guard', () => { (category) => !allowedCaseFileCategories.includes(category as CaseFileCategory), ), - )('a defender can not view %s', (category) => { - let then: Then + )('case file category %s', (category) => { + describe('a defender without case files access can not view', () => { + let then: Then - beforeEach(() => { - mockRequest.mockImplementationOnce(() => ({ - user: { role: UserRole.DEFENDER }, - case: { type, state }, - caseFile: { category }, - })) + beforeEach(() => { + const nationalId = uuid() + mockRequest.mockImplementationOnce(() => ({ + user: { role: UserRole.DEFENDER, nationalId }, + case: { + type, + state, + }, + caseFile: { category }, + })) - then = givenWhenThen() + then = givenWhenThen() + }) + + it('should throw ForbiddenException', () => { + expect(then.error).toBeInstanceOf(ForbiddenException) + expect(then.error.message).toBe( + `Forbidden for ${UserRole.DEFENDER}`, + ) + }) }) - it('should throw ForbiddenException', () => { - expect(then.error).toBeInstanceOf(ForbiddenException) - expect(then.error.message).toBe( - `Forbidden for ${UserRole.DEFENDER}`, - ) + describe('spokesperson without case files access can not view', () => { + let then: Then + + beforeEach(() => { + const nationalId = uuid() + mockRequest.mockImplementationOnce(() => ({ + user: { role: UserRole.DEFENDER, nationalId }, + case: { + type, + state, + civilClaimants: [ + { + hasSpokesperson: true, + spokespersonNationalId: nationalId, + }, + ], + }, + caseFile: { category }, + })) + + then = givenWhenThen() + }) + + it('should throw ForbiddenException', () => { + expect(then.error).toBeInstanceOf(ForbiddenException) + expect(then.error.message).toBe( + `Forbidden for ${UserRole.DEFENDER}`, + ) + }) }) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/file/index.ts b/apps/judicial-system/backend/src/app/modules/file/index.ts index 34a68700f3fc..341743b06666 100644 --- a/apps/judicial-system/backend/src/app/modules/file/index.ts +++ b/apps/judicial-system/backend/src/app/modules/file/index.ts @@ -1,6 +1,6 @@ export { CaseFile } from './models/file.model' export { FileService } from './file.service' export { - canLimitedAcccessUserViewCaseFile, - defenderCaseFileCategoriesForRestrictionAndInvestigationCases, + canLimitedAccessUserViewCaseFile, + defenderCaseFileCategoriesForRequestCases, } from './guards/caseFileCategory' diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts b/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts index 00ecac0eedcf..0da62cac4fa5 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts @@ -21,13 +21,13 @@ import { } from '@island.is/judicial-system/auth' import { indictmentCases } from '@island.is/judicial-system/types' -import { defenderRule } from '../../guards' import { Case, CaseExistsGuard, CaseReadGuard, CaseTypeGuard, CurrentCase, + defenderGeneratedPdfRule, PdfService, } from '../case' import { CurrentDefendant, Defendant, DefendantExistsGuard } from '../defendant' @@ -40,8 +40,8 @@ import { Subpoena } from './models/subpoena.model' ]) @UseGuards( JwtAuthGuard, - RolesGuard, CaseExistsGuard, + RolesGuard, new CaseTypeGuard(indictmentCases), CaseReadGuard, DefendantExistsGuard, @@ -54,7 +54,7 @@ export class LimitedAccessSubpoenaController { @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} - @RolesRules(defenderRule) + @RolesRules(defenderGeneratedPdfRule) @Get() @Header('Content-Type', 'application/pdf') @ApiOkResponse({ diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/getSubpoenaPdfRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/getSubpoenaPdfRolesRules.spec.ts index 125817664eb1..dedee78c29be 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/getSubpoenaPdfRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/getSubpoenaPdfRolesRules.spec.ts @@ -1,4 +1,4 @@ -import { defenderRule } from '../../../../guards' +import { defenderGeneratedPdfRule } from '../../../case' import { LimitedAccessSubpoenaController } from '../../limitedAccessSubpoena.controller' describe('LimitedAccessSubpoenaController - Get custody notice pdf rules', () => { @@ -14,6 +14,6 @@ describe('LimitedAccessSubpoenaController - Get custody notice pdf rules', () => it('should give permission to roles', () => { expect(rules).toHaveLength(1) - expect(rules).toContain(defenderRule) + expect(rules).toContain(defenderGeneratedPdfRule) }) }) diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/limitedAccessSubpoenaControllerGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/limitedAccessSubpoenaControllerGuards.spec.ts index cf4fb036e396..59a5de394aab 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/limitedAccessSubpoenaControllerGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/limitedAccessSubpoenaControllerGuards.spec.ts @@ -17,8 +17,8 @@ describe('LimitedAccessSubpoenaController - guards', () => { it('should have the right guard configuration', () => { expect(guards).toHaveLength(7) expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard) - expect(new guards[1]()).toBeInstanceOf(RolesGuard) - expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard) + expect(new guards[1]()).toBeInstanceOf(CaseExistsGuard) + expect(new guards[2]()).toBeInstanceOf(RolesGuard) expect(guards[3]).toBeInstanceOf(CaseTypeGuard) expect(guards[3]).toEqual({ allowedCaseTypes: indictmentCases, diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx index 77fc6b59b269..2b2b54f9d9b4 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx @@ -32,6 +32,7 @@ import { strings } from './IndictmentCaseFilesList.strings' interface Props { workingCase: Case + displayGeneratedPDFs?: boolean displayHeading?: boolean connectedCaseParentId?: string } @@ -63,6 +64,7 @@ export const RenderFiles: FC = ({ const IndictmentCaseFilesList: FC = ({ workingCase, + displayGeneratedPDFs = true, displayHeading = true, connectedCaseParentId, }) => { @@ -74,9 +76,11 @@ const IndictmentCaseFilesList: FC = ({ }) const showTrafficViolationCaseFiles = isTrafficViolationCase(workingCase) - const showSubpoenaPdf = workingCase.defendants?.some( - (defendant) => defendant.subpoenas && defendant.subpoenas.length > 0, - ) + const showSubpoenaPdf = + displayGeneratedPDFs && + workingCase.defendants?.some( + (defendant) => defendant.subpoenas && defendant.subpoenas.length > 0, + ) const cf = workingCase.caseFiles @@ -123,7 +127,7 @@ const IndictmentCaseFilesList: FC = ({ )} - {showTrafficViolationCaseFiles && ( + {showTrafficViolationCaseFiles && displayGeneratedPDFs && ( {formatMessage(caseFiles.indictmentSection)} @@ -175,25 +179,27 @@ const IndictmentCaseFilesList: FC = ({ )} - - - {formatMessage(strings.caseFileTitle)} - - {workingCase.policeCaseNumbers?.map((policeCaseNumber, index) => ( - - - - ))} - + {displayGeneratedPDFs && ( + + + {formatMessage(strings.caseFileTitle)} + + {workingCase.policeCaseNumbers?.map((policeCaseNumber, index) => ( + + + + ))} + + )} {courtRecords?.length || rulings?.length ? ( diff --git a/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx b/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx index ab4aaafa4416..c4872ae2040a 100644 --- a/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx +++ b/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx @@ -4,6 +4,7 @@ import { useRouter } from 'next/router' import { Accordion, Box, Button } from '@island.is/island-ui/core' import * as constants from '@island.is/judicial-system/consts' +import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' import { isCompletedCase, isDefenceUser, @@ -56,9 +57,34 @@ const IndictmentOverview: FC = () => { workingCase.indictmentReviewer?.id === user?.id && Boolean(!workingCase.indictmentReviewDecision) const canAddFiles = + !isCompletedCase(workingCase.state) && isDefenceUser(user) && + workingCase.defendants?.some( + (defendant) => + defendant?.defenderNationalId && + normalizeAndFormatNationalId(user?.nationalId).includes( + defendant.defenderNationalId, + ), + ) && workingCase.indictmentDecision !== IndictmentDecision.POSTPONING_UNTIL_VERDICT + const shouldDisplayGeneratedPDFs = + workingCase.defendants?.some( + (defendant) => + defendant.defenderNationalId && + normalizeAndFormatNationalId(user?.nationalId).includes( + defendant.defenderNationalId, + ), + ) || + workingCase.civilClaimants?.some( + (civilClaimant) => + civilClaimant.hasSpokesperson && + civilClaimant.spokespersonNationalId && + normalizeAndFormatNationalId(user?.nationalId).includes( + civilClaimant.spokespersonNationalId, + ) && + civilClaimant.caseFilesSharedWithSpokesperson, + ) const handleNavigationTo = useCallback( (destination: string) => router.push(`${destination}/${workingCase.id}`), @@ -145,12 +171,15 @@ const IndictmentOverview: FC = () => { )} )} - {workingCase.caseFiles && ( + {workingCase.caseFiles && ( // TODO: Find a more accurate condition, there may be generated PDFs to display even if there are no uploaded files to display - + )} {canAddFiles && (