diff --git a/app/src/controllers/accessRequest.ts b/app/src/controllers/accessRequest.ts index f49f9039..4ed9b9b5 100644 --- a/app/src/controllers/accessRequest.ts +++ b/app/src/controllers/accessRequest.ts @@ -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 @@ -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) { @@ -58,7 +62,6 @@ const controller = { } const isGroupUpdate = existingUser && accessRequest.grant; - let response; if (isGroupUpdate) { @@ -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( @@ -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) { diff --git a/app/src/controllers/yars.ts b/app/src/controllers/yars.ts index 2eb24e06..f61f3fd5 100644 --- a/app/src/controllers/yars.ts +++ b/app/src/controllers/yars.ts @@ -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) => { @@ -25,6 +26,23 @@ const controller = { } catch (e: unknown) { next(e); } + }, + + deleteSubjectGroup: async ( + req: Request, + 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); + } } }; diff --git a/app/src/db/models/access_request.ts b/app/src/db/models/access_request.ts index fc57a667..bbedeb24 100644 --- a/app/src/db/models/access_request.ts +++ b/app/src/db/models/access_request.ts @@ -7,8 +7,10 @@ import type { AccessRequest } from '../../types/AccessRequest'; // Define types const _accessRequest = Prisma.validator()({}); +const _accessRequestWithGraph = Prisma.validator()({}); type PrismaRelationAccessRequest = Omit, keyof Stamps>; +type PrismaGraphAccessRequest = Prisma.access_requestGetPayload; export default { toPrismaModel(input: AccessRequest): PrismaRelationAccessRequest { @@ -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() }; } }; diff --git a/app/src/db/prisma/schema.prisma b/app/src/db/prisma/schema.prisma index c648fe8b..d6aa5888 100644 --- a/app/src/db/prisma/schema.prisma +++ b/app/src/db/prisma/schema.prisma @@ -1,3 +1,4 @@ + generator client { provider = "prisma-client-js" previewFeatures = ["multiSchema", "views"] diff --git a/app/src/routes/v1/yars.ts b/app/src/routes/v1/yars.ts index a6951043..f674f43a 100644 --- a/app/src/routes/v1/yars.ts +++ b/app/src/routes/v1/yars.ts @@ -3,6 +3,7 @@ import express from 'express'; import { yarsController } from '../../controllers'; import { requireSomeAuth } from '../../middleware/requireSomeAuth'; import { requireSomeGroup } from '../../middleware/requireSomeGroup'; +import { GroupName } from '../../utils/enums/application'; import type { NextFunction, Request, Response } from 'express'; @@ -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, res: Response, next: NextFunction): void => { + yarsController.deleteSubjectGroup(req, res, next); + } +); + export default router; diff --git a/app/src/services/accessRequest.ts b/app/src/services/accessRequest.ts index fb49b8d2..0f3f76f2 100644 --- a/app/src/services/accessRequest.ts +++ b/app/src/services/accessRequest.ts @@ -35,20 +35,6 @@ const service = { return access_request.fromPrismaModel(accessRequestResponse); }, - /** - * @function deleteAccessRequests - * Deletes the access request - * @returns {Promise} 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 @@ -72,6 +58,23 @@ const service = { getAccessRequests: async () => { const response = await prisma.access_request.findMany(); return response.map((x) => access_request.fromPrismaModel(x)); + }, + + /** + * @function updateAccessRequest + * Updates a specific enquiry + * @param {Enquiry} data Enquiry to update + * @returns {Promise} 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); } }; diff --git a/app/src/types/index.ts b/app/src/types/index.ts index 844669b1..492d5cc4 100644 --- a/app/src/types/index.ts +++ b/app/src/types/index.ts @@ -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'; diff --git a/frontend/src/components/user/UserCreateModal.vue b/frontend/src/components/user/UserCreateModal.vue index 97ddc133..779e2917 100644 --- a/frontend/src/components/user/UserCreateModal.vue +++ b/frontend/src/components/user/UserCreateModal.vue @@ -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 } = { @@ -29,9 +29,9 @@ const authzStore = useAuthZStore(); // State const loading: Ref = ref(false); const searchTag: Ref = ref(''); -const selectableGroups: Ref> = ref([]); +const selectableGroups: Ref> = ref(new Map()); const selectedGroup: Ref = ref(undefined); -const selectedParam: Ref = ref(undefined); +const selectedParam: Ref = ref('Last name'); const selectedUser: Ref = ref(undefined); const users: Ref> = ref([]); const visible = defineModel('visible'); @@ -86,12 +86,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 () => { + const yarsGroups: Array = (await yarsService.getGroups()).data; + + const allowedGroups: Array = [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.find((group) => group.name === groupName); + return [group?.label ?? groupName.toLowerCase(), groupName]; + }) + ); }); @@ -106,6 +114,18 @@ onMounted(() => { Create new user
+
@@ -113,22 +133,11 @@ onMounted(() => { v-model="searchTag" placeholder="Search by first name, last name, or email" class="col-12 pl-5" + autofocus @update:model-value="searchIdirUsers" />
-
{ - { 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))" />
- +
@@ -341,11 +378,15 @@ onMounted(async () => {
@@ -368,7 +409,7 @@ onMounted(async () => {