User Docs

Features Overview

  • Fully compliant with RFC 4510 and its children.
  • Pure Python codebase, meaning that it can be used with Python implementations other than CPython.
  • Tested against CPython 2.7, 3.3, 3.4, 3.5, 3.6, PyPy, and PyPy3.
  • Simplified filter syntax (optional, standard filter syntax is fully supported and used by default)
  • Pythonic attributes input and presentation. It’s just a dictionary.
  • Exceedingly easy relative searching. All objects have a suite of search methods which will automatically pass the object’s DN as the search base. In many cases, you wont have to pass any arguments to search methods.
  • Similarly, all objects have a suite of modify methods which allow you to change attributes on already-queried objects without having to pass their DN again.
  • Intelligent modification will never send existing attribute values to the server, nor will it request deletion of attribute values that do not exist. This prevents many unnecessary server errors. Laurelin will go as far as to query the object for you before modifying it to ensure you don’t see pointless errors (if you want it to).
  • Custom validation. You can define validators which check new objects and modify operations for correctness before sending them to the server. Since you control this code, this can be anything from a simple regex check against a particular attribute value, to a complex approval queue mechanism.
  • Highly extensible. New methods can easily and safely be bound to base classes.
  • Seamless integration of controls. Once defined, these are just new keyword arguments on particular methods, and additional attributes on the response object.
  • Includes Python implementations of standard schema elements. This conveys many benefits:
    • Allows changes to be validated before sending the server
    • Allows matching rules to be used to compare attribute values locally. Many attribute types are case-insensitive and have other rules meaning that the standard Python == or in operators won’t tell you what you want to know. Laurelin makes them work according to these rules.

Missing/incomplete features

Some lesser-used features of the LDAP protocol have not yet been implemented or are incomplete. Check the GitHub issues to see if your use case is affected. Please add a comment if so, or open a new issue if you spot anything else. PRs are always welcome.

Walkthrough

Note

I’m assuming that if you’re here, you’re already pretty familiar with LDAP fundamentals. If you don’t know how to write a search filter, you may want to do some more reading on LDAP before continuing.

Getting Started

The first thing you should typically do after importing is configure logging and/or warnings. There is a lot of useful information available at all log levels:

from laurelin.ldap import LDAP

LDAP.enable_logging()
# Enables all log output on stderr
# It also accepts an optional log level argument, e.g. LDAP.enable_logging(logging.ERROR)
# The function also returns the handler it creates for optional further manual handling

import logging

logger = logging.getLogger('laurelin.ldap')
# Manually configure the logger and handlers here using the standard logging module
# Submodules use the logger matching their name, below laurelin.ldap

LDAP.log_warnings()
# emit all LDAP warnings as WARN-level log messages on the laurelin.ldap logger
# all other warnings will take the default action

LDAP.disable_warnings()
# do not emit any LDAP warnings
# all other warnings will take the default action

You can then initialize a connection to an LDAP server. Pass a URI string to the LDAP constructor:

with LDAP('ldap://dir.example.org:389') as ldap:
     # do stuff...

# Its also possible, but not reccommended, to not use the context manager:
ldap = LDAP('ldap://dir.example.org:389')

This will open a connection and query the server to find the “base DN” or DN suffix. An empty LDAPObject will be created with the base DN and stored as the base attribute on the LDAP instance. More on this later. For now we will briefly cover the basic LDAP interface which may seem somewhat familiar if you have used the standard python-ldap client before.

LDAP Methods Intro

LDAP.search() sends a search request and returns an iterable over instances of LDAPObject. Basic arguments are described here (listed in order):

  • base_dn - the absolute DN to start the search from
  • scope - One of:
    • Scope.BASE - only search base_dn itself
    • Scope.ONE - search base_dn and its immediate children
    • Scope.SUB - search base_dn and all of its descendents (default)
  • filter - standard LDAP filter string
  • attrs - a list of attributes to return for each object

Use LDAP.get() if you just need to get a single object by its DN. Also accepts an optional list of attributes.

