feat: updated scim user endpoint

This commit is contained in:
=
2024-09-02 13:50:55 +05:30
parent 7c540b6be8
commit 929dc059c3
9 changed files with 426 additions and 153 deletions
+7 -2
View File
@@ -45,7 +45,9 @@ export const buildScimUser = ({
firstName,
lastName,
groups = [],
active
active,
createdAt,
updatedAt
}: {
orgMembershipId: string;
username: string;
@@ -57,6 +59,8 @@ export const buildScimUser = ({
display: string;
}[];
active: boolean;
createdAt: Date;
updatedAt: Date;
}): TScimUser => {
const scimUser = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -81,7 +85,8 @@ export const buildScimUser = ({
groups,
meta: {
resourceType: "User",
location: null
created: createdAt,
lastModified: updatedAt
}
};
+100 -58
View File
@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken";
import { scimPatch } from "scim-patch";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
@@ -64,12 +65,18 @@ type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick<
TUserDALFactory,
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById"
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById"
| "createMembership"
| "findById"
| "findMembership"
| "findMembershipWithScimFilter"
| "deleteMembershipById"
| "transaction"
| "updateMembershipById"
>;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
@@ -193,7 +200,12 @@ export const scimServiceFactory = ({
};
// SCIM server endpoints
const listScimUsers = async ({ startIndex, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
const listScimUsers = async ({
startIndex = 0,
limit = 100,
filter,
orgId
}: TListScimUsersDTO): Promise<TListScimUsers> => {
const org = await orgDAL.findById(orgId);
if (!org.scimEnabled)
@@ -207,23 +219,20 @@ export const scimServiceFactory = ({
...(limit && { limit })
};
const users = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "id"]: orgId,
...parseScimFilter(filter)
},
findOpts
);
const users = await orgDAL.findMembershipWithScimFilter(orgId, filter, findOpts);
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email, isActive }) =>
buildScimUser({
orgMembershipId: id ?? "",
username: externalId ?? username,
firstName: firstName ?? "",
lastName: lastName ?? "",
email,
active: isActive
})
const scimUsers = users.map(
({ id, externalId, username, firstName, lastName, email, isActive, createdAt, updatedAt }) =>
buildScimUser({
orgMembershipId: id ?? "",
username: externalId ?? username,
firstName: firstName ?? "",
lastName: lastName ?? "",
email,
active: isActive,
createdAt,
updatedAt
})
);
return buildScimUserList({
@@ -273,7 +282,9 @@ export const scimServiceFactory = ({
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
display: group.groupName
}))
})),
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
};
@@ -349,7 +360,11 @@ export const scimServiceFactory = ({
}
if (!user) {
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL);
const uniqueUsername = await normalizeUsername(
// external id is username
`${firstName}-${lastName}`,
userDAL
);
user = await userDAL.create(
{
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
@@ -430,10 +445,13 @@ export const scimServiceFactory = ({
firstName: createdUser.firstName,
lastName: createdUser.lastName,
email: createdUser.email ?? "",
active: createdOrgMembership.isActive
active: createdOrgMembership.isActive,
createdAt: createdOrgMembership.createdAt,
updatedAt: createdOrgMembership.updatedAt
});
};
// partial
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
@@ -459,37 +477,51 @@ export const scimServiceFactory = ({
status: 403
});
let active = true;
operations.forEach((operation) => {
if (operation.op.toLowerCase() === "replace") {
if (operation.path === "active" && operation.value === "False") {
// azure scim op format
active = false;
} else if (typeof operation.value === "object" && operation.value.active === false) {
// okta scim op format
active = false;
}
}
});
if (!active) {
await orgMembershipDAL.updateById(membership.id, {
isActive: false
});
}
return buildScimUser({
const scimUser = buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email,
firstName: membership.firstName,
lastName: membership.lastName,
active
firstName: membership.firstName,
active: membership.isActive,
username: membership.username,
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
scimPatch(scimUser, operations);
const serverCfg = await getServerCfg();
await userDAL.transaction(async (tx) => {
await orgMembershipDAL.updateById(
membership.id,
{
isActive: scimUser.active
},
tx
);
const hasEmailChanged = scimUser.emails[0].value !== membership.email;
await userDAL.updateById(
membership.userId,
{
firstName: scimUser.name.givenName,
email: scimUser.emails[0].value,
lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? serverCfg.trustSamlEmails : true
},
tx
);
});
return scimUser;
};
const replaceScimUser = async ({ orgMembershipId, active, orgId }: TReplaceScimUserDTO) => {
const replaceScimUser = async ({
orgMembershipId,
active,
orgId,
lastName,
firstName,
email
}: TReplaceScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
@@ -514,15 +546,27 @@ export const scimServiceFactory = ({
status: 403
});
await orgMembershipDAL.updateById(membership.id, {
isActive: active
const serverCfg = await getServerCfg();
await userDAL.transaction(async (tx) => {
await orgMembershipDAL.updateById(
membership.id,
{
isActive: active
},
tx
);
await userDAL.updateById(
membership.userId,
{
firstName,
email,
lastName,
isEmailVerified: serverCfg.trustSamlEmails
},
tx
);
});
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
return buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
@@ -530,10 +574,8 @@ export const scimServiceFactory = ({
firstName: membership.firstName,
lastName: membership.lastName,
active,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
display: group.groupName
}))
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
};
+10 -12
View File
@@ -1,3 +1,5 @@
import { ScimPatchOperation } from "scim-patch";
import { TOrgPermission } from "@app/lib/types";
export type TCreateScimTokenDTO = {
@@ -34,29 +36,24 @@ export type TGetScimUserDTO = {
export type TCreateScimUserDTO = {
externalId: string;
email?: string;
firstName: string;
lastName: string;
firstName?: string;
lastName?: string;
orgId: string;
};
export type TUpdateScimUserDTO = {
orgMembershipId: string;
orgId: string;
operations: {
op: string;
path?: string;
value?:
| string
| {
active: boolean;
};
}[];
operations: ScimPatchOperation[];
};
export type TReplaceScimUserDTO = {
orgMembershipId: string;
active: boolean;
orgId: string;
email?: string;
firstName?: string;
lastName?: string;
};
export type TDeleteScimUserDTO = {
@@ -166,7 +163,8 @@ export type TScimUser = {
}[];
meta: {
resourceType: string;
location: null;
created: Date;
lastModified: Date;
};
};