diff --git a/backend/src/db/migrations/20240318183910_group.ts b/backend/src/db/migrations/20240318183910_group.ts index 1c39a6dba..672b575a6 100644 --- a/backend/src/db/migrations/20240318183910_group.ts +++ b/backend/src/db/migrations/20240318183910_group.ts @@ -25,7 +25,7 @@ export async function up(knex: Knex): Promise { await knex.schema.createTable(TableName.UserGroupMembership, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); // link to user and link to groups cascade on groups t.uuid("userId").notNullable(); - t.foreign("userId").references("id").inTable(TableName.Users); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); t.uuid("groupId").notNullable(); t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE"); t.timestamps(true, true, true); diff --git a/backend/src/ee/routes/v1/scim-router.ts b/backend/src/ee/routes/v1/scim-router.ts index e3424b6a9..ca5d6ac26 100644 --- a/backend/src/ee/routes/v1/scim-router.ts +++ b/backend/src/ee/routes/v1/scim-router.ts @@ -250,40 +250,81 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { } }); + // TODO server.route({ - url: "/Users/:userId", - method: "PATCH", + url: "/Groups", + method: "POST", schema: { - params: z.object({ - userId: 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() - ]) - }) - ) + 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.any()).length(0), + meta: z.object({ + resourceType: z.string().trim() + }) + }) + } + }, + 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", + schema: { + 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({}) } }, onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), handler: async (req) => { - const user = await req.server.services.scim.updateScimUser({ - userId: req.params.userId, - orgId: req.permission.orgId, - operations: req.body.Operations + console.log("get scim group endpoint start"); + const group = await req.server.services.scim.getScimGroup({ + groupId: req.params.groupId, + orgId: req.permission.orgId }); - return user; + console.log("get scim group endpoint end: ", group); + return group; } }); diff --git a/backend/src/ee/services/license/__mocks__/licence-fns.ts b/backend/src/ee/services/license/__mocks__/licence-fns.ts index 04c8e59dd..45ee51351 100644 --- a/backend/src/ee/services/license/__mocks__/licence-fns.ts +++ b/backend/src/ee/services/license/__mocks__/licence-fns.ts @@ -17,8 +17,8 @@ export const getDefaultOnPremFeatures = () => { customAlerts: false, auditLogs: false, auditLogsRetentionDays: 0, - samlSSO: false, - scim: false, + samlSSO: true, + scim: true, ldap: false, groups: true, status: null, diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 1097f06fb..17a538881 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -24,8 +24,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ customAlerts: false, auditLogs: false, auditLogsRetentionDays: 0, - samlSSO: false, - scim: false, + samlSSO: true, + scim: true, ldap: false, groups: true, status: null, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 9ed8e5da9..efc1c18dd 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -40,8 +40,8 @@ export type TFeatureSet = { customAlerts: false; auditLogs: false; auditLogsRetentionDays: 0; - samlSSO: false; - scim: false; + samlSSO: true; + scim: true; ldap: false; groups: true; status: null; diff --git a/backend/src/ee/services/scim/scim-fns.ts b/backend/src/ee/services/scim/scim-fns.ts index 8b68870da..bb777e62d 100644 --- a/backend/src/ee/services/scim/scim-fns.ts +++ b/backend/src/ee/services/scim/scim-fns.ts @@ -62,3 +62,28 @@ export const buildScimUser = ({ return scimUser; }; + +export const buildScimGroup = ({ + groupId, + name, + members +}: { + groupId: string; + name: string; + members: { + value: string; + display: string; + }[]; +}) => { + const scimGroup = { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"], + id: groupId, + displayName: name, + members, + meta: { + resourceType: "Group" + } + }; + + return scimGroup; +}; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 6ed594777..ebfa0d6ac 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -1,10 +1,13 @@ import { ForbiddenError } from "@casl/ability"; +import slugify from "@sindresorhus/slugify"; import jwt from "jsonwebtoken"; import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas"; +import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TScimDALFactory } from "@app/ee/services/scim/scim-dal"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TOrgPermission } from "@app/lib/types"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TOrgDALFactory } from "@app/services/org/org-dal"; @@ -17,11 +20,13 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { buildScimUser, buildScimUserList } from "./scim-fns"; +import { buildScimGroup, buildScimUser, buildScimUserList } from "./scim-fns"; import { + TCreateScimGroupDTO, TCreateScimTokenDTO, TCreateScimUserDTO, TDeleteScimTokenDTO, + TGetScimGroupDTO, TGetScimUserDTO, TListScimUsers, TListScimUsersDTO, @@ -39,6 +44,7 @@ type TScimServiceFactoryDep = { >; projectDAL: Pick; projectMembershipDAL: Pick; + groupDAL: Pick; licenseService: Pick; permissionService: Pick; smtpService: TSmtpService; @@ -53,6 +59,7 @@ export const scimServiceFactory = ({ orgDAL, projectDAL, projectMembershipDAL, + groupDAL, permissionService, smtpService }: TScimServiceFactoryDep) => { @@ -423,6 +430,59 @@ 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)}`), + orgId, + role: OrgMembershipRole.NoAccess + }); + + console.log("createScimGroup the group: ", group); + + return buildScimGroup({ + groupId: group.id, + name: group.name, + members: [] + }); + }; + + 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); + + // TODO: get members + + return buildScimGroup({ + groupId: group.id, + name: group.name, + members: users + .filter((user) => user.isPartOfGroup) + .map((user) => ({ + value: user.id, + display: `${user.firstName} ${user.lastName}` + })) + }); + }; + const fnValidateScimToken = async (token: TScimTokenJwtPayload) => { const scimToken = await scimDAL.findById(token.scimTokenId); if (!scimToken) throw new UnauthorizedError(); @@ -455,6 +515,8 @@ export const scimServiceFactory = ({ createScimUser, updateScimUser, replaceScimUser, + createScimGroup, + getScimGroup, fnValidateScimToken }; }; diff --git a/backend/src/ee/services/scim/scim-types.ts b/backend/src/ee/services/scim/scim-types.ts index c99dec794..6a04cedd0 100644 --- a/backend/src/ee/services/scim/scim-types.ts +++ b/backend/src/ee/services/scim/scim-types.ts @@ -59,6 +59,16 @@ export type TReplaceScimUserDTO = { orgId: string; }; +export type TCreateScimGroupDTO = { + displayName: string; + orgId: string; +}; + +export type TGetScimGroupDTO = { + groupId: string; + orgId: string; +}; + export type TScimTokenJwtPayload = { scimTokenId: string; authTokenType: string; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index ca800533d..7f7ddff8a 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -282,6 +282,7 @@ export const registerRoutes = async ( orgDAL, projectDAL, projectMembershipDAL, + groupDAL, permissionService, smtpService });