LDAP.add() adds a new object, and returns the corresponding LDAPObject, just pass the full, absolute DN and an attributes dict

LDAP.delete() deletes an entire object. Just pass the full, absolute DN of the object to delete.

The following methods are preferred for modification, however raw modify methods are also provided.

All accept the absolute DN of the object to modify, and an attributes dictionary.

LDAP.add_attrs() adds new attributes.

LDAP.delete_attrs() deletes attribute values. Pass an empty values list in the attributes dictionary to delete all values for an attribute.

LDAP.replace_attrs() replaces all values for the given attributes with the values passed in the attributes dictionary. Atrributes that are not mentioned are not touched. Passing an empty list removes all values.

For LDAP.delete_attrs() and LDAP.replace_attrs() you can specify the constant LDAP.DELETE_ALL in place of an empty attribute value list to remove all values for the attribute. For example:

ldap.replace_attrs('cn=foo,dc=example,dc=org', {'someAttribute': LDAP.DELETE_ALL})

If you wish to require the use of the constant instead of an empty list, pass error_empty_list=True to the LDAP constructor. You can also pass ignore_empty_list=True to silently prevent these from being sent to the server (this will be the default behavior in a future release).

LDAPObject Methods Intro

Great, right? But specifying absolute DNs all the time is no fun. Enter LDAPObject, and keep in mind the base attribute mentioned earlier.

LDAPObject inherits from AttrsDict to present attributes. This interface is documented here.

LDAPObject defines methods corresponding to all of the LDAP methods, but pass the object’s dn automatically, or only require the RDN prefix, with the object’s dn automatically appended to obtain the absolute DN.

LDAPObject.search() accepts all the same arguments as LDAP.search() except base_dn and scope. The object’s own DN is always used for base_dn, and the relative_search_scope is always used as the scope.

LDAPObject.find() is more or less a better LDAPObject.get_child(). It looks at the object’s relative_search_scope property to determine the most efficient way to find a single object below this one. It will either do a BASE search if relative_seach_scope=Scope.ONE or a SUBTREE search if relative_search_Scope=Scope.SUB. It is an error to use this method if relative_search_scope=Scope.BASE.

LDAPObject.get_child() is analagous to LDAP.get() but it only needs the RDN, appending the object’s own DN as mentioned earlier. (Note that LDAPObject.get() inherits from the native dict.get())

LDAPObject.add_child() is analagous to LDAP.add() again accepting an RDN in place of a full absolute DN.

Use LDAPObject.get_attr() like dict.get() except an empty list will always be returned as default if the attribute is not defined.

LDAPObject’s modify methods update the server first, then update the local attributes dictionary to match if successful. LDAPObject.add_attrs(), LDAPObject.delete_attrs(), and LDAPObject.replace_attrs() require only a new attributes dictionary as an argument, of the same format as for the matching LDAP methods.

LDAPObject Examples:

people = ldap.base.get_child('ou=people')

print(people['objectClass'])
# ['top', 'organizationalUnit']

people.add_attrs({'description':['Contains all users']})

# list all users
for user in people.search(filter='(objectClass=posixAccount)'):
    print(user['uid'][0])

Relative Searching

All objects have LDAPObject.search() and LDAPObject.find() methods which utilize the relative_search_scope attribute of the object. relative_search_scope can be passed as a keyword to any method that creates new objects, including LDAP.obj(), LDAP.get(), LDAP.search(), LDAP.add(), LDAPObject.obj(), LDAPObject.find(), LDAPObject.search(), LDAPObject.get_child(), and LDAPObject.add_child().

When you create an object from another LDAPObject and you don’t specify the relative_search_scope, it is automatically inherited from the parent object. When you create an object from an LDAP method, it defaults to Scope.SUB.

The real win with this feature is when your tree is structured such that you can set this to Scope.ONE as this conveys significant performance benefits, especially when using LDAPObject.find(). This allows laurelin to to construct the absolute DN of the child object and perform a highly efficient BASE search.

Attributes Dictionaries

