Service#

The SCIM protocol, without input or output.

class scim2_server.service.ScimService(provider: ScimProvider, *, secret: str | bytes | None = None)[source]#

The SCIM protocol, without input or output.

A service validates the requests, applies the PUT and PATCH requests to the resources, evaluates the conditional headers and builds the responses. It neither reads nor writes resources: ScimHandler calls the storage between its steps. It knows nothing about the web framework either: the integration reads the request, and turns the ScimResponse into a response of the framework.

A provider without config announces PATCH, and no other capability. The storage must implement each capability the configuration announces.

Every public method can be overridden. base_url is the root URL of the SCIM endpoints, as the client sees it, such as https://example.com/scim/v2.

Parameters:

secret – The secret of the service, which every process of the server shares, such as a random string kept in the configuration. The cursors of RFC 9865 derive their key from it, so a configuration that announces cursor pagination requires it, and the cursor extra.

Raises:
  • ValueError – When the configuration announces cursor pagination without a secret.

  • ModuleNotFoundError – When the configuration announces cursor pagination without the cursor extra.

get_model(resource_type: ResourceType) → type[Resource][source]#

Return the model of a resource type, its extensions included.

get_models() → list[type[Resource]][source]#

Return the models of every resource type.

get_resource_type_by_endpoint(endpoint: str) → ResourceType | None[source]#

Return the resource type an endpoint serves, if any.

static match(request: ScimRequest) → Target[source]#

Return the operation a request asks for (RFC 7644 §3.2).

The target of a /Me request is not resolved yet.

Raises:
route(request: ScimRequest, operation: Operation) → Target[source]#

Return what a request for an operation acts on.

A /Me request acts on the resource of me_target(), or creates a resource of the type of me_creation_type().

Parameters:

operation – The operation the caller serves. A request for another operation is a routing error of the integration.

Raises:
static endpoint_of(resource_type: ResourceType) → str[source]#

Return the endpoint of a resource type, without its slashes.

me_target(request: ScimRequest) → tuple[ResourceType, str][source]#

Return the type and the identifier of the resource /Me stands for (RFC 7644 §3.11).

Override this method to serve /Me. It reads the authenticated subject in ScimRequest.subject. The exceptions it raises answer the request.

Raises:
me_creation_type(request: ScimRequest) → ResourceType[source]#

Return the type of the resource a POST on /Me creates (RFC 7644 §3.11).

Override this method to let the clients register themselves (RFC 7644 §7.6). Linking the created resource to the subject is left to the application. The exceptions it raises answer the request.

Raises:
me_response(request: ScimRequest, target: Target, response: ScimResponse) → ScimResponse[source]#

Add the location of the resource /Me stands for to a response (RFC 7644 §3.11).

authorize(request: ScimRequest, target: Target, resource_type: ResourceType) → None[source]#

Accept or refuse an operation on a resource type (RFC 7644 §2).

Override this method to check the rights of the client. It reads the authenticated subject in ScimRequest.subject. By default, every operation is accepted.

The service calls this method before it validates the request body, and before the handler calls the storage. For a /Me request, the service calls it after me_target() or me_creation_type() finds the resource, and Target.me is True.

The service can call this method several times for one request: once per operation of a bulk request, and once per resource type of a search at the root. The method must not do any input or output. The subject must arrive with everything the decision needs, such as its permissions or the scopes of its token.

In a bulk request, target describes the operation, and request holds the bulk request. Read the operation in target, never in the method or the path of request.

In a search at the root, target.endpoint is None, and resource_type is one of the searched types. A ForbiddenException removes this type from the search. The search answers 403 when every type is removed.

Raises:
  • ForbiddenException – When the client may not perform the operation, for a 403.

  • UnauthorizedException – When the request has no authenticated subject, for an application that accepts anonymous requests. It answers 401.

authorize_bulk_operation(request: ScimRequest, operation: BulkOperation[Resource], resource_type: ResourceType | None) → None[source]#

Authorize a resolved bulk operation with authorize().

An operation without a valid method or path keeps its validation error, without authorization.

