Skip to content

Commit

Permalink
cleaned up usermanagement functionality on backend and frontend, adde…
Browse files Browse the repository at this point in the history
…d revoke
  • Loading branch information
qhanson55 committed Nov 8, 2024
1 parent fc47b9f commit 06644f4
Show file tree
Hide file tree
Showing 13 changed files with 347 additions and 193 deletions.
28 changes: 17 additions & 11 deletions app/src/controllers/accessRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const controller = {
req.currentAuthorization?.groups.some(
(group: GroupName) => group === GroupName.DEVELOPER || group === GroupName.ADMIN
) ?? false;

const existingUser = !!user.userId;

// Groups the current user can modify
Expand All @@ -43,10 +42,15 @@ const controller = {
res.status(404).json({ message: 'User not found' });
} else {
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)) {
if (
accessRequest.grant &&
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) {
Expand All @@ -58,7 +62,6 @@ const controller = {
}

const isGroupUpdate = existingUser && accessRequest.grant;

let response;

if (isGroupUpdate) {
Expand Down Expand Up @@ -149,13 +152,13 @@ const controller = {
if (req.body.approve) {
if (accessRequest.grant) {
if (!accessRequest.group || !accessRequest.group.length) {
res.status(422).json({ message: 'Must provided a role to grant' });
return 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' });
return 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' });
return res.status(409).json({ message: 'User must be an IDIR user to be assigned this role' });
}

await yarsService.assignGroup(
Expand All @@ -176,14 +179,17 @@ const controller = {
await yarsService.removeGroup(userResponse.sub, initiative, g.groupName);
}
}
}

// Delete the request after processing
await accessRequestService.deleteAccessRequest(accessRequest.accessRequestId);
// Update access request status
accessRequest.status = AccessRequestStatus.APPROVED;
await accessRequestService.updateAccessRequest(accessRequest);
} else {
accessRequest.status = AccessRequestStatus.REJECTED;
await accessRequestService.updateAccessRequest(accessRequest);
}
} else {
res.status(404).json({ message: 'User does not exist' });
return res.status(404).json({ message: 'User does not exist' });
}

res.status(204).end();
}
} catch (e: unknown) {
Expand Down
18 changes: 18 additions & 0 deletions app/src/controllers/yars.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { yarsService } from '../services';

import type { NextFunction, Request, Response } from 'express';
import { GroupName, Initiative } from '../utils/enums/application';

const controller = {
getGroups: async (req: Request, res: Response, next: NextFunction) => {
Expand All @@ -25,6 +26,23 @@ const controller = {
} catch (e: unknown) {
next(e);
}
},

deleteSubjectGroup: async (
req: Request<never, never, { sub: string; group: GroupName }>,
res: Response,
next: NextFunction
) => {
try {
const response = await yarsService.removeGroup(req.body.sub, Initiative.HOUSING, req.body.group);

if (!response) {
return res.status(422).json({ message: 'Unable to process revocation.' });
}
res.status(200).json(response);
} catch (e: unknown) {
next(e);
}
}
};

Expand Down
7 changes: 5 additions & 2 deletions app/src/db/models/access_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import type { AccessRequest } from '../../types/AccessRequest';

// Define types
const _accessRequest = Prisma.validator<Prisma.access_requestDefaultArgs>()({});
const _accessRequestWithGraph = Prisma.validator<Prisma.access_requestDefaultArgs>()({});

type PrismaRelationAccessRequest = Omit<Prisma.access_requestGetPayload<typeof _accessRequest>, keyof Stamps>;
type PrismaGraphAccessRequest = Prisma.access_requestGetPayload<typeof _accessRequestWithGraph>;

export default {
toPrismaModel(input: AccessRequest): PrismaRelationAccessRequest {
Expand All @@ -21,13 +23,14 @@ export default {
};
},

fromPrismaModel(input: PrismaRelationAccessRequest): AccessRequest {
fromPrismaModel(input: PrismaGraphAccessRequest): AccessRequest {
return {
accessRequestId: input.access_request_id,
grant: input.grant,
group: input.group as GroupName | null,
userId: input.user_id as string,
status: input.status as AccessRequestStatus
status: input.status as AccessRequestStatus,
createdAt: input.created_at?.toISOString()
};
}
};
1 change: 1 addition & 0 deletions app/src/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

generator client {
provider = "prisma-client-js"
previewFeatures = ["multiSchema", "views"]
Expand Down
8 changes: 8 additions & 0 deletions app/src/routes/v1/yars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { requireSomeAuth } from '../../middleware/requireSomeAuth';
import { requireSomeGroup } from '../../middleware/requireSomeGroup';

import type { NextFunction, Request, Response } from 'express';
import { GroupName } from '../../utils/enums/application';

const router = express.Router();
router.use(requireSomeAuth);
Expand All @@ -18,4 +19,11 @@ router.get('/permissions', (req: Request, res: Response, next: NextFunction): vo
yarsController.getPermissions(req, res, next);
});

router.delete(
'/subject/group',
(req: Request<never, never, { sub: string; group: GroupName }>, res: Response, next: NextFunction): void => {
yarsController.deleteSubjectGroup(req, res, next);
}
);

export default router;
42 changes: 28 additions & 14 deletions app/src/services/accessRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,6 @@ const service = {
return access_request.fromPrismaModel(accessRequestResponse);
},

/**
* @function deleteAccessRequests
* Deletes the access request
* @returns {Promise<object>} The result of running the delete operation
*/
deleteAccessRequest: async (accessRequestId: string) => {
const response = await prisma.access_request.delete({
where: {
access_request_id: accessRequestId
}
});
return access_request.fromPrismaModel(response);
},

/**
* @function getAccessRequest
* Get an access request
Expand All @@ -72,6 +58,34 @@ const service = {
getAccessRequests: async () => {
const response = await prisma.access_request.findMany();
return response.map((x) => access_request.fromPrismaModel(x));
},

// /**
// * @function searchAccessRequests
// * Get all access requests with given access request parameters
// * @param {AccessRequest} accessRequest
// * @return {Promise<Object>}
// */
// searchAccessRequests: async (accessRequest: AccessRequest) => {
// const response = await prisma.access_request.findMany({ where: accessRequest });
// return response.map((x) => access_request.fromPrismaModel(x));
// },

/**
* @function updateAccessRequest
* Updates a specific enquiry
* @param {Enquiry} data Enquiry to update
* @returns {Promise<Enquiry | null>} The result of running the update operation
*/
updateAccessRequest: async (data: AccessRequest) => {
const result = await prisma.access_request.update({
data: { ...access_request.toPrismaModel(data), updated_at: data.updatedAt, updated_by: data.updatedBy },
where: {
access_request_id: data.accessRequestId
}
});

return access_request.fromPrismaModel(result);
}
};

Expand Down
2 changes: 1 addition & 1 deletion app/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type { Activity } from './Activity';
export type { AccessRequest } from './AccessRequest';
export type { Activity } from './Activity';
export type { ATSClientResource } from './ATSClientResource';
export type { ATSUserSearchParameters } from './ATSUserSearchParameters';
export type { BceidSearchParameters } from './BceidSearchParameters';
Expand Down
55 changes: 32 additions & 23 deletions frontend/src/components/user/UserCreateModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { onMounted, ref } from 'vue';
import { Dropdown } from '@/components/form';
import { Spinner } from '@/components/layout';
import { Button, Column, DataTable, Dialog, IconField, InputIcon, InputText, useToast } from '@/lib/primevue';
import { ssoService } from '@/services';
import { ssoService, yarsService } from '@/services';
import { useAuthZStore } from '@/store';
import { GroupName } from '@/utils/enums/application';
import type { DropdownChangeEvent } from 'primevue/dropdown';
import type { Ref } from 'vue';
import type { User } from '@/types';
import type { Group, User } from '@/types';
// Constants
const USER_SEARCH_PARAMS: { [key: string]: string } = {
Expand All @@ -29,12 +29,13 @@ const authzStore = useAuthZStore();
// State
const loading: Ref<boolean> = ref(false);
const searchTag: Ref<string> = ref('');
const selectableGroups: Ref<Array<GroupName>> = ref([]);
const selectableGroups: Ref<Map<string, GroupName>> = ref(new Map());
const selectedGroup: Ref<GroupName | undefined> = ref(undefined);
const selectedParam: Ref<string | undefined> = ref(undefined);
const selectedParam: Ref<string | undefined> = ref('Last name');
const selectedUser: Ref<User | undefined> = ref(undefined);
const users: Ref<Array<User>> = ref([]);
const visible = defineModel<boolean>('visible');
const yarsGroups: Ref<Array<Group>> = ref([]);
// Actions
let cancelTokenSource: any = null;
Expand Down Expand Up @@ -86,12 +87,20 @@ async function searchIdirUsers() {
}
}
onMounted(() => {
// TODO: Map rbac groups to radio list to get cleaner labels
selectableGroups.value = [GroupName.NAVIGATOR, GroupName.NAVIGATOR_READ_ONLY];
onMounted(async () => {
yarsGroups.value = (await yarsService.getGroups()).data;
const allowedGroups: Array<GroupName> = [GroupName.NAVIGATOR, GroupName.NAVIGATOR_READ_ONLY];
if (authzStore.isInGroup([GroupName.ADMIN, GroupName.DEVELOPER])) {
selectableGroups.value.unshift(GroupName.ADMIN, GroupName.SUPERVISOR);
allowedGroups.unshift(GroupName.ADMIN, GroupName.SUPERVISOR);
}
selectableGroups.value = new Map(
allowedGroups.map((groupName) => {
const group = yarsGroups.value.find((group) => group.name === groupName);
return [group?.label ?? groupName.toLowerCase(), groupName];
})
);
});
</script>

Expand All @@ -106,6 +115,18 @@ onMounted(() => {
<span class="p-dialog-title">Create new user</span>
</template>
<div class="flex justify-content-between align-items-center">
<Dropdown
class="col-3 m-0"
name="searchParam"
placeholder="Last name"
:options="Object.values(USER_SEARCH_PARAMS)"
@on-change="
(param: DropdownChangeEvent) => {
selectedParam = param.value;
searchIdirUsers();
}
"
/>
<div class="col-9 mb-2">
<IconField icon-position="left">
<InputIcon class="pi pi-search" />
Expand All @@ -114,21 +135,10 @@ onMounted(() => {
placeholder="Search by first name, last name, or email"
class="col-12 pl-5"
@update:model-value="searchIdirUsers"
autofocus

Check warning on line 138 in frontend/src/components/user/UserCreateModal.vue

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (16.x)

Attribute "autofocus" should go before "@update:model-value"

Check warning on line 138 in frontend/src/components/user/UserCreateModal.vue

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (18.x)

Attribute "autofocus" should go before "@update:model-value"

Check warning on line 138 in frontend/src/components/user/UserCreateModal.vue

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (20.x)

Attribute "autofocus" should go before "@update:model-value"

Check warning on line 138 in frontend/src/components/user/UserCreateModal.vue

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (16.x)

Attribute "autofocus" should go before "@update:model-value"

Check warning on line 138 in frontend/src/components/user/UserCreateModal.vue

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (18.x)

Attribute "autofocus" should go before "@update:model-value"

Check warning on line 138 in frontend/src/components/user/UserCreateModal.vue

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (20.x)

Attribute "autofocus" should go before "@update:model-value"
/>
</IconField>
</div>
<Dropdown
class="col-3 m-0"
name="assignRole"
placeholder="First name"
:options="Object.values(USER_SEARCH_PARAMS)"
@on-change="
(param: DropdownChangeEvent) => {
selectedParam = param.value;
searchIdirUsers();
}
"
/>
</div>
<DataTable
v-model:selection="selectedUser"
Expand All @@ -149,7 +159,6 @@ onMounted(() => {
<template #loading>
<Spinner />
</template>

<Column
field="fullName"
header="Username"
Expand All @@ -175,9 +184,9 @@ onMounted(() => {
class="col-12"
name="assignRole"
label="Assign role"
:options="selectableGroups"
:options="[...selectableGroups.keys()]"
:disabled="!selectedUser"
@on-change="(e: DropdownChangeEvent) => (selectedGroup = e.value)"
@on-change="(e: DropdownChangeEvent) => (selectedGroup = selectableGroups.get(e.value))"
/>
<div class="flex-auto pl-2">
<Button
Expand Down
18 changes: 12 additions & 6 deletions frontend/src/components/user/UserManageModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,25 @@ const authzStore = useAuthZStore();
// State
const visible = defineModel<boolean>('visible');
const selectableGroups: Ref<Array<GroupName>> = ref([]);
const selectableGroups: Ref<Map<string, GroupName>> = ref(new Map());
const group: Ref<GroupName | undefined> = ref(undefined);
const yarsGroups: Ref<Array<Group>> = ref([]);
// Actions
onMounted(async () => {
yarsGroups.value = (await yarsService.getGroups()).data;
// TODO: Map rbac groups to radio list to get cleaner labels
selectableGroups.value = [GroupName.NAVIGATOR, GroupName.NAVIGATOR_READ_ONLY];
const allowedGroups: Array<GroupName> = [GroupName.NAVIGATOR, GroupName.NAVIGATOR_READ_ONLY];
if (authzStore.isInGroup([GroupName.ADMIN, GroupName.DEVELOPER])) {
selectableGroups.value.unshift(GroupName.ADMIN, GroupName.SUPERVISOR);
allowedGroups.unshift(GroupName.ADMIN, GroupName.SUPERVISOR);
}
selectableGroups.value = new Map(
allowedGroups.map((groupName) => {
const group = yarsGroups.value.find((group) => group.name === groupName);
return [group?.label ?? groupName.toLowerCase(), groupName];
})
);
});
</script>

Expand All @@ -48,9 +54,9 @@ onMounted(async () => {
<RadioList
name="role"
:bold="false"
:options="selectableGroups"
:options="[...selectableGroups.keys()]"
class="mt-3 mb-4"
@on-change="(value) => (group = value)"
@on-change="(value) => (group = selectableGroups.get(value))"
/>
<div class="flex-auto">
<Button
Expand Down
Loading

0 comments on commit 06644f4

Please sign in to comment.