Source code for scim2_server.routing

from dataclasses import dataclass
from enum import Enum

from scim2_models import NotFoundException

from scim2_server.errors import MethodNotAllowedException

RESERVED_NAMES = frozenset(
    {"ServiceProviderConfig", "ResourceTypes", "Schemas", "Bulk", "Me"}
)
"""The endpoint names of :rfc:`RFC 7644 §3.2 <7644#section-3.2>`. No resource type endpoint can take them."""


[docs] class Operation(Enum): """A SCIM operation, named after the handler method that serves it.""" create = "create" query = "query" replace = "replace" patch = "patch" delete = "delete" search = "search" search_with_body = "search_with_body" bulk = "bulk" service_provider_config = "service_provider_config" resource_types = "resource_types" resource_type = "resource_type" schemas = "schemas" schema = "schema"
[docs] @dataclass(frozen=True) class Route: """A route of a SCIM server (:rfc:`RFC 7644 §3.2 <7644#section-3.2>`). ``{endpoint}`` in the pattern stands for the endpoint of a resource type, and ``{resource_id}`` for an identifier. """ method: str pattern: str operation: Operation name: str
ROUTES = ( Route("GET", "/ServiceProviderConfig", Operation.service_provider_config, "service_provider_config"), Route("GET", "/ResourceTypes", Operation.resource_types, "resource_types"), Route("GET", "/ResourceTypes/{resource_id}", Operation.resource_type, "resource_type"), Route("GET", "/Schemas", Operation.schemas, "schemas"), Route("GET", "/Schemas/{resource_id}", Operation.schema, "schema"), Route("POST", "/Bulk", Operation.bulk, "bulk"), Route("GET", "/Me", Operation.query, "me_query"), Route("PUT", "/Me", Operation.replace, "me_replace"), Route("PATCH", "/Me", Operation.patch, "me_patch"), Route("DELETE", "/Me", Operation.delete, "me_delete"), Route("POST", "/Me", Operation.create, "me_create"), Route("GET", "/", Operation.search, "search_root"), Route("POST", "/.search", Operation.search_with_body, "search_root_with_body"), Route("POST", "/{endpoint}/.search", Operation.search_with_body, "search_with_body"), Route("GET", "/{endpoint}", Operation.search, "search"), Route("POST", "/{endpoint}", Operation.create, "create"), Route("GET", "/{endpoint}/{resource_id}", Operation.query, "query"), Route("PUT", "/{endpoint}/{resource_id}", Operation.replace, "replace"), Route("PATCH", "/{endpoint}/{resource_id}", Operation.patch, "patch"), Route("DELETE", "/{endpoint}/{resource_id}", Operation.delete, "delete"), ) # fmt: skip """The routes of a SCIM server, the literal ones first. The routes of ``/Me`` serve the resource of the authenticated subject (:rfc:`RFC 7644 §3.11 <7644#section-3.11>`). """
[docs] @dataclass(frozen=True) class Target: """The operation a request asks for, and what it acts on.""" operation: Operation endpoint: str | None = None """The endpoint of the resource type, or :data:`None` at the root.""" resource_id: str | None = None """The identifier of a resource, a resource type or a schema.""" me: bool = False """Whether the request targets ``/Me``, the alias of the resource of the authenticated subject."""
[docs] def match(method: str, path: str) -> Target: """Return the target of a request. :raises ~scim2_models.NotFoundException: When no route has this path. :raises ~scim2_server.errors.MethodNotAllowedException: When the routes of this path do not support the method. """ segments = path.strip("/").split("/") if path.strip("/") else [] allowed = [] for route in ROUTES: values = match_pattern(route.pattern, segments) if values is None: continue if route.method == method: return Target(route.operation, **values, me=route.pattern == "/Me") allowed.append(route.method) if allowed: raise MethodNotAllowedException(allowed=allowed) raise NotFoundException(detail=f"No SCIM endpoint at {path!r}")
def match_pattern(pattern: str, segments: list[str]) -> dict[str, str] | None: """Return the values of the placeholders of a pattern, or None when the segments do not match it.""" parts = pattern.strip("/").split("/") if pattern.strip("/") else [] if len(parts) != len(segments): return None values = {} for part, segment in zip(parts, segments, strict=True): if part == "{endpoint}": if segment in RESERVED_NAMES or segment == ".search": return None values["endpoint"] = segment elif part == "{resource_id}": values["resource_id"] = segment elif part != segment: return None return values