Source code for freeiam.errors

# SPDX-FileCopyrightText: 2025 Florian Best
# SPDX-License-Identifier: MIT OR Apache-2.0
"""Errors."""

import typing
from contextlib import contextmanager

import ldap


if typing.TYPE_CHECKING:
    import freeiam


[docs] class Error(Exception): """Base Error."""
[docs] class InvalidFilter(Error): """Invalid Filter Syntax."""
[docs] class CancelOperation(Error): """Cancel a running operation."""
[docs] class NotUnique(Error): """More than one unique search result.""" args: tuple[list['freeiam.ldap._wrapper.Result']] @property def results(self) -> list['freeiam.ldap._wrapper.Result']: """The non unique search results.""" return self.args[0]
[docs] class LdapError(Error): """LDAP Error wrapper base class.""" _MAP: typing.ClassVar[dict[type[ldap.LDAPError], type[typing.Self]]] = {} exc_class = ldap.LDAPError _controls: list[tuple[str, bool, bytes]] | None _controls_decoded: list['ldap.controls.ResponseControl'] | None @property def result(self) -> int | None: """Numeric code of the error class.""" return self._result @property def description(self) -> str | None: """String giving a description of the error class, as provided by calling OpenLDAP's ldap_err2string on the result.""" return self._description @property def info(self) -> str | None: """ String containing more information that the server may have sent. The value is server-specific: for example, the OpenLDAP server may send different info messages than Active Directory or 389-DS """ return self._info @property def matched(self) -> str | None: """Truncated form of the name provided or alias. dereferenced for the lowest entry (object or alias) that was matched.""" return self._matched @property def errno(self) -> int | None: """The C errno, usually set by system calls or libc rather than the LDAP libraries.""" return self._errno @property def controls(self) -> list['ldap.controls.ResponseControl'] | None: """List of LDAP Control instances attached to the error.""" if self._controls_decoded is None: from freeiam.ldap.controls import decode # noqa: PLC0415 assert self._controls is not None # noqa: S101 self._controls_decoded = decode(self._controls) return self._controls_decoded def __init_subclass__(cls: type[typing.Self], **kwargs: typing.Any): super().__init_subclass__(**kwargs) cls._MAP[cls.exc_class] = cls def __init__(self, args: dict[str, str | int | list[tuple[str, int, bytes]]] | None = None): args = args or {} self._result = typing.cast('int', args.get('result')) self._description = typing.cast('str', args.get('desc')) self._info = typing.cast('str', args.get('info')) self._matched = typing.cast('str', args.get('matched')) self._errno = typing.cast('int', args.get('errno')) self._controls = typing.cast('list[tuple[str, bool, bytes]]', args.get('ctrls')) self._controls_decoded = None super().__init__(args) def __str__(self) -> str: msg = f'{self.description or ""}: {self.info or ""}'.removesuffix(': ') if self.matched: msg = f'{msg} (exists: {self.matched})' return msg _repr_fields: typing.ClassVar = ['description', 'info', 'matched', 'result', 'errno'] def __repr__(self) -> str: msg = ', '.join(f'{f}={getattr(self, f)!r}' for f in self._repr_fields) return f'{type(self).__name__}({msg})'
[docs] @classmethod def from_ldap_exception(cls, exc: ldap.LDAPError) -> typing.Self: """Get instance from the correct child exception.""" error = cls._MAP.get(type(exc), cls) args = exc.args[0] if exc and exc.args and isinstance(exc.args[0], dict) else {} return error(args)
[docs] @classmethod def wrap(cls, hide_parent_exception: bool = True) -> typing.ContextManager[None]: """Context manager to wrap LDAP exceptions.""" @contextmanager def _wrap() -> typing.Generator[None, None, None]: try: yield except ldap.LDAPError as exc: error = cls.from_ldap_exception(exc) if hide_parent_exception: raise error from None raise error from exc return _wrap()
[docs] class AdminlimitExceeded(LdapError): """Adminlimit exceeded.""" exc_class = ldap.ADMINLIMIT_EXCEEDED
[docs] class AffectsMultipleDSAs(LdapError): """Affects multiple Directory System Agent.""" exc_class = ldap.AFFECTS_MULTIPLE_DSAS
[docs] class AliasDerefProblem(LdapError): """A problem was encountered when dereferencing an alias.""" exc_class = ldap.ALIAS_DEREF_PROBLEM
# sets matched
[docs] class AliasProblem(LdapError): """An alias in the directory points to a nonexistent entry.""" exc_class = ldap.ALIAS_PROBLEM
# sets matched
[docs] class AlreadyExists(LdapError): """The entry already exists. E.g. the DN specified with add() already exists in the DIT.""" exc_class = ldap.ALREADY_EXISTS
[docs] class AssertionFailed(LdapError): """Assertion failed.""" exc_class = ldap.ASSERTION_FAILED
[docs] class AuthMethodNotSupported(LdapError): """Authentication method is not supported.""" exc_class = ldap.AUTH_METHOD_NOT_SUPPORTED
[docs] class AuthUnknown(LdapError): """The authentication method specified to bind() is not known.""" exc_class = ldap.AUTH_UNKNOWN
[docs] class Busy(LdapError): """The DSA is busy.""" exc_class = ldap.BUSY
[docs] class Cancelled(LdapError): """Cancelled.""" exc_class = ldap.CANCELLED
[docs] class CannotCancel(LdapError): """Cannot cancel.""" exc_class = ldap.CANNOT_CANCEL
[docs] class ClientLoop(LdapError): """Client loop.""" exc_class = ldap.CLIENT_LOOP
[docs] class CompareFalse(LdapError): """A compare operation returned False.""" exc_class = ldap.COMPARE_FALSE
[docs] class CompareTrue(LdapError): """A compare operation returned true.""" exc_class = ldap.COMPARE_TRUE
[docs] class ConfidentialityRequired(LdapError): """ Indicates that the session is not protected by a protocol such as Transport Layer Security (TLS). which provides session confidentiality. """ exc_class = ldap.CONFIDENTIALITY_REQUIRED
[docs] class ConnectError(LdapError): """Connect error.""" exc_class = ldap.CONNECT_ERROR
[docs] class ConstraintViolation(LdapError): """ An attribute value specified or an operation started violates some server-side constraint. (e.g., a postalAddress has too many lines or a line that is too long or a password is expired). """ exc_class = ldap.CONSTRAINT_VIOLATION
[docs] class ControlNotFound(LdapError): """Control was not found.""" exc_class = ldap.CONTROL_NOT_FOUND
[docs] class DecodingError(LdapError): """An error was encountered decoding a result from the LDAP server.""" exc_class = ldap.DECODING_ERROR
[docs] class EncodingError(LdapError): """An error was encountered encoding parameters to send to the LDAP server.""" exc_class = ldap.ENCODING_ERROR
[docs] class FilterError(LdapError): """The filter syntax is invalid e.g. due to unbalanced parentheses.""" exc_class = ldap.FILTER_ERROR
[docs] class InappropriateAuthentication(LdapError): """ Inappropriate authentication was specified. (e.g. if the user has no userPassword attribute on a simple bind) """ exc_class = ldap.INAPPROPRIATE_AUTH
[docs] class InappropriateMatching(LdapError): """The filter type is not supported for the specified attribute.""" exc_class = ldap.INAPPROPRIATE_MATCHING
[docs] class InsufficientAccess(LdapError): """The user has insufficient access to perform the operation.""" exc_class = ldap.INSUFFICIENT_ACCESS
[docs] class InvalidCredentials(LdapError): """Invalid credentials were presented during bind() or simple_bind(). (e.g., a wrong password).""" exc_class = ldap.INVALID_CREDENTIALS
[docs] class InvalidDN(LdapError): """A syntactically invalid DN was specified.""" exc_class = ldap.INVALID_DN_SYNTAX
# sets the matched field
[docs] class InvalidSyntax(LdapError): """An attribute value specified by the client did not comply to the syntax defined in the server-side schema.""" exc_class = ldap.INVALID_SYNTAX
[docs] class IsLeaf(LdapError): """The object specified is a leaf of the directory tree.""" exc_class = ldap.IS_LEAF
# sets the matched field
[docs] class LocalError(LdapError): """Some local error occurred. Usually caused by failed memory allocation.""" exc_class = ldap.LOCAL_ERROR
[docs] class LoopDetected(LdapError): """A loop was detected.""" exc_class = ldap.LOOP_DETECT
[docs] class MoreResultsToReturn(LdapError): """More results to return.""" exc_class = ldap.MORE_RESULTS_TO_RETURN
[docs] class NamingViolation(LdapError): """A naming violation occurred. This is raised e.g. if the LDAP server has constraints about the tree naming.""" exc_class = ldap.NAMING_VIOLATION
[docs] class NotAllowedOnNonleaf(LdapError): """The operation is not allowed on a non-leaf object.""" exc_class = ldap.NOT_ALLOWED_ON_NONLEAF
[docs] class NotAllowedOnRDN(LdapError): """The operation is not allowed on an RDN.""" exc_class = ldap.NOT_ALLOWED_ON_RDN
[docs] class NotSupported(LdapError): """Not supported.""" exc_class = ldap.NOT_SUPPORTED
[docs] class NoMemory(LdapError): """No memory.""" exc_class = ldap.NO_MEMORY
[docs] class NoObjectClassMods(LdapError): """Modifying the objectClass attribute as requested is not allowed (e.g. modifying structural object class of existing entry).""" exc_class = ldap.NO_OBJECT_CLASS_MODS
[docs] class NoResultsReturned(LdapError): """No results returned.""" exc_class = ldap.NO_RESULTS_RETURNED
[docs] class NoSuchAttribute(LdapError): """The attribute type specified does not exist in the entry.""" exc_class = ldap.NO_SUCH_ATTRIBUTE
[docs] class NoSuchObject(LdapError): """The specified object does not exist in the directory.""" exc_class = ldap.NO_SUCH_OBJECT # sets the matched field _repr_fields: typing.ClassVar = [*LdapError._repr_fields, 'base_dn'] def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: self._base_dn: freeiam.ldap.dn.DN | None = None self.filter: str | None = None self.scope: freeiam.ldap.constants.Scope | None = None self.attrs: list[str] | None = None super().__init__(*args, **kwargs) @property def base_dn(self) -> 'freeiam.ldap.dn.DN | None': """Get search base DN.""" return self._base_dn @base_dn.setter def base_dn(self, value: 'freeiam.ldap.dn.DN') -> None: """Set search base DN.""" self._base_dn = value def __str__(self) -> str: string = super().__str__() if self.base_dn: string = f'{string} (base: {self._base_dn})' return string
[docs] class NoSuchOperation(LdapError): """No such operation.""" exc_class = ldap.NO_SUCH_OPERATION
[docs] class NoUniqueEntry(LdapError): """No unique entry.""" exc_class = ldap.NO_UNIQUE_ENTRY
[docs] class ObjectClassViolation(LdapError): """ An object class violation occurred. When the LDAP server checked the data sent by the client against the server-side schema (e.g. a "must" attribute was missing in the entry data) """ exc_class = ldap.OBJECT_CLASS_VIOLATION
[docs] class OperationsError(LdapError): """An operations error occurred.""" exc_class = ldap.OPERATIONS_ERROR
[docs] class Other(LdapError): """An unclassified error occurred.""" exc_class = ldap.OTHER
[docs] class ParamError(LdapError): """An LDAP routine was called with a bad parameter.""" exc_class = ldap.PARAM_ERROR
[docs] class PartialResults(LdapError): """ Only partial results were returned. This exception is raised if a referral is received when using LDAPv2. This exception should never be seen with LDAPv3. """ exc_class = ldap.PARTIAL_RESULTS
[docs] class ProtocolError(LdapError): """A violation of the LDAP protocol was detected.""" exc_class = ldap.PROTOCOL_ERROR
[docs] class ProxiedAuthorizationDenied(LdapError): """Proxied authorization was denied.""" exc_class = getattr(ldap, 'PROXIED_AUTHORIZATION_DENIED', ldap.LDAPError)
[docs] class Referral(LdapError): """Referral.""" exc_class = ldap.REFERRAL
[docs] class ReferralLimitExceeded(LdapError): """Referral limit exceeded.""" exc_class = ldap.REFERRAL_LIMIT_EXCEEDED
[docs] class ResultsTooLarge(LdapError): """ The result does not fit into a UDP packet. This happens only when using UDP-based CLDAP (connection-less LDAP) which is not supported anyway. """ exc_class = ldap.RESULTS_TOO_LARGE
[docs] class SASLBindInProgress(LdapError): """SASL bind in progress.""" exc_class = ldap.SASL_BIND_IN_PROGRESS
[docs] class ServerDown(LdapError): """The LDAP library can't contact the LDAP server.""" exc_class = ldap.SERVER_DOWN
[docs] class SizelimitExceeded(LdapError): """An LDAP size limit was exceeded. This could be due to a sizelimit configuration on the LDAP server.""" exc_class = ldap.SIZELIMIT_EXCEEDED
[docs] class StrongAuthNotSupported(LdapError): """The LDAP server does not support strong authentication.""" exc_class = ldap.STRONG_AUTH_NOT_SUPPORTED
[docs] class StrongAuthRequired(LdapError): """Strong authentication is required for the operation.""" exc_class = ldap.STRONG_AUTH_REQUIRED
[docs] class Success(LdapError): """Success.""" exc_class = ldap.SUCCESS
[docs] class TimelimitExceeded(LdapError): """An LDAP time limit was exceeded.""" exc_class = ldap.TIMELIMIT_EXCEEDED
[docs] class Timeout(LdapError): """A timelimit was exceeded while waiting for a result from the server.""" exc_class = ldap.TIMEOUT
[docs] class TooLate(LdapError): """Too late.""" exc_class = ldap.TOO_LATE
[docs] class TypeOrValueExists(LdapError): """An attribute type or attribute value specified already exists in the entry.""" exc_class = ldap.TYPE_OR_VALUE_EXISTS
[docs] class Unavailable(LdapError): """The DSA is unavailable.""" exc_class = ldap.UNAVAILABLE
[docs] class UnavailableCriticalExtension(LdapError): """ Indicates that the LDAP server was unable to satisfy a request. Because one or more critical extensions were not available. Either the server does not support the control or the control is not appropriate for the operation type. """ exc_class = ldap.UNAVAILABLE_CRITICAL_EXTENSION
[docs] class UndefinedType(LdapError): """An attribute type used is not defined in the server-side schema.""" exc_class = ldap.UNDEFINED_TYPE
[docs] class UnwillingToPerform(LdapError): """The DSA is unwilling to perform the operation.""" exc_class = ldap.UNWILLING_TO_PERFORM
[docs] class UserCancelled(LdapError): """The operation was cancelled via the abandon() method.""" exc_class = ldap.USER_CANCELLED
[docs] class VLVError(LdapError): """Virtual List View control error.""" exc_class = ldap.VLV_ERROR
[docs] class ProxyAuthZFailure(LdapError): """X-Proxy Authorization failure.""" exc_class = ldap.X_PROXY_AUTHZ_FAILURE