admincheg/ldap-mapper

Simple LDAP mapper wrapper for ldap3 library

★ 0Forks 0PythonGitHub ↗Compare

README

ldap-mapper

ldap-mapper is a typed, dataclass-based object mapper for OpenLDAP-style directories built on top of ldap3. It keeps LDAP concepts explicit: DNs, object classes, mapped attributes, and LDAP result handling remain visible in the API. It is not a relational ORM.

Version 0.1 is OpenLDAP-oriented. Entry identity is read from entryUUID, and the optional password-policy model targets OpenLDAP ppolicy attributes.

Installation

python -m pip install ldap-mapper

Requires Python 3.12 or newer.

For development from a checkout:

python -m pip install -e '.[dev]'

Models

from dataclasses import dataclass
from typing import ClassVar

from ldap_mapper import LdapModel, StringField, mapped_field


@dataclass(slots=True)
class Person(LdapModel):
    uid: str = mapped_field(StringField("uid"))
    common_name: str = mapped_field(StringField("cn"))
    surname: str = mapped_field(StringField("sn"))
    mail: str | None = mapped_field(StringField("mail"), default=None)

    container_dn: ClassVar[str] = "ou=people"
    rdn_field: ClassVar[str] = "uid"
    search_object_class: ClassVar[str] = "person"
    object_classes: ClassVar[tuple[str, ...]] = (
        "top",
        "person",
        "organizationalPerson",
        "inetOrgPerson",
    )

LdapAdapter.add() returns a model bound to the server-assigned entryUUID. replace() finds that UUID before writing. If the RDN has changed, it raises LdapRenameRequired unless allow_rename=True; this preserves identity across DN changes. get() and get_by_uuid() return None for zero entries, return one model for one entry, and raise LdapMultipleEntriesFound otherwise.

Attribute controls

mapped_field() supports three write and output controls:

  • addable=False omits an attribute from LDAP adds.
  • replaceable=False prevents ordinary replacement writes.
  • serializable=False omits the field from to_dict().

Required mapped attributes must be present when decoding. A scalar field that LDAP returns with an invalid value raises a model error instead of silently coercing it.

Extensions

Extensions attach optional object classes and their attributes to a model. The generic mechanism lives in LdapExtension and ldap_extension. Built-in OpenLDAP-friendly extensions are available from ldap_mapper.extensions:

from ldap_mapper import LdapModel, ldap_extension
from ldap_mapper.extensions import LdapSshKey, LdapUnix


class Account(LdapModel):
    unix = ldap_extension(LdapUnix)
    ssh = ldap_extension(LdapSshKey)

Assign an extension instance to attach it; assign None to remove it. On replace, the mapper adds and removes managed object classes as needed.

Connections and passwords

LdapAdapter owns a thread-local ldap3.Connection; each thread gets its own connection after first use. Do not share model mutation across threads without your own synchronization.

LdapConfig.server_info defaults to "none", so the mapper does not read the Root DSE or schema automatically. Set it to "dsa", "schema", or "all" only when the client needs server.info or server.schema. Loading schema or all server information adds LDAP requests and is especially costly for short-lived user-bind connections.

authenticate(dn, password) validates credentials by binding as that user. It returns None on success, raises LdapInvalidCredentialsError for invalid credentials, and raises LdapOperationError when LDAP itself cannot complete the bind.

Password operations intentionally have different inputs:

  • reset_password() and change_password() accept plaintext and use RFC 3062 password modification after validating PasswordPolicy.
  • restore_password() accepts only EncodedPassword: an already encoded LDAP verifier. It writes userPassword directly and does not hash plaintext.

Use LdapPasswordPolicy when your directory exposes OpenLDAP ppolicy attributes. PasswordPolicy can also be created directly for local validation.

Errors

Adapter failures inherit from LdapAdapterError; mapping failures inherit from LdapModelError. Public exceptions include LdapEntryNotFound, LdapMultipleEntriesFound, LdapRenameRequired, LdapOperationError, and password-specific errors. Import public names from ldap_mapper; metadata keys used by mapped_field() are intentionally private.

Development

python -m ruff format .
python -m ruff check .
python -m pyright
python -m pytest

License

MIT. See LICENSE.

Contributors

admincheg

Issues