From f8ea421a0e1fe9624ab6b7f05e0d02ceffbb02cc Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 5 Apr 2024 10:13:47 -0700 Subject: [PATCH] Add group deletion and (name) update support for SCIM integration --- backend/src/ee/routes/v1/scim-router.ts | 104 ++++++++++++++----- backend/src/ee/services/scim/scim-service.ts | 72 +++++++++---- backend/src/ee/services/scim/scim-types.ts | 11 ++ 3 files changed, 142 insertions(+), 45 deletions(-) diff --git a/backend/src/ee/routes/v1/scim-router.ts b/backend/src/ee/routes/v1/scim-router.ts index ebf2485e1..ddb0b39be 100644 --- a/backend/src/ee/routes/v1/scim-router.ts +++ b/backend/src/ee/routes/v1/scim-router.ts @@ -260,7 +260,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { } }); - // TODO server.route({ url: "/Groups", method: "POST", @@ -284,22 +283,15 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), handler: async (req) => { - console.log("create group"); - console.log("create group req.body: ", req.body); - const group = await req.server.services.scim.createScimGroup({ displayName: req.body.displayName, orgId: req.permission.orgId }); - console.log("create group resulting group: ", group); - return group; } }); - // TODO: GET /api/v1/scim/Groups/8432b0aa-93e0-4b55-af3c-cd3fcc176f81 - server.route({ url: "/Groups/:groupId", method: "GET", @@ -307,33 +299,93 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { params: z.object({ groupId: z.string().trim() }), - // body: z.object({ - // schemas: z.array(z.string()), - // Operations: z.array( - // z.object({ - // op: z.string().trim(), - // path: z.string().trim().optional(), - // value: z.union([ - // z.object({ - // active: z.boolean() - // }), - // z.string().trim() - // ]) - // }) - // ) - // }), + response: { + 200: z.object({ + schemas: z.array(z.string()), + id: z.string().trim(), + displayName: z.string().trim(), + members: z.array( + z.object({ + value: z.string(), + display: z.string() + }) + ), + meta: z.object({ + resourceType: z.string().trim() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), + handler: async (req) => { + const group = await req.server.services.scim.getScimGroup({ + groupId: req.params.groupId, + orgId: req.permission.orgId + }); + return group; + } + }); + + server.route({ + url: "/Groups/:groupId", + method: "PUT", + schema: { + params: z.object({ + groupId: z.string().trim() + }), + body: z.object({ + schemas: z.array(z.string()), + id: z.string().trim(), + displayName: z.string().trim(), + members: z.array(z.any()).length(0) + }), + response: { + 200: z.object({ + schemas: z.array(z.string()), + id: z.string().trim(), + displayName: z.string().trim(), + members: z.array( + z.object({ + value: z.string(), + display: z.string() + }) + ), + meta: z.object({ + resourceType: z.string().trim() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), + handler: async (req) => { + const group = await req.server.services.scim.updateScimGroupName({ + groupId: req.params.groupId, + orgId: req.permission.orgId, + displayName: req.body.displayName + }); + + return group; + } + }); + + server.route({ + url: "/Groups/:groupId", + method: "DELETE", + schema: { + params: z.object({ + groupId: z.string().trim() + }), response: { 200: z.object({}) } }, onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), handler: async (req) => { - console.log("get scim group endpoint start"); - const group = await req.server.services.scim.getScimGroup({ + const group = await req.server.services.scim.deleteScimGroup({ groupId: req.params.groupId, orgId: req.permission.orgId }); - console.log("get scim group endpoint end: ", group); + return group; } }); diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index ebfa0d6ac..b43e8a5a1 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -25,6 +25,7 @@ import { TCreateScimGroupDTO, TCreateScimTokenDTO, TCreateScimUserDTO, + TDeleteScimGroupDTO, TDeleteScimTokenDTO, TGetScimGroupDTO, TGetScimUserDTO, @@ -32,6 +33,7 @@ import { TListScimUsersDTO, TReplaceScimUserDTO, TScimTokenJwtPayload, + TUpdateScimGroupNameDTO, TUpdateScimUserDTO } from "./scim-types"; @@ -44,7 +46,7 @@ type TScimServiceFactoryDep = { >; projectDAL: Pick; projectMembershipDAL: Pick; - groupDAL: Pick; + groupDAL: Pick; licenseService: Pick; permissionService: Pick; smtpService: TSmtpService; @@ -431,13 +433,6 @@ export const scimServiceFactory = ({ }; const createScimGroup = async ({ displayName, orgId }: TCreateScimGroupDTO) => { - // TODO 1: impl basic - // TODO 2: impl linking case - console.log("createScimGroup args: ", { - displayName, - orgId - }); - const group = await groupDAL.create({ name: displayName, slug: slugify(`${displayName}-${alphaNumericNanoId(4)}`), @@ -445,8 +440,6 @@ export const scimServiceFactory = ({ role: OrgMembershipRole.NoAccess }); - console.log("createScimGroup the group: ", group); - return buildScimGroup({ groupId: group.id, name: group.name, @@ -455,21 +448,19 @@ export const scimServiceFactory = ({ }; const getScimGroup = async ({ groupId, orgId }: TGetScimGroupDTO) => { - console.log("getScimGroup args: ", { - groupId, - orgId - }); - const group = await groupDAL.findOne({ id: groupId, orgId }); - console.log("getScimGroup group: ", group); - const users = await groupDAL.findAllGroupMembers(group.orgId, group.id); - console.log("getScimGroup users: ", users); + if (!group) { + throw new ScimRequestError({ + detail: "Group Not Found", + status: 404 + }); + } - // TODO: get members + const users = await groupDAL.findAllGroupMembers(group.orgId, group.id); return buildScimGroup({ groupId: group.id, @@ -483,6 +474,47 @@ export const scimServiceFactory = ({ }); }; + const updateScimGroupName = async ({ groupId, orgId, displayName }: TUpdateScimGroupNameDTO) => { + const [group] = await groupDAL.update( + { + id: groupId, + orgId + }, + { + name: displayName + } + ); + + if (!group) { + throw new ScimRequestError({ + detail: "Group Not Found", + status: 404 + }); + } + + return buildScimGroup({ + groupId: group.id, + name: group.name, + members: [] + }); + }; + + const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => { + const [group] = await groupDAL.delete({ + id: groupId, + orgId + }); + + if (!group) { + throw new ScimRequestError({ + detail: "Group Not Found", + status: 404 + }); + } + + return {}; // intentionally return empty object upon success + }; + const fnValidateScimToken = async (token: TScimTokenJwtPayload) => { const scimToken = await scimDAL.findById(token.scimTokenId); if (!scimToken) throw new UnauthorizedError(); @@ -517,6 +549,8 @@ export const scimServiceFactory = ({ replaceScimUser, createScimGroup, getScimGroup, + deleteScimGroup, + updateScimGroupName, fnValidateScimToken }; }; diff --git a/backend/src/ee/services/scim/scim-types.ts b/backend/src/ee/services/scim/scim-types.ts index 6a04cedd0..f0ed84caa 100644 --- a/backend/src/ee/services/scim/scim-types.ts +++ b/backend/src/ee/services/scim/scim-types.ts @@ -69,6 +69,17 @@ export type TGetScimGroupDTO = { orgId: string; }; +export type TUpdateScimGroupNameDTO = { + groupId: string; + orgId: string; + displayName: string; +}; + +export type TDeleteScimGroupDTO = { + groupId: string; + orgId: string; +}; + export type TScimTokenJwtPayload = { scimTokenId: string; authTokenType: string;