Begin add push groups SCIM

This commit is contained in:
Tuan Dang
2024-04-01 11:30:25 -07:00
parent 0c2e566184
commit f0b84d5bc9
9 changed files with 169 additions and 30 deletions

View File

@@ -25,7 +25,7 @@ export async function up(knex: Knex): Promise<void> {
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);

View File

@@ -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;
}
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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<TProjectDALFactory, "find">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "findAllGroupMembers">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
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
};
};

View File

@@ -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;

View File

@@ -282,6 +282,7 @@ export const registerRoutes = async (
orgDAL,
projectDAL,
projectMembershipDAL,
groupDAL,
permissionService,
smtpService
});