Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update/Clean Up User Management & Add Revoke #193

Merged
merged 1 commit into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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';

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;
31 changes: 17 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,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<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
54 changes: 31 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,9 +29,9 @@ 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');
Expand Down Expand Up @@ -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<Group> = (await yarsService.getGroups()).data;

const allowedGroups: Array<GroupName> = [GroupName.NAVIGATOR, GroupName.NAVIGATOR_READ_ONLY];
wilwong89 marked this conversation as resolved.
Show resolved Hide resolved
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];
})
);
});
</script>

Expand All @@ -106,29 +114,30 @@ 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" />
<InputText
v-model="searchTag"
placeholder="Search by first name, last name, or email"
class="col-12 pl-5"
autofocus
qhanson55 marked this conversation as resolved.
Show resolved Hide resolved
@update:model-value="searchIdirUsers"
/>
</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 +158,6 @@ onMounted(() => {
<template #loading>
<Spinner />
</template>

<Column
field="fullName"
header="Username"
Expand All @@ -175,9 +183,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
21 changes: 13 additions & 8 deletions frontend/src/components/user/UserManageModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,24 @@ 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;
const yarsGroups: Array<Group> = (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.find((group) => group.name === groupName);
return [group?.label ?? groupName.toLowerCase(), groupName];
})
);
});
</script>

Expand All @@ -48,9 +53,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