From 3817130fd8314d9fc4b39d7f997598f99e913bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Wed, 9 Oct 2024 14:54:11 +0200 Subject: [PATCH 01/16] Refactors defender login --- .../src/app/modules/auth/auth.controller.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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, } } From b9c4fe2b18353b46ffa2e59a358a992c40821663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Wed, 9 Oct 2024 14:57:29 +0200 Subject: [PATCH 02/16] Improves national id comparison during defender login --- .../backend/src/app/modules/defendant/defendant.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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']], }) } From da24287193af641249cc4b1bdafb42d99e55d532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Wed, 9 Oct 2024 14:58:05 +0200 Subject: [PATCH 03/16] Exports civil claimant service from civil claimant module --- .../backend/src/app/modules/defendant/defendant.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 {} From 38375694f9fdd2c1d4657e2ff2a4a7d01df245e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Wed, 9 Oct 2024 14:58:37 +0200 Subject: [PATCH 04/16] Fixes civil claimant model definition --- .../modules/defendant/models/civilClaimant.model.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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..fdeb981c6b89 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,6 +6,7 @@ import { ForeignKey, Model, Table, + UpdatedAt, } from 'sequelize-typescript' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' @@ -27,14 +28,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, From 04753df65a81c8398282ddf64a5e5d62bd3d798e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Wed, 9 Oct 2024 15:01:41 +0200 Subject: [PATCH 05/16] Implements civil claimant lookup by spokesperson national id --- .../defendant/civilClaimant.service.ts | 29 ++++++++++++++++++- .../src/app/modules/defendant/index.ts | 1 + 2 files changed, 29 insertions(+), 1 deletion(-) 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/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' From 1e4a4198d360d5a9355ce1551d27e9de81891a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Wed, 9 Oct 2024 16:03:20 +0200 Subject: [PATCH 06/16] Allows civil claimant spokespersons to log in --- .../modules/case/limitedAccessCase.service.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) 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..dfab27316e59 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,7 +30,12 @@ import { import { nowFactory, uuidFactory } from '../../factories' import { AwsS3Service } from '../aws-s3' -import { CivilClaimant, Defendant, DefendantService } from '../defendant' +import { + CivilClaimant, + CivilClaimantService, + Defendant, + DefendantService, +} from '../defendant' import { EventLog } from '../event-log' import { CaseFile, @@ -281,6 +286,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 +431,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 +444,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 +453,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') + }) }) }) } From ff767dbbdc3ba4dbccb18600f296c556e0c19b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Wed, 9 Oct 2024 16:09:53 +0200 Subject: [PATCH 07/16] Includes cases in case lists for defenders where they are a civil claimant spokesperson --- .../app/modules/case/filters/cases.filter.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) 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 c0df2270d241..deb879252eef 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 @@ -278,13 +278,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 spokesperson_national_id in ('${normalizedNationalId}', '${formattedNationalId}')) + `), + }, + }, + ], }, ], }, From 711d0784d2c5335bf9dedca7992ee6c46a95a064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Wed, 9 Oct 2024 16:57:08 +0200 Subject: [PATCH 08/16] Allows civil claimant spokespersons to open cases --- .../app/modules/case/filters/case.filter.ts | 119 ++++++++++++------ 1 file changed, 84 insertions(+), 35 deletions(-) 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 59506016d28b..19c35c1ef4df 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 @@ -309,29 +309,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 && @@ -341,44 +339,95 @@ 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 assignment + if ( + theCase.defenderNationalId && + normalizedAndFormattedNationalId.includes(theCase.defenderNationalId) + ) { + return true + } + + return false +} + +const canDefenceUserAccessIndictmentCase = ( + theCase: Case, + user: User, +): 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 } const normalizedAndFormattedNationalId = normalizeAndFormatNationalId( user.nationalId, ) - // Check case defender access + // Check case defender assignment + if ( + theCase.defendants?.some( + (defendant) => + defendant.defenderNationalId && + normalizedAndFormattedNationalId.includes(defendant.defenderNationalId), + ) + ) { + return true + } + + // Check case spokesperson assignment + if ( + theCase.civilClaimants?.some( + (civilClaimant) => + civilClaimant.spokespersonNationalId && + normalizedAndFormattedNationalId.includes( + civilClaimant.spokespersonNationalId, + ), + ) + ) { + return true + } + + return false +} + +const canDefenceUserAccessCase = (theCase: Case, user: User): 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) } - return true + // Other cases are not accessible to defence users + return false } export const canUserAccessCase = ( From ebb6b4aa0a4438a280f31cc6369f32b035f7fde6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Mon, 14 Oct 2024 15:57:21 +0000 Subject: [PATCH 09/16] Limits civil claimant spokesperson access to case files --- .../app/modules/case/filters/case.filter.ts | 24 +-- .../limitedAccessCaseFile.interceptor.ts | 2 + .../modules/case/limitedAccessCase.service.ts | 9 +- .../defendant/models/civilClaimant.model.ts | 31 ++++ .../defendant/models/defendant.model.ts | 14 ++ .../modules/file/guards/caseFileCategory.ts | 152 +++++++++++++----- .../guards/limitedAccessViewCaseFile.guard.ts | 2 + .../backend/src/app/modules/file/index.ts | 2 +- 8 files changed, 171 insertions(+), 65 deletions(-) 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 19c35c1ef4df..925c382ad191 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, @@ -21,9 +20,11 @@ import { isRestrictionCase, RequestSharedWithDefender, ServiceRequirement, + 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' @@ -386,29 +387,16 @@ const canDefenceUserAccessIndictmentCase = ( return false } - const normalizedAndFormattedNationalId = normalizeAndFormatNationalId( - user.nationalId, - ) - // Check case defender assignment - if ( - theCase.defendants?.some( - (defendant) => - defendant.defenderNationalId && - normalizedAndFormattedNationalId.includes(defendant.defenderNationalId), - ) - ) { + if (Defendant.isDefenderOfDefendant(user.nationalId, theCase.defendants)) { return true } // Check case spokesperson assignment if ( - theCase.civilClaimants?.some( - (civilClaimant) => - civilClaimant.spokespersonNationalId && - normalizedAndFormattedNationalId.includes( - civilClaimant.spokespersonNationalId, - ), + CivilClaimant.isSpokespersonOfCivilClaimant( + user.nationalId, + theCase.civilClaimants, ) ) { return true 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..8c5162d81ab1 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 @@ -26,6 +26,8 @@ export class LimitedAccessCaseFileInterceptor implements NestInterceptor { theCase.type, theCase.state, category, + theCase.defendants, + theCase.civilClaimants, ), ) 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 dfab27316e59..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 @@ -37,10 +37,7 @@ import { DefendantService, } from '../defendant' import { EventLog } from '../event-log' -import { - CaseFile, - defenderCaseFileCategoriesForRestrictionAndInvestigationCases, -} from '../file' +import { CaseFile, defenderCaseFileCategoriesForRequestCases } from '../file' import { IndictmentCount } from '../indictment-count' import { Institution } from '../institution' import { Subpoena } from '../subpoena' @@ -510,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/defendant/models/civilClaimant.model.ts b/apps/judicial-system/backend/src/app/modules/defendant/models/civilClaimant.model.ts index fdeb981c6b89..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 @@ -11,6 +11,8 @@ import { import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' +import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' + import { Case } from '../../case/models/case.model' @Table({ @@ -18,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, 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 a72b69edeed0..08a9865dc354 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..f2941a6c1971 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] +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 canDefenceUsaerViewCaseFile = ( + nationalId: string, + caseType: CaseType, + caseState: CaseState, + caseFileCategory: CaseFileCategory, + defendants?: Defendant[], + civilClaimants?: CivilClaimant[], +) => { + if (isRequestCase(caseType)) { + 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 canLimitedAcccessUserViewCaseFile = ( 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 canDefenceUsaerViewCaseFile( + 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..e5eb07b06280 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 @@ -41,6 +41,8 @@ export class LimitedAccessViewCaseFileGuard implements CanActivate { theCase.type, theCase.state, caseFile.category, + theCase.defendants, + theCase.civilClaimants, ) ) { return true 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..6fb151aab6f2 100644 --- a/apps/judicial-system/backend/src/app/modules/file/index.ts +++ b/apps/judicial-system/backend/src/app/modules/file/index.ts @@ -2,5 +2,5 @@ export { CaseFile } from './models/file.model' export { FileService } from './file.service' export { canLimitedAcccessUserViewCaseFile, - defenderCaseFileCategoriesForRestrictionAndInvestigationCases, + defenderCaseFileCategoriesForRequestCases, } from './guards/caseFileCategory' From 136d9158dd5bd3fc8288df80266647e3e203347b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Tue, 15 Oct 2024 09:36:13 +0000 Subject: [PATCH 10/16] Blocks civil claim spokespersons from adding documents to indictments --- .../app/modules/case/filters/case.filter.ts | 22 ++++++++++++------- .../IndictmentOverview/IndictmentOverview.tsx | 9 ++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) 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 925c382ad191..4b3f60e15586 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 @@ -31,7 +31,7 @@ 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)) { @@ -198,7 +198,7 @@ const canAppealsCourtUserAccessCase = (theCase: Case): boolean => { const canPrisonStaffUserAccessCase = ( theCase: Case, - forUpdate = true, + forUpdate: boolean, ): boolean => { // Prison staff users cannot update cases if (forUpdate) { @@ -236,7 +236,7 @@ const canPrisonStaffUserAccessCase = ( const canPrisonAdminUserAccessCase = ( theCase: Case, - forUpdate = true, + forUpdate: boolean, ): boolean => { // Prison admin users cannot update cases if (forUpdate) { @@ -366,6 +366,7 @@ const canDefenceUserAccessRequestCase = ( const canDefenceUserAccessIndictmentCase = ( theCase: Case, user: User, + forUpdate: boolean, ): boolean => { // Check case state access if ( @@ -397,7 +398,8 @@ const canDefenceUserAccessIndictmentCase = ( CivilClaimant.isSpokespersonOfCivilClaimant( user.nationalId, theCase.civilClaimants, - ) + ) && + !forUpdate ) { return true } @@ -405,13 +407,17 @@ const canDefenceUserAccessIndictmentCase = ( return false } -const canDefenceUserAccessCase = (theCase: Case, user: User): boolean => { +const canDefenceUserAccessCase = ( + theCase: Case, + user: User, + forUpdate: boolean, +): boolean => { if (isRequestCase(theCase.type)) { return canDefenceUserAccessRequestCase(theCase, user) } if (isIndictmentCase(theCase.type)) { - return canDefenceUserAccessIndictmentCase(theCase, user) + return canDefenceUserAccessIndictmentCase(theCase, user, forUpdate) } // Other cases are not accessible to defence users @@ -421,7 +427,7 @@ const canDefenceUserAccessCase = (theCase: Case, user: User): boolean => { export const canUserAccessCase = ( theCase: Case, user: User, - forUpdate = true, + forUpdate: boolean, ): boolean => { if (isProsecutionUser(user)) { return canProsecutionUserAccessCase(theCase, user, forUpdate) @@ -444,7 +450,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/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx b/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx index ab4aaafa4416..92c18b2b43a7 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,7 +57,15 @@ 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 From 2483f7299ee70fcc1aca5f034eb736c0b82f3b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Tue, 15 Oct 2024 12:32:59 +0000 Subject: [PATCH 11/16] Blocks civil claimant spokesperson access to generated pdfs unless they have been granted access to case files --- .../src/app/modules/case/guards/rolesRules.ts | 32 ++++++++++++ .../case/limitedAccessCase.controller.ts | 14 +++-- .../limitedAccessSubpoena.controller.ts | 7 ++- .../IndictmentCaseFilesList.tsx | 52 +++++++++++-------- .../IndictmentOverview/IndictmentOverview.tsx | 24 ++++++++- 5 files changed, 95 insertions(+), 34 deletions(-) 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/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/subpoena/limitedAccessSubpoena.controller.ts b/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts index 3db547d3c02c..0eb338bef74e 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 @@ -6,7 +6,6 @@ import { Header, Inject, Param, - Query, Res, UseGuards, } from '@nestjs/common' @@ -22,7 +21,6 @@ import { } from '@island.is/judicial-system/auth' import { indictmentCases } from '@island.is/judicial-system/types' -import { defenderRule } from '../../guards' import { Case, CaseExistsGuard, @@ -31,6 +29,7 @@ import { CurrentCase, PdfService, } from '../case' +import { defenderGeneratedPdfRule } from '../case/guards/rolesRules' import { CurrentDefendant, Defendant, DefendantExistsGuard } from '../defendant' import { CurrentSubpoena } from './guards/subpoena.decorator' import { SubpoenaExistsOptionalGuard } from './guards/subpoenaExists.guard' @@ -41,8 +40,8 @@ import { Subpoena } from './models/subpoena.model' ]) @UseGuards( JwtAuthGuard, - RolesGuard, CaseExistsGuard, + RolesGuard, new CaseTypeGuard(indictmentCases), CaseReadGuard, DefendantExistsGuard, @@ -55,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/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx index 29f1738db99f..acecb217bb61 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 92c18b2b43a7..c4872ae2040a 100644 --- a/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx +++ b/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx @@ -68,6 +68,23 @@ const IndictmentOverview: FC = () => { ) && 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}`), @@ -154,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 && ( From 10da5b40a83905fe519ad938b74302afa97d5354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Tue, 15 Oct 2024 18:39:14 +0000 Subject: [PATCH 12/16] Updates unit tests --- .../app/modules/case/filters/cases.filter.ts | 2 +- .../case/filters/test/cases.filter.spec.ts | 27 ++++-- .../filters/test/defenceUserFilter.spec.ts | 86 +++++++++++++++++-- .../test/publicProsecutionUserFilter.spec.ts | 47 ++++------ .../backend/src/app/modules/case/index.ts | 1 + .../getCaseFilesRecordPdfGuards.spec.ts | 4 +- .../getCaseFilesRecordPdfRolesRules.spec.ts | 4 +- .../getIndictmentPdfGuards.spec.ts | 4 +- .../getIndictmentPdfRolesRules.spec.ts | 4 +- .../limitedAccessSubpoena.controller.ts | 2 +- .../getSubpoenaPdfRolesRules.spec.ts | 4 +- ...itedAccessSubpoenaControllerGuards.spec.ts | 4 +- 12 files changed, 132 insertions(+), 57 deletions(-) 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 deb879252eef..e24e6d8a3293 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 @@ -293,7 +293,7 @@ const getDefenceUserCasesQueryFilter = (user: User): WhereOptions => { [Op.in]: Sequelize.literal(` (SELECT case_id FROM civil_claimant - WHERE spokesperson_national_id in ('${normalizedNationalId}', '${formattedNationalId}')) + 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 65c759346088..5d28b960bbbd 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 @@ -454,13 +454,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/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/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/subpoena/limitedAccessSubpoena.controller.ts b/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts index 0eb338bef74e..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 @@ -27,9 +27,9 @@ import { CaseReadGuard, CaseTypeGuard, CurrentCase, + defenderGeneratedPdfRule, PdfService, } from '../case' -import { defenderGeneratedPdfRule } from '../case/guards/rolesRules' import { CurrentDefendant, Defendant, DefendantExistsGuard } from '../defendant' import { CurrentSubpoena } from './guards/subpoena.decorator' import { SubpoenaExistsOptionalGuard } from './guards/subpoenaExists.guard' 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, From 41aeba86925d72de6752860020d9c11d5ea3db56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Tue, 15 Oct 2024 20:50:41 +0000 Subject: [PATCH 13/16] Updates unit tests --- .../modules/file/guards/caseFileCategory.ts | 2 +- .../limitedAccessViewCaseFileGuard.spec.ts | 242 ++++++++++++++++-- 2 files changed, 224 insertions(+), 20 deletions(-) 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 f2941a6c1971..26e0e11a6622 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 @@ -95,7 +95,7 @@ const canDefenceUsaerViewCaseFile = ( civilClaimants?: CivilClaimant[], ) => { if (isRequestCase(caseType)) { - canDefenceUserViewCaseFileOfRequestCase(caseState, caseFileCategory) + return canDefenceUserViewCaseFileOfRequestCase(caseState, caseFileCategory) } if (isIndictmentCase(caseType)) { 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..b7625bb8d7a7 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}`, + ) + }) }) }) }) From d3f5c4fd978c5b03efbf4d40d0af3cd98c1b6f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Wed, 16 Oct 2024 09:58:38 +0000 Subject: [PATCH 14/16] Updates unit tests --- .../case/test/createTestingCaseModule.ts | 7 +++ .../findDefenderByNationalId.spec.ts | 48 +++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) 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, + }) + }) + }) }) From b8af78568be8cc3679ba627581c88dd4038254aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Thu, 17 Oct 2024 11:48:05 +0000 Subject: [PATCH 15/16] Fixes typos --- .../case/interceptors/limitedAccessCaseFile.interceptor.ts | 4 ++-- .../backend/src/app/modules/file/guards/caseFileCategory.ts | 6 +++--- .../modules/file/guards/limitedAccessViewCaseFile.guard.ts | 4 ++-- apps/judicial-system/backend/src/app/modules/file/index.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) 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 8c5162d81ab1..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,7 +21,7 @@ export class LimitedAccessCaseFileInterceptor implements NestInterceptor { map((theCase) => { const caseFiles = theCase.caseFiles?.filter( ({ category }: { category: CaseFileCategory }) => - canLimitedAcccessUserViewCaseFile( + canLimitedAccessUserViewCaseFile( user, theCase.type, theCase.state, 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 26e0e11a6622..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 @@ -86,7 +86,7 @@ const canDefenceUserViewCaseFileOfIndictmentCase = ( ) } -const canDefenceUsaerViewCaseFile = ( +const canDefenceUserViewCaseFile = ( nationalId: string, caseType: CaseType, caseState: CaseState, @@ -130,7 +130,7 @@ const canPrisonAdminUserViewCaseFile = ( ) } -export const canLimitedAcccessUserViewCaseFile = ( +export const canLimitedAccessUserViewCaseFile = ( user: User, caseType: CaseType, caseState: CaseState, @@ -143,7 +143,7 @@ export const canLimitedAcccessUserViewCaseFile = ( } if (isDefenceUser(user)) { - return canDefenceUsaerViewCaseFile( + return canDefenceUserViewCaseFile( user.nationalId, caseType, caseState, 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 e5eb07b06280..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,7 +36,7 @@ export class LimitedAccessViewCaseFileGuard implements CanActivate { } if ( - canLimitedAcccessUserViewCaseFile( + canLimitedAccessUserViewCaseFile( user, theCase.type, theCase.state, 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 6fb151aab6f2..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, + canLimitedAccessUserViewCaseFile, defenderCaseFileCategoriesForRequestCases, } from './guards/caseFileCategory' From a4a31607da0b71ead892d7109ab54e7d5cb4384d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=C3=B0j=C3=B3n=20Gu=C3=B0j=C3=B3nsson?= Date: Thu, 17 Oct 2024 11:57:01 +0000 Subject: [PATCH 16/16] Fixes typos --- .../file/guards/test/limitedAccessViewCaseFileGuard.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b7625bb8d7a7..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 @@ -267,7 +267,7 @@ describe('Limited Access View Case File Guard', () => { civilClaimants: [ { hasSpokesperson: true, - spokesPersonNationalId: nationalId, + spokespersonNationalId: nationalId, caseFilesSharedWithSpokesperson: true, }, ], @@ -395,7 +395,7 @@ describe('Limited Access View Case File Guard', () => { civilClaimants: [ { hasSpokesperson: true, - spokesPersonNationalId: nationalId, + spokespersonNationalId: nationalId, }, ], },