mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #2364 from akhilmhdh/fix/scim-rfc
Resolved scim failing due to missing rfc cases
This commit is contained in:
30
backend/package-lock.json
generated
30
backend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
121
backend/src/lib/knex/scim.ts
Normal file
121
backend/src/lib/knex/scim.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user