add ServiceProviderConfig endpoint

This commit is contained in:
Jonathan Griffin 2025-04-16 08:23:14 +02:00
parent f0b397708d
commit 7661bcc5e8
3 changed files with 213 additions and 83 deletions

View file

@ -68,10 +68,21 @@ def verify_refresh_token(token: str):
raise HTTPException(status_code=401, detail="Invalid token") raise HTTPException(status_code=401, detail="Invalid token")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") required_oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Authentication Dependency # Authentication Dependency
def auth_required(token: str = Depends(oauth2_scheme)): def auth_required(token: str = Depends(required_oauth2_scheme)):
"""Dependency to check Authorization header.""" """Dependency to check Authorization header."""
if config("SCIM_AUTH_TYPE") == "OAuth2": if config("SCIM_AUTH_TYPE") == "OAuth2":
payload = verify_access_token(token) payload = verify_access_token(token)
return payload["tenant_id"] 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

View file

@ -2,6 +2,7 @@ import logging
import re import re
import uuid import uuid
from typing import Optional from typing import Optional
import copy
from decouple import config from decouple import config
from fastapi import Depends, HTTPException, Header, Query, Response, Request from fastapi import Depends, HTTPException, Header, Query, Response, Request
@ -11,9 +12,9 @@ from pydantic import BaseModel, Field
import schemas import schemas
from chalicelib.core import users, roles, tenants 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.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__) 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 User endpoints
""" """

View file