authorized_types(request: ScimRequest, target: Target, resource_types: list[ResourceType]) → list[ResourceType][source]#

Return the resource types the client may search, according to authorize().

At the root, the refused types are left out of the search (RFC 7644 §3.4.2.1).

Raises:

ForbiddenException – When the client may search none of the types.

static read_conditions(request: ScimRequest) → Conditions[source]#

Return the conditional headers of a request.

max_body_size(request: ScimRequest) → int | None[source]#

Return the largest body the service accepts for a request, in bytes.

An integration reads at most one byte more, and passes the body to the handler. It must not refuse a larger body itself: the service answers, with a 413 or with an error that comes first, such as a 501 when bulk is not supported.

A bulk request is limited by maxPayloadSize (RFC 7644 §3.7.4). A request that fails whatever its body, such as a request on an unknown path, gets 0. Other requests have no limit.

resource_type_at(endpoint: str) → ResourceType[source]#

Return the resource type an endpoint serves.

Raises:

NotFoundException – When no resource type is served at this endpoint.

get_resource_type(name: str) → ResourceType[source]#

Return the resource type of a name, such as User.

Raises:

ValueError – When the provider serves no resource type of this name.

get_resource_type_of(resource: Resource) → ResourceType[source]#

Return the resource type of a stored resource, from its meta.resourceType.

Raises:

ValueError – When the storage returned a resource of a type the provider does not serve.

property etag_supported: bool#

Whether the configuration declares the resources versioned with ETags.

static ensure_supported(capability: Patch | Bulk | Filter | Sort | None, operation: str) → None[source]#

Refuse with a 501 an operation the configuration does not declare supported.

RFC 7644 §3.12 answers 501 when the service provider does not support the request operation.

bulk_max_payload_size() → int | None[source]#

Return the largest bulk request body the service accepts, in bytes.

An integration can use it to stop reading a larger body early.

ensure_bulk_payload_size(size: int) → None[source]#

Refuse with a 413 a bulk request body larger than maxPayloadSize (RFC 7644 §3.7.4).

Parameters:

size – The size of the body, in bytes.

static payload_too_large(limit: int) → PayloadTooLargeException[source]#

Return the 413 error of a bulk request body larger than maxPayloadSize.

static ensure_json(content_type: str | None) → None[source]#

Refuse with a 415 a request body that is not JSON.

Per RFC 7644 §3.8, JSON is the default format: a body without a Content-Type, or with an empty one, is read as JSON.

read_body(model: type[ModelT], body: bytes, content_type: str | None, scim_ctx: Context) → ModelT[source]#

Validate a request body with a model, in the context of its operation.

decode_body(body: bytes, content_type: str | None) → Any[source]#

Return the JSON value of a request body, unvalidated.

resource_location(base_url: str, resource_type: ResourceType, resource_id: str) → str[source]#

Return the URL of a resource.

Override this method to serve the resources at other URLs.

reference_location(base_url: str, reference: str) → str[source]#

Return the URL of a reference to a resource.

A reference relative to the SCIM root, such as Users/2819c223, becomes the location of the resource (RFC 7643 §2.3.7). Absolute references are kept.

publish(base_url: str, resource: Resource) → Resource[source]#

Return a copy of a stored resource in the form sent to the client.

Its location is set, its references to other resources become URLs with reference_location(), and its version is left out when the service does not support ETags.

static resource_response(resource: Resource, scim_ctx: Context, response_parameters: ResponseParameters[Any] | None = None, status: HTTPStatus = HTTPStatus.OK) → ScimResponse[source]#

Return the response carrying a published resource, with its ETag.

Per RFC 7643 §3.1, the Content-Location header holds meta.location.

check_preconditions(resource: Resource, method: str, conditions: Conditions) → bool[source]#

Evaluate the conditional headers of a request against a resource.

Returns:

False when a GET should answer 304 Not Modified.

Raises:

PreconditionFailedException – When the method must not be performed.

read_creation(resource_type: ResourceType, body: bytes, content_type: str | None) → Resource[source]#

