diff --git a/ee/api/routers/scim/api.py b/ee/api/routers/scim/api.py index 07482f10c..6ce194a4e 100644 --- a/ee/api/routers/scim/api.py +++ b/ee/api/routers/scim/api.py @@ -292,7 +292,7 @@ async def get_schema(schema_id: str, tenant_id=Depends(auth_required)): user_config = ResourceConfig( - schema_id="urn:ietf:params:scim:schemas:core:2.0:User", + resource_type_id="User", max_chunk_size=10, get_active_resource_count=users.get_active_resource_count, convert_provider_resource_to_client_resource=users.convert_provider_resource_to_client_resource, @@ -310,7 +310,7 @@ user_config = ResourceConfig( filter_attribute_mapping=users.filter_attribute_mapping, ) group_config = ResourceConfig( - schema_id="urn:ietf:params:scim:schemas:core:2.0:Group", + resource_type_id="Group", max_chunk_size=10, get_active_resource_count=groups.get_active_resource_count, convert_provider_resource_to_client_resource=groups.convert_provider_resource_to_client_resource, diff --git a/ee/api/routers/scim/constants.py b/ee/api/routers/scim/constants.py index 74dd19705..9642c471f 100644 --- a/ee/api/routers/scim/constants.py +++ b/ee/api/routers/scim/constants.py @@ -10,6 +10,9 @@ SCHEMAS = sorted( json.load(open("routers/scim/fixtures/schema_schema.json", "r")), json.load(open("routers/scim/fixtures/user_schema.json", "r")), json.load(open("routers/scim/fixtures/group_schema.json", "r")), + json.load( + open("routers/scim/fixtures/open_replay_user_extension_schema.json", "r") + ), ], key=lambda x: x["id"], ) diff --git a/ee/api/routers/scim/fixtures/open_replay_user_extension_schema.json b/ee/api/routers/scim/fixtures/open_replay_user_extension_schema.json new file mode 100644 index 000000000..b38eeefa7 --- /dev/null +++ b/ee/api/routers/scim/fixtures/open_replay_user_extension_schema.json @@ -0,0 +1,35 @@ +{ + "id": "urn:ietf:params:scim:schemas:extensions:openreplay:2.0:User", + "name": "User", + "description": "User Account Extension", + "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"], + "attributes": [ + { + "name": "permissions", + "type": "string", + "multiValued": true, + "description": "Permissions granted to the users of the group.", + "required": false, + "caseExact": true, + "canonicalValues": ["Session Replay", "Developer Tools", "Dashboard", "Assist (Live)", "Assist (Call)", "Spots", "Change Spot Visibility"], + "mutability": "readWrite", + "returned": "default" + }, + { + "name": "projectKeys", + "type": "string", + "multiValued": true, + "description": "A list of project keys associated with the group.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default" + } + ], + "meta": { + "resourceType": "Schema", + "location": "/v2/Schemas/urn:ietf:params:scim:schemas:extensions:openreplay:2.0:User", + "created": "2025-04-17T15:48:00Z", + "lastModified": "2025-04-17T15:48:00Z" + } +} diff --git a/ee/api/routers/scim/fixtures/resource_type.json b/ee/api/routers/scim/fixtures/resource_type.json index 1c56daebe..3879665b1 100644 --- a/ee/api/routers/scim/fixtures/resource_type.json +++ b/ee/api/routers/scim/fixtures/resource_type.json @@ -14,7 +14,12 @@ "lastModified": "2025-04-16T08:37:00Z", "location": "ResourceType/User" }, - "schemaExtensions": [] + "schemaExtensions": [ + { + "schema": "urn:ietf:params:scim:schemas:extensions:openreplay:2.0:User", + "required": true + } + ] }, { "schemas": [ diff --git a/ee/api/routers/scim/fixtures/user_schema.json b/ee/api/routers/scim/fixtures/user_schema.json index 528c4a69a..c80a084c5 100644 --- a/ee/api/routers/scim/fixtures/user_schema.json +++ b/ee/api/routers/scim/fixtures/user_schema.json @@ -334,14 +334,18 @@ }, { "name": "entitlements", - "type": "string", + "type": "complex", "multiValued": true, "description": "Entitlements granted to the user.", "required": false, - "caseExact": true, - "canonicalValues": ["SESSION_REPLAY", "METRICS", "ASSIST_LIVE", "ASSIST_CALL", "SPOT_PUBLIC"], "mutability": "readWrite", - "returned": "default" + "returned": "default", + "subAttributes": [ + { "name": "value", "type": "string", "multiValued": false, "description": "Entitlement value.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "display", "type": "string", "multiValued": false, "description": "Display name.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "type", "type": "string", "multiValued": false, "description": "Type label.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "primary", "type": "boolean", "multiValued": false, "description": "Primary flag; one per list.", "required": false, "mutability": "readWrite", "returned": "default" } + ] }, { "name": "roles", @@ -372,17 +376,7 @@ { "name": "type", "type": "string", "multiValued": false, "description": "Type label.", "required": false, "caseExact": false, "canonicalValues": [], "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, { "name": "primary", "type": "boolean", "multiValued": false, "description": "Primary flag; one per list.", "required": false, "mutability": "readWrite", "returned": "default" } ] - }, - { - "name": "projectKeys", - "type": "string", - "multiValued": true, - "description": "A list of project keys associated with the group.", - "required": false, - "caseExact": false, - "mutability": "readWrite", - "returned": "default" - } + } ], "meta": { "resourceType": "Schema", diff --git a/ee/api/routers/scim/resource_config.py b/ee/api/routers/scim/resource_config.py index f89877f65..84bbf57f1 100644 --- a/ee/api/routers/scim/resource_config.py +++ b/ee/api/routers/scim/resource_config.py @@ -3,6 +3,7 @@ from typing import Any, Callable from routers.scim.constants import ( SCHEMA_IDS_TO_SCHEMA_DETAILS, + RESOURCE_TYPE_IDS_TO_RESOURCE_TYPE_DETAILS, ) from routers.scim import helpers @@ -17,7 +18,7 @@ ProviderInput = dict[str, Any] @dataclass class ResourceConfig: - schema_id: str + resource_type_id: str max_chunk_size: int get_active_resource_count: Callable[[int], int] convert_provider_resource_to_client_resource: Callable[ @@ -44,7 +45,19 @@ class ResourceConfig: def get_schema(config: ResourceConfig) -> Schema: - return SCHEMA_IDS_TO_SCHEMA_DETAILS[config.schema_id] + resource_type_id = config.resource_type_id + resource_type = RESOURCE_TYPE_IDS_TO_RESOURCE_TYPE_DETAILS[resource_type_id] + main_schema_id = resource_type["schema"] + schema_extension_ids = [ + item["schema"] for item in resource_type["schemaExtensions"] + ] + result = SCHEMA_IDS_TO_SCHEMA_DETAILS[main_schema_id] + for schema_id in schema_extension_ids: + result["attributes"].extend( + SCHEMA_IDS_TO_SCHEMA_DETAILS[schema_id]["attributes"] + ) + result["schemas"] = [main_schema_id, *schema_extension_ids] + return result def convert_provider_resource_to_client_resource( diff --git a/ee/api/routers/scim/users.py b/ee/api/routers/scim/users.py index 9e8623188..41072f5b5 100644 --- a/ee/api/routers/scim/users.py +++ b/ee/api/routers/scim/users.py @@ -11,11 +11,21 @@ from routers.scim.resource_config import ( ClientInput, ProviderInput, ) -from schemas.schemas_ee import ValidIdentityProviderPermissions +from schemas.schemas_ee import Permissions def _is_valid_permission_for_identity_provider(permission: str) -> bool: - return ValidIdentityProviderPermissions.has_value(permission) + permission_display_to_value_mapping = { + "Session Replay": Permissions.SESSION_REPLAY, + "Developer Tools": Permissions.DEV_TOOLS, + "Dashboard": Permissions.METRICS, + "Assist (Live)": Permissions.ASSIST_LIVE, + "Assist (Call)": Permissions.ASSIST_CALL, + "Spots": Permissions.SPOT, + "Change Spot Visibility": Permissions.SPOT_PUBLIC, + } + value = permission_display_to_value_mapping.get(permission) + return Permissions.has_value(value) def convert_client_resource_update_input_to_provider_resource_update_input( @@ -163,7 +173,10 @@ def convert_provider_resource_to_client_resource( ) return { "id": str(provider_resource["user_id"]), - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:ietf:params:scim:schemas:extensions:openreplay:2.0:User", + ], "meta": { "resourceType": "User", "created": provider_resource["created_at"].strftime("%Y-%m-%dT%H:%M:%SZ"), diff --git a/ee/api/schemas/schemas_ee.py b/ee/api/schemas/schemas_ee.py index bde5c14d0..394f88859 100644 --- a/ee/api/schemas/schemas_ee.py +++ b/ee/api/schemas/schemas_ee.py @@ -28,14 +28,6 @@ class ServicePermissions(str, Enum): READ_NOTES = "SERVICE_READ_NOTES" -class ValidIdentityProviderPermissions(str, Enum): - SESSION_REPLAY = "SESSION_REPLAY" - METRICS = "METRICS" - ASSIST_LIVE = "ASSIST_LIVE" - ASSIST_CALL = "ASSIST_CALL" - SPOT_PUBLIC = "SPOT_PUBLIC" - - class CurrentContext(schemas.CurrentContext): permissions: List[Union[Permissions, ServicePermissions]] = Field(...) service_account: bool = Field(default=False)