This common interface is used both for input and output of LDAP attributes. In short: dict keys are attribute names, and dict values are a list of attribute values. For example:

{
    'objectClass': ['posixAccount', 'inetOrgPerson'],
    'uid': ['ashafer01'],
    'uidNumber': ['1000'],
    'gidNumber': ['100'],
    'cn': ['Alex Shafer'],
    'homeDirectory': ['/home/ashafer01'],
    'loginShell': ['/bin/zsh'],
    'mail': ['ashafer01@example.org'],
}

Note that there is an AttrsDict class defined - there is no requirement to create instances of this class to pass as arguments, though you are welcome to if you find the additional methods provided this class convenient, such as AttrsDict.get_attr(). Further, it overrides dict special methods to enforce type requirements and enable case-insensitive keys.

Also note that when passing an attributes dictionary to LDAP.replace_attrs() or LDAP.delete_attrs() it is legal to specify the constant LDAP.DELETE_ALL in place of a value list.

Modify Operations

Raw modify methods

LDAP.modify() and LDAPObject.modify() work similarly to the modify functions in python-ldap, which in turn very closely align with how modify operations are described at the protocol level. A list of Mod instances is required with 3 arguments:

  1. One of the Mod constants which describe the operation to perform on an attribute:
  • Mod.ADD adds new attributes/values
  • Mod.REPLACE replaces all values for an attribute, creating new attributes if necessary
  • Mod.DELETE removes attributes/values.
  1. The name of the attribute to modify. Each entry may only modify one attribute, but an unlimited number of entries may be specified in a single modify operation.
  2. A list of attribute values to use with the modify operation or the constant LDAP.DELETE_ALL:
  • The list may be empty for Mod.REPLACE and Mod.DELETE, both of which will cause all values for the given attribute to be removed from the object. The list may not be empty for Mod.ADD. You can also specify the constant LDAP.DELETE_ALL in place of any empty list. If you wish to warn about empty lists or require the use of the constant, pass warn_empty_list=True or error_empty_list=True to the LDAP constructor. You can also pass ignore_empty_list=True to silently prevent these from being sent to the server (this will be the default behavior in a future release).
  • A non-empty list for Mod.ADD lists all new attribute values to add
  • A non-empty list for Mod.DELETE lists specific attribute values to remove
  • A non-empty list for Mod.REPLACE indicates ALL new values for the attribute - all others will be removed.

Example custom modify operation:

from laurelin.ldap.modify import Mod

ldap.modify('uid=ashafer01,ou=people,dc=example,dc=org', [
    Mod(Mod.ADD, 'mobile', ['+1 401 555 1234', '+1 403 555 4321']),
    Mod(Mod.ADD, 'homePhone', ['+1 404 555 6789']),
    Mod(Mod.REPLACE, 'homeDirectory', ['/export/home/ashafer01']),
])

Using an LDAPObject instead:

ldap.base.obj('uid=ashafer01,ou=people').modify([
    Mod(Mod.DELETE, 'mobile', ['+1 401 555 1234']),
    Mod(Mod.DELETE, 'homePhone', LDAP.DELETE_ALL), # delete all homePhone values
])

Again, an arbitrary number of Mod entries may be specified for each modify call.

Strict modification and higher-level modify functions

The higher-level modify functions (add_attrs, delete_attrs, and replace_attrs) all rely on the concept of strict modification - that is, to only send the modify operation, and to never perform an additional search. By default, strict modification is disabled, meaning that, if necessary, an extra search will be performed before sending a modify request.

You can enable strict modification by passing strict_modify=True to the LDAP constructor.

With strict modification disabled, the LDAP modify functions will engage a more intelligent modification strategy after performing the extra query: for LDAP.add_attrs(), no duplicate values are sent to the server to be added. Likewise for LDAP.delete_attrs(), deletion will not be requested for values that are not known to exist. This prevents many unnecessary failures, as ultimately the final semantic state of the object is unchanged with or without such failures. (Note that with LDAP.replace_attrs() no such failures are possible)

