From 62736ff29bb17386c6942c998afdaa453d0bff1f Mon Sep 17 00:00:00 2001 From: Jonathan Griffin Date: Fri, 18 Apr 2025 15:59:41 +0200 Subject: [PATCH] clean up files --- ee/api/routers/fixtures/group_schema.json | 70 ++ ee/api/routers/fixtures/resource_type.json | 19 + .../fixtures/resource_type_schema.json | 102 +++ ee/api/routers/fixtures/schema_schema.json | 304 +++++++ .../fixtures/service_provider_config.json | 41 + .../service_provider_config_schema.json | 212 +++++ ee/api/routers/fixtures/user_schema.json | 386 +++++++++ ee/api/routers/scim.py | 253 +++--- ee/api/routers/scim_constants.py | 777 +----------------- 9 files changed, 1239 insertions(+), 925 deletions(-) create mode 100644 ee/api/routers/fixtures/group_schema.json create mode 100644 ee/api/routers/fixtures/resource_type.json create mode 100644 ee/api/routers/fixtures/resource_type_schema.json create mode 100644 ee/api/routers/fixtures/schema_schema.json create mode 100644 ee/api/routers/fixtures/service_provider_config.json create mode 100644 ee/api/routers/fixtures/service_provider_config_schema.json create mode 100644 ee/api/routers/fixtures/user_schema.json diff --git a/ee/api/routers/fixtures/group_schema.json b/ee/api/routers/fixtures/group_schema.json new file mode 100644 index 000000000..f1ef0f71f --- /dev/null +++ b/ee/api/routers/fixtures/group_schema.json @@ -0,0 +1,70 @@ +{ + "id": "urn:ietf:params:scim:schemas:core:2.0:Group", + "name": "Group", + "description": "Group", + "attributes": [ + { + "name": "displayName", + "type": "string", + "multiValued": false, + "description": "Human readable name for the Group. REQUIRED.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "members", + "type": "complex", + "multiValued": true, + "description": "A list of members of the Group.", + "required": false, + "subAttributes": [ + { + "name": "value", + "type": "string", + "multiValued": false, + "description": "Identifier of the member of this Group.", + "required": false, + "caseExact": false, + "mutability": "immutable", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "$ref", + "type": "reference", + "referenceTypes": ["User", "Group"], + "multiValued": false, + "description": "The URI of the corresponding member resource of this Group.", + "required": false, + "caseExact": false, + "mutability": "immutable", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "A label indicating the type of resource; e.g., 'User' or 'Group'.", + "required": false, + "caseExact": false, + "canonicalValues": ["User", "Group"], + "mutability": "immutable", + "returned": "default", + "uniqueness": "none" + } + ], + "mutability": "readWrite", + "returned": "default" + } + ], + "meta": { + "resourceType": "Schema", + "location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:Group", + "created": "2025-04-17T15:48:00", + "lastModified": "2025-04-17T15:48:00" + } +} diff --git a/ee/api/routers/fixtures/resource_type.json b/ee/api/routers/fixtures/resource_type.json new file mode 100644 index 000000000..284266c4e --- /dev/null +++ b/ee/api/routers/fixtures/resource_type.json @@ -0,0 +1,19 @@ +[ + { + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:ResourceType" + ], + "id": "User", + "name": "User", + "endpoint": "/Users", + "description": "User account", + "schema": "urn:ietf:params:scim:schemas:core:2.0:User", + "meta": { + "resourceType": "ResourceType", + "created": "2025-04-16T08:37:00Z", + "lastModified": "2025-04-16T08:37:00Z", + "location": "ResourceType/User" + }, + "schemaExtensions": [] + } +] diff --git a/ee/api/routers/fixtures/resource_type_schema.json b/ee/api/routers/fixtures/resource_type_schema.json new file mode 100644 index 000000000..040fd071c --- /dev/null +++ b/ee/api/routers/fixtures/resource_type_schema.json @@ -0,0 +1,102 @@ +{ + "id": "urn:ietf:params:scim:schemas:core:2.0:ResourceType", + "name": "ResourceType", + "description": "Specifies the schema that describes a SCIM Resource Type", + "attributes": [ + { + "name": "id", + "type": "string", + "multiValued": false, + "description": "The resource type's server unique id. May be the same as the 'name' attribute.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "name", + "type": "string", + "multiValued": false, + "description": "The resource type name.", + "required": true, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "description", + "type": "string", + "multiValued": false, + "description": "The resource type's human readable description.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "endpoint", + "type": "reference", + "referenceTypes": ["uri"], + "multiValued": false, + "description": "The resource type's HTTP addressable endpoint relative to the Base URL; e.g., /Users.", + "required": true, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "schema", + "type": "reference", + "referenceTypes": ["uri"], + "multiValued": false, + "description": "The resource types primary/base schema URI.", + "required": true, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "schemaExtensions", + "type": "complex", + "multiValued": true, + "description": "A list of URIs of the resource type's schema extensions", + "required": false, + "mutability": "readOnly", + "returned": "default", + "subAttributes": [ + { + "name": "schema", + "type": "reference", + "referenceTypes": ["uri"], + "multiValued": false, + "description": "The URI of a schema extension.", + "required": true, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "required", + "type": "boolean", + "multiValued": false, + "description": "Specifies whether the schema extension is required for the resource type.", + "required": true, + "mutability": "readOnly", + "returned": "default" + } + ] + } + ], + "meta": { + "resourceType": "Schema", + "location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:ResourceType", + "created": "2025-04-17T15:48:00", + "lastModified": "2025-04-17T15:48:00" + } +} diff --git a/ee/api/routers/fixtures/schema_schema.json b/ee/api/routers/fixtures/schema_schema.json new file mode 100644 index 000000000..4099700f3 --- /dev/null +++ b/ee/api/routers/fixtures/schema_schema.json @@ -0,0 +1,304 @@ +{ + "id": "urn:ietf:params:scim:schemas:core:2.0:Schema", + "name": "Schema", + "description": "Specifies the schema that describes a SCIM Schema", + "attributes": [ + { + "name": "id", + "type": "string", + "multiValued": false, + "description": "The unique URI of the schema.", + "required": true, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "name", + "type": "string", + "multiValued": false, + "description": "The schema's human readable name.", + "required": true, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "description", + "type": "string", + "multiValued": false, + "description": "The schema's human readable description.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "attributes", + "type": "complex", + "multiValued": true, + "description": "A complex attribute that includes the attributes of a schema", + "required": true, + "mutability": "readOnly", + "returned": "default", + "subAttributes": [ + { + "name": "name", + "type": "string", + "multiValued": false, + "description": "The attribute's name", + "required": true, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "The attribute's data type.", + "required": true, + "canonicalValues": ["string","complex","boolean","decimal","integer","dateTime","reference"], + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "multiValued", + "type": "boolean", + "multiValued": false, + "description": "Boolean indicating an attribute's plurality.", + "required": true, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "description", + "type": "string", + "multiValued": false, + "description": "A human readable description of the attribute.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "required", + "type": "boolean", + "multiValued": false, + "description": "A boolean indicating if the attribute is required.", + "required": false, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "canonicalValues", + "type": "string", + "multiValued": true, + "description": "A collection of canonical values.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "caseExact", + "type": "boolean", + "multiValued": false, + "description": "Indicates if a string attribute is case-sensitive.", + "required": false, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "mutability", + "type": "string", + "multiValued": false, + "description": "Indicates if an attribute is modifiable.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": ["readOnly","readWrite","immutable","writeOnly"] + }, + { + "name": "returned", + "type": "string", + "multiValued": false, + "description": "Indicates when an attribute is returned in a response.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": ["always","never","default","request"] + }, + { + "name": "uniqueness", + "type": "string", + "multiValued": false, + "description": "Indicates how unique a value must be.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": ["none","server","global"] + }, + { + "name": "referenceTypes", + "type": "string", + "multiValued": true, + "description": "Specifies a resourceType that a reference attribute may refer to.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "subAttributes", + "type": "complex", + "multiValued": true, + "description": "Used to define the sub-attributes of a complex attribute", + "required": false, + "mutability": "readOnly", + "returned": "default", + "subAttributes": [ + { + "name": "name", + "type": "string", + "multiValued": false, + "description": "The sub-attribute's name", + "required": true, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "type", + "type": "string", + "multiValued": false, + "description": "The sub-attribute's data type.", + "required": true, + "canonicalValues": ["string","complex","boolean","decimal","integer","dateTime","reference"], + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "multiValued", + "type": "boolean", + "multiValued": false, + "description": "Boolean indicating sub-attribute plurality.", + "required": true, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "description", + "type": "string", + "multiValued": false, + "description": "Human readable description of the sub-attribute.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "required", + "type": "boolean", + "multiValued": false, + "description": "Whether the sub-attribute is required.", + "required": false, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "canonicalValues", + "type": "string", + "multiValued": true, + "description": "A collection of canonical values for the sub-attribute.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "caseExact", + "type": "boolean", + "multiValued": false, + "description": "Case sensitivity of the sub-attribute.", + "required": false, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "mutability", + "type": "string", + "multiValued": false, + "description": "Modifiability of the sub-attribute.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "canonicalValues": ["readOnly","readWrite","immutable","writeOnly"] + }, + { + "name": "returned", + "type": "string", + "multiValued": false, + "description": "When the sub-attribute is returned.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "canonicalValues": ["always","never","default","request"] + }, + { + "name": "uniqueness", + "type": "string", + "multiValued": false, + "description": "Uniqueness constraint of the sub-attribute.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none", + "canonicalValues": ["none","server","global"] + }, + { + "name": "referenceTypes", + "type": "string", + "multiValued": true, + "description": "ResourceTypes that the sub-attribute may reference.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + } + ] + } + ] + } + ], + "meta": { + "resourceType": "Schema", + "location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:Schema", + "created": "2025-04-17T15:48:00", + "lastModified": "2025-04-17T15:48:00" + } +} diff --git a/ee/api/routers/fixtures/service_provider_config.json b/ee/api/routers/fixtures/service_provider_config.json new file mode 100644 index 000000000..ebb37bec4 --- /dev/null +++ b/ee/api/routers/fixtures/service_provider_config.json @@ -0,0 +1,41 @@ +{ + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig" + ], + "patch": { + "supported": false + }, + "bulk": { + "supported": false, + "maxOperations": 0, + "maxPayloadSize": 0 + }, + "filter": { + "supported": false, + "maxResults": 0 + }, + "changePassword": { + "supported": false + }, + "sort": { + "supported": false + }, + "etag": { + "supported": false + }, + "authenticationSchemes": [ + { + "type": "oauthbearertoken", + "name": "OAuth Bearer Token", + "description": "Authentication scheme using the OAuth Bearer Token Standard. The access token should be sent in the 'Authorization' header using the Bearer schema.", + "specUri": "https://tools.ietf.org/html/rfc6750", + "primary": true + } + ], + "meta": { + "resourceType": "ServiceProviderConfig", + "created": "2025-04-15T15:45:00Z", + "lastModified": "2025-04-15T15:45:00Z", + "location": "/ServiceProviderConfig" + } +} diff --git a/ee/api/routers/fixtures/service_provider_config_schema.json b/ee/api/routers/fixtures/service_provider_config_schema.json new file mode 100644 index 000000000..c17ca5e18 --- /dev/null +++ b/ee/api/routers/fixtures/service_provider_config_schema.json @@ -0,0 +1,212 @@ +{ + "id": "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig", + "name": "Service Provider Configuration", + "description": "Schema for representing the service provider's configuration", + "attributes": [ + { + "name": "documentationUri", + "type": "reference", + "referenceTypes": ["external"], + "multiValued": false, + "description": "An HTTP addressable URL pointing to the service provider's human consumable help documentation.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "patch", + "type": "complex", + "multiValued": false, + "description": "A complex type that specifies PATCH configuration options.", + "required": true, + "returned": "default", + "mutability": "readOnly", + "subAttributes": [ + { + "name": "supported", + "type": "boolean", + "multiValued": false, + "description": "Boolean value specifying whether the operation is supported.", + "required": true, + "mutability": "readOnly", + "returned": "default" + } + ] + }, + { + "name": "bulk", + "type": "complex", + "multiValued": false, + "description": "A complex type that specifies BULK configuration options.", + "required": true, + "returned": "default", + "mutability": "readOnly", + "subAttributes": [ + { + "name": "supported", + "type": "boolean", + "multiValued": false, + "description": "Boolean value specifying whether the operation is supported.", + "required": true, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "maxOperations", + "type": "integer", + "multiValued": false, + "description": "An integer value specifying the maximum number of operations.", + "required": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "maxPayloadSize", + "type": "integer", + "multiValued": false, + "description": "An integer value specifying the maximum payload size in bytes.", + "required": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "filter", + "type": "complex", + "multiValued": false, + "description": "A complex type that specifies FILTER options.", + "required": true, + "returned": "default", + "mutability": "readOnly", + "subAttributes": [ + { + "name": "supported", + "type": "boolean", + "multiValued": false, + "description": "Boolean value specifying whether the operation is supported.", + "required": true, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "maxResults", + "type": "integer", + "multiValued": false, + "description": "Integer value specifying the maximum number of resources returned in a response.", + "required": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "changePassword", + "type": "complex", + "multiValued": false, + "description": "A complex type that specifies change password options.", + "required": true, + "returned": "default", + "mutability": "readOnly", + "subAttributes": [ + { + "name": "supported", + "type": "boolean", + "multiValued": false, + "description": "Boolean value specifying whether the operation is supported.", + "required": true, + "mutability": "readOnly", + "returned": "default" + } + ] + }, + { + "name": "sort", + "type": "complex", + "multiValued": false, + "description": "A complex type that specifies sort result options.", + "required": true, + "returned": "default", + "mutability": "readOnly", + "subAttributes": [ + { + "name": "supported", + "type": "boolean", + "multiValued": false, + "description": "Boolean value specifying whether the operation is supported.", + "required": true, + "mutability": "readOnly", + "returned": "default" + } + ] + }, + { + "name": "authenticationSchemes", + "type": "complex", + "multiValued": true, + "description": "A complex type that specifies supported Authentication Scheme properties.", + "required": true, + "returned": "default", + "mutability": "readOnly", + "subAttributes": [ + { + "name": "name", + "type": "string", + "multiValued": false, + "description": "The common authentication scheme name; e.g., HTTP Basic.", + "required": true, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "description", + "type": "string", + "multiValued": false, + "description": "A description of the authentication scheme.", + "required": true, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "specUri", + "type": "reference", + "referenceTypes": ["external"], + "multiValued": false, + "description": "An HTTP addressable URL pointing to the Authentication Scheme's specification.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "documentationUri", + "type": "reference", + "referenceTypes": ["external"], + "multiValued": false, + "description": "An HTTP addressable URL pointing to the Authentication Scheme's usage documentation.", + "required": false, + "caseExact": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + } + ] + } + ], + "meta": { + "resourceType": "Schema", + "location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig", + "created": "2025-04-17T15:48:00", + "lastModified": "2025-04-17T15:48:00" + } +} diff --git a/ee/api/routers/fixtures/user_schema.json b/ee/api/routers/fixtures/user_schema.json new file mode 100644 index 000000000..736694c91 --- /dev/null +++ b/ee/api/routers/fixtures/user_schema.json @@ -0,0 +1,386 @@ +{ + "id": "urn:ietf:params:scim:schemas:core:2.0:User", + "name": "User", + "description": "User Account", + "attributes": [ + { + "name": "schemas", + "type": "string", + "multiValued": true, + "description": "An array of Strings containing URI that are used to indicate the namespaces of the SCIM schemas that define the attributes present in the current JSON structure.", + "required": true, + "caseExact": false, + "mutability": "immutable", + "returned": "always", + "uniqueness": "none" + }, + { + "name": "id", + "type": "string", + "multiValued": false, + "description": "Unique identifier for the resource, assigned by the service provider. MUST be non-empty, unique, stable, and non-reassignable. Clients MUST NOT specify this value.", + "required": true, + "caseExact": true, + "mutability": "readOnly", + "returned": "always", + "uniqueness": "server" + }, + { + "name": "externalId", + "type": "string", + "multiValued": false, + "description": "Identifier for the resource as defined by the provisioning client. OPTIONAL; clients MAY include a non-empty value.", + "required": false, + "caseExact": true, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "meta", + "type": "complex", + "multiValued": false, + "description": "Resource metadata. MUST be ignored when provided by clients.", + "required": false, + "mutability": "readOnly", + "returned": "default", + "subAttributes": [ + { + "name": "resourceType", + "type": "string", + "multiValued": false, + "description": "The resource type name.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "created", + "type": "dateTime", + "multiValued": false, + "description": "The date and time the resource was added.", + "required": false, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "lastModified", + "type": "dateTime", + "multiValued": false, + "description": "The most recent date and time the resource was modified.", + "required": false, + "mutability": "readOnly", + "returned": "default" + }, + { + "name": "location", + "type": "reference", + "referenceTypes": ["external"], + "multiValued": false, + "description": "The URI of the resource being returned.", + "required": false, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "version", + "type": "string", + "multiValued": false, + "description": "The version (ETag) of the resource being returned.", + "required": false, + "caseExact": true, + "mutability": "readOnly", + "returned": "default", + "uniqueness": "none" + } + ] + }, + { + "name": "userName", + "type": "string", + "multiValued": false, + "description": "Unique identifier for the User, used to authenticate. REQUIRED.", + "required": true, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "server" + }, + { + "name": "name", + "type": "complex", + "multiValued": false, + "description": "Components of the user's real name.", + "required": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { "name": "formatted", "type": "string", "multiValued": false, "description": "Complete name, formatted for display.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "familyName", "type": "string", "multiValued": false, "description": "Family name.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "givenName", "type": "string", "multiValued": false, "description": "Given name.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "middleName", "type": "string", "multiValued": false, "description": "Middle name(s).", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "honorificPrefix","type": "string", "multiValued": false, "description": "Honorific prefix.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "honorificSuffix","type": "string", "multiValued": false, "description": "Honorific suffix.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" } + ] + }, + { + "name": "displayName", + "type": "string", + "multiValued": false, + "description": "Full name, suitable for display.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "nickName", + "type": "string", + "multiValued": false, + "description": "Casual name.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "profileUrl", + "type": "reference", + "referenceTypes": ["external"], + "multiValued": false, + "description": "URL of the user's profile.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "title", + "type": "string", + "multiValued": false, + "description": "User's title (e.g., 'Vice President').", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "userType", + "type": "string", + "multiValued": false, + "description": "Relationship between organization and user.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "preferredLanguage", + "type": "string", + "multiValued": false, + "description": "Preferred language, e.g., 'en_US'.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "locale", + "type": "string", + "multiValued": false, + "description": "Locale for formatting, e.g., 'en-US'.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "timezone", + "type": "string", + "multiValued": false, + "description": "Time zone in Olson format, e.g., 'America/Los_Angeles'.", + "required": false, + "caseExact": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none" + }, + { + "name": "active", + "type": "boolean", + "multiValued": false, + "description": "Administrative status.", + "required": false, + "mutability": "readWrite", + "returned": "default" + }, + { + "name": "password", + "type": "string", + "multiValued": false, + "description": "Cleartext password for create/reset operations.", + "required": false, + "caseExact": false, + "mutability": "writeOnly", + "returned": "never", + "uniqueness": "none" + }, + { + "name": "emails", + "type": "complex", + "multiValued": true, + "description": "Email addresses for the user.", + "required": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { "name": "value", "type": "string", "multiValued": false, "description": "Email address.", "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: 'work','home','other'.", "required": false, "caseExact": false, "canonicalValues": ["work","home","other"], "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": "phoneNumbers", + "type": "complex", + "multiValued": true, + "description": "Phone numbers for the user.", + "required": false, + "mutability": "readWrite", + "returned": "default", + "subAttributes": [ + { "name": "value", "type": "string", "multiValued": false, "description": "Phone number (tel URI).", "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: 'work','home','mobile','fax','pager','other'.", "required": false, "caseExact": false, "canonicalValues": ["work","home","mobile","fax","pager","other"], "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": "ims", + "type": "complex", + "multiValued": true, + "description": "Instant messaging addresses.", + "required": false, + "mutability": "readWrite", + "returned": "default", + "subAttributes": [ + { "name": "value", "type": "string", "multiValued": false, "description": "IM address.", "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: 'aim','gtalk','icq','xmpp','msn','skype','qq','yahoo'.", "required": false, "caseExact": false, "canonicalValues": ["aim","gtalk","icq","xmpp","msn","skype","qq","yahoo"], "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": "photos", + "type": "complex", + "multiValued": true, + "description": "URLs of photos of the user.", + "required": false, + "mutability": "readWrite", + "returned": "default", + "subAttributes": [ + { "name": "value", "type": "reference", "referenceTypes": ["external"], "multiValued": false, "description": "Photo URL.", "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: 'photo','thumbnail'.", "required": false, "caseExact": false, "canonicalValues": ["photo","thumbnail"], "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": "addresses", + "type": "complex", + "multiValued": true, + "description": "Physical mailing addresses.", + "required": false, + "mutability": "readWrite", + "returned": "default", + "uniqueness": "none", + "subAttributes": [ + { "name": "formatted", "type": "string", "multiValued": false, "description": "Full address, may contain newlines.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "streetAddress", "type": "string", "multiValued": false, "description": "Street address.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "locality", "type": "string", "multiValued": false, "description": "City or locality.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "region", "type": "string", "multiValued": false, "description": "State or region.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "postalCode", "type": "string", "multiValued": false, "description": "Zip or postal code.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "country", "type": "string", "multiValued": false, "description": "Country name.", "required": false, "caseExact": false, "mutability": "readWrite", "returned": "default", "uniqueness": "none" }, + { "name": "type", "type": "string", "multiValued": false, "description": "Type: 'work','home','other'.", "required": false, "caseExact": false, "canonicalValues": ["work","home","other"], "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": "groups", + "type": "complex", + "multiValued": true, + "description": "Groups to which the user belongs.", + "required": false, + "mutability": "readOnly", + "returned": "default", + "subAttributes": [ + { "name": "value", "type": "string", "multiValued": false, "description": "Group identifier.", "required": false, "caseExact": false, "mutability": "readOnly", "returned": "default", "uniqueness": "none" }, + { "name": "$ref", "type": "reference", "referenceTypes": ["User","Group"], "multiValued": false, "description": "URI of the Group resource.", "required": false, "caseExact": false, "mutability": "readOnly", "returned": "default", "uniqueness": "none" }, + { "name": "display", "type": "string", "multiValued": false, "description": "Display name.", "required": false, "caseExact": false, "mutability": "readOnly", "returned": "default", "uniqueness": "none" } + ] + }, + { + "name": "entitlements", + "type": "complex", + "multiValued": true, + "description": "Entitlements granted to the user.", + "required": false, + "mutability": "readWrite", + "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", + "type": "complex", + "multiValued": true, + "description": "Roles granted to the user.", + "required": false, + "mutability": "readWrite", + "returned": "default", + "subAttributes": [ + { "name": "value", "type": "string", "multiValued": false, "description": "Role 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": "x509Certificates", + "type": "complex", + "multiValued": true, + "description": "X.509 certificates issued to the user.", + "required": false, + "mutability": "readWrite", + "returned": "default", + "subAttributes": [ + { "name": "value", "type": "binary", "multiValued": false, "description": "Certificate 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, "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" } + ] + } + ], + "meta": { + "resourceType": "Schema", + "location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:User", + "created": "2025-04-17T15:48:00", + "lastModified": "2025-04-17T15:48:00" + } +} diff --git a/ee/api/routers/scim.py b/ee/api/routers/scim.py index 40230c14f..cf21cb878 100644 --- a/ee/api/routers/scim.py +++ b/ee/api/routers/scim.py @@ -1,13 +1,11 @@ import logging -from typing import Any, Literal -import copy -from datetime import datetime +from typing import Any from decouple import config from fastapi import Depends, HTTPException, Header, Query, Response, Request from fastapi.responses import JSONResponse -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from pydantic import BaseModel, field_serializer +from fastapi.security import OAuth2PasswordRequestForm +from pydantic import BaseModel from chalicelib.core import users, tenants from chalicelib.utils.scim_auth import ( @@ -26,19 +24,8 @@ logger = logging.getLogger(__name__) public_app, app, app_apikey = get_routers(prefix="/sso/scim/v2") -"""Authentication endpoints""" - - -class RefreshRequest(BaseModel): - refresh_token: str - - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -# Login endpoint to generate tokens @public_app.post("/token") -async def login( +async def post_token( host: str = Header(..., alias="Host"), form_data: OAuth2PasswordRequestForm = Depends(), ): @@ -56,17 +43,19 @@ async def login( return { "access_token": access_token, "refresh_token": refresh_token, - "token_type": "bearer", + "token_type": "Bearer", } -# Refresh token endpoint +class RefreshRequest(BaseModel): + refresh_token: str + + @public_app.post("/refresh") -async def refresh_token(r: RefreshRequest): +async def post_refresh(r: RefreshRequest): payload = verify_refresh_token(r.refresh_token) new_access_token, _ = create_tokens(tenant_id=payload["tenant_id"]) - - return {"access_token": new_access_token, "token_type": "bearer"} + return {"access_token": new_access_token, "token_type": "Bearer"} RESOURCE_TYPE_IDS_TO_RESOURCE_TYPE_DETAILS = { @@ -110,17 +99,33 @@ def _mutability_error_response(): ) +def _operation_not_permitted_error_response(): + return JSONResponse( + status_code=403, + content={ + "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], + "detail": "Operation is not permitted based on the supplied authorization", + "status": "403", + }, + ) + + +def _invalid_value_error_response(): + return JSONResponse( + status_code=400, + content={ + "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], + "detail": "A required value was missing, or the value specified was not compatible with the operation or attribtue type, or resource schema.", + "status": "400", + "scimType": "invalidValue", + }, + ) + + @public_app.get("/ResourceTypes", dependencies=[Depends(auth_required)]) async def get_resource_types(filter_param: str | None = Query(None, alias="filter")): if filter_param is not None: - return JSONResponse( - status_code=403, - content={ - "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], - "detail": "Operation is not permitted based on the supplied authorization", - "status": "403", - }, - ) + return _operation_not_permitted_error_response() return JSONResponse( status_code=200, content={ @@ -151,14 +156,7 @@ SCHEMA_IDS_TO_SCHEMA_DETAILS = { @public_app.get("/Schemas", dependencies=[Depends(auth_required)]) async def get_schemas(filter_param: str | None = Query(None, alias="filter")): if filter_param is not None: - return JSONResponse( - status_code=403, - content={ - "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], - "detail": "Operation is not permitted based on the supplied authorization", - "status": "403", - }, - ) + return _operation_not_permitted_error_response() return JSONResponse( status_code=200, content={ @@ -189,81 +187,19 @@ async def get_schema(schema_id: str): async def get_service_provider_config( r: Request, tenant_id: str | None = Depends(auth_optional) ): - content = copy.deepcopy(SERVICE_PROVIDER_CONFIG) - content["meta"]["location"] = str(r.url) is_authenticated = tenant_id is not None if not is_authenticated: - content = { - "schemas": content["schemas"], - "authenticationSchemes": content["authenticationSchemes"], - "meta": content["meta"], - } - return JSONResponse( - status_code=200, - content=content, - ) - - -""" -User endpoints -""" - - -class UserRequest(BaseModel): - userName: str - - -class PatchUserRequest(BaseModel): - schemas: list[str] - Operations: list[dict] - - -class ResourceMetaResponse(BaseModel): - resourceType: ( - Literal["ServiceProviderConfig", "ResourceType", "Schema", "User"] | None - ) = None - created: datetime | None = None - lastModified: datetime | None = None - location: str | None = None - version: str | None = None - - @field_serializer("created", "lastModified") - def serialize_datetime(self, dt: datetime) -> str | None: - if not dt: - return None - return dt.strftime("%Y-%m-%dT%H:%M:%SZ") - - -class CommonResourceResponse(BaseModel): - id: str - externalId: str | None = None - schemas: list[ - Literal[ - "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig", - "urn:ietf:params:scim:schemas:core:2.0:ResourceType", - "urn:ietf:params:scim:schemas:core:2.0:Schema", - "urn:ietf:params:scim:schemas:core:2.0:User", - ] - ] - meta: ResourceMetaResponse | None = None - - -class UserResponse(CommonResourceResponse): - schemas: list[Literal["urn:ietf:params:scim:schemas:core:2.0:User"]] = [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ] - userName: str | None = None - - -class QueryResourceResponse(BaseModel): - schemas: list[Literal["urn:ietf:params:scim:api:messages:2.0:ListResponse"]] = [ - "urn:ietf:params:scim:api:messages:2.0:ListResponse" - ] - totalResults: int - # todo(jon): add the other schemas - Resources: list[UserResponse] - startIndex: int - itemsPerPage: int + return JSONResponse( + status_code=200, + content={ + "schemas": SERVICE_PROVIDER_CONFIG["schemas"], + "authenticationSchemes": SERVICE_PROVIDER_CONFIG[ + "authenticationSchemes" + ], + "meta": SERVICE_PROVIDER_CONFIG["meta"], + }, + ) + return JSONResponse(status_code=200, content=SERVICE_PROVIDER_CONFIG) MAX_USERS_PER_PAGE = 10 @@ -273,7 +209,7 @@ def _convert_db_user_to_scim_user( db_user: dict[str, Any], attributes: list[str] | None = None, excluded_attributes: list[str] | None = None, -) -> UserResponse: +) -> dict[str, Any]: user_schema = SCHEMA_IDS_TO_SCHEMA_DETAILS[ "urn:ietf:params:scim:schemas:core:2.0:User" ] @@ -289,19 +225,19 @@ def _convert_db_user_to_scim_user( ) scim_user = { "id": str(db_user["userId"]), + "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "meta": { "resourceType": "User", - "created": db_user["createdAt"], - "lastModified": db_user[ - "createdAt" - ], # todo(jon): we currently don't keep track of this in the db + "created": db_user["createdAt"].strftime("%Y-%m-%dT%H:%M:%SZ"), + # todo(jon): we currently don't keep track of this in the db + "lastModified": db_user["createdAt"].strftime("%Y-%m-%dT%H:%M:%SZ"), "location": f"Users/{db_user['userId']}", }, "userName": db_user["email"], } scim_user = scim_helpers.filter_attributes(scim_user, included_attributes) scim_user = scim_helpers.exclude_attributes(scim_user, excluded_attributes) - return UserResponse(**scim_user) + return scim_user @public_app.get("/Users") @@ -318,79 +254,81 @@ async def get_users( ) # todo(jon): this might not be the most efficient thing to do. could be better to just do a count. # but this is the fastest thing at the moment just to test that it's working - total_users = users.get_users_paginated(1, tenant_id) - db_users = users.get_users_paginated(start_index, tenant_id, count=items_per_page) - scim_users = [ - _convert_db_user_to_scim_user(user, attributes, excluded_attributes) - for user in db_users + total_resources = users.get_users_paginated(1, tenant_id) + db_resources = users.get_users_paginated( + start_index, tenant_id, count=items_per_page + ) + scim_resources = [ + _convert_db_user_to_scim_user(resource, attributes, excluded_attributes) + for resource in db_resources ] return JSONResponse( status_code=200, - content=QueryResourceResponse( - totalResults=len(total_users), - startIndex=start_index, - itemsPerPage=len(scim_users), - Resources=scim_users, - ).model_dump(mode="json", exclude_none=True), + content={ + "totalResults": len(total_resources), + "startIndex": start_index, + "itemsPerPage": len(scim_resources), + "Resources": scim_resources, + }, ) @public_app.get("/Users/{user_id}") -def get_user( +async def get_user( user_id: str, tenant_id=Depends(auth_required), attributes: list[str] | None = Query(None), excluded_attributes: list[str] | None = Query(None, alias="excludedAttributes"), ): - db_user = users.get_scim_user_by_id(user_id, tenant_id) - if not db_user: + db_resource = users.get_scim_user_by_id(user_id, tenant_id) + if not db_resource: return _not_found_error_response(user_id) - scim_user = _convert_db_user_to_scim_user(db_user, attributes, excluded_attributes) - return JSONResponse( - status_code=200, content=scim_user.model_dump(mode="json", exclude_none=True) + scim_resource = _convert_db_user_to_scim_user( + db_resource, attributes, excluded_attributes ) + return JSONResponse(status_code=200, content=scim_resource) @public_app.post("/Users") -async def create_user(r: UserRequest, tenant_id=Depends(auth_required)): +async def create_user(r: Request, tenant_id=Depends(auth_required)): + payload = await r.json() + if "userName" not in payload: + return _invalid_value_error_response() # note(jon): this method will return soft deleted users as well - existing_db_user = users.get_existing_scim_user_by_unique_values(r.userName) - if existing_db_user and existing_db_user["deletedAt"] is None: + existing_db_resource = users.get_existing_scim_user_by_unique_values( + payload["userName"] + ) + if existing_db_resource and existing_db_resource["deletedAt"] is None: return _uniqueness_error_response() - if existing_db_user and existing_db_user["deletedAt"] is not None: - db_user = users.restore_scim_user(existing_db_user["userId"], tenant_id) + if existing_db_resource and existing_db_resource["deletedAt"] is not None: + db_resource = users.restore_scim_user(existing_db_resource["userId"], tenant_id) else: - db_user = users.create_scim_user( - email=r.userName, + db_resource = users.create_scim_user( + email=payload["userName"], # note(jon): scim schema does not require the `name.formatted` attribute, but we require `name`. # so, we have to define the value ourselves here name="", tenant_id=tenant_id, ) - scim_user = _convert_db_user_to_scim_user(db_user) - response = JSONResponse( - status_code=201, content=scim_user.model_dump(mode="json", exclude_none=True) - ) - response.headers["Location"] = scim_user.meta.location + scim_resource = _convert_db_user_to_scim_user(db_resource) + response = JSONResponse(status_code=201, content=scim_resource) + response.headers["Location"] = scim_resource["meta"]["location"] return response @public_app.put("/Users/{user_id}") -def update_user(user_id: str, r: UserRequest, tenant_id=Depends(auth_required)): +async def update_user(user_id: str, r: Request, tenant_id=Depends(auth_required)): db_resource = users.get_scim_user_by_id(user_id, tenant_id) if not db_resource: return _not_found_error_response(user_id) - current_scim_resource = _convert_db_user_to_scim_user(db_resource).model_dump( - mode="json", exclude_none=True - ) - changes = r.model_dump(mode="json") + current_scim_resource = _convert_db_user_to_scim_user(db_resource) + changes = await r.json() schema = SCHEMA_IDS_TO_SCHEMA_DETAILS["urn:ietf:params:scim:schemas:core:2.0:User"] try: valid_mutable_changes = scim_helpers.filter_mutable_attributes( schema, changes, current_scim_resource ) except ValueError: - # todo(jon): will need to add a test for this once we have an immutable field return _mutability_error_response() try: updated_db_resource = users.update_scim_user( @@ -399,19 +337,16 @@ def update_user(user_id: str, r: UserRequest, tenant_id=Depends(auth_required)): email=valid_mutable_changes["userName"], ) updated_scim_resource = _convert_db_user_to_scim_user(updated_db_resource) - return JSONResponse( - status_code=200, - content=updated_scim_resource.model_dump(mode="json", exclude_none=True), - ) + return JSONResponse(status_code=200, content=updated_scim_resource) except Exception: # note(jon): for now, this is the only error that would happen when updating the scim user return _uniqueness_error_response() @public_app.delete("/Users/{user_id}") -def delete_user(user_id: str, tenant_id=Depends(auth_required)): - user = users.get_scim_user_by_id(user_id, tenant_id) - if not user: +async def delete_user(user_id: str, tenant_id=Depends(auth_required)): + db_resource = users.get_scim_user_by_id(user_id, tenant_id) + if not db_resource: return _not_found_error_response(user_id) users.soft_delete_scim_user_by_id(user_id, tenant_id) return Response(status_code=204, content="") diff --git a/ee/api/routers/scim_constants.py b/ee/api/routers/scim_constants.py index 74e00ee01..090135ffc 100644 --- a/ee/api/routers/scim_constants.py +++ b/ee/api/routers/scim_constants.py @@ -1,778 +1,23 @@ # note(jon): please see https://datatracker.ietf.org/doc/html/rfc7643 for details on these constants -from typing import Any - - -def _attribute_characteristics( - name: str, - description: str, - type: str = "string", - sub_attributes: dict[str, Any] | None = None, - # note(jon): no default for multiValued is defined in the docs and it is marked as optional. - # from our side, we'll default it to False. - multi_valued: bool = False, - required: bool = False, - canonical_values: list[str] | None = None, - case_exact: bool = False, - mutability: str = "readWrite", - returned: str = "default", - uniqueness: str = "none", - reference_types: list[str] | None = None, -): - characteristics = { - "name": name, - "type": type, - "subAttributes": sub_attributes, - "multiValued": multi_valued, - "description": description, - "required": required, - "canonicalValues": canonical_values, - "caseExact": case_exact, - "mutability": mutability, - "returned": returned, - "uniqueness": uniqueness, - "referenceTypes": reference_types, - } - characteristics_without_none = { - key: value for key, value in characteristics.items() if value is not None - } - return characteristics_without_none - - -def _multi_valued_attributes( - type_canonical_values: list[str], - type_required: bool = False, - type_mutability="readWrite", -): - return [ - _attribute_characteristics( - name="type", - description="A label indicating the attribute's function.", - canonical_values=type_canonical_values, - case_exact=True, - required=type_required, - mutability=type_mutability, - ), - _attribute_characteristics( - name="primary", - type="boolean", - description="A Boolean value indicating the 'primary' or preferred attribute value for this attribute.", - ), - _attribute_characteristics( - name="display", - description="A human-readable name.", - mutability="immutable", - ), - _attribute_characteristics( - name="value", - description="The attribute's significant value.", - ), - _attribute_characteristics( - name="$ref", - type="reference", - reference_types=["uri"], - description="The reference URI of a target resource.", - ), - ] - - -# note(jon): the docs are a little confusing regarding this, but -# in section 3.1 of RFC7643, it is specified that ResourceType and -# ServiceProviderConfig are not included in the common attributes. but -# in other references, they treat them as a resource. -def _common_resource_attributes(id_required: bool = True, id_uniqueness: str = "none"): - return [ - _attribute_characteristics( - name="id", - description="A unique identifier for the SCIM resource.", - case_exact=True, - mutability="readOnly", - returned="always", - required=id_required, - uniqueness=id_uniqueness, - ), - _attribute_characteristics( - name="externalId", - description="A String that is an identifier for the resource as defined by the provisioning client.", - case_exact=True, - ), - _attribute_characteristics( - name="schemas", - type="reference", - reference_types=["uri"], - description="An array of Strings containing URI that are used to indicate the namespaces of the SCIM schemas that define the attributes present in the current JSON structure.", - multi_valued=True, - canonical_values=[ - "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig", - "urn:ietf:params:scim:schemas:core:2.0:ResourceType", - "urn:ietf:params:scim:schemas:core:2.0:Schema", - "urn:ietf:params:scim:schemas:core:2.0:User", - ], - case_exact=True, - mutability="readOnly", - required=True, - returned="always", - ), - _attribute_characteristics( - name="meta", - type="complex", - description="A complex attribute containing resource metadata.", - required=True, - sub_attributes=[ - _attribute_characteristics( - name="resourceType", - description="The name of the resource type of the resource.", - mutability="readOnly", - case_exact=True, - required=True, - ), - _attribute_characteristics( - name="created", - type="dateTime", - description="The 'DateTime' that the resource was added to the service provider.", - mutability="readOnly", - required=True, - ), - _attribute_characteristics( - name="lastModified", - type="dateTime", - description="The most recent DateTime that the details of this resource were updated at the service provider.", - mutability="readOnly", - required=True, - ), - _attribute_characteristics( - name="location", - type="reference", - reference_types=["uri"], - description="The URI of the resource being returned.", - mutability="readOnly", - required=True, - ), - # todo(jon): decide if we'll handle versioning. for now, we won't do it. - ], - ), - ] - - -SERVICE_PROVIDER_CONFIG_SCHEMA = { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"], - "id": "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig", - "name": "Service Provider Configuration", - "description": "Schema for representing the service provider's configuration.", - "meta": { - "resourceType": "Schema", - "created": "2025-04-16T14:48:00Z", - # note(jon): we might want to think about adding this resource as part of our db - # and then updating these timestamps from an api and such. for now, if we update - # the configuration, we should update the timestamp here. - "lastModified": "2025-04-16T14:48:00Z", - "location": "Schemas/urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig", - }, - "attributes": [ - *_common_resource_attributes(id_required=False), - _attribute_characteristics( - name="documentationUri", - type="reference", - reference_types=["external"], - description="An HTTP-addressable URL pointing to the service provider's human-consumable help documentation.", - mutability="readOnly", - ), - _attribute_characteristics( - name="patch", - type="complex", - description="A complex type that specifies PATCH configuration options.", - required=True, - mutability="readOnly", - sub_attributes=[ - _attribute_characteristics( - name="supported", - type="boolean", - description="A Boolean value specifying whether or not the operation is supported.", - required=True, - mutability="readOnly", - ), - ], - ), - _attribute_characteristics( - name="bulk", - type="complex", - description="A complex type that specifies bulk configuration options.", - required=True, - mutability="readOnly", - sub_attributes=[ - _attribute_characteristics( - name="supported", - type="boolean", - description="A Boolean value specifying whether or not the operation is supported.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="maxOperations", - type="integer", - description="An integer value specifying the maximum number of operations.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="maxPayloadSize", - type="integer", - description="An integer value specifying the maximum payload size in bytes.", - required=True, - mutability="readOnly", - ), - ], - ), - _attribute_characteristics( - name="filter", - type="complex", - description="A complex type that specifies FILTER options.", - required=True, - mutability="readOnly", - sub_attributes=[ - _attribute_characteristics( - name="supported", - type="boolean", - description="A Boolean value specifying whether or not the operation is supported.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="maxResults", - type="integer", - description="The integer value specifying the maximum number of resources returned in a response.", - required=True, - mutability="readOnly", - ), - ], - ), - _attribute_characteristics( - name="changePassword", - type="complex", - description="A complex type that specifies configuration options related to changing a password.", - required=True, - mutability="readOnly", - sub_attributes=[ - _attribute_characteristics( - name="supported", - type="boolean", - description="A Boolean value specifying whether or not the operation is supported.", - required=True, - mutability="readOnly", - ), - ], - ), - _attribute_characteristics( - name="sort", - type="complex", - description="A complex type that specifies sort result options.", - required=True, - mutability="readOnly", - sub_attributes=[ - _attribute_characteristics( - name="supported", - type="boolean", - description="A Boolean value specifying whether or not the operation is supported.", - required=True, - mutability="readOnly", - ), - ], - ), - _attribute_characteristics( - name="etag", - type="complex", - description="A complex type that specifies ETag configuration options.", - required=True, - mutability="readOnly", - sub_attributes=[ - _attribute_characteristics( - name="supported", - type="boolean", - description="A Boolean value specifying whether or not the operation is supported.", - required=True, - mutability="readOnly", - ), - ], - ), - _attribute_characteristics( - name="authenticationSchemes", - type="complex", - multi_valued=True, - description="A complex type that specifies supported authentication scheme properties.", - required=True, - mutability="readOnly", - sub_attributes=[ - *_multi_valued_attributes( - type_canonical_values=[ - "oauth", - "oauth2", - "oauthbearertoken", - "httpbasic", - "httpdigest", - ], - type_required=True, - type_mutability="readOnly", - ), - _attribute_characteristics( - name="name", - description="The common authentication scheme name, e.g., HTTP Basic.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="description", - description="A description of the authentication scheme.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="specUri", - type="reference", - reference_types=["external"], - description="An HTTP-addressable URL pointing to the authentication scheme's specification.", - mutability="readOnly", - ), - _attribute_characteristics( - name="documentationUri", - type="reference", - reference_types=["external"], - description="An HTTP-addressable URL pointing to the authentication scheme's usage documentation.", - mutability="readOnly", - ), - ], - ), - ], -} - - -RESOURCE_TYPE_SCHEMA = { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"], - "id": "urn:ietf:params:scim:schemas:core:2.0:ResourceType", - "name": "Resource Type", - "description": "Specifies the schema that describes a SCIM resource type.", - "meta": { - "resourceType": "Schema", - "created": "2025-04-16T14:48:00Z", - # note(jon): we might want to think about adding this resource as part of our db - # and then updating these timestamps from an api and such. for now, if we update - # the configuration, we should update the timestamp here. - "lastModified": "2025-04-16T14:48:00Z", - "location": "Schemas/urn:ietf:params:scim:schemas:core:2.0:ResourceType", - }, - "attributes": [ - *_common_resource_attributes(id_required=False, id_uniqueness="global"), - _attribute_characteristics( - name="name", - description="The resource type name.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="description", - description="The resource type's human-readable description.", - mutability="readOnly", - ), - # todo(jon): figure out what the correct type/reference_type is here - _attribute_characteristics( - name="endpoint", - type="reference", - reference_types=["uri"], - description="The resource type's HTTP-addressable endpoint relative to the Base URL of the service provider.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="schema", - type="reference", - reference_types=["uri"], - description="The resource type's primary/base schema URI.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="schemaExtensions", - type="complex", - multi_valued=True, - description="A list of URIs of the resource type's schema extensions.", - mutability="readOnly", - sub_attributes=[ - _attribute_characteristics( - name="schema", - type="reference", - reference_types=["uri"], - description="The URI of an extended schema.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="required", - type="boolean", - description="A Boolean value that specifies whether or not the schema extension is required for the resource type.", - required=True, - mutability="readOnly", - ), - ], - ), - ], -} - -SCHEMA_SCHEMA = { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"], - "id": "urn:ietf:params:scim:schemas:core:2.0:Schema", - "name": "Schema", - "description": "Specifies the schema that describes a SCIM schema.", - "meta": { - "resourceType": "Schema", - "created": "2025-04-16T14:48:00Z", - # note(jon): we might want to think about adding this resource as part of our db - # and then updating these timestamps from an api and such. for now, if we update - # the configuration, we should update the timestamp here. - "lastModified": "2025-04-16T14:48:00Z", - "location": "Schemas/urn:ietf:params:scim:schemas:core:2.0:Schema", - }, - "attributes": [ - *_common_resource_attributes(id_uniqueness="global"), - _attribute_characteristics( - name="name", - description="The schema's human‐readable name.", - mutability="readOnly", - ), - _attribute_characteristics( - name="description", - description="The schema's human-readable name.", - mutability="readOnly", - ), - _attribute_characteristics( - name="attributes", - type="complex", - multi_valued=True, - description="A complex attribute that defines service provider attributes and their qualities.", - required=True, - mutability="readOnly", - sub_attributes=[ - _attribute_characteristics( - name="name", - description="The attribute's name.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="type", - description="The attribute's data type.", - required=True, - canonical_values=[ - "string", - "complex", - "boolean", - "decimal", - "integer", - "dateTime", - "reference", - ], - case_exact=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="multiValued", - type="boolean", - description="A Boolean value indicating an attribute's plurality.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="description", - description="The attribute's human-readable description.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="required", - type="boolean", - description="A Boolean value indicating whether or not the attribute is required.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="canonicalValues", - multi_valued=True, - description="A collection of canonical values.", - mutability="readOnly", - ), - _attribute_characteristics( - name="caseExact", - type="boolean", - description="A Boolean that specifies whether or not a string attribute is case sensitive.", - mutability="readOnly", - ), - _attribute_characteristics( - name="mutability", - description="A single keyword indicating the circumstances under which the value of the attribute can be (re)defined.", - required=True, - mutability="readOnly", - canonical_values=[ - "readOnly", - "readWrite", - "immutable", - "writeOnly", - ], - case_exact=True, - ), - _attribute_characteristics( - name="returned", - description="A single keyword that indicates when an attribute and associated values are returned in response to a GET request or in response to a PUT, POST, or PATCH request.", - required=True, - mutability="readOnly", - canonical_values=[ - "always", - "never", - "default", - "request", - ], - case_exact=True, - ), - _attribute_characteristics( - name="uniqueness", - description="A single keyword value that specifies how the service provider enforces uniqueness of attribute values.", - required=True, - mutability="readOnly", - canonical_values=[ - "none", - "server", - "global", - ], - case_exact=True, - ), - _attribute_characteristics( - name="referenceTypes", - multi_valued=True, - description="A multi-valued array of JSON strings that indicate the SCIM resource types that may be referenced.", - mutability="readOnly", - canonical_values=[ - # todo(jon): add "User" and "Group" once those are done. - "external", - "uri", - ], - case_exact=True, - ), - _attribute_characteristics( - name="subAttributes", - type="complex", - multi_valued=True, - description="When an attribute is of type 'complex', this defines a set of sub-attributes.", - mutability="readOnly", - sub_attributes=[ - _attribute_characteristics( - name="name", - description="The attribute's name.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="type", - description="The attribute's data type.", - required=True, - canonical_values=[ - "string", - "complex", - "boolean", - "decimal", - "integer", - "dateTime", - "reference", - ], - case_exact=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="multiValued", - type="boolean", - description="A Boolean value indicating an attribute's plurality.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="description", - description="The attribute's human-readable description.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="required", - type="boolean", - description="A Boolean value indicating whether or not the attribute is required.", - required=True, - mutability="readOnly", - ), - _attribute_characteristics( - name="canonicalValues", - multi_valued=True, - description="A collection of canonical values.", - mutability="readOnly", - ), - _attribute_characteristics( - name="caseExact", - type="boolean", - description="A Boolean that specifies whether or not a string attribute is case sensitive.", - mutability="readOnly", - ), - _attribute_characteristics( - name="mutability", - description="A single keyword indicating the circumstances under which the value of the attribute can be (re)defined.", - required=True, - mutability="readOnly", - canonical_values=[ - "readOnly", - "readWrite", - "immutable", - "writeOnly", - ], - case_exact=True, - ), - _attribute_characteristics( - name="returned", - description="A single keyword that indicates when an attribute and associated values are returned in response to a GET request or in response to a PUT, POST, or PATCH request.", - required=True, - mutability="readOnly", - canonical_values=[ - "always", - "never", - "default", - "request", - ], - case_exact=True, - ), - _attribute_characteristics( - name="uniqueness", - description="A single keyword value that specifies how the service provider enforces uniqueness of attribute values.", - required=True, - mutability="readOnly", - canonical_values=[ - "none", - "server", - "global", - ], - case_exact=True, - ), - _attribute_characteristics( - name="referenceTypes", - multi_valued=True, - description="A multi-valued array of JSON strings that indicate the SCIM resource types that may be referenced.", - mutability="readOnly", - canonical_values=[ - # todo(jon): add "User" and "Group" once those are done. - "external", - "uri", - ], - case_exact=True, - ), - ], - ), - ], - ), - ], -} - - -USER_SCHEMA = { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"], - "id": "urn:ietf:params:scim:schemas:core:2.0:User", - "name": "User", - "description": "User account.", - "meta": { - "resourceType": "Schema", - "created": "2025-04-16T14:48:00Z", - # note(jon): we might want to think about adding this resource as part of our db - # and then updating these timestamps from an api and such. for now, if we update - # the configuration, we should update the timestamp here. - "lastModified": "2025-04-16T14:48:00Z", - "location": "Schemas/urn:ietf:params:scim:schemas:core:2.0:User", - }, - "attributes": [ - *_common_resource_attributes(), - _attribute_characteristics( - name="userName", - description="A service provider's unique identifier for the user.", - required=True, - ), - ], -} - +import json SCHEMAS = sorted( [ - SERVICE_PROVIDER_CONFIG_SCHEMA, - RESOURCE_TYPE_SCHEMA, - SCHEMA_SCHEMA, - USER_SCHEMA, + json.load(open("routers/fixtures/service_provider_config_schema.json", "r")), + json.load(open("routers/fixtures/resource_type_schema.json", "r")), + json.load(open("routers/fixtures/schema_schema.json", "r")), + json.load(open("routers/fixtures/user_schema.json", "r")), + # todo(jon): add this when we have groups + # json.load(open("routers/schemas/group_schema.json", "r")), ], key=lambda x: x["id"], ) -SERVICE_PROVIDER_CONFIG = { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"], - "patch": { - # todo(jon): this needs to be updated to True once we properly implement patching for users and groups - "supported": False, - }, - "bulk": { - "supported": False, - "maxOperations": 0, - "maxPayloadSize": 0, - }, - "filter": { - # todo(jon): this needs to be updated to True once we properly implement filtering for users and groups - "supported": False, - "maxResults": 0, - }, - "changePassword": { - "supported": False, - }, - "sort": { - "supported": False, - }, - "etag": { - "supported": False, - }, - "authenticationSchemes": [ - { - "type": "oauthbearertoken", - "name": "OAuth Bearer Token", - "description": "Authentication scheme using the OAuth Bearer Token Standard. The access token should be sent in the 'Authorization' header using the Bearer schema.", - "specUri": "https://tools.ietf.org/html/rfc6750", - # todo(jon): see if we have our own documentation for this - # "documentationUri": "", # optional - "primary": True, - }, - ], - "meta": { - "resourceType": "ServiceProviderConfig", - "created": "2025-04-15T15:45:00Z", - # note(jon): we might want to think about adding this resource as part of our db - # and then updating these timestamps from an api and such. for now, if we update - # the configuration, we should update the timestamp here. - "lastModified": "2025-04-15T15:45:00Z", - "location": "", # note(jon): this field will be computed in the /ServiceProviderConfig endpoint - }, -} +SERVICE_PROVIDER_CONFIG = json.load( + open("routers/fixtures/service_provider_config.json", "r") +) RESOURCE_TYPES = sorted( - [ - { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"], - "id": "User", - "name": "User", - "endpoint": "/Users", - "description": "User account", - "schema": "urn:ietf:params:scim:schemas:core:2.0:User", - "meta": { - "resourceType": "ResourceType", - "created": "2025-04-16T08:37:00Z", - # note(jon): we might want to think about adding this resource as part of our db - # and then updating these timestamps from an api and such. for now, if we update - # the configuration, we should update the timestamp here. - "lastModified": "2025-04-16T08:37:00Z", - "location": "ResourceType/User", - }, - } - ], + json.load(open("routers/fixtures/resource_type.json", "r")), key=lambda x: x["id"], )