Skip to content

Commit

Permalink
Add yars service and hasPermission middleware
Browse files Browse the repository at this point in the history
Middleware added to routes. Submission controller checking api scope
  • Loading branch information
kyle1morel committed Jul 22, 2024
1 parent 0e4bd5b commit b34d2a4
Show file tree
Hide file tree
Showing 21 changed files with 373 additions and 114 deletions.
9 changes: 0 additions & 9 deletions app/src/controllers/sso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
};

Expand Down
26 changes: 22 additions & 4 deletions app/src/controllers/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
permitService,
userService
} from '../services';
import { BasicResponse, Initiative } from '../utils/enums/application';
import { BasicResponse, Initiative, Scope } from '../utils/enums/application';
import {
ApplicationStatus,
IntakeStatus,
Expand All @@ -21,7 +21,15 @@ import {
import { camelCaseToTitleCase, deDupeUnsure, getCurrentIdentity, isTruthy, toTitleCase } from '../utils/utils';

import type { NextFunction, Request, Response } from '../interfaces/IExpress';
import type { ChefsFormConfig, ChefsFormConfigData, Submission, ChefsSubmissionExport, Permit, Email } from '../types';
import type {
ChefsFormConfig,
ChefsFormConfigData,
Submission,
ChefsSubmissionExport,
Permit,
Email,
ApiScope
} from '../types';

const controller = {
checkAndStoreNewSubmissions: async () => {
Expand Down Expand Up @@ -368,13 +376,15 @@ const controller = {

getSubmissions: async (req: Request<never, { self?: string }>, res: Response, next: NextFunction) => {
try {
const apiScope = req.currentUser?.apiScope as ApiScope;

// Check for and store new submissions in CHEFS
await controller.checkAndStoreNewSubmissions();

// Pull from PCNS database
let response = await submissionService.getSubmissions();

if (isTruthy(req.query.self)) {
if (apiScope.name === Scope.SELF) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
response = response.filter((x) => x?.submittedBy === (req.currentUser?.tokenPayload as any)?.idir_username);
}
Expand All @@ -399,10 +409,18 @@ const controller = {
next: NextFunction
) => {
try {
const response = await submissionService.searchSubmissions({
const apiScope = req.currentUser?.apiScope as ApiScope;

let response = await submissionService.searchSubmissions({
...req.query,
includeUser: isTruthy(req.query.includeUser)
});

if (apiScope.name === Scope.SELF) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
response = response.filter((x) => x?.submittedBy === (req.currentUser?.tokenPayload as any)?.idir_username);
}

res.status(200).json(response);
} catch (e: unknown) {
next(e);
Expand Down
1 change: 1 addition & 0 deletions app/src/interfaces/IExpress.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as core from 'express-serve-static-core';

import type { CurrentUser } from '../types/CurrentUser';
import { AuthType } from '../utils/enums/application';

interface Query extends core.Query {}

Expand Down
2 changes: 1 addition & 1 deletion app/src/middleware/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const currentUser = async (req: Request, res: Response, next: NextFunctio
}

// Inject currentUser data into request
req.currentUser = Object.freeze(currentUser);
req.currentUser = currentUser;

// Continue middleware
next();
Expand Down
75 changes: 56 additions & 19 deletions app/src/middleware/authorization.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,69 @@
// @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 { userService, yarsService } from '../services';

import type { NextFunction, Request, Response } from '../interfaces/IExpress';
import { Scope } from '../utils/enums/application';
import { getCurrentIdentity } from '../utils/utils';
import { NIL } from 'uuid';

// Converts a primitive string to a Scope enum type
function convertStringToScope(value: string): Scope | undefined {
return (Object.values(Scope) as Array<string>).includes(value) ? (value as Scope) : undefined;
}

/**
* @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
* Obtains the full permission mappings for the given resource/action pair for any of the users roles
* 403 if none are found
* Checks for highest priority scope and injects into the currentUser
* Defaults scope to self if none were found
* @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 {
if (req.currentUser) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const roles = await yarsService.getIdentityRoles((req.currentUser?.tokenPayload as any).preferred_username);

const permissions = await Promise.all(
roles.map((x) => yarsService.getRolePermissionDetails(x.roleId, resource, action))
).then((x) => x.flat());

if (!permissions || permissions.length === 0) {
throw new Error('Invalid role authorization');
}

const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, NIL), NIL);

if (!userId) {
throw new Error('Invalid role user');
}

const scopes = permissions
.filter((x) => !!x.scopeName)
.map((x) => ({ scopeName: x.scopeName as string, scopePriority: x.scopePriority as number }))
.sort((a, b) => (a.scopePriority > b.scopePriority ? 1 : -1));

req.currentUser.apiScope = {
name: scopes.length ? convertStringToScope(scopes[0].scopeName) ?? Scope.SELF : Scope.SELF,
userId: userId
};
} else {
throw new Error('No current user');
}
// 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();
};
};
16 changes: 13 additions & 3 deletions app/src/routes/v1/document.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
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';

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);
Expand All @@ -22,6 +31,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);
Expand Down
47 changes: 35 additions & 12 deletions app/src/routes/v1/enquiry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
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';
Expand All @@ -23,23 +26,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);
Expand All @@ -49,20 +65,27 @@ 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);
}
);

/** 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);
Expand Down
4 changes: 2 additions & 2 deletions app/src/routes/v1/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { currentUser } from '../../middleware/authentication';

import express from 'express';

import { currentUser } from '../../middleware/authentication';

import document from './document';
import enquiry from './enquiry';
import note from './note';
Expand Down
60 changes: 43 additions & 17 deletions app/src/routes/v1/note.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
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';
Expand All @@ -9,26 +12,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);
}
);

router.get(
'/bringForward',
hasPermission(Resource.NOTE, Action.READ),
(req: Request, res: Response, next: NextFunction): void => {
noteController.listBringForward(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(
'/list/:activityId',
hasPermission(Resource.NOTE, Action.READ),
noteValidator.listNotes,
(req: Request, res: Response, next: NextFunction): void => {
noteController.listNotes(req, res, next);
}
);

export default router;
Loading

0 comments on commit b34d2a4

Please sign in to comment.