Source code for scim2_models.exceptions

"""SCIM exceptions corresponding to RFC 7644 error types.

This module provides a hierarchy of exceptions that map to SCIM protocol errors.
Each exception can be converted to a :class:`~scim2_models.Error` response object
or to a :class:`~pydantic_core.PydanticCustomError` for use in Pydantic validators.
"""

from typing import TYPE_CHECKING
from typing import Any

from pydantic_core import PydanticCustomError

from .context import Context

if TYPE_CHECKING:
    from .messages.error import Error


[docs] class SCIMException(Exception): """Base exception for SCIM protocol errors. Most subclasses correspond to a scimType defined in :rfc:`RFC 7644 Table 9 <7644#section-3.12>`. The others correspond to an HTTP status of :rfc:`RFC 7644 Table 8 <7644#section-3.12>` that comes with no scimType, such as ``404``. :param detail: The error detail message. :param scim_ctx: The SCIM context in which the exception occurred. """ status: int = 400 scim_type: str = "" _default_detail: str = "A SCIM error occurred" def __init__( self, *, detail: str | None = None, scim_ctx: Context | None = None, **context: Any, ): self.context = context self._detail = detail self.scim_ctx = scim_ctx self._error: Error | None = None super().__init__(detail or self._default_detail) @property def detail(self) -> str: """The error detail message.""" return self._detail or self._default_detail
[docs] def to_error(self) -> "Error": """Convert this exception to a SCIM Error response object. Exceptions built by :meth:`~scim2_models.SCIMException.from_error` return the very object they were built from. """ from .messages.error import Error if self._error is not None: return self._error return Error( status=self.status, scim_type=self.scim_type or None, detail=str(self), )
[docs] def as_pydantic_error(self) -> PydanticCustomError: """Convert to PydanticCustomError for use in Pydantic validators.""" return PydanticCustomError( f"scim_{self.scim_type}" if self.scim_type else "scim_error", str(self), {"scim_type": self.scim_type, "status": self.status, **self.context}, )
[docs] @classmethod def from_error( cls, error: "Error", scim_ctx: Context | None = None ) -> "SCIMException": """Create an exception from a SCIM Error object. The error object is kept as-is, and :meth:`~scim2_models.SCIMException.to_error` gives it back. This matters for the values no exception class describes, such as a ``429`` status or a vendor specific scimType. :param error: The SCIM Error object to convert. :param scim_ctx: The SCIM context in which the exception occurred. :return: The appropriate SCIMException subclass instance. """ from .messages.error import Error if not isinstance(error, Error): raise TypeError(f"Expected Error, got {type(error).__name__}") exception_class = ( _SCIM_TYPE_TO_EXCEPTION.get(error.scim_type or "") or _STATUS_TO_EXCEPTION.get(error.status or 0) or cls ) exc = exception_class(detail=error.detail, scim_ctx=scim_ctx) exc._error = error if error.status is not None: exc.status = error.status if error.scim_type: exc.scim_type = error.scim_type return exc
[docs] class InvalidFilterException(SCIMException): """The specified filter syntax was invalid. Corresponds to scimType ``invalidFilter`` with HTTP status 400. :rfc:`RFC 7644 Section 3.4.2.2 <7644#section-3.4.2.2>` """ status = 400 scim_type = "invalidFilter" _default_detail = ( "The specified filter syntax was invalid, " "or the specified attribute and filter comparison combination is not supported" ) def __init__(self, *, filter: str | None = None, **kw: Any): self.filter = filter super().__init__(**kw)
[docs] class TooManyException(SCIMException): """The specified filter yields too many results. Corresponds to scimType ``tooMany`` with HTTP status 400. :rfc:`RFC 7644 Section 3.4.2.2 <7644#section-3.4.2.2>` """ status = 400 scim_type = "tooMany" _default_detail = ( "The specified filter yields many more results " "than the server is willing to calculate or process" )
[docs] class UnauthorizedException(SCIMException): """The authorization header is invalid or missing. Corresponds to HTTP status 401, with no scimType. :rfc:`RFC 7644 Section 3.12 <7644#section-3.12>` """ status = 401 _default_detail = ( "Authorization failure. The authorization header is invalid or missing" )
[docs] class ForbiddenException(SCIMException): """The operation is not permitted based on the supplied authorization. Corresponds to HTTP status 403, with no scimType. :rfc:`RFC 7644 Section 3.12 <7644#section-3.12>` """ status = 403 _default_detail = "Operation is not permitted based on the supplied authorization"
[docs] class NotFoundException(SCIMException): """The specified resource or endpoint does not exist. Corresponds to HTTP status 404, with no scimType. :rfc:`RFC 7644 Section 3.12 <7644#section-3.12>` """ status = 404 _default_detail = "Specified resource or endpoint does not exist"
[docs] class ConflictException(SCIMException): """The request conflicts with the current state of the service provider. Corresponds to HTTP status 409. Per :rfc:`RFC 7644 Section 3.12 <7644#section-3.12>`, this covers a version number that does not match the latest one of the resource, and a duplicate resource. A duplicate resource raises :class:`UniquenessException`, which is a subclass of this one. """ status = 409 _default_detail = "The request conflicts with the current state of the resource"
[docs] class PreconditionFailedException(SCIMException): """The resource has changed on the server. Corresponds to HTTP status 412, with no scimType. :rfc:`RFC 7644 Section 3.12 <7644#section-3.12>` """ status = 412 _default_detail = "Failed to update. Resource has changed on the server"
[docs] class PayloadTooLargeException(SCIMException): """The request exceeds the bulk limits of the service provider. Corresponds to HTTP status 413, with no scimType. :rfc:`RFC 7644 Section 3.7.4 <7644#section-3.7.4>` """ status = 413 _default_detail = ( "The request exceeds the maximum number of operations " "or the maximum payload size of the service provider" )
[docs] class NotImplementedException(SCIMException): """The service provider does not support the request operation. Corresponds to HTTP status 501, with no scimType. :rfc:`RFC 7644 Section 3.12 <7644#section-3.12>` """ status = 501 _default_detail = "Service provider does not support the request operation"
[docs] class UniquenessException(ConflictException): """One or more attribute values are already in use or reserved. Corresponds to scimType ``uniqueness`` with HTTP status 409. :rfc:`RFC 7644 Section 3.3.1 <7644#section-3.3.1>` """ status = 409 scim_type = "uniqueness" _default_detail = ( "One or more of the attribute values are already in use or are reserved" ) def __init__( self, *, attribute: str | None = None, value: Any | None = None, **kw: Any ): self.attribute = attribute self.value = value super().__init__(**kw)
[docs] class MutabilityException(SCIMException): """The attempted modification is not compatible with the attribute's mutability. Corresponds to scimType ``mutability`` with HTTP status 400. :rfc:`RFC 7644 Section 3.5.2 <7644#section-3.5.2>` """ status = 400 scim_type = "mutability" _default_detail = ( "The attempted modification is not compatible with the target attribute's " "mutability or current state" ) def __init__( self, *, attribute: str | None = None, mutability: str | None = None, operation: str | None = None, **kw: Any, ): self.attribute = attribute self.mutability = mutability self.operation = operation super().__init__(**kw)
[docs] class InvalidSyntaxException(SCIMException): """The request body message structure was invalid. Corresponds to scimType ``invalidSyntax`` with HTTP status 400. :rfc:`RFC 7644 Section 3.12 <7644#section-3.12>` """ status = 400 scim_type = "invalidSyntax" _default_detail = ( "The request body message structure was invalid " "or did not conform to the request schema" )
[docs] class InvalidPathException(SCIMException): """The path attribute was invalid or malformed. Corresponds to scimType ``invalidPath`` with HTTP status 400. :rfc:`RFC 7644 Section 3.5.2 <7644#section-3.5.2>` """ status = 400 scim_type = "invalidPath" _default_detail = "The path attribute was invalid or malformed" def __init__(self, *, path: str | None = None, **kw: Any): self.path = path super().__init__(**kw)
[docs] class PathNotFoundException(InvalidPathException): """The path references a non-existent field. This is a specialized form of :class:`InvalidPathException`. """ _default_detail = "The specified path references a non-existent field" def __init__(self, *, path: str | None = None, field: str | None = None, **kw: Any): self.field = field super().__init__(path=path, **kw) def __str__(self) -> str: if self._detail: return self._detail if self.field: return f"Field not found: {self.field}" return self._default_detail
[docs] class NoTargetException(SCIMException): """The specified path did not yield a target that could be operated on. Corresponds to scimType ``noTarget`` with HTTP status 400. :rfc:`RFC 7644 Section 3.5.2 <7644#section-3.5.2>` """ status = 400 scim_type = "noTarget" _default_detail = ( "The specified path did not yield an attribute or attribute value " "that could be operated on" ) def __init__(self, *, path: str | None = None, **kw: Any): self.path = path super().__init__(**kw)
[docs] class InvalidValueException(SCIMException): """A required value was missing or the value was not compatible. Corresponds to scimType ``invalidValue`` with HTTP status 400. :rfc:`RFC 7644 Section 3.12 <7644#section-3.12>` """ status = 400 scim_type = "invalidValue" _default_detail = ( "A required value was missing, or the value specified was not compatible " "with the operation or attribute type, or resource schema" ) def __init__( self, *, attribute: str | None = None, reason: str | None = None, **kw: Any ): self.attribute = attribute self.reason = reason super().__init__(**kw)
[docs] class InvalidVersionException(SCIMException): """The specified SCIM protocol version is not supported. Corresponds to scimType ``invalidVers`` with HTTP status 400. :rfc:`RFC 7644 Section 3.13 <7644#section-3.13>` """ status = 400 scim_type = "invalidVers" _default_detail = "The specified SCIM protocol version is not supported"
[docs] class SensitiveException(SCIMException): """The request cannot be completed due to sensitive information in the URI. Corresponds to scimType ``sensitive`` with HTTP status 400. :rfc:`RFC 7644 Section 7.5.2 <7644#section-7.5.2>` """ status = 400 scim_type = "sensitive" _default_detail = ( "The specified request cannot be completed, due to the passing of sensitive " "information in a request URI" )
class InvalidCursorException(SCIMException): """Cursor value is invalid. Corresponds to scimType ``invalidCursor`` with HTTP status 400. :rfc:`RFC 9865 Section 2.1 <9865#section-2.1>` """ status = 400 scim_type = "invalidCursor" _default_detail = "Cursor value is invalid. Cursor value SHOULD be empty to request the first page and set to the nextCursor or previousCursor value for subsequent queries." class ExpiredCursorException(SCIMException): """Cursor has expired. Corresponds to scimType ``expiredCursor`` with HTTP status 400. :rfc:`RFC 9865 Section 2.3 <9865#section-2.3>` """ status = 400 scim_type = "expiredCursor" _default_detail = "Cursor has expired. Do not wait longer than service provider's cursorTimeout to request additional pages." class InvalidCountException(SCIMException): """Count value is invalid. Corresponds to scimType ``invalidCount`` with HTTP status 400. :rfc:`RFC 9865 Section 2.4 <9865#section-2.4>` """ status = 400 scim_type = "invalidCount" _default_detail = "Count value is invalid. Count value must be between 0 and service provider's maxPageSize and must be equal to the count value of the initial query." _SCIM_TYPE_TO_EXCEPTION: dict[str, type[SCIMException]] = { "invalidFilter": InvalidFilterException, "tooMany": TooManyException, "uniqueness": UniquenessException, "mutability": MutabilityException, "invalidSyntax": InvalidSyntaxException, "invalidPath": InvalidPathException, "noTarget": NoTargetException, "invalidValue": InvalidValueException, "invalidVers": InvalidVersionException, "sensitive": SensitiveException, "invalidCursor": InvalidCursorException, "expiredCursor": ExpiredCursorException, "invalidCount": InvalidCountException, } _STATUS_TO_EXCEPTION: dict[int, type[SCIMException]] = { 401: UnauthorizedException, 403: ForbiddenException, 404: NotFoundException, 409: ConflictException, 412: PreconditionFailedException, 413: PayloadTooLargeException, 501: NotImplementedException, }