Merge pull request #2364 from akhilmhdh/fix/scim-rfc

Resolved scim failing due to missing rfc cases
This commit is contained in:
Maidul Islam
2024-09-02 18:59:20 -04:00
committed by GitHub
9 changed files with 687 additions and 502 deletions

View File

@@ -79,6 +79,8 @@
"posthog-node": "^3.6.2", "posthog-node": "^3.6.2",
"probot": "^13.0.0", "probot": "^13.0.0",
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",
"scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10",
"smee-client": "^2.0.0", "smee-client": "^2.0.0",
"tedious": "^18.2.1", "tedious": "^18.2.1",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
@@ -15495,6 +15497,34 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="
}, },
"node_modules/scim-patch": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/scim-patch/-/scim-patch-0.8.3.tgz",
"integrity": "sha512-3d0wD4THAt03Zgi08kCQwc+lvPJ2v4wwk41b0xViVa4gLYSgRUCmGkJNBsaE+yoKg0fufTOJCcSrufZaqYn/og==",
"dependencies": {
"@types/node": "^22.0.0",
"fast-deep-equal": "3.1.3",
"scim2-parse-filter": "0.2.10"
}
},
"node_modules/scim-patch/node_modules/@types/node": {
"version": "22.5.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz",
"integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==",
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/scim-patch/node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/scim2-parse-filter": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/scim2-parse-filter/-/scim2-parse-filter-0.2.10.tgz",
"integrity": "sha512-k5TgGSuQEbR4jXRgw/GPAYVL9fMp1pWA2abLF5z3q9IGWSuZTqbrZBOSUezvc+rtViXr+czSZjg3eAN4QSTvxQ=="
},
"node_modules/secure-json-parse": { "node_modules/secure-json-parse": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",

View File

@@ -177,6 +177,8 @@
"posthog-node": "^3.6.2", "posthog-node": "^3.6.2",
"probot": "^13.0.0", "probot": "^13.0.0",
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",
"scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10",
"smee-client": "^2.0.0", "smee-client": "^2.0.0",
"tedious": "^18.2.1", "tedious": "^18.2.1",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",

View File

@@ -5,22 +5,47 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerScimRouter = async (server: FastifyZodProvider) => { const ScimUserSchema = z.object({
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => { schemas: z.array(z.string()),
try { id: z.string().trim(),
const strBody = body instanceof Buffer ? body.toString() : body; userName: z.string().trim(),
if (!strBody) { name: z
done(null, undefined); .object({
return; familyName: z.string().trim().optional(),
} givenName: z.string().trim().optional()
const json: unknown = JSON.parse(strBody); })
done(null, json); .optional(),
} catch (err) { emails: z
const error = err as Error; .array(
done(error, undefined); z.object({
} primary: z.boolean(),
}); value: z.string().email(),
type: z.string().trim()
})
)
.optional(),
displayName: z.string().trim(),
active: z.boolean()
});
const ScimGroupSchema = 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().optional()
})
)
.optional(),
meta: z.object({
resourceType: z.string().trim()
})
});
export const registerScimRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
url: "/scim-tokens", url: "/scim-tokens",
method: "POST", method: "POST",
@@ -127,25 +152,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
Resources: z.array( Resources: z.array(ScimUserSchema),
z.object({
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
),
itemsPerPage: z.number(), itemsPerPage: z.number(),
schemas: z.array(z.string()), schemas: z.array(z.string()),
startIndex: z.number(), startIndex: z.number(),
@@ -173,30 +180,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
orgMembershipId: z.string().trim() orgMembershipId: z.string().trim()
}), }),
response: { response: {
201: z.object({ 200: ScimUserSchema
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean(),
groups: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim()
})
)
})
} }
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -216,10 +200,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
schemas: z.array(z.string()), schemas: z.array(z.string()),
userName: z.string().trim(), userName: z.string().trim(),
name: z.object({ name: z
familyName: z.string().trim(), .object({
givenName: z.string().trim() familyName: z.string().trim().optional(),
}), givenName: z.string().trim().optional()
})
.optional(),
emails: z emails: z
.array( .array(
z.object({ z.object({
@@ -229,28 +215,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}) })
) )
.optional(), .optional(),
// displayName: z.string().trim(), active: z.boolean().default(true)
active: z.boolean()
}), }),
response: { response: {
200: z.object({ 200: ScimUserSchema
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
} }
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -260,8 +228,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const user = await req.server.services.scim.createScimUser({ const user = await req.server.services.scim.createScimUser({
externalId: req.body.userName, externalId: req.body.userName,
email: primaryEmail, email: primaryEmail,
firstName: req.body.name.givenName, firstName: req.body?.name?.givenName,
lastName: req.body.name.familyName, lastName: req.body?.name?.familyName,
orgId: req.permission.orgId orgId: req.permission.orgId
}); });
@@ -291,6 +259,116 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
url: "/Users/:orgMembershipId",
method: "PUT",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z
.object({
familyName: z.string().trim().optional(),
givenName: z.string().trim().optional()
})
.optional(),
displayName: z.string().trim(),
emails: z
.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
)
.optional(),
active: z.boolean()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
const user = await req.server.services.scim.replaceScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
firstName: req.body?.name?.givenName,
lastName: req.body?.name?.familyName,
active: req.body?.active,
email: primaryEmail,
externalId: req.body.userName
});
return user;
}
});
server.route({
url: "/Users/:orgMembershipId",
method: "PATCH",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
Operations: z.array(
z.union([
z.object({
op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim(),
value: z
.object({
value: z.string()
})
.array()
.optional()
}),
z.object({
op: z.union([z.literal("add"), z.literal("Add"), z.literal("replace"), z.literal("Replace")]),
path: z.string().trim().optional(),
value: z.any().optional()
})
])
)
}),
response: {
200: ScimUserSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.updateScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
operations: req.body.Operations
});
return user;
}
});
server.route({ server.route({
url: "/Groups", url: "/Groups",
method: "POST", method: "POST",
@@ -305,25 +383,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
display: z.string() display: z.string()
}) })
) )
.optional() // okta-specific .optional()
}), }),
response: { response: {
200: z.object({ 200: ScimGroupSchema
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z
.array(
z.object({
value: z.string(),
display: z.string()
})
)
.optional(),
meta: z.object({
resourceType: z.string().trim()
})
})
} }
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -344,26 +407,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
querystring: z.object({ querystring: z.object({
startIndex: z.coerce.number().default(1), startIndex: z.coerce.number().default(1),
count: z.coerce.number().default(20), count: z.coerce.number().default(20),
filter: z.string().trim().optional() filter: z.string().trim().optional(),
excludedAttributes: z.string().trim().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
Resources: z.array( Resources: z.array(ScimGroupSchema),
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()
})
})
),
itemsPerPage: z.number(), itemsPerPage: z.number(),
schemas: z.array(z.string()), schemas: z.array(z.string()),
startIndex: z.number(), startIndex: z.number(),
@@ -377,7 +426,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
orgId: req.permission.orgId, orgId: req.permission.orgId,
startIndex: req.query.startIndex, startIndex: req.query.startIndex,
filter: req.query.filter, filter: req.query.filter,
limit: req.query.count limit: req.query.count,
isMembersExcluded: req.query.excludedAttributes === "members"
}); });
return groups; return groups;
@@ -392,20 +442,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
groupId: z.string().trim() groupId: z.string().trim()
}), }),
response: { response: {
200: z.object({ 200: ScimGroupSchema
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]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@@ -414,6 +451,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
groupId: req.params.groupId, groupId: req.params.groupId,
orgId: req.permission.orgId orgId: req.permission.orgId
}); });
return group; return group;
} }
}); });
@@ -437,25 +475,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
) )
}), }),
response: { response: {
200: z.object({ 200: ScimGroupSchema
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]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
const group = await req.server.services.scim.updateScimGroupNamePut({ const group = await req.server.services.scim.replaceScimGroup({
groupId: req.params.groupId, groupId: req.params.groupId,
orgId: req.permission.orgId, orgId: req.permission.orgId,
...req.body ...req.body
@@ -476,55 +501,35 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
schemas: z.array(z.string()), schemas: z.array(z.string()),
Operations: z.array( Operations: z.array(
z.union([ z.union([
z.object({
op: z.union([z.literal("replace"), z.literal("Replace")]),
value: z.object({
id: z.string().trim(),
displayName: z.string().trim()
})
}),
z.object({ z.object({
op: z.union([z.literal("remove"), z.literal("Remove")]), op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim() path: z.string().trim(),
value: z
.object({
value: z.string()
})
.array()
.optional()
}), }),
z.object({ z.object({
op: z.union([z.literal("add"), z.literal("Add")]), op: z.union([z.literal("add"), z.literal("Add"), z.literal("replace"), z.literal("Replace")]),
path: z.string().trim(), path: z.string().trim().optional(),
value: z.array( value: z.any()
z.object({
value: z.string().trim(),
display: z.string().trim().optional()
})
)
}) })
]) ])
) )
}), }),
response: { response: {
200: z.object({ 200: ScimGroupSchema
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]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
const group = await req.server.services.scim.updateScimGroupNamePatch({ const group = await req.server.services.scim.updateScimGroup({
groupId: req.params.groupId, groupId: req.params.groupId,
orgId: req.permission.orgId, orgId: req.permission.orgId,
operations: req.body.Operations operations: req.body.Operations
}); });
return group; return group;
} }
}); });
@@ -550,60 +555,4 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
return group; return group;
} }
}); });
server.route({
url: "/Users/:orgMembershipId",
method: "PUT",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
displayName: z.string().trim(),
active: z.boolean()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean(),
groups: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.replaceScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
active: req.body.active
});
return user;
}
});
}; };