Validate the body of a creation request.

creation_response(base_url: str, created: Resource) → ScimResponse[source]#

Return the 201 response to a creation, with the location of the resource.

read_response_parameters(resource_type: ResourceType, query: Mapping[str, str]) → ResponseParameters[Any][source]#

Read the “attributes” and “excludedAttributes” query parameters.

query_response(base_url: str, resource: Resource, response_parameters: ResponseParameters[Any], conditions: Conditions) → ScimResponse[source]#

Return the response to the read of a resource, or a 304.

read_search_query(resource_types: list[ResourceType], query: Mapping[str, str]) → tuple[SearchRequest[Any], Any][source]#

Read the query parameters of a search with GET (RFC 7644 §3.4.2).

See read_search() for what it returns.

read_search_body(resource_types: list[ResourceType], body: bytes, content_type: str | None) → tuple[SearchRequest[Any], Any][source]#

Read the body of a search with POST on “.search” (RFC 7644 §3.4.3).

See read_search() for what it returns.

Validate a search request, and return it with the position of the page its cursor asks for.

The search pages by cursor or by index, as the request asks, or else as the pagination of the configuration says. Its count is bounded by maxResults and maxPageSize, and is defaultPageSize when the request has none.

A search paged by cursor keeps an empty cursor, which the storage expects. The position is None on the first page.

Raises:
searched_types(endpoint: str | None) → list[ResourceType][source]#

Return the resource types a search covers: those of the endpoint, or all of them at the root.

search_response(base_url: str, resource_types: list[ResourceType], page: SearchPage, search_request: SearchRequest[Any]) → ScimResponse[source]#

Return the response listing a page of found resources.

Raises:

ValueError – When the storage gives no total for a search paged by index.

read_replacement(resource_type: ResourceType, body: bytes, content_type: str | None) → Resource[source]#

Validate the body of a replacement request.

apply_replacement(current: Resource, replacement: Resource, conditions: Conditions) → Resource | None[source]#

Apply a replacement to a stored resource.

Returns:

The new state of the resource, or None when the replacement changes nothing. Such a PUT keeps meta.lastModified and the ETag.

replacement_response(base_url: str, resource: Resource, response_parameters: ResponseParameters[Any]) → ScimResponse[source]#

Return the response to a replacement.

ensure_patch_supported() → None[source]#

Refuse with a 501 a PATCH when the configuration does not support it.

read_patch(resource_type: ResourceType, body: bytes, content_type: str | None) → PatchOp[Any][source]#

Validate the body of a PATCH request.

apply_patch(current: Resource, patch_op: PatchOp[Any], conditions: Conditions) → Resource | None[source]#

Apply a PATCH to a stored resource.

Returns:

The new state of the resource, or None when the PATCH changes nothing. Such a PATCH keeps meta.lastModified and the ETag.

patch_response(base_url: str, resource: Resource, response_parameters: ResponseParameters[Any]) → ScimResponse[source]#

Return the response to a PATCH.

RFC 7644 §3.5.2: a PATCH MAY answer 204 when no attributes were requested.

check_deletion(current: Resource, conditions: Conditions) → None[source]#

Evaluate the conditional headers of a deletion.

static deletion_response() → ScimResponse[source]#

Return the 204 response to a deletion.

read_bulk(body: bytes, content_type: str | None) → BulkPlan[source]#

Validate a bulk request against the limits of the service, and plan its operations (RFC 7644 §3.7).

static bulk_outcome(base_url: str, operation: BulkOperation[Resource]) → dict[str, Any][source]#

Start the outcome of a bulk operation, before it runs.

RFC 7644 §3.7.3 requires a location for every operation but a failed POST. Until the operation is located, its location is the URL of its path, so that an operation with an unknown endpoint or an unresolved reference still has one.

route_bulk_operation(operation: BulkOperation[Resource]) → ResourceType | None[source]#

Return the resource type a bulk operation acts on.

The path of the operation is routed as the path of a single request, so that the operation fails with the same status (RFC 7644 §3.7.3). An operation without a valid method or path keeps its validation error, and has no resource type.