@ -1,3 +1,5 @@
# note(jon): please see https://datatracker.ietf.org/doc/html/rfc7643 for details on these constants
RESOURCE_TYPES = sorted( RESOURCE_TYPES = sorted(
[ [
{ {
@ -15,6 +17,7 @@ RESOURCE_TYPES = sorted(
SCHEMAS = sorted( SCHEMAS = sorted(
# todo(jon): add the user schema # 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", "id": "urn:ietf:params:scim:schemas:core:2.0:ResourceType",
"name": "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", "id": "urn:ietf:params:scim:schemas:core:2.0:Schema",
"name": "Schema", "name": "Schema",
@ -249,39 +253,38 @@ SCHEMAS = sorted(
}, },
{ {
"id": "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig", "id": "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig",
"name": "ServiceProviderConfig", "name": "Service Provider Configuration",
"description": "Defines the configuration options for the SCIM service provider.", "description": "Schema for representing the service provider's configuration.",
"attributes": [ "attributes": [
{ {
"name": "documentationUri", "name": "documentationUri",
"type": "string", "type": "reference",
"referenceTypes": ["external"],
"multiValued": False, "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, "required": False,
"caseExact": False, "caseExact": False,
"mutability": "readWrite", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none" "uniqueness": "none",
}, },
{ {
"name": "patch", "name": "patch",
"type": "complex", "type": "complex",
"multiValued": False, "multiValued": False,
"description": "A complex attribute indicating the service provider's support for PATCH requests.", "description": "A complex type that specifies PATCH configuration options.",
"required": False, "required": True,
"mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none", "mutability": "readOnly",
"subAttributes": [ "subAttributes": [
{ {
"name": "supported", "name": "supported",
"type": "boolean", "type": "boolean",
"multiValued": False, "multiValued": False,
"description": "A Boolean value that indicates whether PATCH is supported.", "description": "A Boolean value specifying whether or not the operation is supported.",
"required": False, "required": True,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none"
} }
] ]
}, },
@ -289,41 +292,39 @@ SCHEMAS = sorted(
"name": "bulk", "name": "bulk",
"type": "complex", "type": "complex",
"multiValued": False, "multiValued": False,
"description": "A complex attribute that indicates the service provider's support for bulk operations.", "description": "A complex type that specifies bulk configuration options.",
"required": False, "required": True,
"mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none", "mutability": "readOnly",
"subAttributes": [ "subAttributes": [
{ {
"name": "supported", "name": "supported",
"type": "boolean", "type": "boolean",
"multiValued": False, "multiValued": False,
"description": "A Boolean value that indicates whether bulk operations are supported.", "description": "A Boolean value specifying whether or not the operation is supported.",
"required": False, "required": True,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none"
}, },
{ {
"name": "maxOperations", "name": "maxOperations",
"type": "integer", "type": "integer",
"multiValued": False, "multiValued": False,
"description": "The maximum number of operations that can be performed in a bulk request.", "description": "An integer value specifying the maximum number of operations.",
"required": False, "required": True,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none" "uniqueness": "none",
}, },
{ {
"name": "maxPayloadSize", "name": "maxPayloadSize",
"type": "integer", "type": "integer",
"multiValued": False, "multiValued": False,
"description": "The maximum payload size in bytes for a bulk request.", "description": "An integer value specifying the maximum payload size in bytes.",
"required": False, "required": True,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none" "uniqueness": "none",
} }
] ]
}, },
@ -331,31 +332,29 @@ SCHEMAS = sorted(
"name": "filter", "name": "filter",
"type": "complex", "type": "complex",
"multiValued": False, "multiValued": False,
"description": "A complex attribute that indicates the service provider's support for filtering.", "description": "A complex type that specifies FILTER options.",
"required": False, "required": True,
"mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none", "mutability": "readOnly",
"subAttributes": [ "subAttributes": [
{ {
"name": "supported", "name": "supported",
"type": "boolean", "type": "boolean",
"multiValued": False, "multiValued": False,
"description": "A Boolean value that indicates whether filtering is supported.", "description": "A Boolean value specifying whether or not the operation is supported.",
"required": False, "required": True,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none"
}, },
{ {
"name": "maxResults", "name": "maxResults",
"type": "integer", "type": "integer",
"multiValued": False, "multiValued": False,
"description": "The maximum number of resources returned in a search response.", "description": "The integer value specifying the maximum number of resources returned in a response.",
"required": False, "required": True,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none" "uniqueness": "none",
} }
] ]
}, },
@ -363,137 +362,237 @@ SCHEMAS = sorted(
"name": "changePassword", "name": "changePassword",
"type": "complex", "type": "complex",
"multiValued": False, "multiValued": False,
"description": "A complex attribute that indicates the service provider's support for change password requests.", "description": "A complex type that specifies configuration options related to changing a password.",
"required": False, "required": True,
"mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none", "mutability": "readOnly",
"subAttributes": [ "subAttributes": [
{ {
"name": "supported", "name": "supported",
"type": "boolean", "type": "boolean",
"multiValued": False, "multiValued": False,
"description": "A Boolean value that indicates whether the change password operation is supported.", "description": "A Boolean value specifying whether or not the operation is supported.",
"required": False, "required": True,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none"
} }
] ],
}, },
{ {
"name": "sort", "name": "sort",
"type": "complex", "type": "complex",
"multiValued": False, "multiValued": False,
"description": "A complex attribute that indicates the service provider's support for sorting.", "description": "A complex type that specifies sort result options.",
"required": False, "required": True,
"mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none", "mutability": "readOnly",
"subAttributes": [ "subAttributes": [
{ {
"name": "supported", "name": "supported",
"type": "boolean", "type": "boolean",
"multiValued": False, "multiValued": False,
"description": "A Boolean value that indicates whether sorting is supported.", "description": "A Boolean value specifying whether or not the operation is supported.",
"required": False, "required": True,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none"
} }
] ],
}, },
{ {
"name": "etag", "name": "etag",
"type": "complex", "type": "complex",
"multiValued": False, "multiValued": False,
"description": "A complex attribute that indicates the service provider's support for ETag in responses.", "description": "A complex type that specifies ETag configuration options.",
"required": False, "required": True,
"mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none", "mutability": "readOnly",
"subAttributes": [ "subAttributes": [
{ {
"name": "supported", "name": "supported",
"type": "boolean", "type": "boolean",
"multiValued": False, "multiValued": False,
"description": "A Boolean value that indicates whether ETag is supported.", "description": "A Boolean value specifying whether or not the operation is supported.",
"required": False, "required": True,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none"
} }
] ],
}, },
{ {
"name": "authenticationSchemes", "name": "authenticationSchemes",
"type": "complex", "type": "complex",
"multiValued": True, "multiValued": True,
"description": "A multi-valued complex attribute that defines the authentication schemes supported by the service provider.", "description": "A complex type that specifies supported authentication scheme properties.",
"required": False, "required": True,
"mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none", "mutability": "readOnly",
"subAttributes": [ "subAttributes": [
{ {
"name": "type", "name": "type",
"type": "string", "type": "string",
"multiValued": False, "multiValued": False,
"description": "The type of the authentication scheme.", "description": "The authentication scheme. This specification defines the values 'oauth', 'oauth2', 'oauthbearertoken', 'httpbasic', and 'httpdigest'.",
"required": False, "required": True,
"caseExact": False, "caseExact": False,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none" "uniqueness": "none",
}, },
{ {
"name": "name", "name": "name",
"type": "string", "type": "string",
"multiValued": False, "multiValued": False,
"description": "The common name of the authentication scheme.", "description": "The common authentication scheme name, e.g., HTTP Basic.",
"required": False, "required": True,
"caseExact": False, "caseExact": False,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none" "uniqueness": "none",
}, },
{ {
"name": "description", "name": "description",
"type": "string", "type": "string",
"multiValued": False, "multiValued": False,
"description": "A description of the authentication scheme.", "description": "A description of the authentication scheme.",
"required": False, "required": True,
"caseExact": False, "caseExact": False,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none" "uniqueness": "none",
}, },
{ {
"name": "specUri", "name": "specUri",
"type": "string", "type": "reference",
"referenceTypes": ["external"],
"multiValued": False, "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, "required": False,
"caseExact": False, "caseExact": False,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
"uniqueness": "none" "uniqueness": "none",
}, },
{ {
"name": "documentationUri", "name": "documentationUri",
"type": "string", "type": "reference",
"referenceTypes": ["external"],
"multiValued": False, "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, "required": False,
"caseExact": False, "caseExact": False,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "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"], 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
},
}