With the LDAPObject modify functions, the situaiton is slightly more complex. Regardless of the strict_modify setting, the more intelligent modify strategy will always be used, using at least any already-queried attribute data stored with the object (which could be complete data depending on how the object was originally obtained). If strict_modify is disabled, however, another search may still be performed to fill in any missing attributes that are mentioned in the passed attributes dict.

The raw modify functions on both LDAP and LDAPObject are unaffected by the strict_modify setting - they will always attempt the modify operation exactly as specified.

Global Defaults, LDAP instance attributes, and LDAP constructor arguments

All of the LDAP constructor arguments are set to None by default. In the constructor, any explicitly is None arguments are set to their associated global default. These are attributes of the LDAP class, have the same name as the argument, upper-cased, and with a DEFAULT_ prefix (but the prefix wont be repeated).

For example, the server argument has global default LDAP.DEFAULT_SERVER, and default_criticality is LDAP.DEFAULT_CRITICALITY.

Most arguments also have an associated instance property. A complete table is below:

Global Default LDAP instance attribute LDAP constructor keyword
LDAP.DEFAULT_SERVER host_uri server
LDAP.DEFAULT_BASE_DN base_dn base_dn
LDAP.DEFAULT_FILTER none none
LDAP.DEFAULT_DEREF_ALIASES default_deref_aliases deref_aliases
LDAP.DEFAULT_SEARCH_TIMEOUT default_search_timeout search_timeout
LDAP.DEFAULT_CONNECT_TIMEOUT sock_params[0] connect_timeout
LDAP.DEFAULT_STRICT_MODIFY strict_modify strict_modify
LDAP.DEFAULT_REUSE_CONNECTION none reuse_connection
LDAP.DEFAULT_SSL_VERIFY ssl_verify ssl_verify
LDAP.DEFAULT_SSL_CA_FILE ssl_ca_file ssl_ca_file
LDAP.DEFAULT_SSL_CA_PATH ssl_ca_path ssl_ca_path
LDAP.DEFAULT_SSL_CA_DATA ssl_ca_data ssl_ca_data
LDAP.DEFAULT_FETCH_RESULT_REFS default_fetch_result_refs fetch_result_refs
LDAP.DEFAULT_FOLLOW_REFERRALS default_follow_referrals follow_referrals
LDAP.DEFAULT_SASL_MECH default_sasl_mech default_sasl_mech
LDAP.DEFAULT_SASL_FATAL_DOWNGRADE_CHECK sasl_fatal_downgrade_check sasl_fatal_downgrade_check
LDAP.DEFAULT_CRITICALITY default_criticality default_criticality
LDAP.DEFAULT_VALIDATORS validators validators
LDAP.DEFAULT_WARN_EMPTY_LIST warn_empty_list warn_empty_list
LDAP.DEFAULT_ERROR_EMPTY_LIST error_empty_list error_empty_list
LDAP.DEFAULT_IGNORE_EMPTY_LIST ignore_empty_list ignore_empty_list
LDAP.DEFAULT_FILTER_SYNTAX default_filter_syntax filter_syntax
LDAP.DEFAULT_BUILT_IN_EXTENSIONS_ONLY` none public built_in_extensions_only

The LDAP instance attributes beginning with default_ are used as the defaults for corresponding arguments on other methods. default_sasl_mech is used with LDAP.sasl_bind(), default_criticality is the default criticality of all controls, the other default_ attributes are used with LDAP.search().

The ssl_ prefixed instances attributes are used as the defaults for LDAP.start_tls(), as well as the socket configuration when connecting to an ldaps:// socket.

Basic usage examples

1. Connect to local LDAP instance and iterate all objects

from laurelin.ldap import LDAP

with LDAP('ldapi:///') as ldap:
    ldap.sasl_bind()
    for obj in ldap.base.search():
        print(obj.format_ldif())

LDAP.sasl_bind() defaults to the EXTERNAL mechanism when an ldapi: URI is given, which uses the current user for authorization via the unix socket (Known as “autobind” with 389 Directory Server)