Raises:
locate_bulk_operation(base_url: str, resource_type: ResourceType | None, operation: BulkOperation[Resource], outcome: dict[str, Any]) → None[source]#

Locate the resource of a resolved bulk operation.

The location is set before the operation runs, so a failure still knows it. A POST only gets a location when it succeeds.

static bulk_validation_failure(operation: BulkOperation[Resource], outcome: dict[str, Any]) → dict[str, Any] | None[source]#

Return the outcome of a bulk operation that failed its validation, if it did.

bulk_failure(outcome: dict[str, Any], exception: Exception) → dict[str, Any][source]#

Return the outcome of a bulk operation that raised an exception.

bulk_success(base_url: str, operation: BulkOperation[Resource], outcome: dict[str, Any], resource: Resource | None) → dict[str, Any][source]#

Return the outcome of a bulk operation that succeeded.

bulk_response(plan: BulkPlan) → ScimResponse[source]#

Return the response listing the outcome of each operation that ran.

static forbid_filter(query: Mapping[str, str]) → None[source]#

Refuse a filter on a discovery endpoint.

RFC 7644 §4: “If a “filter” is provided, the service provider SHOULD respond with HTTP status code 403 (Forbidden)”.

static locate(resource: DiscoveryResourceT, location: str) → DiscoveryResourceT[source]#

Return a copy of a discovery resource carrying its meta.

static discovery_response(resource: DiscoveryResourceT) → ScimResponse[source]#

Return the response carrying a discovery resource, with its Content-Location.

service_provider_config(location: str, query: Mapping[str, str]) → ScimResponse[source]#

Return the ServiceProviderConfig.

Parameters:

location – The URL of the endpoint.

resource_types(location: str, query: Mapping[str, str]) → ScimResponse[source]#

Return the list of the resource types.

resource_type(location: str, resource_type_id: str, query: Mapping[str, str]) → ScimResponse[source]#

Return a single resource type.

schemas(location: str, query: Mapping[str, str]) → ScimResponse[source]#

Return the list of the schemas.

schema(location: str, schema_id: str, query: Mapping[str, str]) → ScimResponse[source]#

Return a single schema.

static error_of(exception: Exception) → Error[source]#

Return the SCIM error of an exception raised while serving a request.

Any other exception than a SCIMException is unexpected, and gives a 500, without its message nor its traceback.

error_response(exception: Exception) → ScimResponse[source]#

Return the SCIM error response of an exception raised while serving a request.

A 401 response carries the WWW-Authenticate header of www_authenticate().

www_authenticate(exception: SCIMException) → str | None[source]#

Return the WWW-Authenticate header of a 401 response (RFC 7644 §2).

It holds one challenge per authentication scheme of the service provider configuration, for the Bearer and Basic schemes. Override this method to announce other schemes, or to add parameters to the challenges, such as resource_metadata (RFC 9728 §5.1).

scim2_server.service.is_json_media_type(content_type: str | None) → bool[source]#

Tell whether a Content-Type header designates JSON.

application/scim+json, application/json and any other application/*+json are accepted, whatever their parameters.

class scim2_server.cursor.Cursors(secret: str | bytes)[source]#

Seal the position of a page into a cursor, and open the cursor back.

A cursor is encrypted and authenticated with AES-GCM, so the client can neither read nor forge it (RFC 9865 §5.2). It holds the query it belongs to, the count of the query, and the time it was issued.

Each cursor has its own key, derived from the secret and a random salt. The number of cursors a secret seals is then not bounded by the number of messages AES-GCM allows for a single key.

Parameters:

secret – The secret the keys of the cursors derive from. Every process of the server needs the same secret.

seal(query: str, count: int | None, position: Any) → str[source]#

Return the cursor of a position, for the query and the count it belongs to.

open(cursor: str, query: str, count: int | None, timeout: int | None) → Any[source]#

Return the position a cursor holds.

Parameters:

timeout – The number of seconds a cursor stays valid, if any.

Raises: