diff --git a/app/src/controllers/sso.ts b/app/src/controllers/sso.ts index 0cba97da8..b8d48e878 100644 --- a/app/src/controllers/sso.ts +++ b/app/src/controllers/sso.ts @@ -29,15 +29,6 @@ const controller = { } catch (e: unknown) { next(e); } - }, - - getRoles: async (req: Request, res: Response, next: NextFunction) => { - try { - const response = await ssoService.getRoles(); - res.status(response.status).json(response.data); - } catch (e: unknown) { - next(e); - } } }; diff --git a/app/src/middleware/authorization.ts b/app/src/middleware/authorization.ts index 0fd19dceb..25895576d 100644 --- a/app/src/middleware/authorization.ts +++ b/app/src/middleware/authorization.ts @@ -1,32 +1,36 @@ // @ts-expect-error api-problem lacks a defined interface; code still works fine import Problem from 'api-problem'; -import { ACCESS_ROLES_LIST } from '../utils/constants/application'; +import { yarsService } from '../services'; import type { NextFunction, Request, Response } from '../interfaces/IExpress'; /** - * @function hasAccess - * Check if the currentUser has at least one assigned role - * @param {Request} req Express request object - * @param {Response} res Express response object - * @param {NextFunction} next The next callback function + * @function hasPermission + * Obtains the roles for the current users identity + * Checks if the permission mappings contain the given resource/action pair for any of the users roles + * @param {string} resource a resource name + * @param {string} action an action name * @returns {function} Express middleware function * @throws The error encountered upon failure */ -export const hasAccess = async (req: Request, res: Response, next: NextFunction) => { - try { - // TODO: Can we expand tokenPayload to include client_roles? - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const roles = (req.currentUser?.tokenPayload as any)?.client_roles; - if (!roles || ACCESS_ROLES_LIST.some((r) => roles.includes(r))) { - throw new Error('Invalid role authorization'); +export const hasPermission = (resource: string, action: string) => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const roles = await yarsService.getIdentityRoles((req.currentUser?.tokenPayload as any).preferred_username); + + const results = await Promise.all(roles.map((x) => yarsService.roleHasPermission(x.roleId, resource, action))); + + if (!results.includes(true)) { + throw new Error('Invalid role authorization'); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + return next(new Problem(403, { detail: err.message, instance: req.originalUrl })); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - return next(new Problem(403, { detail: err.message, instance: req.originalUrl })); - } - // Continue middleware - next(); + // Continue middleware + next(); + }; }; diff --git a/app/src/routes/v1/document.ts b/app/src/routes/v1/document.ts index f327595fc..0e9b818ec 100644 --- a/app/src/routes/v1/document.ts +++ b/app/src/routes/v1/document.ts @@ -1,6 +1,8 @@ import express from 'express'; import { documentController } from '../../controllers'; +import { hasPermission } from '../../middleware/authorization'; import { requireSomeAuth } from '../../middleware/requireSomeAuth'; +import { Action, Resource } from '../../utils/enums/application'; import { documentValidator } from '../../validators'; import type { NextFunction, Request, Response } from '../../interfaces/IExpress'; @@ -8,12 +10,18 @@ import type { NextFunction, Request, Response } from '../../interfaces/IExpress' const router = express.Router(); router.use(requireSomeAuth); -router.put('/', documentValidator.createDocument, (req: Request, res: Response, next: NextFunction): void => { - documentController.createDocument(req, res, next); -}); +router.put( + '/', + hasPermission(Resource.DOCUMENT, Action.CREATE), + documentValidator.createDocument, + (req: Request, res: Response, next: NextFunction): void => { + documentController.createDocument(req, res, next); + } +); router.delete( '/:documentId', + hasPermission(Resource.DOCUMENT, Action.DELETE), documentValidator.deleteDocument, (req: Request, res: Response, next: NextFunction): void => { documentController.deleteDocument(req, res, next); @@ -22,6 +30,7 @@ router.delete( router.get( '/list/:activityId', + hasPermission(Resource.DOCUMENT, Action.READ), documentValidator.listDocuments, (req: Request, res: Response, next: NextFunction): void => { documentController.listDocuments(req, res, next); diff --git a/app/src/routes/v1/enquiry.ts b/app/src/routes/v1/enquiry.ts index 2b1fdae6f..8e7486eaf 100644 --- a/app/src/routes/v1/enquiry.ts +++ b/app/src/routes/v1/enquiry.ts @@ -1,6 +1,8 @@ import express from 'express'; import { enquiryController } from '../../controllers'; +import { hasPermission } from '../../middleware/authorization'; import { requireSomeAuth } from '../../middleware/requireSomeAuth'; +import { Action, Resource } from '../../utils/enums/application'; import { enquiryValidator } from '../../validators'; import type { NextFunction, Request, Response } from '../../interfaces/IExpress'; @@ -23,23 +25,36 @@ const decideValidation = (validator: Middleware) => { }; /** Gets a list of enquiries */ -router.get('/', (req: Request, res: Response, next: NextFunction): void => { - enquiryController.getEnquiries(req, res, next); -}); +router.get( + '/', + hasPermission(Resource.ENQUIRY, Action.READ), + (req: Request, res: Response, next: NextFunction): void => { + enquiryController.getEnquiries(req, res, next); + } +); /** Gets a specific enquiry */ -router.get('/:enquiryId', (req: Request, res: Response, next: NextFunction): void => { - enquiryController.getEnquiry(req, res, next); -}); +router.get( + '/:enquiryId', + hasPermission(Resource.ENQUIRY, Action.READ), + (req: Request, res: Response, next: NextFunction): void => { + enquiryController.getEnquiry(req, res, next); + } +); /** Deletes an enquiry */ -router.delete('/:enquiryId', (req: Request, res: Response, next: NextFunction): void => { - enquiryController.deleteEnquiry(req, res, next); -}); +router.delete( + '/:enquiryId', + hasPermission(Resource.ENQUIRY, Action.DELETE), + (req: Request, res: Response, next: NextFunction): void => { + enquiryController.deleteEnquiry(req, res, next); + } +); /** Creates an enquiry with Draft status */ router.put( '/draft', + hasPermission(Resource.ENQUIRY, Action.CREATE), decideValidation(enquiryValidator.createDraft), (req: Request, res: Response, next: NextFunction): void => { enquiryController.createDraft(req, res, next); @@ -49,6 +64,7 @@ router.put( /** Updates an enquiry with Draft status */ router.put( '/draft/:enquiryId', + hasPermission(Resource.ENQUIRY, Action.UPDATE), decideValidation(enquiryValidator.updateDraft), (req: Request, res: Response, next: NextFunction): void => { enquiryController.updateDraft(req, res, next); @@ -56,13 +72,19 @@ router.put( ); /** Updates an enquiry */ -router.put('/:enquiryId', enquiryValidator.updateEnquiry, (req: Request, res: Response, next: NextFunction): void => { - enquiryController.updateEnquiry(req, res, next); -}); +router.put( + '/:enquiryId', + hasPermission(Resource.ENQUIRY, Action.UPDATE), + enquiryValidator.updateEnquiry, + (req: Request, res: Response, next: NextFunction): void => { + enquiryController.updateEnquiry(req, res, next); + } +); /** Updates is_deleted flag for an enquiry */ router.patch( '/:enquiryId/delete', + hasPermission(Resource.ENQUIRY, Action.DELETE), enquiryValidator.updateIsDeletedFlag, (req: Request, res: Response, next: NextFunction): void => { enquiryController.updateIsDeletedFlag(req, res, next); diff --git a/app/src/routes/v1/note.ts b/app/src/routes/v1/note.ts index b6deb95ce..7ca1df91b 100644 --- a/app/src/routes/v1/note.ts +++ b/app/src/routes/v1/note.ts @@ -1,6 +1,8 @@ import express from 'express'; import { noteController } from '../../controllers'; +import { hasPermission } from '../../middleware/authorization'; import { requireSomeAuth } from '../../middleware/requireSomeAuth'; +import { Action, Resource } from '../../utils/enums/application'; import { noteValidator } from '../../validators'; import type { NextFunction, Request, Response } from '../../interfaces/IExpress'; @@ -9,26 +11,49 @@ const router = express.Router(); router.use(requireSomeAuth); // Note create endpoint -router.put('/', noteValidator.createNote, (req: Request, res: Response, next: NextFunction): void => { - noteController.createNote(req, res, next); -}); - -router.put('/:noteId', noteValidator.updateNote, (req: Request, res: Response, next: NextFunction): void => { - noteController.updateNote(req, res, next); -}); +router.put( + '/', + hasPermission(Resource.NOTE, Action.CREATE), + noteValidator.createNote, + (req: Request, res: Response, next: NextFunction): void => { + noteController.createNote(req, res, next); + } +); + +router.put( + '/:noteId', + hasPermission(Resource.NOTE, Action.UPDATE), + noteValidator.updateNote, + (req: Request, res: Response, next: NextFunction): void => { + noteController.updateNote(req, res, next); + } +); // Note delete endpoint -router.delete('/:noteId', (req: Request, res: Response, next: NextFunction): void => { - noteController.deleteNote(req, res, next); -}); +router.delete( + '/:noteId', + hasPermission(Resource.NOTE, Action.DELETE), + (req: Request, res: Response, next: NextFunction): void => { + noteController.deleteNote(req, res, next); + } +); // Note list endpoints -router.get('/bringForward', (req: Request, res: Response, next: NextFunction): void => { - noteController.listBringForward(req, res, next); -}); - -router.get('/list/:activityId', noteValidator.listNotes, (req: Request, res: Response, next: NextFunction): void => { - noteController.listNotes(req, res, next); -}); +router.get( + '/bringForward', + hasPermission(Resource.NOTE, Action.READ), + (req: Request, res: Response, next: NextFunction): void => { + noteController.listBringForward(req, res, next); + } +); + +router.get( + '/list/:activityId', + hasPermission(Resource.NOTE, Action.READ), + noteValidator.listNotes, + (req: Request, res: Response, next: NextFunction): void => { + noteController.listNotes(req, res, next); + } +); export default router; diff --git a/app/src/routes/v1/permit.ts b/app/src/routes/v1/permit.ts index 0f1514db1..549404f95 100644 --- a/app/src/routes/v1/permit.ts +++ b/app/src/routes/v1/permit.ts @@ -1,6 +1,8 @@ import express from 'express'; import { permitController } from '../../controllers'; +import { hasPermission } from '../../middleware/authorization'; import { requireSomeAuth } from '../../middleware/requireSomeAuth'; +import { Action, Resource } from '../../utils/enums/application'; import { permitValidator } from '../../validators'; import type { NextFunction, Request, Response } from '../../interfaces/IExpress'; @@ -9,23 +11,39 @@ const router = express.Router(); router.use(requireSomeAuth); // Permit create endpoint -router.put('/', permitValidator.createPermit, (req: Request, res: Response, next: NextFunction): void => { - permitController.createPermit(req, res, next); -}); +router.put( + '/', + hasPermission(Resource.PERMIT, Action.CREATE), + permitValidator.createPermit, + (req: Request, res: Response, next: NextFunction): void => { + permitController.createPermit(req, res, next); + } +); // Permit update endpoint -router.put('/:permitId', permitValidator.updatePermit, (req: Request, res: Response, next: NextFunction): void => { - permitController.updatePermit(req, res, next); -}); +router.put( + '/:permitId', + hasPermission(Resource.PERMIT, Action.UPDATE), + permitValidator.updatePermit, + (req: Request, res: Response, next: NextFunction): void => { + permitController.updatePermit(req, res, next); + } +); // Permit delete endpoint -router.delete('/:permitId', permitValidator.deletePermit, (req: Request, res: Response, next: NextFunction): void => { - permitController.deletePermit(req, res, next); -}); +router.delete( + '/:permitId', + hasPermission(Resource.PERMIT, Action.DELETE), + permitValidator.deletePermit, + (req: Request, res: Response, next: NextFunction): void => { + permitController.deletePermit(req, res, next); + } +); // Permit list by activity endpoint router.get( '/list/:activityId', + hasPermission(Resource.PERMIT, Action.READ), permitValidator.listPermits, (req: Request, res: Response, next: NextFunction): void => { permitController.listPermits(req, res, next); @@ -33,8 +51,12 @@ router.get( ); // Permit types endpoint -router.get('/types', (req: Request, res: Response, next: NextFunction): void => { - permitController.getPermitTypes(req, res, next); -}); +router.get( + '/types', + hasPermission(Resource.PERMIT, Action.READ), + (req: Request, res: Response, next: NextFunction): void => { + permitController.getPermitTypes(req, res, next); + } +); export default router; diff --git a/app/src/routes/v1/roadmap.ts b/app/src/routes/v1/roadmap.ts index 4522b09dd..549950c4a 100644 --- a/app/src/routes/v1/roadmap.ts +++ b/app/src/routes/v1/roadmap.ts @@ -1,6 +1,8 @@ import express from 'express'; import { roadmapController } from '../../controllers'; +import { hasPermission } from '../../middleware/authorization'; import { requireSomeAuth } from '../../middleware/requireSomeAuth'; +import { Action, Resource } from '../../utils/enums/application'; import { roadmapValidator } from '../../validators'; import type { NextFunction, Request, Response } from '../../interfaces/IExpress'; @@ -9,8 +11,13 @@ const router = express.Router(); router.use(requireSomeAuth); // Send an email with the roadmap data -router.put('/', roadmapValidator.send, (req: Request, res: Response, next: NextFunction): void => { - roadmapController.send(req, res, next); -}); +router.put( + '/', + hasPermission(Resource.ROADMAP, Action.CREATE), + roadmapValidator.send, + (req: Request, res: Response, next: NextFunction): void => { + roadmapController.send(req, res, next); + } +); export default router; diff --git a/app/src/routes/v1/sso.ts b/app/src/routes/v1/sso.ts index e9564e4eb..2aabe04c1 100644 --- a/app/src/routes/v1/sso.ts +++ b/app/src/routes/v1/sso.ts @@ -1,6 +1,8 @@ import express from 'express'; import { ssoController } from '../../controllers'; +import { hasPermission } from '../../middleware/authorization'; import { requireSomeAuth } from '../../middleware/requireSomeAuth'; +import { Action, Resource } from '../../utils/enums/application'; import type { NextFunction, Request, Response } from '../../interfaces/IExpress'; @@ -11,16 +13,20 @@ router.post('/requestBasicAccess', (req: Request, res: Response, next: NextFunct ssoController.requestBasicAccess(req, res, next); }); -router.get('/idir/users', (req: Request, res: Response, next: NextFunction): void => { - ssoController.searchIdirUsers(req, res, next); -}); - -router.get('/basic-bceid/users', (req: Request, res: Response, next: NextFunction): void => { - ssoController.searchBasicBceidUsers(req, res, next); -}); +router.get( + '/idir/users', + hasPermission(Resource.SSO, Action.READ), + (req: Request, res: Response, next: NextFunction): void => { + ssoController.searchIdirUsers(req, res, next); + } +); -router.get('/roles', (req: Request, res: Response, next: NextFunction): void => { - ssoController.getRoles(req, res, next); -}); +router.get( + '/basic-bceid/users', + hasPermission(Resource.SSO, Action.READ), + (req: Request, res: Response, next: NextFunction): void => { + ssoController.searchBasicBceidUsers(req, res, next); + } +); export default router; diff --git a/app/src/routes/v1/submission.ts b/app/src/routes/v1/submission.ts index b20a8089a..55b20847d 100644 --- a/app/src/routes/v1/submission.ts +++ b/app/src/routes/v1/submission.ts @@ -4,18 +4,25 @@ import { requireSomeAuth } from '../../middleware/requireSomeAuth'; import { submissionValidator } from '../../validators'; import type { NextFunction, Request, Response } from '../../interfaces/IExpress'; +import { hasPermission } from '../../middleware/authorization'; +import { Action, Resource } from '../../utils/enums/application'; const router = express.Router(); router.use(requireSomeAuth); /** Gets a list of submissions */ -router.get('/', (req: Request, res: Response, next: NextFunction): void => { - submissionController.getSubmissions(req, res, next); -}); +router.get( + '/', + hasPermission(Resource.SUBMISSION, Action.READ), + (req: Request, res: Response, next: NextFunction): void => { + submissionController.getSubmissions(req, res, next); + } +); /** Search submissions */ router.get( '/search', + hasPermission(Resource.SUBMISSION, Action.READ), submissionValidator.searchSubmissions, (req: Request, res: Response, next: NextFunction): void => { submissionController.searchSubmissions(req, res, next); @@ -25,6 +32,7 @@ router.get( /** Gets submission statistics*/ router.get( '/statistics', + hasPermission(Resource.SUBMISSION, Action.READ), submissionValidator.getStatistics, (req: Request, res: Response, next: NextFunction): void => { submissionController.getStatistics(req, res, next); @@ -32,18 +40,27 @@ router.get( ); /** Creates a submission with Draft status */ -router.put('/draft', (req: Request, res: Response, next: NextFunction): void => { - submissionController.createDraft(req, res, next); -}); +router.put( + '/draft', + hasPermission(Resource.SUBMISSION, Action.CREATE), + (req: Request, res: Response, next: NextFunction): void => { + submissionController.createDraft(req, res, next); + } +); /** Updates a submission with Draft status */ -router.put('/draft/:submissionId', (req: Request, res: Response, next: NextFunction): void => { - submissionController.updateDraft(req, res, next); -}); +router.put( + '/draft/:submissionId', + hasPermission(Resource.SUBMISSION, Action.UPDATE), + (req: Request, res: Response, next: NextFunction): void => { + submissionController.updateDraft(req, res, next); + } +); // Send an email with the confirmation of submission router.put( '/emailConfirmation', + hasPermission(Resource.SUBMISSION, Action.CREATE), submissionValidator.emailConfirmation, (req: Request, res: Response, next: NextFunction): void => { submissionController.emailConfirmation(req, res, next); @@ -51,13 +68,19 @@ router.put( ); /** Creates a submission */ -router.put('/', submissionValidator.createSubmission, (req: Request, res: Response, next: NextFunction): void => { - submissionController.createSubmission(req, res, next); -}); +router.put( + '/', + hasPermission(Resource.SUBMISSION, Action.CREATE), + submissionValidator.createSubmission, + (req: Request, res: Response, next: NextFunction): void => { + submissionController.createSubmission(req, res, next); + } +); /** Deletes a submission */ router.delete( '/:submissionId', + hasPermission(Resource.SUBMISSION, Action.DELETE), submissionValidator.deleteSubmission, (req: Request, res: Response, next: NextFunction): void => { submissionController.deleteSubmission(req, res, next); @@ -65,11 +88,9 @@ router.delete( ); /** Gets a specific submission */ -router.get('/search', (req: Request, res: Response, next: NextFunction): void => { - submissionController.searchSubmissions(req, res, next); -}); router.get( '/:submissionId', + hasPermission(Resource.SUBMISSION, Action.READ), submissionValidator.getSubmission, (req: Request, res: Response, next: NextFunction): void => { submissionController.getSubmission(req, res, next); @@ -79,6 +100,7 @@ router.get( /** Updates a submission*/ router.put( '/:submissionId', + hasPermission(Resource.SUBMISSION, Action.UPDATE), submissionValidator.updateSubmission, (req: Request, res: Response, next: NextFunction): void => { submissionController.updateSubmission(req, res, next); @@ -88,6 +110,7 @@ router.put( /** Updates is_deleted flag for a submission */ router.patch( '/:submissionId/delete', + hasPermission(Resource.SUBMISSION, Action.DELETE), submissionValidator.updateIsDeletedFlag, (req: Request, res: Response, next: NextFunction): void => { submissionController.updateIsDeletedFlag(req, res, next); diff --git a/app/src/routes/v1/user.ts b/app/src/routes/v1/user.ts index b09b9c170..ea193f8df 100644 --- a/app/src/routes/v1/user.ts +++ b/app/src/routes/v1/user.ts @@ -1,6 +1,8 @@ import express from 'express'; import { userController } from '../../controllers'; +import { hasPermission } from '../../middleware/authorization'; import { requireSomeAuth } from '../../middleware/requireSomeAuth'; +import { Action, Resource } from '../../utils/enums/application'; import { userValidator } from '../../validators'; import type { NextFunction, Request, Response } from 'express'; @@ -9,8 +11,13 @@ const router = express.Router(); router.use(requireSomeAuth); // Submission endpoint -router.get('/', userValidator.searchUsers, (req: Request, res: Response, next: NextFunction): void => { - userController.searchUsers(req, res, next); -}); +router.get( + '/', + hasPermission(Resource.USER, Action.READ), + userValidator.searchUsers, + (req: Request, res: Response, next: NextFunction): void => { + userController.searchUsers(req, res, next); + } +); export default router; diff --git a/app/src/services/index.ts b/app/src/services/index.ts index 4264a3fdd..d5c308762 100644 --- a/app/src/services/index.ts +++ b/app/src/services/index.ts @@ -8,3 +8,4 @@ export { default as permitService } from './permit'; export { default as ssoService } from './sso'; export { default as submissionService } from './submission'; export { default as userService } from './user'; +export { default as yarsService } from './yars'; diff --git a/app/src/services/yars.ts b/app/src/services/yars.ts new file mode 100644 index 000000000..a6d4d6566 --- /dev/null +++ b/app/src/services/yars.ts @@ -0,0 +1,43 @@ +/* eslint-disable no-useless-catch */ + +import prisma from '../db/dataConnection'; + +const service = { + /** + * @function getEnquiry + * Gets roles for the specified identity + * @param {string} identityId Identity ID to search + * @returns {Promise} The result of running the findMany operation + */ + getIdentityRoles: async (identityId: string) => { + try { + const result = await prisma.identity_role.findMany({ + where: { + identity_id: identityId + } + }); + + return result.map((x) => ({ roleId: x.role_id })); + } catch (e: unknown) { + throw e; + } + }, + + roleHasPermission: async (roleId: number, resourceName: string, actionName: string) => { + try { + const result = await prisma.role_permission_vw.count({ + where: { + role_id: roleId, + resource_name: resourceName, + action_name: actionName + } + }); + + return result > 0; + } catch (e: unknown) { + throw e; + } + } +}; + +export default service; diff --git a/app/src/utils/enums/application.ts b/app/src/utils/enums/application.ts index 4db04a522..ac0afa670 100644 --- a/app/src/utils/enums/application.ts +++ b/app/src/utils/enums/application.ts @@ -6,6 +6,13 @@ export enum AccessRole { PCNS_SUPERVISOR = 'PCNS_SUPERVISOR' } +export enum Action { + CREATE = 'create', + READ = 'read', + UPDATE = 'update', + DELETE = 'delete' +} + /** Current user authentication type */ export enum AuthType { /** OIDC JWT Authentication header provided */ @@ -38,3 +45,14 @@ export enum Regex { EMAIL = '^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]{2,})+$', PHONE_NUMBER = '^(\\+\\d{1,2}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$' } + +export enum Resource { + DOCUMENT = 'document', + ENQUIRY = 'enquiry', + NOTE = 'note', + PERMIT = 'permit', + ROADMAP = 'roadmap', + SSO = 'sso', + SUBMISSION = 'submission', + USER = 'user' +}