View File

@@ -44,19 +44,18 @@ export const buildScimUser = ({
email, email,
firstName, firstName,
lastName, lastName,
groups = [], active,
active createdAt,
updatedAt
}: { }: {
orgMembershipId: string; orgMembershipId: string;
username: string; username: string;
email?: string | null; email?: string | null;
firstName: string | null | undefined; firstName: string | null | undefined;
lastName: string | null | undefined; lastName: string | null | undefined;
groups?: {
value: string;
display: string;
}[];
active: boolean; active: boolean;
createdAt: Date;
updatedAt: Date;
}): TScimUser => { }): TScimUser => {
const scimUser = { const scimUser = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
@@ -78,10 +77,10 @@ export const buildScimUser = ({
] ]
: [], : [],
active, active,
groups,
meta: { meta: {
resourceType: "User", resourceType: "User",
location: null created: createdAt,
lastModified: updatedAt
} }
}; };
@@ -109,14 +108,18 @@ export const buildScimGroupList = ({
export const buildScimGroup = ({ export const buildScimGroup = ({
groupId, groupId,
name, name,
members members,
updatedAt,
createdAt
}: { }: {
groupId: string; groupId: string;
name: string; name: string;
members: { members: {
value: string; value: string;
display: string; display?: string;
}[]; }[];
createdAt: Date;
updatedAt: Date;
}): TScimGroup => { }): TScimGroup => {
const scimGroup = { const scimGroup = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"], schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
@@ -125,7 +128,8 @@ export const buildScimGroup = ({
members, members,
meta: { meta: {
resourceType: "Group", resourceType: "Group",
location: null created: createdAt,
lastModified: updatedAt
} }
}; };

View File

@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { scimPatch } from "scim-patch";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
@@ -9,7 +10,6 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal"; import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
@@ -32,14 +32,7 @@ import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
buildScimGroup,
buildScimGroupList,
buildScimUser,
buildScimUserList,
extractScimValueFromPath,
parseScimFilter
} from "./scim-fns";
import { import {
TCreateScimGroupDTO, TCreateScimGroupDTO,
TCreateScimTokenDTO, TCreateScimTokenDTO,
@@ -64,12 +57,18 @@ type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">; scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick< userDAL: Pick<
TUserDALFactory, TUserDALFactory,
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" "find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById"
>; >;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete">; userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete" | "update">;
orgDAL: Pick< orgDAL: Pick<
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById" | "createMembership"
| "findById"
| "findMembership"
| "findMembershipWithScimFilter"
| "deleteMembershipById"
| "transaction"
| "updateMembershipById"
>; >;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
@@ -193,7 +192,12 @@ export const scimServiceFactory = ({
}; };
// SCIM server endpoints // 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); const org = await orgDAL.findById(orgId);
if (!org.scimEnabled) if (!org.scimEnabled)
@@ -207,23 +211,20 @@ export const scimServiceFactory = ({
...(limit && { limit }) ...(limit && { limit })
}; };
const users = await orgDAL.findMembership( const users = await orgDAL.findMembershipWithScimFilter(orgId, filter, findOpts);
{
[`${TableName.OrgMembership}.orgId` as "id"]: orgId,
...parseScimFilter(filter)
},
findOpts
);
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email, isActive }) => const scimUsers = users.map(
buildScimUser({ ({ id, externalId, username, firstName, lastName, email, isActive, createdAt, updatedAt }) =>
orgMembershipId: id ?? "", buildScimUser({
username: externalId ?? username, orgMembershipId: id ?? "",
firstName: firstName ?? "", username: externalId ?? username,
lastName: lastName ?? "", firstName: firstName ?? "",
email, lastName: lastName ?? "",
active: isActive email,
}) active: isActive,
createdAt,
updatedAt
})
); );
return buildScimUserList({ return buildScimUserList({
@@ -258,11 +259,6 @@ export const scimServiceFactory = ({
status: 403 status: 403
}); });
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
return buildScimUser({ return buildScimUser({
orgMembershipId: membership.id, orgMembershipId: membership.id,
username: membership.externalId ?? membership.username, username: membership.externalId ?? membership.username,
@@ -270,10 +266,8 @@ export const scimServiceFactory = ({
firstName: membership.firstName, firstName: membership.firstName,
lastName: membership.lastName, lastName: membership.lastName,
active: membership.isActive, active: membership.isActive,
groups: groupMembershipsInOrg.map((group) => ({ createdAt: membership.createdAt,
value: group.groupId, updatedAt: membership.updatedAt
display: group.groupName
}))
}); });
}; };
@@ -322,7 +316,7 @@ export const scimServiceFactory = ({
userId: userAlias.userId, userId: userAlias.userId,
inviteEmail: email, inviteEmail: email,
orgId, orgId,
role: OrgMembershipRole.Member, role: OrgMembershipRole.NoAccess,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true
}, },
@@ -349,7 +343,11 @@ export const scimServiceFactory = ({
} }
if (!user) { if (!user) {
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL); const uniqueUsername = await normalizeUsername(
// external id is username
`${firstName}-${lastName}`,
userDAL
);
user = await userDAL.create( user = await userDAL.create(
{ {
username: serverCfg.trustSamlEmails ? email : uniqueUsername, username: serverCfg.trustSamlEmails ? email : uniqueUsername,
@@ -430,10 +428,13 @@ export const scimServiceFactory = ({
firstName: createdUser.firstName, firstName: createdUser.firstName,
lastName: createdUser.lastName, lastName: createdUser.lastName,
email: createdUser.email ?? "", email: createdUser.email ?? "",
active: createdOrgMembership.isActive active: createdOrgMembership.isActive,
createdAt: createdOrgMembership.createdAt,
updatedAt: createdOrgMembership.updatedAt
}); });
}; };
// partial
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => { const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
@@ -459,37 +460,52 @@ export const scimServiceFactory = ({
status: 403 status: 403
}); });
let active = true; const scimUser = buildScimUser({
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({
orgMembershipId: membership.id, orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email, email: membership.email,
firstName: membership.firstName,
lastName: membership.lastName, lastName: membership.lastName,
active firstName: membership.firstName,
active: membership.isActive,
username: membership.externalId ?? 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,
externalId
}: TReplaceScimUserDTO) => {
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
@@ -514,26 +530,47 @@ export const scimServiceFactory = ({
status: 403 status: 403
}); });
await orgMembershipDAL.updateById(membership.id, { const serverCfg = await getServerCfg();
isActive: active await userDAL.transaction(async (tx) => {
await userAliasDAL.update(
{
orgId,
aliasType: UserAliasType.SAML,
userId: membership.userId
},
{
externalId
},
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({ return buildScimUser({
orgMembershipId: membership.id, orgMembershipId: membership.id,
username: membership.externalId ?? membership.username, username: externalId,
email: membership.email, email: membership.email,
firstName: membership.firstName, firstName: membership.firstName,
lastName: membership.lastName, lastName: membership.lastName,
active, active,
groups: groupMembershipsInOrg.map((group) => ({ createdAt: membership.createdAt,
value: group.groupId, updatedAt: membership.updatedAt
display: group.groupName
}))
}); });
}; };
@@ -570,7 +607,7 @@ export const scimServiceFactory = ({
return {}; // intentionally return empty object upon success return {}; // intentionally return empty object upon success
}; };
const listScimGroups = async ({ orgId, startIndex, limit, filter }: TListScimGroupsDTO) => { const listScimGroups = async ({ orgId, startIndex, limit, filter, isMembersExcluded }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (!plan.groups) if (!plan.groups)
throw new BadRequestError({ throw new BadRequestError({
@@ -603,6 +640,21 @@ export const scimServiceFactory = ({
); );
const scimGroups: TScimGroup[] = []; const scimGroups: TScimGroup[] = [];
if (isMembersExcluded) {
return buildScimGroupList({
scimGroups: groups.map((group) =>
buildScimGroup({
groupId: group.id,
name: group.name,
members: [],
createdAt: group.createdAt,
updatedAt: group.updatedAt
})
),
startIndex,
limit
});
}
for await (const group of groups) { for await (const group of groups) {
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId); const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
@@ -612,7 +664,9 @@ export const scimServiceFactory = ({
members: members.map((member) => ({ members: members.map((member) => ({
value: member.orgMembershipId, value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}` display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
})) })),
createdAt: group.createdAt,
updatedAt: group.updatedAt
}); });
scimGroups.push(scimGroup); scimGroups.push(scimGroup);
} }
@@ -696,7 +750,9 @@ export const scimServiceFactory = ({
members: orgMemberships.map(({ id, firstName, lastName }) => ({ members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id, value: id,
display: `${firstName} ${lastName}` display: `${firstName} ${lastName}`
})) })),
createdAt: newGroup.group.createdAt,
updatedAt: newGroup.group.updatedAt
}); });
}; };
@@ -739,31 +795,17 @@ export const scimServiceFactory = ({
members: orgMemberships.map(({ id, firstName, lastName }) => ({ members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id, value: id,
display: `${firstName} ${lastName}` display: `${firstName} ${lastName}`
})) })),
createdAt: group.createdAt,
updatedAt: group.updatedAt
}); });
}; };
const updateScimGroupNamePut = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => { const $replaceGroupDAL = async (
const plan = await licenseService.getPlan(orgId); groupId: string,
if (!plan.groups) orgId: string,
throw new BadRequestError({ { displayName, members = [] }: { displayName: string; members: { value: string }[] }
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group." ) => {
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const updatedGroup = await groupDAL.transaction(async (tx) => { const updatedGroup = await groupDAL.transaction(async (tx) => {
const [group] = await groupDAL.update( const [group] = await groupDAL.update(
{ {
@@ -782,74 +824,96 @@ export const scimServiceFactory = ({
}); });
} }
if (members) { const orgMemberships = members.length
const orgMemberships = await orgMembershipDAL.find({ ? await orgMembershipDAL.find({
$in: { $in: {
id: members.map((member) => member.value) id: members.map((member) => member.value)
} }
})
: [];
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
const userGroupMembers = await userGroupMembershipDAL.find({
groupId: group.id
});
const directMemberUserIds = userGroupMembers.filter((el) => !el.isPending).map((membership) => membership.userId);
const pendingGroupAdditionsUserIds = userGroupMembers
.filter((el) => el.isPending)
.map((pendingGroupAddition) => pendingGroupAddition.userId);
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) {
await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
}); });
}
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId)); if (toRemoveUserIds.length) {
await removeUsersFromGroupByUserIds({
const directMemberUserIds = ( group,
await userGroupMembershipDAL.find({ userIds: toRemoveUserIds,
groupId: group.id, userDAL,
isPending: false userGroupMembershipDAL,
}) groupProjectDAL,
).map((membership) => membership.userId); projectKeyDAL,
tx
const pendingGroupAdditionsUserIds = ( });
await userGroupMembershipDAL.find({
groupId: group.id,
isPending: true
})
).map((pendingGroupAddition) => pendingGroupAddition.userId);
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) {
await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
if (toRemoveUserIds.length) {
await removeUsersFromGroupByUserIds({
group,
userIds: toRemoveUserIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx
});
}
} }
return group; return group;
}); });
return updatedGroup;
};
const replaceScimGroup = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const updatedGroup = await $replaceGroupDAL(groupId, orgId, { displayName, members });
return buildScimGroup({ return buildScimGroup({
groupId: updatedGroup.id, groupId: updatedGroup.id,
name: updatedGroup.name, name: updatedGroup.name,
members members,
updatedAt: updatedGroup.updatedAt,
createdAt: updatedGroup.createdAt
}); });
}; };
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => { const updateScimGroup = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (!plan.groups) if (!plan.groups)
throw new BadRequestError({ throw new BadRequestError({
@@ -871,7 +935,7 @@ export const scimServiceFactory = ({
status: 403 status: 403
}); });
let group = await groupDAL.findOne({ const group = await groupDAL.findOne({
id: groupId, id: groupId,
orgId orgId
}); });
@@ -883,64 +947,28 @@ export const scimServiceFactory = ({
}); });
} }
for await (const operation of operations) {
if (operation.op === "replace" || operation.op === "Replace") {
group = await groupDAL.updateById(group.id, {
name: operation.value.displayName
});
} else if (operation.op === "add" || operation.op === "Add") {
try {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: operation.value.map((member) => member.value)
}
});
await addUsersToGroupByUserIds({
group,
userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
});
} catch {
logger.info("Repeat SCIM user-group add operation");
}
} else if (operation.op === "remove" || operation.op === "Remove") {
const orgMembershipId = extractScimValueFromPath(operation.path);
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
await removeUsersFromGroupByUserIds({
group,
userIds: [orgMembership.userId as string],
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL
});
} else {
throw new ScimRequestError({
detail: "Invalid Operation",
status: 400
});
}
}
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId); const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
const scimGroup = buildScimGroup({
return buildScimGroup({
groupId: group.id, groupId: group.id,
name: group.name, name: group.name,
members: members.map((member) => ({ members: members.map((member) => ({
value: member.orgMembershipId
})),
createdAt: group.createdAt,
updatedAt: group.updatedAt
});
scimPatch(scimGroup, operations);
// remove members is a weird case not following scim convention
await $replaceGroupDAL(groupId, orgId, { displayName: scimGroup.displayName, members: scimGroup.members });
const updatedScimMembers = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
return {
...scimGroup,
members: updatedScimMembers.map((member) => ({
value: member.orgMembershipId, value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}` display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
})) }))
}); };
}; };
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => { const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
@@ -1016,8 +1044,8 @@ export const scimServiceFactory = ({
createScimGroup, createScimGroup,
getScimGroup, getScimGroup,
deleteScimGroup, deleteScimGroup,
updateScimGroupNamePut, replaceScimGroup,
updateScimGroupNamePatch, updateScimGroup,
fnValidateScimToken fnValidateScimToken
}; };
}; };

View File

@@ -1,3 +1,5 @@
import { ScimPatchOperation } from "scim-patch";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
export type TCreateScimTokenDTO = { export type TCreateScimTokenDTO = {
@@ -34,29 +36,25 @@ export type TGetScimUserDTO = {
export type TCreateScimUserDTO = { export type TCreateScimUserDTO = {
externalId: string; externalId: string;
email?: string; email?: string;
firstName: string; firstName?: string;
lastName: string; lastName?: string;
orgId: string; orgId: string;
}; };
export type TUpdateScimUserDTO = { export type TUpdateScimUserDTO = {
orgMembershipId: string; orgMembershipId: string;
orgId: string; orgId: string;
operations: { operations: ScimPatchOperation[];
op: string;
path?: string;
value?:
| string
| {
active: boolean;
};
}[];
}; };
export type TReplaceScimUserDTO = { export type TReplaceScimUserDTO = {
orgMembershipId: string; orgMembershipId: string;
active: boolean; active: boolean;
orgId: string; orgId: string;
email?: string;
firstName?: string;
lastName?: string;
externalId: string;
}; };
export type TDeleteScimUserDTO = { export type TDeleteScimUserDTO = {
@@ -69,6 +67,7 @@ export type TListScimGroupsDTO = {
filter?: string; filter?: string;
limit: number; limit: number;
orgId: string; orgId: string;
isMembersExcluded?: boolean;
}; };
export type TListScimGroups = { export type TListScimGroups = {
@@ -107,31 +106,7 @@ export type TUpdateScimGroupNamePutDTO = {
export type TUpdateScimGroupNamePatchDTO = { export type TUpdateScimGroupNamePatchDTO = {
groupId: string; groupId: string;
orgId: string; orgId: string;
operations: (TRemoveOp | TReplaceOp | TAddOp)[]; operations: ScimPatchOperation[];
};
// akhilmhdh: I know, this is done due to lack of time. Need to change later to support as normalized rather than like this
// Forgive akhil blame tony
type TReplaceOp = {
op: "replace" | "Replace";
value: {
id: string;
displayName: string;
};
};
type TRemoveOp = {
op: "remove" | "Remove";
path: string;
};
type TAddOp = {
op: "add" | "Add";
path: string;
value: {
value: string;
display?: string;
}[];
}; };
export type TDeleteScimGroupDTO = { export type TDeleteScimGroupDTO = {
@@ -160,13 +135,10 @@ export type TScimUser = {
type: string; type: string;
}[]; }[];
active: boolean; active: boolean;
groups: {
value: string;
display: string;
}[];
meta: { meta: {
resourceType: string; resourceType: string;
location: null; created: Date;
lastModified: Date;
}; };
}; };
@@ -176,10 +148,11 @@ export type TScimGroup = {
displayName: string; displayName: string;
members: { members: {
value: string; value: string;
display: string; display?: string;
}[]; }[];
meta: { meta: {
resourceType: string; resourceType: string;
location: null; created: Date;
lastModified: Date;
}; };
}; };

View File

@@ -0,0 +1,121 @@
import { Knex } from "knex";
import { Compare, Filter, parse } from "scim2-parse-filter";
const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") {
return { ...filter, attrPath: `${parentPath}.${(filter as Compare).attrPath}` };
}
return filter;
};
export const generateKnexQueryFromScim = (
rootQuery: Knex.QueryBuilder,
rootScimFilter: string,
getAttributeField: (attr: string) => string | null
) => {
const scimRootFilterAst = parse(rootScimFilter);
const stack = [
{
scimFilterAst: scimRootFilterAst,
query: rootQuery
}
];
while (stack.length) {
const { scimFilterAst, query } = stack.pop()!;
switch (scimFilterAst.op) {
case "eq": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, scimFilterAst.compValue);
break;
}
case "pr": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereNotNull(attrPath);
break;
}
case "gt": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, ">", scimFilterAst.compValue);
break;
}
case "ge": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, ">=", scimFilterAst.compValue);
break;
}
case "lt": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, "<", scimFilterAst.compValue);
break;
}
case "le": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, "<=", scimFilterAst.compValue);
break;
}
case "sw": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `${scimFilterAst.compValue}%`);
break;
}
case "ew": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}`);
break;
}
case "co": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}%`);
break;
}
case "ne": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereNot(attrPath, "=", scimFilterAst.compValue);
break;
}
case "and": {
void query.andWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
});
});
break;
}
case "or": {
void query.orWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
});
});
break;
}
case "not": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: scimFilterAst.filter
});
});
break;
}
case "[]": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter)
});
});
break;
}
default:
break;
}
}
};

