-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Enhance SCIM payload handling and attribute creation #18427
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,14 +3,15 @@ import { describe, expect, it } from "vitest"; | |
|
||
import getAttributesFromScimPayload from "../getAttributesFromScimPayload"; | ||
|
||
const directoryId = "xxx-xxx-xxx-xxx"; | ||
describe("getAttributesFromScimPayload", () => { | ||
it("should return empty object for unsupported events", () => { | ||
const event = { | ||
event: "user.deleted", | ||
data: { raw: { schemas: [] } }, | ||
} as DirectorySyncEvent; | ||
|
||
const result = getAttributesFromScimPayload(event); | ||
const result = getAttributesFromScimPayload({ event, directoryId }); | ||
expect(result).toEqual({}); | ||
}); | ||
|
||
|
@@ -28,7 +29,7 @@ describe("getAttributesFromScimPayload", () => { | |
}, | ||
} as DirectorySyncEvent; | ||
|
||
const result = getAttributesFromScimPayload(event); | ||
const result = getAttributesFromScimPayload({ event, directoryId }); | ||
expect(result).toEqual({ | ||
department: "Engineering", | ||
title: "Software Engineer", | ||
|
@@ -48,7 +49,7 @@ describe("getAttributesFromScimPayload", () => { | |
}, | ||
} as DirectorySyncEvent; | ||
|
||
const result = getAttributesFromScimPayload(event); | ||
const result = getAttributesFromScimPayload({ event, directoryId }); | ||
expect(result).toEqual({ | ||
skills: ["JavaScript", "TypeScript", "React"], | ||
}); | ||
|
@@ -68,7 +69,7 @@ describe("getAttributesFromScimPayload", () => { | |
}, | ||
} as DirectorySyncEvent; | ||
|
||
const result = getAttributesFromScimPayload(event); | ||
const result = getAttributesFromScimPayload({ event, directoryId }); | ||
expect(result).toEqual({ | ||
department: "Engineering", | ||
}); | ||
|
@@ -87,7 +88,7 @@ describe("getAttributesFromScimPayload", () => { | |
}, | ||
} as DirectorySyncEvent; | ||
|
||
const result = getAttributesFromScimPayload(event); | ||
const result = getAttributesFromScimPayload({ event, directoryId }); | ||
expect(result).toEqual({}); | ||
}); | ||
|
||
|
@@ -108,7 +109,7 @@ describe("getAttributesFromScimPayload", () => { | |
}, | ||
} as DirectorySyncEvent; | ||
|
||
const result = getAttributesFromScimPayload(event); | ||
const result = getAttributesFromScimPayload({ event, directoryId }); | ||
expect(result).toEqual({ | ||
department: "Engineering", | ||
location: "Remote", | ||
|
@@ -128,13 +129,13 @@ describe("getAttributesFromScimPayload", () => { | |
}, | ||
} as DirectorySyncEvent; | ||
|
||
const result = getAttributesFromScimPayload(event); | ||
const result = getAttributesFromScimPayload({ event, directoryId }); | ||
expect(result).toEqual({ | ||
department: "Engineering", | ||
}); | ||
}); | ||
|
||
it("should extract from core namespace as well.", () => { | ||
it("should extract from core namespace as well ignoring the core attributes as defined in the SCIM spec.", () => { | ||
const event = { | ||
event: "user.created", | ||
data: { | ||
|
@@ -152,13 +153,14 @@ describe("getAttributesFromScimPayload", () => { | |
}, | ||
} as DirectorySyncEvent; | ||
|
||
const result = getAttributesFromScimPayload(event); | ||
const result = getAttributesFromScimPayload({ event, directoryId }); | ||
expect(result).toEqual({ | ||
userName: "[email protected]", | ||
displayName: "Kush", | ||
territory: "XANAM", | ||
externalId: "00ulb1kpy4EMATtuS5d7", | ||
groups: [], | ||
// Core Attributes won't be there - It avoids unnecessary warnings about attributes not defined in cal.com | ||
// userName: "[email protected]", | ||
// displayName: "Kush", | ||
// externalId: "00ulb1kpy4EMATtuS5d7", | ||
// groups: [], | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,7 +45,7 @@ async function syncCustomAttributesToUser({ | |
return; | ||
} | ||
|
||
const customAttributes = getAttributesFromScimPayload(event); | ||
const customAttributes = getAttributesFromScimPayload({ event, directoryId }); | ||
await assignValueToUserInOrgBulk({ | ||
orgId: org.id, | ||
userId: user.id, | ||
|
@@ -69,11 +69,6 @@ const handleUserEvents = async (event: DirectorySyncEvent, organizationId: numbe | |
select: dSyncUserSelect, | ||
}); | ||
|
||
// User is already a part of that org | ||
if (user && UserRepository.isAMemberOfOrganization({ user, organizationId }) && eventData.active) { | ||
return; | ||
} | ||
|
||
const translation = await getTranslation(user?.locale || "en", "common"); | ||
|
||
const org = await getTeamOrThrow(organizationId); | ||
|
@@ -84,36 +79,37 @@ const handleUserEvents = async (event: DirectorySyncEvent, organizationId: numbe | |
|
||
if (user) { | ||
if (eventData.active) { | ||
// If data.active is true then provision the user into the org | ||
const addedUser = await inviteExistingUserToOrg({ | ||
user: user as UserWithMembership, | ||
org, | ||
translation, | ||
}); | ||
|
||
await sendExistingUserTeamInviteEmails({ | ||
currentUserName: user.username, | ||
currentUserTeamName: org.name, | ||
existingUsersWithMemberships: [ | ||
{ | ||
...addedUser, | ||
profile: null, | ||
}, | ||
], | ||
language: translation, | ||
isOrg: true, | ||
teamId: org.id, | ||
isAutoJoin: true, | ||
currentUserParentTeamName: org?.parent?.name, | ||
orgSlug: org.slug, | ||
}); | ||
|
||
await syncCustomAttributesToUser({ | ||
event, | ||
userEmail, | ||
org, | ||
directoryId, | ||
}); | ||
if (UserRepository.isAMemberOfOrganization({ user, organizationId })) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still allow attributes sync for existing user. |
||
await syncCustomAttributesToUser({ | ||
event, | ||
userEmail, | ||
org, | ||
directoryId, | ||
}); | ||
} else { | ||
// If data.active is true then provision the user into the org | ||
const addedUser = await inviteExistingUserToOrg({ | ||
user: user as UserWithMembership, | ||
org, | ||
translation, | ||
}); | ||
await sendExistingUserTeamInviteEmails({ | ||
currentUserName: user.username, | ||
currentUserTeamName: org.name, | ||
existingUsersWithMemberships: [ | ||
{ | ||
...addedUser, | ||
profile: null, | ||
}, | ||
], | ||
language: translation, | ||
isOrg: true, | ||
teamId: org.id, | ||
isAutoJoin: true, | ||
currentUserParentTeamName: org?.parent?.name, | ||
orgSlug: org.slug, | ||
}); | ||
} | ||
} else { | ||
// If data.active is false then remove the user from the org | ||
await removeUserFromOrg({ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ import { TRPCError } from "@trpc/server"; | |
|
||
import type { TrpcSessionUser } from "../../../trpc"; | ||
import type { ZCreateAttributeSchema } from "./create.schema"; | ||
import { getOptionsWithValidContains } from "./utils"; | ||
|
||
type GetOptions = { | ||
ctx: { | ||
|
@@ -28,8 +29,8 @@ const createAttributesHandler = async ({ input, ctx }: GetOptions) => { | |
} | ||
|
||
const slug = slugify(input.name); | ||
const options = input.options.map((v) => v.value); | ||
const optionsWithoutDuplicates = Array.from(new Set(options)); | ||
const uniqueOptions = getOptionsWithValidContains(input.options); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using common utility |
||
|
||
const typeHasOptions = typesWithOptions.includes(input.type); | ||
|
||
let attributes: Attribute; | ||
|
@@ -55,10 +56,11 @@ const createAttributesHandler = async ({ input, ctx }: GetOptions) => { | |
// TEXT/NUMBER don't have options | ||
if (typeHasOptions) { | ||
await prisma.attributeOption.createMany({ | ||
data: optionsWithoutDuplicates.map((value) => ({ | ||
data: uniqueOptions.map(({ value, isGroup }) => ({ | ||
attributeId: attributes.id, | ||
value, | ||
slug: slugify(value), | ||
isGroup, | ||
})), | ||
}); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ export const createAttributeSchema = z.object({ | |
name: z.string(), | ||
type: z.enum(["TEXT", "NUMBER", "SINGLE_SELECT", "MULTI_SELECT"]), | ||
isLocked: z.boolean().optional(), | ||
options: z.array(z.object({ value: z.string() })), | ||
options: z.array(z.object({ value: z.string(), isGroup: z.boolean().optional() })), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was causing a groupOption to become regular option when created during the attribute creation itself. |
||
}); | ||
|
||
export type ZCreateAttributeSchema = z.infer<typeof createAttributeSchema>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was causing the update to not sync attribues as the user is already a part of org.