diff --git a/app/server/lib/scim/v2/ScimUtils.ts b/app/server/lib/scim/v2/ScimUtils.ts index 8254321cb2..4aed3fecad 100644 --- a/app/server/lib/scim/v2/ScimUtils.ts +++ b/app/server/lib/scim/v2/ScimUtils.ts @@ -12,6 +12,7 @@ import SCIMMY from "scimmy"; const SCIM_API_BASE_PATH = "/api/scim/v2"; const SCIMMY_USER_TYPE = "User"; const SCIMMY_GROUP_TYPE = "Group"; +const SCIMMY_ROLE_TYPE = "Role"; /** * Converts a user from your database to a SCIMMY user @@ -65,13 +66,22 @@ function toSCIMMYMembers(group: Group): SCIMMY.Schemas.Group["members"] { $ref: `${SCIM_API_BASE_PATH}/Users/${member.id}`, type: SCIMMY_USER_TYPE, })), - // As of 2025-01-12, we don't support nested groups, so it should always be empty - ...group.memberGroups.map((member: Group) => ({ - value: String(member.id), - display: member.name, - $ref: `${SCIM_API_BASE_PATH}/Groups/${member.id}`, - type: SCIMMY_GROUP_TYPE, - })), + ...group.memberGroups + .filter((member: Group) => member.type === Group.TEAM_TYPE) + .map((member: Group) => ({ + value: String(member.id), + display: member.name, + $ref: `${SCIM_API_BASE_PATH}/Groups/${member.id}`, + type: SCIMMY_GROUP_TYPE, + })), + ...group.memberGroups + .filter((member: Group) => member.type === Group.ROLE_TYPE) + .map((member: Group) => ({ + value: String(member.id), + display: member.name, + $ref: `${SCIM_API_BASE_PATH}/Roles/${member.id}`, + type: SCIMMY_ROLE_TYPE, + })), ]; } diff --git a/app/server/lib/scim/v2/roles/SCIMMYRoleSchema.ts b/app/server/lib/scim/v2/roles/SCIMMYRoleSchema.ts index bdb4ba68ce..0c6afd91a2 100644 --- a/app/server/lib/scim/v2/roles/SCIMMYRoleSchema.ts +++ b/app/server/lib/scim/v2/roles/SCIMMYRoleSchema.ts @@ -15,10 +15,32 @@ export class SCIMMYRoleSchema extends SCIMMY.Types.Schema { private static _definition = (function() { // Clone the Groups schema definition return new SchemaDefinition( - "Role", "urn:ietf:params:scim:schemas:Grist:1.0:Role", "Role in Grist (Owner)", [ + "Role", "urn:ietf:params:scim:schemas:Grist:1.0:Role", "Role in Grist", [ new Attribute("string", "displayName", { mutable: false, direction: "out" }), - SCIMMY.Schemas.Group.definition.attribute("members"), + new Attribute("complex", "members", + { multiValued: true, uniqueness: false, description: "A list of members of the Role." }, + [ + new Attribute("string", "value", + { mutable: "immutable", description: "Identifier of the member of this Role." }), + new Attribute("string", "display", + { mutable: "immutable", description: "Human-readable name of the member of this Role." }), + new Attribute("reference", "$ref", + { + mutable: "immutable", + referenceTypes: ["User", "Group", "Role"], + description: "The URI corresponding to a SCIM resource that is a member of this Role.", + }, + ), + new Attribute("string", "type", + { + mutable: "immutable", + canonicalValues: ["User", "Group", "Role"], + description: "A label indicating the type of resource, e.g., 'User', 'Role' or 'Group'.", + }, + ), + ], + ), new Attribute("string", "docId", { required: false, description: "The docId associated to this role.", mutable: false, direction: "out" }), new Attribute("integer", "workspaceId", { required: false, description: "The workspaceId for this role", diff --git a/test/server/lib/Scim.ts b/test/server/lib/Scim.ts index 8ebabd9481..73e67d03f5 100644 --- a/test/server/lib/Scim.ts +++ b/test/server/lib/Scim.ts @@ -1276,7 +1276,7 @@ describe("Scim", () => { async ([role1Name, role2Name, group1Name]) => { const beforeAdditions = await axios.get(scimUrl("/Roles?count=300"), chimpy); const beforeAdditionsIds = new Set(beforeAdditions.data.Resources.map(({ id}: { id: number }) => id)); - await getDbManager().createGroup({ + const group1 = await getDbManager().createGroup({ name: group1Name, type: Group.TEAM_TYPE, memberUsers: [userIdByName.chimpy!], @@ -1290,6 +1290,7 @@ describe("Scim", () => { name: role2Name, type: Group.ROLE_TYPE, memberUsers: [userIdByName.kiwi!], + memberGroups: [role1.id, group1.id], }); const res = await axios.get(scimUrl("/Roles?count=300"), chimpy); @@ -1310,7 +1311,21 @@ describe("Scim", () => { schemas: ["urn:ietf:params:scim:schemas:Grist:1.0:Role"], id: String(role2.id), displayName: role2Name, - members: [getUserMemberWithRef("kiwi")], + members: [ + getUserMemberWithRef("kiwi"), + { + value: String(group1.id), + display: group1Name, + type: "Group", + $ref: `/api/scim/v2/Groups/${group1.id}`, + }, + { + value: String(role1.id), + display: role1Name, + type: "Role", + $ref: `/api/scim/v2/Roles/${role1.id}`, + }, + ], meta: { resourceType: "Role", location: `/api/scim/v2/Roles/${role2.id}` }, }, ]);