"""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 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,
}