@ministryofjustice/hmpps-prison-permissions-lib 0.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +116 -0
  4. package/dist/index.cjs +256 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.ts +147 -0
  7. package/dist/index.esm.js +251 -0
  8. package/dist/index.esm.js.map +1 -0
  9. package/dist/main/data/hmppsPrisonerSearch/PrisonerSearchClient.d.ts +7 -0
  10. package/dist/main/data/hmppsPrisonerSearch/interfaces/Prisoner.d.ts +6 -0
  11. package/dist/main/data/prisonApi/PrisonApiClient.d.ts +6 -0
  12. package/dist/main/index.d.ts +6 -0
  13. package/dist/main/middleware/PrisonerPermissionsGuard.d.ts +7 -0
  14. package/dist/main/middleware/PrisonerPermissionsGuard.test.d.ts +1 -0
  15. package/dist/main/services/permissions/PermissionsLogger.d.ts +12 -0
  16. package/dist/main/services/permissions/PermissionsLogger.test.d.ts +1 -0
  17. package/dist/main/services/permissions/PermissionsService.d.ts +27 -0
  18. package/dist/main/services/permissions/PermissionsService.test.d.ts +1 -0
  19. package/dist/main/services/permissions/checks/baseCheck/BaseCheck.d.ts +4 -0
  20. package/dist/main/services/permissions/checks/baseCheck/BaseCheck.test.d.ts +1 -0
  21. package/dist/main/services/permissions/checks/baseCheck/ReleasedPrisonerCheck.d.ts +3 -0
  22. package/dist/main/services/permissions/checks/baseCheck/RestrictedPatientCheck.d.ts +4 -0
  23. package/dist/main/services/permissions/checks/baseCheck/TransferingPrisonerCheck.d.ts +3 -0
  24. package/dist/main/services/permissions/utils/PermissionUtils.d.ts +3 -0
  25. package/dist/main/services/permissions/utils/PermissionUtils.test.d.ts +1 -0
  26. package/dist/main/types/errors/PrisonerPermissionError.d.ts +6 -0
  27. package/dist/main/types/permissions/PermissionCheckStatus.d.ts +8 -0
  28. package/dist/main/types/permissions/PermissionsOptions.d.ts +9 -0
  29. package/dist/main/types/permissions/prisoner/PrisonerPermissions.d.ts +11 -0
  30. package/dist/main/types/permissions/prisoner/PrisonerPermissions.test.d.ts +1 -0
  31. package/dist/main/types/user/CaseLoad.d.ts +7 -0
  32. package/dist/main/types/user/HmppsUser.d.ts +58 -0
  33. package/dist/main/types/user/Role.d.ts +6 -0
  34. package/dist/test/prisonerMocks.d.ts +44 -0
  35. package/dist/test/userMocks.d.ts +31 -0
  36. package/package.json +59 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # Change log
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Crown Copyright (Ministry of Justice)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # hmpps-prison-permissions-lib
2
+
3
+ [![repo standards badge](https://img.shields.io/badge/endpoint.svg?&style=flat&logo=github&url=https%3A%2F%2Foperations-engineering-reports.cloud-platform.service.justice.gov.uk%2Fapi%2Fv1%2Fcompliant_public_repositories%2Fhmpps-prison-permissions-lib)](https://operations-engineering-reports.cloud-platform.service.justice.gov.uk/public-report/hmpps-prison-permissions-lib "Link to report")
4
+
5
+ A Node.js client library to centralise the process of determining user permissions for prison services and data.
6
+
7
+ We welcome feedback on this library and README [here](https://moj.enterprise.slack.com/archives/C04JFG3QJE6)
8
+ in order to improve it.
9
+
10
+ ## Table of Contents
11
+
12
+ 1. [Introduction](#-introduction)
13
+ 2. [What checks are made?](#-what-checks-are-made)
14
+ 3. [Where does the data come from?](#-where-does-the-data-come-from)
15
+ 4. [How do I implement this library?](#-how-do-i-implement-this-library)
16
+
17
+ ## ✨ Introduction
18
+
19
+ Determining whether a user has access to a particular resource (e.g. a service, or prisoner data) consists of a
20
+ number of checks and is not necessarily just determined by what roles a user has been assigned.
21
+
22
+ This permissions library aims to share the logic centrally so that all services agree on what a user should and
23
+ should not be able to access. It is used by the prisoner profile to determine if a user can access certain parts
24
+ of the prisoner's profile for example.
25
+
26
+ ## 🔎 What checks are made?
27
+
28
+ The permissions use a variety of checks, based on:
29
+ * The user's DPS roles
30
+ * The user's case load list (and active case load)
31
+ * The user's key worker status at a prison
32
+ * The prisoner's location (which prison they are at, or whether they are transferring or out of prison)
33
+ * The prisoner's restricted patient status
34
+
35
+ ## 📊 Where does the data come from?
36
+
37
+ We do not yet have a centralised permissions service, so this library requires some input data to determine
38
+ the user permissions.
39
+
40
+ We expect that the user's roles and case loads are already available at:
41
+ * `res.locals.user.userRoles`,
42
+ * `res.locals.user.caseLoads`,
43
+ * `res.locals.user.activeCaseLoad`,
44
+ * `res.locals.user.activeCaseLoadId`,
45
+
46
+ User roles are already part of the Typescript Template project [here](https://github.com/ministryofjustice/hmpps-template-typescript/blob/main/server/middleware/setUpCurrentUser.ts#L26)
47
+ and the retrieval of case load data is available in [hmpps-connect-dps-components](https://github.com/ministryofjustice/hmpps-connect-dps-components?tab=readme-ov-file#populating-reslocalsuser-with-the-shared-case-load-data)
48
+ middleware.
49
+
50
+ The library will make its own calls to Prison API to determine the user's key worker status, caching it in the user
51
+ session and storing it at `res.locals.user.keyWorkerAtPrisons`.
52
+
53
+ Finally the library will also retrieve data about a prisoner from [hmpps-prisoner-search](https://github.com/ministryofjustice/hmpps-prisoner-search)
54
+ and store it at `req.middleware.prisonerData`.
55
+
56
+ ## ✍️ How do I implement this library?
57
+
58
+ ### 1. Install the library
59
+
60
+ ```shell
61
+ npm install @ministryofjustice/hmpps-prison-permissions-lib
62
+ ```
63
+
64
+ ### 2. Create the PermissionsService
65
+
66
+ The permissions service should be created just like any other of your services. It requires the following:
67
+
68
+ * `prisonApiConfig`: [Prison API](https://github.com/ministryofjustice/prison-api) configuration conforming to the `hmpps-typescript-lib`'s `ApiConfig` [interface](https://github.com/ministryofjustice/hmpps-typescript-lib/blob/main/packages/rest-client/src/main/types/ApiConfig.ts)
69
+ * `prisonerSearchConfig`: [Prisoner Search](https://github.com/ministryofjustice/hmpps-prisoner-search) configuration conforming to the `hmpps-typescript-lib`'s `ApiConfig` [interface](https://github.com/ministryofjustice/hmpps-typescript-lib/blob/main/packages/rest-client/src/main/types/ApiConfig.ts)
70
+ * `authenticationClient`: An `AuthenticationClient` instance (see [hmpps-typescript-lib](https://github.com/ministryofjustice/hmpps-typescript-lib/blob/main/packages/auth-clients/src/main/AuthenticationClient.ts))
71
+ in order to make authorized client credentials calls to Prisoner Search.
72
+ * `logger`: Bunyan logger for logging permissions events. Defaults to using `console`.
73
+ * `telemetryClient`: Optional but recommended. Instead of just logging permissions events, this provides richer metadata to Application Insights.
74
+
75
+ e.g.
76
+
77
+ ```typescript
78
+ import { PermissionsService } from '@ministryofjustice/hmpps-prison-permissions-lib'
79
+
80
+ ...
81
+
82
+ const prisonPermissionsService = PermissionsService.create({
83
+ prisonApiConfig: config.apis.prisonApi,
84
+ prisonerSearchConfig: config.apis.prisonerSearchApi,
85
+ authenticationClient: new AuthenticationClient(config.apis.hmppsAuth, logger, tokenStore),
86
+ logger,
87
+ telemetryClient,
88
+ })
89
+ ```
90
+
91
+ ### 3. Ensure your client has the role `ROLE_VIEW_PRISONER_DATA`...
92
+
93
+ ...in order to be able to successfully call Prisoner Search ([see Swagger docs](https://prisoner-search-dev.prison.service.justice.gov.uk/swagger-ui/index.html)).
94
+
95
+ ### 4. Add the `prisonerPermissionsGuard` middleware to your service's routes:
96
+
97
+ e.g.
98
+
99
+ ```typescript
100
+ import { PrisonerBasePermission, prisonerPermissionsGuard } from '@ministryofjustice/hmpps-prison-permissions-lib'
101
+
102
+ ...
103
+
104
+ get(
105
+ `prisoner/{prisonerNumber}/somepage`,
106
+ ...
107
+ prisonerPermissionsGuard(permissionsService, { requestDependentOn: [PrisonerBasePermission.read] }),
108
+ async (req, res, next) => {
109
+ ...
110
+ ```
111
+
112
+ ### 5. Ensure you handle 403s as required
113
+
114
+ If the user does not have the required permissions listed in `requestDependentOn`, then the middleware will
115
+ throw a `PrisonerPermissionError` with a status code of 403. The Typescript Template by default logs the user
116
+ out when encountering an error status of 403, see [here](https://github.com/ministryofjustice/hmpps-template-typescript/blob/main/server/errorHandler.ts#L9).
package/dist/index.cjs ADDED
@@ -0,0 +1,256 @@
1
+ 'use strict';
2
+
3
+ var hmppsRestClient = require('@ministryofjustice/hmpps-rest-client');
4
+
5
+ class PrisonApiClient extends hmppsRestClient.RestClient {
6
+ constructor(logger, config) {
7
+ super('Prison API', config, logger);
8
+ }
9
+ isUserAKeyWorker(userToken, staffId, agencyId) {
10
+ return this.get({ path: `/api/staff/${staffId}/${agencyId}/roles/KW` }, hmppsRestClient.asUser(userToken));
11
+ }
12
+ }
13
+
14
+ // eslint-disable-next-line import/prefer-default-export
15
+ var PermissionCheckStatus;
16
+ (function (PermissionCheckStatus) {
17
+ PermissionCheckStatus["NOT_PERMITTED"] = "NOT_PERMITTED";
18
+ PermissionCheckStatus["NOT_IN_CASELOAD"] = "NOT_IN_CASELOAD";
19
+ PermissionCheckStatus["RESTRICTED_PATIENT"] = "RESTRICTED_PATIENT";
20
+ PermissionCheckStatus["PRISONER_IS_RELEASED"] = "PRISONER_IS_RELEASED";
21
+ PermissionCheckStatus["PRISONER_IS_TRANSFERRING"] = "PRISONER_IS_TRANSFERRING";
22
+ PermissionCheckStatus["OK"] = "OK";
23
+ })(PermissionCheckStatus || (PermissionCheckStatus = {}));
24
+
25
+ class PermissionsLogger {
26
+ logger;
27
+ telemetryClient;
28
+ constructor(logger, telemetryClient) {
29
+ this.logger = logger;
30
+ this.telemetryClient = telemetryClient;
31
+ }
32
+ logPermissionCheckStatus(user, prisoner, permission, permissionCheckStatus) {
33
+ if (permissionCheckStatus === PermissionCheckStatus.OK)
34
+ return;
35
+ if (this.telemetryClient) {
36
+ this.telemetryClient.trackEvent({
37
+ name: 'prisoner-permission-check-failed',
38
+ properties: {
39
+ username: user.username,
40
+ prisonerNumber: prisoner.prisonerNumber,
41
+ activeCaseLoad: user.authSource === 'nomis' && user.activeCaseLoadId,
42
+ permissionChecked: permission,
43
+ status: permissionCheckStatus,
44
+ },
45
+ });
46
+ }
47
+ else {
48
+ this.logger.info(`Prisoner permission check failed: ${permission} (${permissionCheckStatus}) for user ${user.username}`);
49
+ }
50
+ }
51
+ }
52
+
53
+ exports.PrisonerBasePermission = void 0;
54
+ (function (PrisonerBasePermission) {
55
+ PrisonerBasePermission["read"] = "prisoner:base-record:read";
56
+ })(exports.PrisonerBasePermission || (exports.PrisonerBasePermission = {}));
57
+ function checkPrisonerAccess(permission, permissions) {
58
+ if (permission === exports.PrisonerBasePermission.read) {
59
+ return permissions[exports.PrisonerBasePermission.read];
60
+ }
61
+ // TODO: Check each domain group for the permission
62
+ return false;
63
+ }
64
+
65
+ const isInUsersCaseLoad = (prisonId, user) => user.authSource === 'nomis' && user.caseLoads?.some(caseLoad => caseLoad.caseLoadId === prisonId);
66
+ const userHasRoles = (rolesToCheck, userRoles) => {
67
+ const normaliseRoleText = (role) => role.replace(/ROLE_/, '');
68
+ return rolesToCheck.map(normaliseRoleText).some(role => userRoles.map(normaliseRoleText).includes(role));
69
+ };
70
+
71
+ // eslint-disable-next-line import/prefer-default-export
72
+ var Role;
73
+ (function (Role) {
74
+ Role["GlobalSearch"] = "ROLE_GLOBAL_SEARCH";
75
+ Role["InactiveBookings"] = "ROLE_INACTIVE_BOOKINGS";
76
+ Role["PomUser"] = "ROLE_POM";
77
+ Role["PrisonUser"] = "ROLE_PRISON";
78
+ })(Role || (Role = {}));
79
+
80
+ /*
81
+ * Restricted patients can be accessed in the following circumstances:
82
+ * - The user has the "Inactive Bookings" role
83
+ * - The user is a POM user and has the supporting prison ID in their caseloads
84
+ */
85
+ function restrictedPatientCheck(user, prisoner) {
86
+ const { userRoles } = user;
87
+ const pomUser = userHasRoles([Role.PomUser], userRoles);
88
+ const inactiveBookingsUser = userHasRoles([Role.InactiveBookings], userRoles);
89
+ const userHasPrisonersSupportingPrisonInTheirCaseLoad = isInUsersCaseLoad(prisoner.supportingPrisonId, user);
90
+ const userIsPomUserWithSupportingPrisonInTheirCaseLoad = pomUser && userHasPrisonersSupportingPrisonInTheirCaseLoad;
91
+ if (userIsPomUserWithSupportingPrisonInTheirCaseLoad)
92
+ return PermissionCheckStatus.OK;
93
+ if (inactiveBookingsUser)
94
+ return PermissionCheckStatus.OK;
95
+ return PermissionCheckStatus.RESTRICTED_PATIENT;
96
+ }
97
+
98
+ /*
99
+ * Released prisoners can be accessed in the following circumstances:
100
+ * - The user has the "Inactive Bookings" role
101
+ */
102
+ function releasedPrisonerCheck(user) {
103
+ const { userRoles } = user;
104
+ const inactiveBookingsUser = userHasRoles([Role.InactiveBookings], userRoles);
105
+ if (inactiveBookingsUser)
106
+ return PermissionCheckStatus.OK;
107
+ return PermissionCheckStatus.PRISONER_IS_RELEASED;
108
+ }
109
+
110
+ /*
111
+ * Transferring prisoners can be accessed in the following circumstances:
112
+ * - The user has the "Global search" role
113
+ * - The user has the "Inactive Bookings" role
114
+ */
115
+ function transferringPrisonerCheck(user) {
116
+ const { userRoles } = user;
117
+ const inactiveBookingsUser = userHasRoles([Role.InactiveBookings], userRoles);
118
+ const globalSearchUser = userHasRoles([Role.GlobalSearch], userRoles);
119
+ if (globalSearchUser)
120
+ return PermissionCheckStatus.OK;
121
+ if (inactiveBookingsUser)
122
+ return PermissionCheckStatus.OK;
123
+ return PermissionCheckStatus.PRISONER_IS_TRANSFERRING;
124
+ }
125
+
126
+ function baseCheck(user, prisoner) {
127
+ const inUsersCaseLoad = isInUsersCaseLoad(prisoner.prisonId, user);
128
+ const globalSearchUser = userHasRoles([Role.GlobalSearch], user.userRoles);
129
+ if (prisoner.restrictedPatient)
130
+ return restrictedPatientCheck(user, prisoner);
131
+ if (prisoner.prisonId === 'OUT')
132
+ return releasedPrisonerCheck(user);
133
+ if (prisoner.prisonId === 'TRN')
134
+ return transferringPrisonerCheck(user);
135
+ if (inUsersCaseLoad || globalSearchUser)
136
+ return PermissionCheckStatus.OK;
137
+ return PermissionCheckStatus.NOT_IN_CASELOAD;
138
+ }
139
+
140
+ class PrisonerSearchClient extends hmppsRestClient.RestClient {
141
+ constructor(logger, config, authenticationClient) {
142
+ super('Prisoner Search', config, logger, authenticationClient);
143
+ }
144
+ getPrisonerDetails(prisonerNumber) {
145
+ return this.get({ path: `/prisoner/${prisonerNumber}` }, hmppsRestClient.asSystem());
146
+ }
147
+ }
148
+
149
+ class PermissionsService {
150
+ prisonApiClient;
151
+ prisonerSearchClient;
152
+ permissionsLogger;
153
+ static create({ prisonApiConfig, prisonerSearchConfig, authenticationClient, logger = console, telemetryClient, }) {
154
+ return new PermissionsService(new PrisonApiClient(logger, prisonApiConfig), new PrisonerSearchClient(logger, prisonerSearchConfig, authenticationClient), new PermissionsLogger(logger, telemetryClient));
155
+ }
156
+ constructor(prisonApiClient, prisonerSearchClient, permissionsLogger) {
157
+ this.prisonApiClient = prisonApiClient;
158
+ this.prisonerSearchClient = prisonerSearchClient;
159
+ this.permissionsLogger = permissionsLogger;
160
+ }
161
+ getPrisonerPermissions({ user, prisoner, requestDependentOn, }) {
162
+ const baseCheckStatus = baseCheck(user, prisoner);
163
+ const basePermission = exports.PrisonerBasePermission.read;
164
+ if (baseCheckStatus !== PermissionCheckStatus.OK && requestDependentOn.includes(basePermission)) {
165
+ this.permissionsLogger.logPermissionCheckStatus(user, prisoner, basePermission, baseCheckStatus);
166
+ }
167
+ return {
168
+ [exports.PrisonerBasePermission.read]: baseCheckStatus === PermissionCheckStatus.OK,
169
+ // TODO:
170
+ // domainGroups: {
171
+ // ...
172
+ // },
173
+ };
174
+ }
175
+ async isUserAKeyWorkerAtPrison(token, user, prison) {
176
+ if (user.authSource !== 'nomis') {
177
+ return Promise.resolve(false);
178
+ }
179
+ return this.prisonApiClient.isUserAKeyWorker(token, user.staffId, prison);
180
+ }
181
+ getPrisonerDetails(prisonerNumber) {
182
+ return this.prisonerSearchClient.getPrisonerDetails(prisonerNumber);
183
+ }
184
+ }
185
+
186
+ class PrisonerPermissionError extends Error {
187
+ status = 403;
188
+ failedPermissionChecks;
189
+ constructor(message = 'Access not permitted', failedPermissionChecks = []) {
190
+ super(message);
191
+ this.name = 'NotFoundError';
192
+ this.failedPermissionChecks = failedPermissionChecks;
193
+ }
194
+ }
195
+
196
+ function prisonerPermissionsGuard(permissionsService, options) {
197
+ const { requestDependentOn, getPrisonerNumberFunction = getPrisonerNumberFrom } = options;
198
+ if (!requestDependentOn?.length)
199
+ throw Error('Unprotected route, must provide at least one dependent permission');
200
+ return async (req, res, next) => {
201
+ const prisonerData = await getPrisonerData(req, permissionsService, getPrisonerNumberFunction);
202
+ if (!prisonerData)
203
+ return next(Error('Could not retrieve prisoner data'));
204
+ await populateKeyWorkerData(req, res, prisonerData, permissionsService);
205
+ const prisonerPermissions = permissionsService.getPrisonerPermissions({
206
+ user: res.locals.user,
207
+ prisoner: prisonerData,
208
+ requestDependentOn,
209
+ });
210
+ const failedChecks = requestDependentOn.filter(permission => !checkPrisonerAccess(permission, prisonerPermissions));
211
+ if (failedChecks.length)
212
+ return next(new PrisonerPermissionError('Failed permissions checks', failedChecks));
213
+ req.middleware = { ...req.middleware, prisonerData };
214
+ res.locals.prisonerPermissions = prisonerPermissions;
215
+ return next();
216
+ };
217
+ }
218
+ function getPrisonerNumberFrom(req) {
219
+ if (req.params?.prisonerNumber) {
220
+ return req.params.prisonerNumber;
221
+ }
222
+ if (req.query?.prisonerNumber) {
223
+ return req.query.prisonerNumber;
224
+ }
225
+ return undefined;
226
+ }
227
+ async function getPrisonerData(req, permissionsService, getPrisonerNumberFunction) {
228
+ if (req.middleware?.prisonerData)
229
+ return req.middleware?.prisonerData;
230
+ const prisonerNumber = getPrisonerNumberFunction(req);
231
+ return prisonerNumber ? permissionsService.getPrisonerDetails(prisonerNumber) : undefined;
232
+ }
233
+ async function populateKeyWorkerData(req, res, prisoner, permissionsService) {
234
+ if (res.locals.user?.authSource !== 'nomis')
235
+ return;
236
+ const userCaseLoads = res.locals.user?.caseLoads?.map(caseLoad => caseLoad.caseLoadId);
237
+ if (!req.session)
238
+ throw new Error('User session required in order to cache key worker status');
239
+ const keyWorkerAtPrisons = req.session.keyWorkerAtPrisons ?? {};
240
+ if (prisoner.prisonId &&
241
+ userCaseLoads?.includes(prisoner.prisonId) &&
242
+ keyWorkerAtPrisons[prisoner.prisonId] === undefined) {
243
+ req.session.keyWorkerAtPrisons = {
244
+ ...keyWorkerAtPrisons,
245
+ [prisoner.prisonId]: await permissionsService.isUserAKeyWorkerAtPrison(res.locals.user.token, res.locals.user, prisoner.prisonId),
246
+ };
247
+ }
248
+ // This information is then provided to the user object on res.locals
249
+ res.locals.user.keyWorkerAtPrisons = req.session.keyWorkerAtPrisons || {};
250
+ }
251
+
252
+ exports.HmppsPermissionsError = PrisonerPermissionError;
253
+ exports.PermissionsService = PermissionsService;
254
+ exports.checkPrisonerAccess = checkPrisonerAccess;
255
+ exports.prisonerPermissionsGuard = prisonerPermissionsGuard;
256
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/main/data/prisonApi/PrisonApiClient.ts","../src/main/types/permissions/PermissionCheckStatus.ts","../src/main/services/permissions/PermissionsLogger.ts","../src/main/types/permissions/prisoner/PrisonerPermissions.ts","../src/main/services/permissions/utils/PermissionUtils.ts","../src/main/types/user/Role.ts","../src/main/services/permissions/checks/baseCheck/RestrictedPatientCheck.ts","../src/main/services/permissions/checks/baseCheck/ReleasedPrisonerCheck.ts","../src/main/services/permissions/checks/baseCheck/TransferingPrisonerCheck.ts","../src/main/services/permissions/checks/baseCheck/BaseCheck.ts","../src/main/data/hmppsPrisonerSearch/PrisonerSearchClient.ts","../src/main/services/permissions/PermissionsService.ts","../src/main/types/errors/PrisonerPermissionError.ts","../src/main/middleware/PrisonerPermissionsGuard.ts"],"sourcesContent":["import { RestClient, ApiConfig, asUser } from '@ministryofjustice/hmpps-rest-client'\nimport type Logger from 'bunyan'\n\nexport default class PrisonApiClient extends RestClient {\n constructor(logger: Logger | Console, config: ApiConfig) {\n super('Prison API', config, logger)\n }\n\n isUserAKeyWorker(userToken: string, staffId: number, agencyId: string): Promise<boolean> {\n return this.get<boolean>({ path: `/api/staff/${staffId}/${agencyId}/roles/KW` }, asUser(userToken))\n }\n}\n","// eslint-disable-next-line import/prefer-default-export\nexport enum PermissionCheckStatus {\n NOT_PERMITTED = 'NOT_PERMITTED', // Generic not permitted status - default\n NOT_IN_CASELOAD = 'NOT_IN_CASELOAD',\n RESTRICTED_PATIENT = 'RESTRICTED_PATIENT',\n PRISONER_IS_RELEASED = 'PRISONER_IS_RELEASED',\n PRISONER_IS_TRANSFERRING = 'PRISONER_IS_TRANSFERRING',\n OK = 'OK',\n}\n","import { TelemetryClient } from 'applicationinsights'\nimport type Logger from 'bunyan'\nimport { PermissionCheckStatus } from '../../types/permissions/PermissionCheckStatus'\nimport { HmppsUser } from '../../types/user/HmppsUser'\nimport Prisoner from '../../data/hmppsPrisonerSearch/interfaces/Prisoner'\nimport { PrisonerPermission } from '../../types/permissions/prisoner/PrisonerPermissions'\n\nexport default class PermissionsLogger {\n constructor(\n private readonly logger: Logger | Console,\n private readonly telemetryClient?: TelemetryClient,\n ) {}\n\n logPermissionCheckStatus(\n user: HmppsUser,\n prisoner: Prisoner,\n permission: PrisonerPermission,\n permissionCheckStatus: PermissionCheckStatus,\n ) {\n if (permissionCheckStatus === PermissionCheckStatus.OK) return\n\n if (this.telemetryClient) {\n this.telemetryClient.trackEvent({\n name: 'prisoner-permission-check-failed',\n properties: {\n username: user.username,\n prisonerNumber: prisoner.prisonerNumber,\n activeCaseLoad: user.authSource === 'nomis' && user.activeCaseLoadId,\n permissionChecked: permission,\n status: permissionCheckStatus,\n },\n })\n } else {\n this.logger.info(\n `Prisoner permission check failed: ${permission} (${permissionCheckStatus}) for user ${user.username}`,\n )\n }\n }\n}\n","export enum PrisonerBasePermission {\n read = 'prisoner:base-record:read',\n}\n\n/**\n * These permissions define what a HMPPS user can do with respect to data held about a prisoner.\n */\nexport interface PrisonerPermissions {\n [PrisonerBasePermission.read]: boolean\n\n // TODO:\n // domainGroups: {\n // ...\n // }\n}\n\nexport type PrisonerPermission = PrisonerBasePermission\n\nexport function checkPrisonerAccess(permission: PrisonerPermission, permissions: PrisonerPermissions): boolean {\n if (permission === PrisonerBasePermission.read) {\n return permissions[PrisonerBasePermission.read]\n }\n\n // TODO: Check each domain group for the permission\n\n return false\n}\n","import { HmppsUser } from '../../../types/user/HmppsUser'\n\nexport const isInUsersCaseLoad = (prisonId: string | undefined, user: HmppsUser): boolean =>\n user.authSource === 'nomis' && user.caseLoads?.some(caseLoad => caseLoad.caseLoadId === prisonId)\n\nexport const userHasRoles = (rolesToCheck: string[], userRoles: string[]): boolean => {\n const normaliseRoleText = (role: string): string => role.replace(/ROLE_/, '')\n return rolesToCheck.map(normaliseRoleText).some(role => userRoles.map(normaliseRoleText).includes(role))\n}\n","// eslint-disable-next-line import/prefer-default-export\nexport enum Role {\n GlobalSearch = 'ROLE_GLOBAL_SEARCH',\n InactiveBookings = 'ROLE_INACTIVE_BOOKINGS',\n PomUser = 'ROLE_POM',\n PrisonUser = 'ROLE_PRISON',\n}\n","import { HmppsUser } from '../../../../types/user/HmppsUser'\nimport Prisoner from '../../../../data/hmppsPrisonerSearch/interfaces/Prisoner'\nimport { isInUsersCaseLoad, userHasRoles } from '../../utils/PermissionUtils'\nimport { Role } from '../../../../types/user/Role'\nimport { PermissionCheckStatus } from '../../../../types/permissions/PermissionCheckStatus'\n\n/*\n * Restricted patients can be accessed in the following circumstances:\n * - The user has the \"Inactive Bookings\" role\n * - The user is a POM user and has the supporting prison ID in their caseloads\n */\nexport default function restrictedPatientCheck(user: HmppsUser, prisoner: Prisoner): PermissionCheckStatus {\n const { userRoles } = user\n const pomUser = userHasRoles([Role.PomUser], userRoles)\n const inactiveBookingsUser = userHasRoles([Role.InactiveBookings], userRoles)\n\n const userHasPrisonersSupportingPrisonInTheirCaseLoad = isInUsersCaseLoad(prisoner.supportingPrisonId, user)\n const userIsPomUserWithSupportingPrisonInTheirCaseLoad = pomUser && userHasPrisonersSupportingPrisonInTheirCaseLoad\n\n if (userIsPomUserWithSupportingPrisonInTheirCaseLoad) return PermissionCheckStatus.OK\n if (inactiveBookingsUser) return PermissionCheckStatus.OK\n\n return PermissionCheckStatus.RESTRICTED_PATIENT\n}\n","import { HmppsUser } from '../../../../types/user/HmppsUser'\nimport { userHasRoles } from '../../utils/PermissionUtils'\nimport { Role } from '../../../../types/user/Role'\nimport { PermissionCheckStatus } from '../../../../types/permissions/PermissionCheckStatus'\n\n/*\n * Released prisoners can be accessed in the following circumstances:\n * - The user has the \"Inactive Bookings\" role\n */\nexport default function releasedPrisonerCheck(user: HmppsUser): PermissionCheckStatus {\n const { userRoles } = user\n const inactiveBookingsUser = userHasRoles([Role.InactiveBookings], userRoles)\n\n if (inactiveBookingsUser) return PermissionCheckStatus.OK\n\n return PermissionCheckStatus.PRISONER_IS_RELEASED\n}\n","import { HmppsUser } from '../../../../types/user/HmppsUser'\nimport { userHasRoles } from '../../utils/PermissionUtils'\nimport { Role } from '../../../../types/user/Role'\nimport { PermissionCheckStatus } from '../../../../types/permissions/PermissionCheckStatus'\n\n/*\n * Transferring prisoners can be accessed in the following circumstances:\n * - The user has the \"Global search\" role\n * - The user has the \"Inactive Bookings\" role\n */\nexport default function transferringPrisonerCheck(user: HmppsUser): PermissionCheckStatus {\n const { userRoles } = user\n const inactiveBookingsUser = userHasRoles([Role.InactiveBookings], userRoles)\n const globalSearchUser = userHasRoles([Role.GlobalSearch], userRoles)\n\n if (globalSearchUser) return PermissionCheckStatus.OK\n if (inactiveBookingsUser) return PermissionCheckStatus.OK\n\n return PermissionCheckStatus.PRISONER_IS_TRANSFERRING\n}\n","/*\n * The \"default\" access checks for accessing information about a prisoner.\n *\n * This function can be used for checking what we consider the \"base checks\" for accessing a page on the prisoner profile\n * when no other special cases are given.\n *\n * It provides the following options for special circumstances:\n *\n * - allowGlobal (default: true): Does the page allow access to users outside the prisoners current case load\n */\nimport { HmppsUser } from '../../../../types/user/HmppsUser'\nimport Prisoner from '../../../../data/hmppsPrisonerSearch/interfaces/Prisoner'\nimport { isInUsersCaseLoad, userHasRoles } from '../../utils/PermissionUtils'\nimport { Role } from '../../../../types/user/Role'\nimport { PermissionCheckStatus } from '../../../../types/permissions/PermissionCheckStatus'\nimport restrictedPatientCheck from './RestrictedPatientCheck'\nimport releasedPrisonerCheck from './ReleasedPrisonerCheck'\nimport transferringPrisonerCheck from './TransferingPrisonerCheck'\n\nexport default function baseCheck(user: HmppsUser, prisoner: Prisoner): PermissionCheckStatus {\n const inUsersCaseLoad = isInUsersCaseLoad(prisoner.prisonId, user)\n const globalSearchUser = userHasRoles([Role.GlobalSearch], user.userRoles)\n\n if (prisoner.restrictedPatient) return restrictedPatientCheck(user, prisoner)\n if (prisoner.prisonId === 'OUT') return releasedPrisonerCheck(user)\n if (prisoner.prisonId === 'TRN') return transferringPrisonerCheck(user)\n if (inUsersCaseLoad || globalSearchUser) return PermissionCheckStatus.OK\n\n return PermissionCheckStatus.NOT_IN_CASELOAD\n}\n","import { RestClient, ApiConfig, AuthenticationClient, asSystem } from '@ministryofjustice/hmpps-rest-client'\nimport type Logger from 'bunyan'\nimport Prisoner from './interfaces/Prisoner'\n\nexport default class PrisonerSearchClient extends RestClient {\n constructor(logger: Logger | Console, config: ApiConfig, authenticationClient: AuthenticationClient) {\n super('Prisoner Search', config, logger, authenticationClient)\n }\n\n getPrisonerDetails(prisonerNumber: string): Promise<Prisoner> {\n return this.get<Prisoner>({ path: `/prisoner/${prisonerNumber}` }, asSystem())\n }\n}\n","import type bunyan from 'bunyan'\nimport { ApiConfig, AuthenticationClient } from '@ministryofjustice/hmpps-rest-client'\nimport { TelemetryClient } from 'applicationinsights'\nimport Prisoner from '../../data/hmppsPrisonerSearch/interfaces/Prisoner'\nimport PrisonApiClient from '../../data/prisonApi/PrisonApiClient'\nimport { HmppsUser } from '../../types/user/HmppsUser'\nimport PermissionsLogger from './PermissionsLogger'\nimport {\n PrisonerBasePermission,\n PrisonerPermission,\n PrisonerPermissions,\n} from '../../types/permissions/prisoner/PrisonerPermissions'\nimport baseCheck from './checks/baseCheck/BaseCheck'\nimport { PermissionCheckStatus } from '../../types/permissions/PermissionCheckStatus'\nimport PrisonerSearchClient from '../../data/hmppsPrisonerSearch/PrisonerSearchClient'\n\nexport default class PermissionsService {\n private readonly prisonApiClient: PrisonApiClient\n\n private readonly prisonerSearchClient: PrisonerSearchClient\n\n readonly permissionsLogger: PermissionsLogger\n\n static create({\n prisonApiConfig,\n prisonerSearchConfig,\n authenticationClient,\n logger = console,\n telemetryClient,\n }: {\n prisonApiConfig: ApiConfig\n prisonerSearchConfig: ApiConfig\n authenticationClient: AuthenticationClient\n logger?: bunyan | typeof console\n telemetryClient?: TelemetryClient\n }): PermissionsService {\n return new PermissionsService(\n new PrisonApiClient(logger, prisonApiConfig),\n new PrisonerSearchClient(logger, prisonerSearchConfig, authenticationClient),\n new PermissionsLogger(logger, telemetryClient),\n )\n }\n\n private constructor(\n prisonApiClient: PrisonApiClient,\n prisonerSearchClient: PrisonerSearchClient,\n permissionsLogger: PermissionsLogger,\n ) {\n this.prisonApiClient = prisonApiClient\n this.prisonerSearchClient = prisonerSearchClient\n this.permissionsLogger = permissionsLogger\n }\n\n public getPrisonerPermissions({\n user,\n prisoner,\n requestDependentOn,\n }: {\n user: HmppsUser\n prisoner: Prisoner\n requestDependentOn: PrisonerPermission[]\n }): PrisonerPermissions {\n const baseCheckStatus = baseCheck(user, prisoner)\n const basePermission = PrisonerBasePermission.read\n\n if (baseCheckStatus !== PermissionCheckStatus.OK && requestDependentOn.includes(basePermission)) {\n this.permissionsLogger.logPermissionCheckStatus(user, prisoner, basePermission, baseCheckStatus)\n }\n\n return {\n [PrisonerBasePermission.read]: baseCheckStatus === PermissionCheckStatus.OK,\n\n // TODO:\n // domainGroups: {\n // ...\n // },\n } as PrisonerPermissions\n }\n\n public async isUserAKeyWorkerAtPrison(token: string, user: HmppsUser, prison: string): Promise<boolean> {\n if (user.authSource !== 'nomis') {\n return Promise.resolve(false)\n }\n return this.prisonApiClient.isUserAKeyWorker(token, user.staffId, prison)\n }\n\n public getPrisonerDetails(prisonerNumber: string): Promise<Prisoner> {\n return this.prisonerSearchClient.getPrisonerDetails(prisonerNumber)\n }\n}\n","import { PrisonerPermission } from '../permissions/prisoner/PrisonerPermissions'\n\nexport default class PrisonerPermissionError extends Error {\n public status = 403\n\n public failedPermissionChecks: PrisonerPermission[]\n\n constructor(message = 'Access not permitted', failedPermissionChecks: PrisonerPermission[] = []) {\n super(message)\n this.name = 'NotFoundError'\n this.failedPermissionChecks = failedPermissionChecks\n }\n}\n","import type { NextFunction, Request, Response } from 'express'\nimport { checkPrisonerAccess, PrisonerPermission } from '../types/permissions/prisoner/PrisonerPermissions'\nimport PermissionsService from '../services/permissions/PermissionsService'\nimport PrisonerPermissionError from '../types/errors/PrisonerPermissionError'\nimport Prisoner from '../data/hmppsPrisonerSearch/interfaces/Prisoner'\n\nexport default function prisonerPermissionsGuard(\n permissionsService: PermissionsService,\n options: {\n requestDependentOn: PrisonerPermission[]\n getPrisonerNumberFunction?: (req: Request) => string | undefined\n },\n) {\n const { requestDependentOn, getPrisonerNumberFunction = getPrisonerNumberFrom } = options\n\n if (!requestDependentOn?.length) throw Error('Unprotected route, must provide at least one dependent permission')\n\n return async (req: Request, res: Response, next: NextFunction) => {\n const prisonerData = await getPrisonerData(req, permissionsService, getPrisonerNumberFunction)\n if (!prisonerData) return next(Error('Could not retrieve prisoner data'))\n\n await populateKeyWorkerData(req, res, prisonerData, permissionsService)\n\n const prisonerPermissions = permissionsService.getPrisonerPermissions({\n user: res.locals.user,\n prisoner: prisonerData,\n requestDependentOn,\n })\n\n const failedChecks = requestDependentOn.filter(permission => !checkPrisonerAccess(permission, prisonerPermissions))\n\n if (failedChecks.length) return next(new PrisonerPermissionError('Failed permissions checks', failedChecks))\n\n req.middleware = { ...req.middleware, prisonerData }\n res.locals.prisonerPermissions = prisonerPermissions\n return next()\n }\n}\n\nfunction getPrisonerNumberFrom(req: Request): string | undefined {\n if (req.params?.prisonerNumber) {\n return req.params.prisonerNumber\n }\n if (req.query?.prisonerNumber) {\n return req.query.prisonerNumber as string\n }\n return undefined\n}\n\nasync function getPrisonerData(\n req: Request,\n permissionsService: PermissionsService,\n getPrisonerNumberFunction: (req: Request) => string | undefined,\n): Promise<Prisoner | undefined> {\n if (req.middleware?.prisonerData) return req.middleware?.prisonerData\n const prisonerNumber = getPrisonerNumberFunction(req)\n return prisonerNumber ? permissionsService.getPrisonerDetails(prisonerNumber) : undefined\n}\n\nasync function populateKeyWorkerData(\n req: Request,\n res: Response,\n prisoner: Prisoner,\n permissionsService: PermissionsService,\n): Promise<void> {\n if (res.locals.user?.authSource !== 'nomis') return\n\n const userCaseLoads = res.locals.user?.caseLoads?.map(caseLoad => caseLoad.caseLoadId)\n\n if (!req.session) throw new Error('User session required in order to cache key worker status')\n const keyWorkerAtPrisons = req.session.keyWorkerAtPrisons ?? {}\n\n if (\n prisoner.prisonId &&\n userCaseLoads?.includes(prisoner.prisonId) &&\n keyWorkerAtPrisons[prisoner.prisonId] === undefined\n ) {\n req.session.keyWorkerAtPrisons = {\n ...keyWorkerAtPrisons,\n [prisoner.prisonId]: await permissionsService.isUserAKeyWorkerAtPrison(\n res.locals.user.token!,\n res.locals.user,\n prisoner.prisonId,\n ),\n }\n }\n\n // This information is then provided to the user object on res.locals\n res.locals.user.keyWorkerAtPrisons = req.session.keyWorkerAtPrisons || {}\n}\n"],"names":["RestClient","asUser","PrisonerBasePermission","asSystem"],"mappings":"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"}
@@ -0,0 +1,147 @@
1
+ import { ApiConfig, AuthenticationClient } from '@ministryofjustice/hmpps-rest-client';
2
+ import { TelemetryClient } from 'applicationinsights';
3
+ import bunyan from 'bunyan';
4
+ import { Request, Response, NextFunction } from 'express';
5
+
6
+ interface PermissionsOptions {
7
+ prisonApiConfig: ApiConfig;
8
+ prisonerSearchConfig: ApiConfig;
9
+ logger?: bunyan | typeof console;
10
+ telemetryClient?: TelemetryClient;
11
+ }
12
+
13
+ declare enum PrisonerBasePermission {
14
+ read = "prisoner:base-record:read"
15
+ }
16
+ /**
17
+ * These permissions define what a HMPPS user can do with respect to data held about a prisoner.
18
+ */
19
+ interface PrisonerPermissions {
20
+ [PrisonerBasePermission.read]: boolean;
21
+ }
22
+ type PrisonerPermission = PrisonerBasePermission;
23
+ declare function checkPrisonerAccess(permission: PrisonerPermission, permissions: PrisonerPermissions): boolean;
24
+
25
+ interface Prisoner {
26
+ prisonerNumber: string;
27
+ prisonId?: string;
28
+ restrictedPatient: boolean;
29
+ supportingPrisonId?: string;
30
+ }
31
+
32
+ interface CaseLoad {
33
+ caseLoadId: string;
34
+ description: string;
35
+ type: string;
36
+ caseloadFunction: string;
37
+ currentlyActive: boolean;
38
+ }
39
+
40
+ type AuthSource = 'nomis' | 'delius' | 'external' | 'azuread';
41
+ /**
42
+ * These are the details that all user types share.
43
+ */
44
+ interface BaseUser {
45
+ authSource: AuthSource;
46
+ username: string;
47
+ userId: string;
48
+ name: string;
49
+ displayName: string;
50
+ userRoles: string[];
51
+ token?: string;
52
+ backLink?: string;
53
+ }
54
+ /**
55
+ * Prison users are those that have a user account in NOMIS.
56
+ * HMPPS Auth automatically grants these users a `ROLE_PRISON` role.
57
+ * Prison users have an additional numerical staffId. The userId is
58
+ * a stringified version of the staffId. Some teams may need to separately
59
+ * retrieve the user case load (which prisons that a prison user has access
60
+ * to) and store it here, an example can be found in `hmpps-prisoner-profile`.
61
+ */
62
+ interface PrisonUser extends BaseUser {
63
+ authSource: 'nomis';
64
+ staffId: number;
65
+ activeCaseLoadId: string;
66
+ caseLoads: CaseLoad[];
67
+ keyWorkerAtPrisons: Record<string, boolean>;
68
+ }
69
+ /**
70
+ * Probation users are those that have a user account in nDelius.
71
+ * HMPPS Auth automatically grants these users a `ROLE_PROBATION` role.
72
+ */
73
+ interface ProbationUser extends BaseUser {
74
+ authSource: 'delius';
75
+ }
76
+ /**
77
+ * External users are those that have a user account in our External Users
78
+ * database. These accounts are created for users that need access to HMPPS
79
+ * services but have neither NOMIS nor nDelius access.
80
+ */
81
+ interface ExternalUser extends BaseUser {
82
+ authSource: 'external';
83
+ }
84
+ /**
85
+ * AzureAD users are those that have a justice.gov.uk email address and
86
+ * an account in MoJ's Azure AD (now called Entra ID) tenant. HMPPS Auth
87
+ * will normally check to see if there is a Prison/Probation/External
88
+ * user with the same email address and request that the user to pick one
89
+ * to use to access the service, however if there is no match, it is
90
+ * possible that a user of this type could attempt to access the service,
91
+ * and would have no user roles associated.
92
+ */
93
+ interface AzureADUser extends BaseUser {
94
+ authSource: 'azuread';
95
+ }
96
+ type HmppsUser = PrisonUser | ProbationUser | ExternalUser | AzureADUser;
97
+
98
+ declare enum PermissionCheckStatus {
99
+ NOT_PERMITTED = "NOT_PERMITTED",// Generic not permitted status - default
100
+ NOT_IN_CASELOAD = "NOT_IN_CASELOAD",
101
+ RESTRICTED_PATIENT = "RESTRICTED_PATIENT",
102
+ PRISONER_IS_RELEASED = "PRISONER_IS_RELEASED",
103
+ PRISONER_IS_TRANSFERRING = "PRISONER_IS_TRANSFERRING",
104
+ OK = "OK"
105
+ }
106
+
107
+ declare class PermissionsLogger {
108
+ private readonly logger;
109
+ private readonly telemetryClient?;
110
+ constructor(logger: bunyan | Console, telemetryClient?: TelemetryClient | undefined);
111
+ logPermissionCheckStatus(user: HmppsUser, prisoner: Prisoner, permission: PrisonerPermission, permissionCheckStatus: PermissionCheckStatus): void;
112
+ }
113
+
114
+ declare class PermissionsService {
115
+ private readonly prisonApiClient;
116
+ private readonly prisonerSearchClient;
117
+ readonly permissionsLogger: PermissionsLogger;
118
+ static create({ prisonApiConfig, prisonerSearchConfig, authenticationClient, logger, telemetryClient, }: {
119
+ prisonApiConfig: ApiConfig;
120
+ prisonerSearchConfig: ApiConfig;
121
+ authenticationClient: AuthenticationClient;
122
+ logger?: bunyan | typeof console;
123
+ telemetryClient?: TelemetryClient;
124
+ }): PermissionsService;
125
+ private constructor();
126
+ getPrisonerPermissions({ user, prisoner, requestDependentOn, }: {
127
+ user: HmppsUser;
128
+ prisoner: Prisoner;
129
+ requestDependentOn: PrisonerPermission[];
130
+ }): PrisonerPermissions;
131
+ isUserAKeyWorkerAtPrison(token: string, user: HmppsUser, prison: string): Promise<boolean>;
132
+ getPrisonerDetails(prisonerNumber: string): Promise<Prisoner>;
133
+ }
134
+
135
+ declare function prisonerPermissionsGuard(permissionsService: PermissionsService, options: {
136
+ requestDependentOn: PrisonerPermission[];
137
+ getPrisonerNumberFunction?: (req: Request) => string | undefined;
138
+ }): (req: Request, res: Response, next: NextFunction) => Promise<void>;
139
+
140
+ declare class PrisonerPermissionError extends Error {
141
+ status: number;
142
+ failedPermissionChecks: PrisonerPermission[];
143
+ constructor(message?: string, failedPermissionChecks?: PrisonerPermission[]);
144
+ }
145
+
146
+ export { PrisonerPermissionError as HmppsPermissionsError, PermissionsService, PrisonerBasePermission, checkPrisonerAccess, prisonerPermissionsGuard };
147
+ export type { PermissionsOptions, PrisonerPermission };