diff --git a/ee/api/chalicelib/utils/scim_auth.py b/ee/api/chalicelib/utils/scim_auth.py index 83e779c40..fb73b9dbb 100644 --- a/ee/api/chalicelib/utils/scim_auth.py +++ b/ee/api/chalicelib/utils/scim_auth.py @@ -68,10 +68,21 @@ def verify_refresh_token(token: str): raise HTTPException(status_code=401, detail="Invalid token") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") +required_oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # Authentication Dependency -def auth_required(token: str = Depends(oauth2_scheme)): +def auth_required(token: str = Depends(required_oauth2_scheme)): """Dependency to check Authorization header.""" if config("SCIM_AUTH_TYPE") == "OAuth2": payload = verify_access_token(token) return payload["tenant_id"] + + +optional_oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) +def auth_optional(token: str | None = Depends(optional_oauth2_scheme)): + if token is None: + return None + try: + tenant_id = auth_required(token) + return tenant_id + except HTTPException: + return None diff --git a/ee/api/routers/scim.py b/ee/api/routers/scim.py index 4fddeda2f..1a34620a6 100644 --- a/ee/api/routers/scim.py +++ b/ee/api/routers/scim.py @@ -2,6 +2,7 @@ import logging import re import uuid from typing import Optional +import copy from decouple import config from fastapi import Depends, HTTPException, Header, Query, Response, Request @@ -11,9 +12,9 @@ from pydantic import BaseModel, Field import schemas from chalicelib.core import users, roles, tenants -from chalicelib.utils.scim_auth import auth_required, create_tokens, verify_refresh_token +from chalicelib.utils.scim_auth import auth_optional, auth_required, create_tokens, verify_refresh_token from routers.base import get_routers -from routers.scim_constants import RESOURCE_TYPES, SCHEMAS +from routers.scim_constants import RESOURCE_TYPES, SCHEMAS, SERVICE_PROVIDER_CONFIG logger = logging.getLogger(__name__) @@ -160,6 +161,25 @@ async def get_schema(schema_id: str): ) +# note(jon): it was recommended to make this endpoint partially open +# so that clients can view the `authenticationSchemes` prior to being authenticated. +@public_app.get("/ServiceProviderConfig") +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 """ diff --git a/ee/api/routers/scim_constants.py b/ee/api/routers/scim_constants.py index 25c07fc58..f349fc5f7 100644 --- a/ee/api/routers/scim_constants.py +++ b/ee/api/routers/scim_constants.py @@ -1,3 +1,5 @@ +# note(jon): please see https://datatracker.ietf.org/doc/html/rfc7643 for details on these constants + RESOURCE_TYPES = sorted( [ { @@ -15,6 +17,7 @@ RESOURCE_TYPES = sorted( SCHEMAS = sorted( # todo(jon): add the user schema [ + # todo(jon): update the ResourceType schema to have the correct values { "id": "urn:ietf:params:scim:schemas:core:2.0:ResourceType", "name": "ResourceType", @@ -110,6 +113,7 @@ SCHEMAS = sorted( } ] }, + # todo(jon): update the Schema schema to have the correct values { "id": "urn:ietf:params:scim:schemas:core:2.0:Schema", "name": "Schema", @@ -249,39 +253,38 @@ SCHEMAS = sorted( }, { "id": "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig", - "name": "ServiceProviderConfig", - "description": "Defines the configuration options for the SCIM service provider.", + "name": "Service Provider Configuration", + "description": "Schema for representing the service provider's configuration.", "attributes": [ { "name": "documentationUri", - "type": "string", + "type": "reference", + "referenceTypes": ["external"], "multiValued": False, - "description": "The base or canonical URL of the service provider's documentation.", + "description": "An HTTP-addressable URL pointing to the service provider's human consumable help documentation.", "required": False, "caseExact": False, - "mutability": "readWrite", + "mutability": "readOnly", "returned": "default", - "uniqueness": "none" + "uniqueness": "none", }, { "name": "patch", "type": "complex", "multiValued": False, - "description": "A complex attribute indicating the service provider's support for PATCH requests.", - "required": False, - "mutability": "readOnly", + "description": "A complex type that specifies PATCH configuration options.", + "required": True, "returned": "default", - "uniqueness": "none", + "mutability": "readOnly", "subAttributes": [ { "name": "supported", "type": "boolean", "multiValued": False, - "description": "A Boolean value that indicates whether PATCH is supported.", - "required": False, + "description": "A Boolean value specifying whether or not the operation is supported.", + "required": True, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" } ] }, @@ -289,41 +292,39 @@ SCHEMAS = sorted( "name": "bulk", "type": "complex", "multiValued": False, - "description": "A complex attribute that indicates the service provider's support for bulk operations.", - "required": False, - "mutability": "readOnly", + "description": "A complex type that specifies bulk configuration options.", + "required": True, "returned": "default", - "uniqueness": "none", + "mutability": "readOnly", "subAttributes": [ { "name": "supported", "type": "boolean", "multiValued": False, - "description": "A Boolean value that indicates whether bulk operations are supported.", - "required": False, + "description": "A Boolean value specifying whether or not the operation is supported.", + "required": True, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" }, { "name": "maxOperations", "type": "integer", "multiValued": False, - "description": "The maximum number of operations that can be performed in a bulk request.", - "required": False, + "description": "An integer value specifying the maximum number of operations.", + "required": True, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" + "uniqueness": "none", }, { "name": "maxPayloadSize", "type": "integer", "multiValued": False, - "description": "The maximum payload size in bytes for a bulk request.", - "required": False, + "description": "An integer value specifying the maximum payload size in bytes.", + "required": True, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" + "uniqueness": "none", } ] }, @@ -331,31 +332,29 @@ SCHEMAS = sorted( "name": "filter", "type": "complex", "multiValued": False, - "description": "A complex attribute that indicates the service provider's support for filtering.", - "required": False, - "mutability": "readOnly", + "description": "A complex type that specifies FILTER options.", + "required": True, "returned": "default", - "uniqueness": "none", + "mutability": "readOnly", "subAttributes": [ { "name": "supported", "type": "boolean", "multiValued": False, - "description": "A Boolean value that indicates whether filtering is supported.", - "required": False, + "description": "A Boolean value specifying whether or not the operation is supported.", + "required": True, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" }, { "name": "maxResults", "type": "integer", "multiValued": False, - "description": "The maximum number of resources returned in a search response.", - "required": False, + "description": "The integer value specifying the maximum number of resources returned in a response.", + "required": True, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" + "uniqueness": "none", } ] }, @@ -363,137 +362,237 @@ SCHEMAS = sorted( "name": "changePassword", "type": "complex", "multiValued": False, - "description": "A complex attribute that indicates the service provider's support for change password requests.", - "required": False, - "mutability": "readOnly", + "description": "A complex type that specifies configuration options related to changing a password.", + "required": True, "returned": "default", - "uniqueness": "none", + "mutability": "readOnly", "subAttributes": [ { "name": "supported", "type": "boolean", "multiValued": False, - "description": "A Boolean value that indicates whether the change password operation is supported.", - "required": False, + "description": "A Boolean value specifying whether or not the operation is supported.", + "required": True, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" } - ] + ], }, { "name": "sort", "type": "complex", "multiValued": False, - "description": "A complex attribute that indicates the service provider's support for sorting.", - "required": False, - "mutability": "readOnly", + "description": "A complex type that specifies sort result options.", + "required": True, "returned": "default", - "uniqueness": "none", + "mutability": "readOnly", "subAttributes": [ { "name": "supported", "type": "boolean", "multiValued": False, - "description": "A Boolean value that indicates whether sorting is supported.", - "required": False, + "description": "A Boolean value specifying whether or not the operation is supported.", + "required": True, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" } - ] + ], }, { "name": "etag", "type": "complex", "multiValued": False, - "description": "A complex attribute that indicates the service provider's support for ETag in responses.", - "required": False, - "mutability": "readOnly", + "description": "A complex type that specifies ETag configuration options.", + "required": True, "returned": "default", - "uniqueness": "none", + "mutability": "readOnly", "subAttributes": [ { "name": "supported", "type": "boolean", "multiValued": False, - "description": "A Boolean value that indicates whether ETag is supported.", - "required": False, + "description": "A Boolean value specifying whether or not the operation is supported.", + "required": True, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" } - ] + ], }, { "name": "authenticationSchemes", "type": "complex", "multiValued": True, - "description": "A multi-valued complex attribute that defines the authentication schemes supported by the service provider.", - "required": False, - "mutability": "readOnly", + "description": "A complex type that specifies supported authentication scheme properties.", + "required": True, "returned": "default", - "uniqueness": "none", + "mutability": "readOnly", "subAttributes": [ { "name": "type", "type": "string", "multiValued": False, - "description": "The type of the authentication scheme.", - "required": False, + "description": "The authentication scheme. This specification defines the values 'oauth', 'oauth2', 'oauthbearertoken', 'httpbasic', and 'httpdigest'.", + "required": True, "caseExact": False, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" + "uniqueness": "none", }, { "name": "name", "type": "string", "multiValued": False, - "description": "The common name of the authentication scheme.", - "required": False, + "description": "The common authentication scheme name, e.g., HTTP Basic.", + "required": True, "caseExact": False, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" + "uniqueness": "none", }, { "name": "description", "type": "string", "multiValued": False, "description": "A description of the authentication scheme.", - "required": False, + "required": True, "caseExact": False, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" + "uniqueness": "none", }, { "name": "specUri", - "type": "string", + "type": "reference", + "referenceTypes": ["external"], "multiValued": False, - "description": "A URI that points to the authentication scheme's specification.", + "description": "An HTTP-addressable URL pointing to the authentication scheme's specification.", "required": False, "caseExact": False, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" + "uniqueness": "none", }, { "name": "documentationUri", - "type": "string", + "type": "reference", + "referenceTypes": ["external"], "multiValued": False, - "description": "A URI that points to the documentation for this scheme.", + "description": "An HTTP-addressable URL pointing to the authentication scheme's usage documentation.", "required": False, "caseExact": False, "mutability": "readOnly", "returned": "default", - "uniqueness": "none" + "uniqueness": "none", + }, + { + "name": "primary", + "type": "boolean", + "multiValued": False, + "description": "A Boolean value specifying whether or not the attribute is the preferred attribute value.", + "required": True, + "mutability": "readOnly", + "returned": "default", } ] - } + }, + { + "name": "meta", + "type": "complex", + "multiValued": True, + "description": "A complex type that specifies resource metadata.", + "required": True, + "returned": "default", + "mutability": "readOnly", + "subAttributes": [ + { + "name": "resourceType", + "type": "string", + "multiValued": False, + "description": "The name of the resource type of the resource.", + "required": True, + "caseExact": True, + "mutability": "readOnly", + "returned": "default", + }, + { + "name": "created", + "type": "datetime", + "multiValued": False, + "description": " The 'DateTime' that the resource was added to the service provider.", + "required": True, + "mutability": "readOnly", + "returned": "default", + }, + { + "name": "lastModified", + "type": "datetime", + "multiValued": False, + "description": "The most recent DateTime that the details of this resource were updated at the service provider.", + "required": True, + "mutability": "readOnly", + "returned": "default", + }, + { + "name": "location", + "type": "reference", + "referenceTypes": ["ServiceProviderConfig"], + "multiValued": False, + "description": "The URI of the resource being returned.", + "required": True, + "mutability": "readOnly", + "returned": "default", + }, + ] + }, ] }, ], 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", + # 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", + "location": "", # note(jon): this field will be computed in the /ServiceProviderConfig endpoint + }, +}