Skip to content

Commit

Permalink
Merge pull request #147 from bcgov/feature/rbac-user-int
Browse files Browse the repository at this point in the history
Partial user management integration with RBAC
  • Loading branch information
kyle1morel authored Sep 5, 2024
2 parents bab8f09 + 9ff696b commit cd06eb5
Show file tree
Hide file tree
Showing 36 changed files with 799 additions and 416 deletions.
202 changes: 166 additions & 36 deletions app/src/controllers/accessRequest.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,191 @@
import { GroupName } from '../utils/enums/application';
import { userService, accessRequestService } from '../services';
import { AccessRequestStatus, GroupName, IdentityProvider, Initiative } from '../utils/enums/application';
import { userService, accessRequestService, yarsService } from '../services';

import type { NextFunction, Request, Response } from '../interfaces/IExpress';
import type { JwtPayload } from 'jsonwebtoken';
import type { AccessRequest, User, UserAccessRequest } from '../types';
import type { AccessRequest, User } from '../types';

const controller = {
// Request to create user & access
createUserAccessRevokeRequest: async (
req: Request<never, never, { user: User; accessRequest: AccessRequest }>,
createUserAccessRequest: async (
req: Request<never, never, { accessRequest: AccessRequest; user: User }>,
res: Response,
next: NextFunction
) => {
// Check if the requestee is an admin
const admin =
(req.currentContext?.tokenPayload as JwtPayload)?.client_roles?.some(
(role: string) => role === GroupName.DEVELOPER || role === GroupName.ADMIN
) ?? false;

try {
let response;
const { user, accessRequest } = req.body;
if (accessRequest?.grant === false) {
response = await accessRequestService.createUserAccessRevokeRequest(accessRequest);
res.status(200).json(response);
const { accessRequest, user } = req.body;

// Check if the requestee is an admin
const admin =
req.currentAuthorization?.groups.some(
(group: GroupName) => group === GroupName.DEVELOPER || group === GroupName.ADMIN
) ?? false;

const existingUser = !!user.userId;

// Groups the current user can modify
const modifyableGroups = [GroupName.NAVIGATOR, GroupName.NAVIGATOR_READ_ONLY];
if (admin) {
modifyableGroups.unshift(GroupName.ADMIN, GroupName.SUPERVISOR);
}

let userResponse;

if (!user.userId) userResponse = await userService.createUser(user);
else userResponse = await userService.readUser(user.userId);

let userGroups: Array<{
initiativeId?: string;
groupId?: number;
groupName: GroupName;
}> = [];

if (!userResponse) {
res.status(404).json({ message: 'User not found' });
} else {
// Perform create request
const userResponse = await userService.createUserIfNew(user);
if (userResponse) {
accessRequest.userId = userResponse.userId;
response = userResponse as UserAccessRequest;
if (!admin) response.accessRequest = await accessRequestService.createUserAccessRevokeRequest(accessRequest);
else {
// TODO: call put/role api to update role without access request
userGroups = await yarsService.getSubjectGroups(userResponse.sub);
if (accessRequest.grant && !modifyableGroups.includes(accessRequest.group as GroupName)) {
res.status(403).json({ message: 'Cannot modify requested group' });
}
if (accessRequest.group && userGroups.map((x) => x.groupName).includes(accessRequest.group)) {
res.status(409).json({ message: 'User is already assigned this group' });
}
if (userResponse.idp !== IdentityProvider.IDIR) {
res.status(409).json({ message: 'User must be an IDIR user to be assigned this group' });
}
if (accessRequest.grant && (!accessRequest.group || !accessRequest.group.length)) {
res.status(422).json({ message: 'Must provided a group to grant' });
}
}

const isGroupUpdate = existingUser && accessRequest.grant;

let response;

if (isGroupUpdate) {
// Remove all user groups
for (const g of userGroups) {
let initiative = req.currentContext?.initiative as Initiative;
if (g.groupName === GroupName.DEVELOPER) {
initiative = Initiative.PCNS;
}
res.status(200).json(response);

response = await yarsService.removeGroup(userResponse?.sub as string, initiative, g.groupName);
}

// Assign new group
await yarsService.assignGroup(
req.currentContext.bearerToken,
user.sub,
req.currentContext?.initiative as Initiative,
accessRequest.group as GroupName
);

// Mock an access request for the response
response = {
userId: userResponse?.userId,
grant: accessRequest.grant,
group: accessRequest.group,
status: AccessRequestStatus.APPROVED
};
} else if (admin) {
if (accessRequest.grant) {
await yarsService.assignGroup(
req.currentContext.bearerToken,
user.sub,
req.currentContext?.initiative as Initiative,
accessRequest.group as GroupName
);
// Mock an access request for the response
response = {
userId: userResponse?.userId,
grant: accessRequest.grant,
group: accessRequest.group,
status: AccessRequestStatus.APPROVED
};
} else {
// TODO check if the new user is a proponent
// Remove requested group if provided - otherwise remove all user groups
const groupsToRemove = accessRequest.group ? [{ groupName: accessRequest.group }] : userGroups;
for (const g of groupsToRemove) {
let initiative = req.currentContext?.initiative as Initiative;
if (g.groupName === GroupName.DEVELOPER) {
initiative = Initiative.PCNS;
}

if (admin) {
// TODO: Call put/role to update role
} else {
// TODO: Put an entry in accessRequest table
response = await yarsService.removeGroup(userResponse?.sub as string, initiative, g.groupName);
}
// Send 409 if the user is not a proponent
res.status(409).json({ message: 'User already exists' });
}
} else {
response = await accessRequestService.createUserAccessRequest({
...accessRequest,
userId: userResponse?.userId as string
});
}

res.status(200).json(response);
} catch (e: unknown) {
next(e);
}
},

deleteAccessRequests: async (req: Request<{ accessRequestId: string }>, res: Response, next: NextFunction) => {
processUserAccessRequest: async (
req: Request<{ accessRequestId: string }, never, { approve: boolean }>,
res: Response,
next: NextFunction
) => {
try {
const response = await accessRequestService.deleteAccessRequests(req.params.accessRequestId);
res.status(200).json(response);
const accessRequest = await accessRequestService.getAccessRequest(req.params.accessRequestId);

if (accessRequest) {
const userResponse = await userService.readUser(accessRequest.userId);

if (userResponse) {
const groups: Array<{
initiativeId?: string;
groupId?: number;
groupName: GroupName;
}> = await yarsService.getSubjectGroups(userResponse.sub);

// If request is approved then grant or remove access
if (req.body.approve) {
if (accessRequest.grant) {
if (!accessRequest.group || !accessRequest.group.length) {
res.status(422).json({ message: 'Must provided a role to grant' });
}
if (accessRequest.group && groups.map((x) => x.groupName).includes(accessRequest.group)) {
res.status(409).json({ message: 'User is already assigned this role' });
}
if (userResponse.idp !== IdentityProvider.IDIR) {
res.status(409).json({ message: 'User must be an IDIR user to be assigned this role' });
}

await yarsService.assignGroup(
undefined,
userResponse.sub,
req.currentContext?.initiative as Initiative,
accessRequest.group as GroupName
);
} else {
// Remove requested group if provided - otherwise remove all user groups
const groupsToRemove = accessRequest.group ? [{ groupName: accessRequest.group }] : groups;
for (const g of groupsToRemove) {
let initiative = req.currentContext?.initiative as Initiative;
if (g.groupName === GroupName.DEVELOPER) {
initiative = Initiative.PCNS;
}

await yarsService.removeGroup(userResponse.sub, initiative, g.groupName);
}
}
}

// Delete the request after processing
await accessRequestService.deleteAccessRequest(accessRequest.accessRequestId);
} else {
res.status(404).json({ message: 'User does not exist' });
}

res.status(204).end();
}
} catch (e: unknown) {
next(e);
}
Expand Down
35 changes: 27 additions & 8 deletions app/src/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { userService } from '../services';
import { userService, yarsService } from '../services';
import { User } from '../types';
import { GroupName } from '../utils/enums/application';
import { addDashesToUuid, mixedQueryToArray, isTruthy } from '../utils/utils';

Expand All @@ -7,28 +8,46 @@ import type { NextFunction, Request, Response } from 'express';
const controller = {
searchUsers: async (req: Request, res: Response, next: NextFunction) => {
try {
const reqGroup = mixedQueryToArray(req.query.group as string) as GroupName[];
const userIds = mixedQueryToArray(req.query.userId as string);

const response = await userService.searchUsers({
userId: userIds ? userIds.map((id) => addDashesToUuid(id)) : userIds,
identityId: mixedQueryToArray(req.query.identityId as string),
idp: mixedQueryToArray(req.query.idp as string),
sub: req.query.username as string,
sub: req.query.sub as string,
email: req.query.email as string,
firstName: req.query.firstName as string,
fullName: req.query.fullName as string,
lastName: req.query.lastName as string,
active: isTruthy(req.query.active as string)
});

if (
req.query.role &&
[GroupName.NAVIGATOR, GroupName.NAVIGATOR_READ_ONLY].includes(req.query.role as GroupName)
) {
// TODO: filter out uses without a role of NAVIGATOR|NAVIGATOR_READ_ONLY or a pending access request from the response
type UserWithGroup = User & { groups?: GroupName[] };

// Inject found users with their groups if necessary
let userWithGroups: Array<UserWithGroup> = response;

if (reqGroup?.length || isTruthy(req.query.includeUserGroups)) {
for (const user of userWithGroups) {
const groups = await yarsService.getSubjectGroups(user.sub);
user.groups = groups.map((x) => x.groupName);
}

// Filters users based on searched groups
if (reqGroup?.length) {
userWithGroups = userWithGroups.filter((user) => reqGroup.some((g) => user.groups?.some((ug) => ug === g)));
}

// Remove groups if not requested
if (!isTruthy(req.query.includeUserGroups)) {
for (const user of userWithGroups) {
delete user.groups;
}
}
}

res.status(200).json(response);
res.status(200).json(userWithGroups);
} catch (e: unknown) {
next(e);
}
Expand Down
10 changes: 10 additions & 0 deletions app/src/controllers/yars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import { yarsService } from '../services';
import type { NextFunction, Request, Response } from '../interfaces/IExpress';

const controller = {
getGroups: async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await yarsService.getGroups(req.currentContext.initiative);

res.status(200).json(response);
} catch (e: unknown) {
next(e);
}
},

getPermissions: async (req: Request, res: Response, next: NextFunction) => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Loading

0 comments on commit cd06eb5

Please sign in to comment.