Files
calendar/packages/features/ee/dsync/lib/getAttributesFromScimPayload.ts
T
Hariom BalharaandGitHub 727f7df2c2 fix: Enhance SCIM payload handling and attribute creation (#18427)
* feat: Enhance SCIM user attribute handling and sync process

- Introduced a new PR_TODO.md file to track ongoing tasks and fixes.
- Updated `getAttributesFromScimPayload` to accept a directoryId and ignore core user attributes during syncing.
- Modified `handleUserEvents` to pass directoryId when syncing custom attributes.
- Improved attribute creation logic to handle unique options and added support for optional isGroup property in attribute options.
- Added utility function `getOptionsWithValidContains` to ensure valid sub-options in attribute options.
- Updated tests to reflect changes in attribute handling and ensure proper functionality.

This commit addresses issues with user attribute syncing and enhances the overall attribute management process.

* test: Add unit tests for getOptionsWithValidContains utility function

- Introduced a new test file for the getOptionsWithValidContains function.
- Added tests to verify the removal of duplicate options, initialization of empty contains arrays, filtering of non-existent sub-options, and handling of empty options arrays.
- Ensured comprehensive coverage of the utility's functionality to enhance reliability and maintainability.
2025-01-03 13:36:21 +05:30

160 lines
4.1 KiB
TypeScript

import type { DirectorySyncEvent } from "@boxyhq/saml-jackson";
import { DIRECTORY_IDS_TO_LOG } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
const log = logger.getSubLogger({ prefix: ["getAttributesFromScimPayload"] });
type ScimUserAttributeName = string;
type ScimUserAttributeValue = string | string[];
const coreSchemaUrn = "urn:ietf:params:scim:schemas:core:2.0:User";
//It avoids unnecessary warnings about attributes not defined in cal.com
const coreUserAttributesToIgnore = [
"userName",
"name",
"displayName",
"emails",
"active",
"externalId",
"id",
"groups",
"meta",
"locale",
"password",
"phoneNumbers",
"photos",
"profileUrl",
"timezone",
"title",
"addresses",
"entitlements",
"ims",
"roles",
"x509Certificates",
];
/**
* event.data.raw has this format
* {
* "schemas": [
* "urn:ietf:params:scim:schemas:core:2.0:User",
* "segment",
* "territory"
* ],
* "userName": "member@samldemo.com",
* "name": {
* "givenName": "Member SAML Demo",
* "familyName": "Member SAML Demo"
* },
* "emails": [
* {
* "primary": true,
* "value": "member@samldemo.com"
* }
* ],
* "displayName": "Member SAML Demo",
* "territory": {
* "territory": "NAM"
* },
* "segment": {
* "segment": "SMB"
* },
* "externalId": "00ukzk1wrsKZqofit5d7",
* "groups": [],
* "active": true,
* "id": "b36ba9fa-783b-44e6-a770-a652cb9d71ba"
* }
*
* Transforms above to
* {
* "territory": "NAM",
* "segment": "SMB"
* }
*/
function getAttributesFromScimPayload({
event,
directoryId,
}: {
event: DirectorySyncEvent;
directoryId: string;
}): Record<ScimUserAttributeName, ScimUserAttributeValue> {
const scimUserAttributes: Record<ScimUserAttributeName, ScimUserAttributeValue> = {};
if (event.event !== "user.created" && event.event !== "user.updated") {
log.error("getAttributesFromScimPayload", `Unsupported event: ${event.event}`);
return scimUserAttributes;
}
const raw = event.data.raw;
raw.schemas.forEach((schema: unknown) => {
if (schema === coreSchemaUrn) {
// Core schema has payload in the root
const { schemas: _schemas, ...namespaceData } = raw;
collectAttributes({ data: namespaceData, ignoreList: coreUserAttributesToIgnore });
return;
}
const namespaceName = schema;
if (typeof namespaceName !== "string") {
log.error(
"getAttributesFromScimPayload",
`Namespace name is not a string ${safeStringify(namespaceName)}`
);
return;
}
const namespaceData = raw[namespaceName];
if (!namespaceData) {
log.warn("getAttributesFromScimPayload", `Namespace data for ${namespaceName} is null. Ignoring it.`);
return;
}
collectAttributes({ data: namespaceData });
});
const shouldLog = DIRECTORY_IDS_TO_LOG.includes(directoryId);
if (shouldLog) {
console.log("Collected Attributes:", `${safeStringify(scimUserAttributes)}`);
}
return scimUserAttributes;
function collectAttributes({
data,
ignoreList = [],
}: {
data: Record<string, unknown>;
ignoreList?: string[];
}) {
Object.entries(data).forEach(([customAttributeName, value]) => {
if (ignoreList.includes(customAttributeName)) {
return;
}
if (!value) {
log.warn(
"getAttributesFromScimPayload",
`Custom attribute ${customAttributeName} is null. Ignoring it.`
);
return;
}
if (scimUserAttributes[customAttributeName]) {
log.warn(
"getAttributesFromScimPayload",
`Custom attribute ${customAttributeName} already exists. Might be coming from different namespace. Ignoring it.`
);
return;
}
// TODO: Support number as well as Attribute support number type
if (
typeof value === "string" ||
(value instanceof Array && value.every((item) => typeof item === "string"))
) {
scimUserAttributes[customAttributeName] = value;
}
});
}
}
export default getAttributesFromScimPayload;