View File

@@ -49,6 +49,21 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
server.setValidatorCompiler(validatorCompiler); server.setValidatorCompiler(validatorCompiler);
server.setSerializerCompiler(serializerCompiler); server.setSerializerCompiler(serializerCompiler);
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try {
const strBody = body instanceof Buffer ? body.toString() : body;
if (!strBody) {
done(null, undefined);
return;
}
const json: unknown = JSON.parse(strBody);
done(null, json);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
try { try {
await server.register<FastifyCookieOptions>(cookie, { await server.register<FastifyCookieOptions>(cookie, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY secret: appCfg.COOKIE_SECRET_SIGN_KEY

View File

@@ -12,6 +12,7 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, withTransaction } from "@app/lib/knex"; import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, withTransaction } from "@app/lib/knex";
import { generateKnexQueryFromScim } from "@app/lib/knex/scim";
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>; export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
@@ -280,6 +281,67 @@ export const orgDALFactory = (db: TDbClient) => {
.select( .select(
selectAllTableCols(TableName.OrgMembership), selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization),
db.ref("externalId").withSchema(TableName.UserAliases)
)
.where({ isGhost: false });
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const res = await query;
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
};
const findMembershipWithScimFilter = async (
orgId: string,
scimFilter: string | undefined,
{ offset, limit, sort, tx }: TFindOpt<TOrgMemberships> = {}
) => {
try {
const query = (tx || db.replicaNode())(TableName.OrgMembership)
// eslint-disable-next-line
.where(`${TableName.OrgMembership}.orgId`, orgId)
.where((qb) => {
if (scimFilter) {
void generateKnexQueryFromScim(qb, scimFilter, (attrPath) => {
switch (attrPath) {
case "active":
return `${TableName.OrgMembership}.isActive`;
case "userName":
return `${TableName.UserAliases}.externalId`;
case "name.givenName":
return `${TableName.Users}.firstName`;
case "name.familyName":
return `${TableName.Users}.lastName`;
case "email.value":
return `${TableName.Users}.email`;
default:
return null;
}
});
}
})
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
.leftJoin(TableName.UserAliases, function joinUserAlias() {
this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`)
.andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`)
.andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"]));
})
.select(
selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users),
@@ -314,6 +376,7 @@ export const orgDALFactory = (db: TDbClient) => {
updateById, updateById,
deleteById, deleteById,
findMembership, findMembership,
findMembershipWithScimFilter,
createMembership, createMembership,
updateMembershipById, updateMembershipById,
deleteMembershipById, deleteMembershipById,