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
==orinoperators 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 fromscope- One of:
Scope.BASE- only searchbase_dnitselfScope.ONE- searchbase_dnand its immediate childrenScope.SUB- searchbase_dnand all of its descendents (default)filter- standard LDAP filter stringattrs- 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:
- One of the
Modconstants which describe the operation to perform on an attribute:
Mod.ADDadds new attributes/valuesMod.REPLACEreplaces all values for an attribute, creating new attributes if necessaryMod.DELETEremoves attributes/values.
- 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.
- A list of attribute values to use with the modify operation or the constant
LDAP.DELETE_ALL:
- The list may be empty for
Mod.REPLACEandMod.DELETE, both of which will cause all values for the given attribute to be removed from the object. The list may not be empty forMod.ADD. You can also specify the constantLDAP.DELETE_ALLin place of any empty list. If you wish to warn about empty lists or require the use of the constant, passwarn_empty_list=Trueorerror_empty_list=Trueto theLDAPconstructor. You can also passignore_empty_list=Trueto 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.ADDlists all new attribute values to add- A non-empty list for
Mod.DELETElists specific attribute values to remove- A non-empty list for
Mod.REPLACEindicates 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)