node.ext.ldap
.. image:: https://img.shields.io/pypi/v/node.ext.ldap.svg
:target: https://pypi.python.org/pypi/node.ext.ldap
:alt: Latest PyPI version
.. image:: https://img.shields.io/pypi/dm/node.ext.ldap.svg
:target: https://pypi.python.org/pypi/node.ext.ldap
:alt: Number of PyPI downloads
.. image:: https://github.com/conestack/node.ext.ldap/actions/workflows/test.yaml/badge.svg
:target: https://github.com/conestack/node.ext.ldap/actions/workflows/test.yaml
:alt: Test node.ext.ldap
Overview
node.ext.ldap
is a LDAP convenience library for LDAP communication based on
python-ldap <http://pypi.python.org/pypi/python-ldap>
_ (version 2.4 or later)
and node <http://pypi.python.org/pypi/node>
_.
The package contains base configuration and communication objects, a LDAP node
object and a LDAP node based user and group management implementation utilizing
node.ext.ugm <http://pypi.python.org/pypi/node.ext.ugm>
_.
.. _RFC 2251
: http://www.ietf.org/rfc/rfc2251.txt
This package is the successor of
bda.ldap <http://pypi.python.org/pypi/bda.ldap>
_.
.. contents::
:depth: 2
API changes compared to 0.9.x
-
LDAPNode
instances cannot have direct children of subtree any longer.
This was a design flaw because of possible duplicate RDN's.
-
LDAPNode.search
returns DN's instead of RDN's by default.
-
Secondary keys and alternative key attribute features have been removed
entirely from LDAPNode
.
-
LDAPProps.check_duplicates
setting has been removed.
Usage
LDAP Properties
To define connection properties for LDAP use node.ext.ldap.LDAPProps
object:
.. code-block:: pycon
>>> from node.ext.ldap import LDAPProps
>>> props = LDAPProps(
... uri='ldap://localhost:12345/',
... user='cn=Manager,dc=my-domain,dc=com',
... password='secret',
... cache=False
... )
Test server connectivity with node.ext.ldap.testLDAPConnectivity
:
.. code-block:: pycon
>>> from node.ext.ldap import testLDAPConnectivity
>>> assert testLDAPConnectivity(props=props) == 'success'
LDAP Connection
For handling LDAP connections, node.ext.ldap.LDAPConnector
is used. It
expects a LDAPProps
instance in the constructor. Normally there is no
need to instantiate this object directly, this happens during creation of
higher abstractions, see below:
.. code-block:: pycon
>>> from node.ext.ldap import LDAPConnector
>>> import ldap
>>> connector = LDAPConnector(props=props)
Calling bind
creates and returns the LDAP connection:
.. code-block:: pycon
>>> conn = connector.bind()
>>> assert isinstance(conn, ldap.ldapobject.ReconnectLDAPObject)
Calling unbind
destroys the connection:
.. code-block:: pycon
>>> connector.unbind()
LDAP Communication
For communicating with an LDAP server, node.ext.ldap.LDAPCommunicator
is
used. It provides all the basic functions needed to search and modify the
directory.
LDAPCommunicator
expects a LDAPConnector
instance at creation time:
.. code-block:: pycon
>>> from node.ext.ldap import LDAPCommunicator
>>> communicator = LDAPCommunicator(connector)
Bind to server:
.. code-block:: pycon
>>> communicator.bind()
Adding directory entry:
.. code-block:: pycon
>>> communicator.add(
... 'cn=foo,ou=demo,dc=my-domain,dc=com',
... {
... 'cn': 'foo',
... 'sn': 'Mustermann',
... 'userPassword': 'secret',
... 'objectClass': ['person'],
... }
... )
Set default search DN:
.. code-block:: pycon
>>> communicator.baseDN = 'ou=demo,dc=my-domain,dc=com'
Search in directory:
.. code-block:: pycon
>>> import node.ext.ldap
>>> res = communicator.search(
... '(objectClass=person)',
... node.ext.ldap.SUBTREE
... )
>>> assert res == [(
... 'cn=foo,ou=demo,dc=my-domain,dc=com',
... {
... 'objectClass': ['person'],
... 'userPassword': ['secret'],
... 'cn': ['foo'],
... 'sn': ['Mustermann']
... }
... )]
Modify directory entry:
.. code-block:: pycon
>>> from ldap import MOD_REPLACE
>>> communicator.modify(
... 'cn=foo,ou=demo,dc=my-domain,dc=com',
... [(MOD_REPLACE, 'sn', 'Musterfrau')]
... )
>>> res = communicator.search(
... '(objectClass=person)',
... node.ext.ldap.SUBTREE,
... attrlist=['cn']
... )
>>> assert res == [('cn=foo,ou=demo,dc=my-domain,dc=com', {'cn': ['foo']})]
Change the password of a directory entry which represents a user:
.. code-block:: pycon
>>> communicator.passwd(
... 'cn=foo,ou=demo,dc=my-domain,dc=com',
... 'secret',
... '12345'
... )
>>> res = communicator.search(
... '(objectClass=person)',
... node.ext.ldap.SUBTREE,
... attrlist=['userPassword']
... )
>>> assert res == [(
... 'cn=foo,ou=demo,dc=my-domain,dc=com',
... {'userPassword': ['{SSHA}...']}
... )]
Delete directory entry:
.. code-block:: pycon
>>> communicator.delete('cn=foo,ou=demo,dc=my-domain,dc=com')
>>> res = communicator.search(
... '(objectClass=person)',
... node.ext.ldap.SUBTREE
... )
>>> assert res == []
Close connection:
.. code-block:: pycon
>>> communicator.unbind()
LDAP Session
A more convenient way for dealing with LDAP is provided by
node.ext.ldap.LDAPSession
. It basically provides the same functionality
as LDAPCommunicator
, but automatically creates the connectivity objects
and checks the connection state before performing actions.
Instantiate LDAPSession
object. Expects LDAPProps
instance:
.. code-block:: pycon
>>> from node.ext.ldap import LDAPSession
>>> session = LDAPSession(props)
LDAP session has a convenience to check given properties:
.. code-block:: pycon
>>> res = session.checkServerProperties()
>>> assert res == (True, 'OK')
Set default search DN for session:
.. code-block:: pycon
>>> session.baseDN = 'ou=demo,dc=my-domain,dc=com'
Search in directory:
.. code-block:: pycon
>>> res = session.search()
>>> assert res == [
... ('ou=demo,dc=my-domain,dc=com',
... {
... 'objectClass': ['top', 'organizationalUnit'],
... 'ou': ['demo'],
... 'description': ['Demo organizational unit']
... }
... )]
Add directory entry:
.. code-block:: pycon
>>> session.add(
... 'cn=foo,ou=demo,dc=my-domain,dc=com',
... {
... 'cn': 'foo',
... 'sn': 'Mustermann',
... 'userPassword': 'secret',
... 'objectClass': ['person'],
... }
... )
Change the password of a directory entry which represents a user:
.. code-block:: pycon
>>> session.passwd('cn=foo,ou=demo,dc=my-domain,dc=com', 'secret', '12345')
Authenticate a specific user:
.. code-block:: pycon
>>> res = session.authenticate('cn=foo,ou=demo,dc=my-domain,dc=com', '12345')
>>> assert res is True
Modify directory entry:
.. code-block:: pycon
>>> session.modify(
... 'cn=foo,ou=demo,dc=my-domain,dc=com',
... [(MOD_REPLACE, 'sn', 'Musterfrau')]
... )
>>> res = session.search(
... '(objectClass=person)',
... node.ext.ldap.SUBTREE,
... attrlist=['cn']
... )
>>> assert res == [(
... 'cn=foo,ou=demo,dc=my-domain,dc=com',
... {'cn': ['foo']}
... )]
Delete directory entry:
.. code-block:: pycon
>>> session.delete('cn=foo,ou=demo,dc=my-domain,dc=com')
>>> res = session.search('(objectClass=person)', node.ext.ldap.SUBTREE)
>>> assert res == []
Close session:
.. code-block:: pycon
>>> session.unbind()
LDAP Nodes
One can deal with LDAP entries as node objects. Therefor
node.ext.ldap.LDAPNode
is used. To get a clue of the complete
node API, see node <http://pypi.python.org/pypi/node>
_ package.
Create a LDAP node. The root Node expects the base DN and a LDAPProps
instance:
.. code-block:: pycon
>>> from node.ext.ldap import LDAPNode
>>> root = LDAPNode('ou=demo,dc=my-domain,dc=com', props=props)
Every LDAP node has a DN and a RDN:
.. code-block:: pycon
>>> root.DN
u'ou=demo,dc=my-domain,dc=com'
>>> root.rdn_attr
u'ou'
Check whether created node exists in the database:
.. code-block:: pycon
>>> root.exists
True
Directory entry has no children yet:
.. code-block:: pycon
>>> root.keys()
[]
Add children to root node:
.. code-block:: pycon
>>> person = LDAPNode()
>>> person.attrs['objectClass'] = ['person', 'inetOrgPerson']
>>> person.attrs['sn'] = 'Mustermann'
>>> person.attrs['userPassword'] = 'secret'
>>> root['cn=person1'] = person
>>> person = LDAPNode()
>>> person.attrs['objectClass'] = ['person', 'inetOrgPerson']
>>> person.attrs['sn'] = 'Musterfrau'
>>> person.attrs['userPassword'] = 'secret'
>>> root['cn=person2'] = person
If the RDN attribute was not set during node creation, it is computed from
node key and set automatically:
.. code-block:: pycon
>>> person.attrs['cn']
u'person2'
Fetch children DN by key from LDAP node:
.. code-block:: pycon
>>> root.child_dn('cn=person1')
u'cn=person1,ou=demo,dc=my-domain,dc=com'
Have a look at the tree:
.. code-block:: pycon
>>> root.printtree()
<ou=demo,dc=my-domain,dc=com - True>
<cn=person2,ou=demo,dc=my-domain,dc=com:cn=person2 - True>
<cn=person1,ou=demo,dc=my-domain,dc=com:cn=person1 - True>
The entries have not been written to the directory yet. When modifying a LDAP
node tree, everything happens im memory. Persisting is done by calling the
tree, or a part of it. You can check sync state of a node with its changed
flag. If changed is True
it means either that the node attributes or node
children has changed:
.. code-block:: pycon
>>> root.changed
True
>>> root()
>>> root.changed
False
Modify a LDAP node:
.. code-block:: pycon
>>> person = root['cn=person1']
Modify existing attribute:
.. code-block:: pycon
>>> person.attrs['sn'] = 'Mustermensch'
Add new attribute:
.. code-block:: pycon
>>> person.attrs['description'] = 'Mustermensch description'
>>> person()
Delete an attribute:
.. code-block:: pycon
>>> del person.attrs['description']
>>> person()
Delete LDAP node:
.. code-block:: pycon
>>> del root['cn=person2']
>>> root()
>>> root.printtree()
<ou=demo,dc=my-domain,dc=com - False>
<cn=person1,ou=demo,dc=my-domain,dc=com:cn=person1 - False>
Searching LDAP
Add some users and groups we'll search for:
.. code-block:: pycon
>>> for i in range(2, 6):
... node = LDAPNode()
... node.attrs['objectClass'] = ['person', 'inetOrgPerson']
... node.attrs['sn'] = 'Surname %s' % i
... node.attrs['userPassword'] = 'secret%s' % i
... node.attrs['description'] = 'description%s' % i
... node.attrs['businessCategory'] = 'group1'
... root['cn=person%s' % i] = node
>>> node = LDAPNode()
>>> node.attrs['objectClass'] = ['groupOfNames']
>>> node.attrs['member'] = [
... root.child_dn('cn=person1'),
... root.child_dn('cn=person2'),
... ]
... node.attrs['description'] = 'IT'
>>> root['cn=group1'] = node
>>> node = LDAPNode()
>>> node.attrs['objectClass'] = ['groupOfNames']
>>> node.attrs['member'] = [
... root.child_dn('cn=person4'),
... root.child_dn('cn=person5'),
... ]
>>> root['cn=group2'] = node
>>> root()
>>> root.printtree()
<ou=demo,dc=my-domain,dc=com - False>
<cn=person1,ou=demo,dc=my-domain,dc=com:cn=person1 - False>
<cn=person2,ou=demo,dc=my-domain,dc=com:cn=person2 - False>
<cn=person3,ou=demo,dc=my-domain,dc=com:cn=person3 - False>
<cn=person4,ou=demo,dc=my-domain,dc=com:cn=person4 - False>
<cn=person5,ou=demo,dc=my-domain,dc=com:cn=person5 - False>
<cn=group1,ou=demo,dc=my-domain,dc=com:cn=group1 - False>
<cn=group2,ou=demo,dc=my-domain,dc=com:cn=group2 - False>
For defining search criteria LDAP filters are used, which can be combined by
bool operators '&' and '|':
.. code-block:: pycon
>>> from node.ext.ldap import LDAPFilter
>>> filter = LDAPFilter('(objectClass=person)')
>>> filter |= LDAPFilter('(objectClass=groupOfNames)')
>>> res = sorted(root.search(queryFilter=filter))
>>> assert res == [
... u'cn=group1,ou=demo,dc=my-domain,dc=com',
... u'cn=group2,ou=demo,dc=my-domain,dc=com',
... u'cn=person1,ou=demo,dc=my-domain,dc=com',
... u'cn=person2,ou=demo,dc=my-domain,dc=com',
... u'cn=person3,ou=demo,dc=my-domain,dc=com',
... u'cn=person4,ou=demo,dc=my-domain,dc=com',
... u'cn=person5,ou=demo,dc=my-domain,dc=com'
... ]
Define multiple criteria LDAP filter:
.. code-block:: pycon
>>> from node.ext.ldap import LDAPDictFilter
>>> filter = LDAPDictFilter({
... 'objectClass': ['person'],
... 'cn': 'person1'
... })
>>> res = root.search(queryFilter=filter)
>>> assert res == [u'cn=person1,ou=demo,dc=my-domain,dc=com']
Define a relation LDAP filter. In this case we build a relation between group
'cn' and person 'businessCategory':
.. code-block:: pycon
>>> from node.ext.ldap import LDAPRelationFilter
>>> filter = LDAPRelationFilter(root['cn=group1'], 'cn:businessCategory')
>>> res = root.search(queryFilter=filter)
>>> assert res == [
... u'cn=person2,ou=demo,dc=my-domain,dc=com',
... u'cn=person3,ou=demo,dc=my-domain,dc=com',
... u'cn=person4,ou=demo,dc=my-domain,dc=com',
... u'cn=person5,ou=demo,dc=my-domain,dc=com'
... ]
Different LDAP filter types can be combined:
.. code-block:: pycon
>>> filter &= LDAPFilter('(cn=person2)')
>>> str(filter)
'(&(businessCategory=group1)(cn=person2))'
The following keyword arguments are accepted by LDAPNode.search
. If
multiple keywords are used, combine search criteria with '&' where appropriate.
If attrlist
is given, the result items consists of 2-tuples with a dict
containing requested attributes at position 1:
queryFilter
Either a LDAP filter instance or a string. If given argument is string type,
a LDAPFilter
instance is created.
criteria
A dictionary containing search criteria. A LDAPDictFilter
instance is
created.
attrlist
List of attribute names to return. Special attributes rdn
and dn
are allowed.
relation
Either LDAPRelationFilter
instance or a string defining the relation.
If given argument is string type, a LDAPRelationFilter
instance is
created.
relation_node
In combination with relation
argument, when given as string, use
relation_node
instead of self for filter creation.
exact_match
Flag whether 1-length result is expected. Raises an error if empty result
or more than one entry found.
or_search
In combination with criteria
, this parameter is passed to the creation
of LDAPDictFilter. This flag controls whether to combine criteria keys
and values with '&' or '|'.
or_keys
In combination with criteria
, this parameter is passed to the creation
of LDAPDictFilter. This flag controls whether criteria keys are
combined with '|' instead of '&'.
or_values
In combination with criteria
, this parameter is passed to the creation
of LDAPDictFilter. This flag controls whether criteria values are
combined with '|' instead of '&'.
page_size
Used in conjunction with cookie
for querying paged results.
cookie
Used in conjunction with page_size
for querying paged results.
get_nodes
If True
result contains LDAPNode
instances instead of DN's
You can define search defaults on the node which are always considered when
calling search
on this node. If set, they are always '&' combined with
any (optional) passed filters.
Define the default search scope:
.. code-block:: pycon
>>> from node.ext.ldap import SUBTREE
>>> root.search_scope = SUBTREE
Define default search filter, could be of type LDAPFilter, LDAPDictFilter,
LDAPRelationFilter or string:
.. code-block:: pycon
>>> root.search_filter = LDAPFilter('objectClass=groupOfNames')
>>> res = root.search()
>>> assert res == [
... u'cn=group1,ou=demo,dc=my-domain,dc=com',
... u'cn=group2,ou=demo,dc=my-domain,dc=com'
... ]
>>> root.search_filter = None
Define default search criteria as dict:
.. code-block:: pycon
>>> root.search_criteria = {'objectClass': 'person'}
>>> res = root.search()
>>> assert res == [
... u'cn=person1,ou=demo,dc=my-domain,dc=com',
... u'cn=person2,ou=demo,dc=my-domain,dc=com',
... u'cn=person3,ou=demo,dc=my-domain,dc=com',
... u'cn=person4,ou=demo,dc=my-domain,dc=com',
... u'cn=person5,ou=demo,dc=my-domain,dc=com'
... ]
Define default search relation:
.. code-block:: pycon
>>> root.search_relation = LDAPRelationFilter(
... root['cn=group1'],
... 'cn:businessCategory'
... )
>>> res = root.search()
>>> assert res == [
... u'cn=person2,ou=demo,dc=my-domain,dc=com',
... u'cn=person3,ou=demo,dc=my-domain,dc=com',
... u'cn=person4,ou=demo,dc=my-domain,dc=com',
... u'cn=person5,ou=demo,dc=my-domain,dc=com'
... ]
Again, like with the keyword arguments, multiple defined defaults are '&'
combined:
.. code-block:: pycon
# empty result, there are no groups with group 'cn' as 'description'
>>> root.search_criteria = {'objectClass': 'group'}
>>> res = root.search()
>>> assert res == []
JSON Serialization
Serialize and deserialize LDAP nodes:
.. code-block:: pycon
>>> root = LDAPNode('ou=demo,dc=my-domain,dc=com', props=props)
Serialize children:
.. code-block:: pycon
>>> from node.serializer import serialize
>>> json_dump = serialize(root.values())
Clear and persist root:
.. code-block:: pycon
>>> root.clear()
>>> root()
Deserialize JSON dump:
.. code-block:: pycon
>>> from node.serializer import deserialize
>>> deserialize(json_dump, root=root)
[<cn=person1,ou=demo,dc=my-domain,dc=com:cn=person1 - True>,
<cn=person2,ou=demo,dc=my-domain,dc=com:cn=person2 - True>,
<cn=person3,ou=demo,dc=my-domain,dc=com:cn=person3 - True>,
<cn=person4,ou=demo,dc=my-domain,dc=com:cn=person4 - True>,
<cn=person5,ou=demo,dc=my-domain,dc=com:cn=person5 - True>,
<cn=group1,ou=demo,dc=my-domain,dc=com:cn=group1 - True>,
<cn=group2,ou=demo,dc=my-domain,dc=com:cn=group2 - True>]
Since root has been given, created nodes were added:
.. code-block:: pycon
>>> root()
>>> root.printtree()
<ou=demo,dc=my-domain,dc=com - False>
<cn=person1,ou=demo,dc=my-domain,dc=com:cn=person1 - False>
<cn=person2,ou=demo,dc=my-domain,dc=com:cn=person2 - False>
<cn=person3,ou=demo,dc=my-domain,dc=com:cn=person3 - False>
<cn=person4,ou=demo,dc=my-domain,dc=com:cn=person4 - False>
<cn=person5,ou=demo,dc=my-domain,dc=com:cn=person5 - False>
<cn=group1,ou=demo,dc=my-domain,dc=com:cn=group1 - False>
<cn=group2,ou=demo,dc=my-domain,dc=com:cn=group2 - False>
Non simple vs simple mode. Create container with children:
.. code-block:: pycon
>>> container = LDAPNode()
>>> container.attrs['objectClass'] = ['organizationalUnit']
>>> root['ou=container'] = container
>>> person = LDAPNode()
>>> person.attrs['objectClass'] = ['person', 'inetOrgPerson']
>>> person.attrs['sn'] = 'Mustermann'
>>> person.attrs['userPassword'] = 'secret'
>>> container['cn=person1'] = person
>>> root()
Serialize in default mode contains type specific information. Thus JSON dump
can be deserialized later:
.. code-block:: pycon
>>> serialized = serialize(container)
>>> assert serialized == (
... '{'
... '"__node__": {'
... '"attrs": {'
... '"objectClass": ["organizationalUnit"], '
... '"ou": "container"'
... '}, '
... '"children": [{'
... '"__node__": {'
... '"attrs": {'
... '"objectClass": ["person", "inetOrgPerson"], '
... '"userPassword": "secret", '
... '"sn": "Mustermann", '
... '"cn": "person1"'
... '},'
... '"class": "node.ext.ldap._node.LDAPNode", '
... '"name": "cn=person1"'
... '}'
... '}], '
... '"class": "node.ext.ldap._node.LDAPNode", '
... '"name": "ou=container"'
... '}'
... '}'
... )
Serialize in simple mode is better readable, but not deserialzable any more:
.. code-block:: pycon
>>> serialized = serialize(container, simple_mode=True)
>>> assert serialized == (
... '{'
... '"attrs": {'
... '"objectClass": ["organizationalUnit"], '
... '"ou": "container"'
... '}, '
... '"name": "ou=container", '
... '"children": [{'
... '"name": "cn=person1", '
... '"attrs": {'
... '"objectClass": ["person", "inetOrgPerson"], '
... '"userPassword": "secret", '
... '"sn": "Mustermann", '
... '"cn": "person1"'
... '}'
... '}]'
... '}'
... )
User and Group management
LDAP is often used to manage Authentication, thus node.ext.ldap
provides
an API for User and Group management. The API follows the contract of
node.ext.ugm <http://pypi.python.org/pypi/node.ext.ugm>
_:
.. code-block:: pycon
>>> from node.ext.ldap import ONELEVEL
>>> from node.ext.ldap.ugm import UsersConfig
>>> from node.ext.ldap.ugm import GroupsConfig
>>> from node.ext.ldap.ugm import RolesConfig
>>> from node.ext.ldap.ugm import Ugm
Instantiate users, groups and roles configuration. They are based on
PrincipalsConfig
class and expect this settings:
baseDN
Principals container base DN.
attrmap
Principals Attribute map as odict.odict
. This object must contain the
mapping between reserved keys and the real LDAP attribute, as well as
mappings to all accessible attributes for principal nodes if instantiated
in strict mode, see below.
scope
Search scope for principals.
queryFilter
Search Query filter for principals
objectClasses
Object classes used for creation of new principals. For some objectClasses
default value callbacks are registered, which are used to generate default
values for mandatory attributes if not already set on principal vessel node.
defaults
Dict like object containing default values for principal creation. A value
could either be static or a callable accepting the principals node and the
new principal id as arguments. This defaults take precedence to defaults
detected via set object classes.
strict
Define whether all available principal attributes must be declared in attmap,
or only reserved ones. Defaults to True.
memberOfSupport
Flag whether to use 'memberOf' attribute (AD) or memberOf overlay
(openldap) for Group membership resolution where appropriate.
Reserved attrmap keys for Users, Groups and roles:
id
The attribute containing the user id (mandatory).
rdn
The attribute representing the RDN of the node (mandatory)
XXX: get rid of, should be detected automatically
Reserved attrmap keys for Users:
login
Alternative login name attribute (optional)
Create config objects:
.. code-block:: pycon
>>> ucfg = UsersConfig(
... baseDN='ou=demo,dc=my-domain,dc=com',
... attrmap={
... 'id': 'cn',
... 'rdn': 'cn',
... 'login': 'sn',
... },
... scope=ONELEVEL,
... queryFilter='(objectClass=person)',
... objectClasses=['person'],
... defaults={},
... strict=False,
... )
>>> gcfg = GroupsConfig(
... baseDN='ou=demo,dc=my-domain,dc=com',
... attrmap={
... 'id': 'cn',
... 'rdn': 'cn',
... },
... scope=ONELEVEL,
... queryFilter='(objectClass=groupOfNames)',
... objectClasses=['groupOfNames'],
... defaults={},
... strict=False,
... memberOfSupport=False,
... )
Roles are represented in LDAP like groups. Note, if groups and roles are mixed
up in the same container, make sure that query filter fits. For our demo,
different group object classes are used. Anyway, in real world it might be
worth considering a seperate container for roles:
.. code-block:: pycon
>>> rcfg = GroupsConfig(
... baseDN='ou=demo,dc=my-domain,dc=com',
... attrmap={
... 'id': 'cn',
... 'rdn': 'cn',
... },
... scope=ONELEVEL,
... queryFilter='(objectClass=groupOfUniqueNames)',
... objectClasses=['groupOfUniqueNames'],
... defaults={},
... strict=False,
... )
Instantiate Ugm
object:
.. code-block:: pycon
>>> ugm = Ugm(props=props, ucfg=ucfg, gcfg=gcfg, rcfg=rcfg)
The Ugm object has 2 children, the users container and the groups container.
The are accessible via node API, but also on users
respective groups
attribute:
.. code-block:: pycon
>>> ugm.keys()
['users', 'groups']
>>> ugm.users
<Users object 'users' at ...>
>>> ugm.groups
<Groups object 'groups' at ...>
Fetch user:
.. code-block:: pycon
>>> user = ugm.users['person1']
>>> user
<User object 'person1' at ...>
User attributes. Reserved keys are available on user attributes:
.. code-block:: pycon
>>> user.attrs['id']
u'person1'
>>> user.attrs['login']
u'Mustermensch'
'login' maps to 'sn':
.. code-block:: pycon
>>> user.attrs['sn']
u'Mustermensch'
>>> user.attrs['login'] = u'Mustermensch1'
>>> user.attrs['sn']
u'Mustermensch1'
>>> user.attrs['description'] = 'Some description'
>>> user()
Check user credentials:
.. code-block:: pycon
>>> user.authenticate('secret')
True
Change user password:
.. code-block:: pycon
>>> user.passwd('secret', 'newsecret')
>>> user.authenticate('newsecret')
True
Groups user is member of:
.. code-block:: pycon
>>> user.groups
[<Group object 'group1' at ...>]
Add new User:
.. code-block:: pycon
>>> user = ugm.users.create('person99', sn='Person 99')
>>> user()
>>> res = ugm.users.keys()
>>> assert res == [
... u'person1',
... u'person2',
... u'person3',
... u'person4',
... u'person5',
... u'person99'
... ]
Delete User:
.. code-block:: pycon
>>> del ugm.users['person99']
>>> ugm.users()
>>> res = ugm.users.keys()
>>> assert res == [
... u'person1',
... u'person2',
... u'person3',
... u'person4',
... u'person5'
... ]
Fetch Group:
.. code-block:: pycon
>>> group = ugm.groups['group1']
Group members:
.. code-block:: pycon
>>> res = group.member_ids
>>> assert res == [u'person1', u'person2']
>>> group.users
[<User object 'person1' at ...>, <User object 'person2' at ...>]
Add group member:
.. code-block:: pycon
>>> group.add('person3')
>>> member_ids = group.member_ids
>>> assert member_ids == [u'person1', u'person2', u'person3']
Delete group member:
.. code-block:: pycon
>>> del group['person3']
>>> member_ids = group.member_ids
>>> assert member_ids == [u'person1', u'person2']
Group attribute manipulation works the same way as on user objects.
Manage roles for users and groups. Roles can be queried, added and removed via
ugm or principal object. Fetch a user:
.. code-block:: pycon
>>> user = ugm.users['person1']
Add role for user via ugm:
.. code-block:: pycon
>>> ugm.add_role('viewer', user)
Add role for user directly:
.. code-block:: pycon
>>> user.add_role('editor')
Query roles for user via ugm:
.. code-block:: pycon
>>> roles = sorted(ugm.roles(user))
>>> assert roles == ['editor', 'viewer']
Query roles directly:
.. code-block:: pycon
>>> roles = sorted(user.roles)
>>> assert roles == ['editor', 'viewer']
Call UGM to persist roles:
.. code-block:: pycon
>>> ugm()
Delete role via ugm:
.. code-block:: pycon
>>> ugm.remove_role('viewer', user)
>>> roles = user.roles
>>> assert roles == ['editor']
Delete role directly:
.. code-block:: pycon
>>> user.remove_role('editor')
>>> roles = user.roles
>>> assert roles == []
Call UGM to persist roles:
.. code-block:: pycon
>>> ugm()
Same with group. Fetch a group:
.. code-block:: pycon
>>> group = ugm.groups['group1']
Add roles:
.. code-block:: pycon
>>> ugm.add_role('viewer', group)
>>> group.add_role('editor')
>>> roles = sorted(ugm.roles(group))
>>> assert roles == ['editor', 'viewer']
>>> roles = sorted(group.roles)
>>> assert roles == ['editor', 'viewer']
>>> ugm()
Remove roles:
.. code-block:: pycon
>>> ugm.remove_role('viewer', group)
>>> group.remove_role('editor')
>>> roles = group.roles
>>> assert roles == []
>>> ugm()
Character Encoding
LDAP (v3 at least, RFC 2251
_) uses utf-8
string encoding only.
LDAPNode
does the encoding for you. Consider it a bug, if you receive
anything else than unicode from LDAPNode
, except attributes configured as
binary. The LDAPSession
, LDAPConnector
and LDAPCommunicator
are
encoding-neutral, they do no decoding or encoding.
Unicode strings you pass to nodes or sessions are automatically encoded as uft8
for LDAP, except if configured binary. If you feed them ordinary strings they are
decoded as utf8 and reencoded as utf8 to make sure they are utf8 or compatible,
e.g. ascii.
If you have an LDAP server that does not use utf8, monkey-patch
node.ext.ldap._node.CHARACTER_ENCODING
.
Caching Support
node.ext.ldap
can cache LDAP searches using bda.cache
. You need
to provide a cache factory utility in you application in order to make caching
work. If you don't, node.ext.ldap
falls back to use bda.cache.NullCache
,
which does not cache anything and is just an API placeholder.
To provide a cache based on Memcached
install memcached server and
configure it. Then you need to provide the factory utility:
.. code-block:: pycon
>>> from zope.interface import registry
>>> components = registry.Components('comps')
>>> from node.ext.ldap.cache import MemcachedProviderFactory
>>> cache_factory = MemcachedProviderFactory()
>>> components.registerUtility(cache_factory)
In case of multiple memcached backends on various IPs and ports initialization
of the factory looks like this:
.. code-block:: pycon
>>> components = registry.Components('comps')
>>> cache_factory = MemcachedProviderFactory(servers=[
... '10.0.0.10:22122',
... '10.0.0.11:22322'
... ])
>>> components.registerUtility(cache_factory)
Dependencies
-
python-ldap
-
passlib
-
argparse
-
plumber
-
node
-
node.ext.ugm
-
bda.cache
Contributors
-
Robert Niederreiter
-
Florian Friesdorf
-
Jens Klein
-
Georg Bernhard
-
Johannes Raggam
-
Alexander Pilz
-
Domen Kožar
-
Daniel Widerin
-
Asko Soukka
-
Alex Milosz Sielicki
-
Manuel Reinhardt
-
Philip Bauer
History
1.2 (2022-12-05)
-
Implement expires
and expired
properties on
node.ext.ldap.ugm._api.LDAPUser
as introduced on
node.ext.ugm.interfaces.IUser
as of node.ext.ugm 1.1.
[rnix]
-
Introduce node.ext.ldap.ugm.expires.AccountExpiration
and use it for
account expiration management.
[rnix]
-
Remove node.ext.ldap.ugm._api.AccountExpired
singleton.
LDAPUsers.authenticate
always returns False
if authentication fails.
[rnix]
-
node >= 1.1 is required by node.behaviors.suppress_lifecycle_events
support
[mamico]
-
Backward compatibility with pas.plugins.ldap <= 1.8.1 where LdapProps does not have
timeout properties.
[mamico]
1.1 (2022-10-06)
-
Add properties conn_timeout
and op_timeout
(both not set by default)
to configure ReconnectLDAPObject
.
[mamico]
-
Adopt lifecycle related changes from node
1.1.
[rnix]
-
Move ensure_connection
from LDAPSession
to LDAPCommunicator
to
prevent binds on searches that return cached results.
[enfold-josh]
1.0 (2022-03-19)
-
Call ensure_connection
in LDAPSession.delete
.
[rnix]
-
Remove usage of Nodespaces
behavior.
[rnix]
-
Replace deprecated use of Storage
by MappingStorage
.
[rnix]
-
Replace deprecated use of IStorage
by IMappingStorage
.
[rnix]
-
Replace deprecated use of Nodify
by MappingNode
.
[rnix]
-
Replace deprecated use of NodeChildValidate
by MappingConstraints
.
[rnix]
-
Replace deprecated use of Adopt
by MappingAdopt
.
[rnix]
-
Replace deprecated use of allow_non_node_children
by child_constraints
.
[rnix]
1.0rc2 (2022-03-01)
- Fix #61: Close open connections to LDAP on GC.
[jensens]
1.0rc1 (2021-11-08)
-
Rename deprecated allow_non_node_childs
to allow_non_node_children
on PrincipalAliasedAttributes
.
[rnix]
-
Allow to generate MD5 hashes in FIPS enabled environments.
[frapell]
-
Fix DN comparison in LDAPStorage.node_by_dn
to ignore case sensitivity.
[rnix]
1.0b12 (2020-05-28)
-
Make sure LDAPPrincipals._login_attr
has a value. This way
LDAPUsers.id_for_login
always returns the principal id as stored in the
database.
[rnix]
-
Improve value comparison in LDAPAttributesBehavior.__setitem__
to avoid
unicode warnings.
[rnix]
-
Implement invalidate
on node.ext.ldap.ugm._api.Ugm
.
[rnix]
-
Support for group DNs in memberOf
attribute that are outside of the UGMs configured group.
[jensens]
1.0b11 (2019-09-08)
-
Return empty search result list when an LDAP error occurs.
Fixes issue #50 <https://github.com/conestack/node.ext.ldap/issues/50>
_.
[maurits]
-
Skip objects that were found in LDAP while searching on several attributes but don't contain the required attribute.
[fredvd, maurits]
1.0b10 (2019-06-30)
- Fix cache key generation.
[rnix, pbauer]
1.0b9 (2019-05-07)
-
Refactor mapping from object-class to format and attributes to increase readability.
[jensens]
-
Increase Exception verbosity to ease debugging.
[jensens]
-
Add missing object classes from principal config when persisting principals.
[rnix]
-
Remove attribute from entry if setting it's value to node.utils.UNSET
or
empty string. Most LDAP implementations not allow setting empty values, thus
we delete the entire attribute in this case.
[rnix]
-
Add debug-level logging if search fails with no-such-object.
[jensens]
-
Fix problem with missing LDAP batching cookie in search.
[jensens, rnix]
-
Remove smbpasswd
dependency. Use passlib
instead.
[rnix]
-
Use bytes_mode=False
when using python-ldap
. This is the default
behavior in python 3 and handles everything as unicode/text except
entry attribute values.
For more details see https://www.python-ldap.org/en/latest/bytes_mode.html
[rnix]
-
Add ensure_bytes_py2
in node.ext.ldap.base
.
[rnix]
-
Rename decode_utf8
to ensure_text
in node.ext.ldap.base
.
[rnix]
-
Rename encode_utf8
to ensure_bytes
in node.ext.ldap.base
.
[rnix]
-
Python 3 Support.
[rnix, reinhardt]
-
Convert doctests to unittests.
[rnix]
1.0b8 (2018-10-22)
-
Use ldap.ldapobject.ReconnectLDAPObject
instead of SimpleLDAPObject
to create
the connection object. This makes the connection more robust.
Add properties retry_max
(default 1) and retry_delay
(default 10) to
node.ext.ldap.properties.LDAPServerProperties
to configure ReconnectLDAPObject
.
[joka]
-
Use explode_dn
in LDAPPrincipals.__getitem__
to prevent KeyError
if DN contains comma.
[dmunicio]
1.0b7 (2017-12-15)
-
Do not catch ValueError
in
node.ext.ldap._node.LDAPStorage.batched_search
.
[rnix]
-
Use property decorators for node.ext.ldap._node.LDAPStorage.changed
and node.ext.ldap.session.LDAPSession.baseDN
.
[rnix]
-
Fix signature of node.ext.ldap.interfaces.ILDAPStorage.search
to match
the actual implementation in node.ext.ldap._node.LDAPStorage.search
.
[rnix]
-
Fix signature of node.ext.ldap.ugm.LDAPPrincipals.search
according to
node.ext.ugm.interfaces.IPrincipals.search
. The implementation exposed
LDAP related arguments and has been renamed to raw_search
.
[rnix]
-
Add exists
property to LDAPStorage
.
[rnix]
-
Add objectSid
and objectGUID
from Active Directory schema to
properties.BINARY_DEFAULTS
.
[rnix]
-
Fix default value of LDAPStorage._multivalued_attributes
and
LDAPStorage._binary_attributes
.
[rnix]
1.0b6 (2017-10-27)
-
Switch to use mdb as default db for slapd i testing layer.
[jensens]
-
fix tests, where output order could be random.
[jensens]
1.0b5 (2017-10-27)
- make db-type in test layer configurable
[jensens]
1.0b4 (2017-06-07)
-
Turning referrals off to fix problems with MS AD if it contains aliases.
[alexsielicki]
-
Fix search to check list of binary attributes directly from the root node
data (not from attr behavior) to avoid unnecessarily initializing attribute
behavior just a simple search
[datakurre]
-
Fix to skip group DNs outside the base DN to allow users' memberOf
attribute contain groups outside the group base DN
[datakurre]
1.0b3 (2016-10-18)
-
Add a batched_search
generator function, which do the actual batching for us.
Use this function internally too.
[jensens, rnix]
-
In testing set size_limit to 3 in slapd.conf
in order to catch problems with batching.
[jensens, rnix]
-
Fix missing paging in UGM group mapping method member_ids
.
[jensens]
1.0b2 (2016-09-09)
1.0b1 (31.12.2015)
-
Remove ILDAPProps.check_duplicates
respective
LDAPProps.check_duplicates
.
[rnix]
-
rdn
can be queried via attrlist
in LDAPNode.search
explicitely.
[rnix]
-
Introduce get_nodes
keyword argument in LDAPNode.search
. When set,
search result contains LDAPNode
instances instead of DN's in result.
[rnix]
-
LDAPNode.search
returns DN's instead of RDN's in result. This fixes
searches with scope SUBTREE where result items can potentially contain
duplicate RDN's.
[rnix]
-
Introduce node_by_dn
on LDAPNode
.
[rnix]
-
remove bbb code: no python 2.4 support (2.7+ now), usage of LDAPProperties
mandatory now.
[jensens]
-
Overhaul LDAP UGM implementation.
[rnix]
-
LDAP Node only returns direct children in __iter__
, even if search
scope subtree.
[rnix]
-
LDAPNode keys cannot be aliased any longer. Removed _key_attr
and
_rdn_attr
.
child.
-
LDAPNode does not provide secondary keys any longer. Removed
_seckey_attrs
.
[rnix]
-
Deprecate node.ext.ldap._node.AttributesBehavior
in favor of
node.ext.ldap._node.LDAPAttributesBehavior
.
[rnix]
-
Remove deprecated node.ext.ldap._node.AttributesPart
.
[rnix]
-
Don't fail on UNWILLING_TO_PERFORM
exceptions when authenticating. That
might be thrown, if the LDAP server disallows us to authenticate an admin
user, while we are interested in the local admin
user.
[thet]
-
Add ignore_cert
option to ignore TLS/SSL certificate errors for self
signed certificates when using the ldaps
uri schema.
[thet]
-
Housekeeping.
[rnix]
0.9.7
0.9.6
-
Add new property to allow disable check_duplicates
.
This avoids following Exception when connecting ldap servers with
non-unique attributes used as keys. [saily]
::
Traceback (most recent call last):
...
RuntimeError: Key not unique: =''.
-
ensure attrlist values are strings
[rnix, 2013-12-03]
0.9.5
-
Add expired
property to node.ext.ldap.ugm._api.LDAPUser
.
[rnix, 2012-12-17]
-
Introduce node.ext.ldap.ugm._api.calculate_expired
helper function.
[rnix, 2012-12-17]
-
Lookup expired
attribut from LDAP in
node.ext.ldap.ugm._api.LDAPUser.authenticate
.
[rnix, 2012-12-17]
0.9.4
-
Encode DN in node.ext.ldap._node.LDAPStorage._ldap_modify
.
[rnix, 2012-11-08]
-
Encode DN in node.ext.ldap._node.LDAPStorage._ldap_delete
.
[rnix, 2012-11-08]
-
Encode DN in node.ext.ldap.ugm._api.LDAPUsers.passwd
.
[rnix, 2012-11-08]
-
Encode DN in node.ext.ldap.ugm._api.LDAPUsers.authenticate
.
[rnix, 2012-11-07]
-
Encode baseDN
in LDAPPrincipal.member_of_attr
.
[rnix, 2012-11-06]
-
Encode baseDN
in AttributesBehavior.load
.
[rnix, 2012-11-06]
-
Python 2.7 compatibility.
[rnix, 2012-10-16]
-
PEP-8.
[rnix, 2012-10-16]
-
Fix LDAPPrincipals.idbydn
handling UTF-8 DN's properly.
[rnix, 2012-10-16]
-
Rename parts to behaviors.
[rnix, 2012-07-29]
-
adopt to node
0.9.8.
[rnix, 2012-07-29]
-
Adopt to plumber
1.2.
[rnix, 2012-07-29]
-
Do not convert cookie to unicode in LDAPSession.search
. Cookie value is
no utf-8 string but octet string as described in
http://tools.ietf.org/html/rfc2696.html.
[rnix, 2012-07-27]
-
Add User.group_ids
.
[rnix, 2012-07-26]
0.9.3
- Fix schema to not bind to test BaseDN only and make binding deferred.
[jensens, 2012-05-30]
0.9.2
-
Remove escape_queries
property from
node.ext.ldap.properties.LDAPProps
.
[rnix, 2012-05-18]
-
Use zope.interface.implementer
instead of zope.interface.implements
.
[rnix, 2012-05-18]
-
Structural object class inetOrgPerson
instead of account
on posix
users and groups related test LDIF's
[rnix, 2012-04-23]
-
session no longer magically decodes everything and prevents binary data from
being fetched from ldap. LDAP-Node has semantic knowledge to determine binary
data LDAP-Node converts all non binary data and all keys to unicode.
[jensens, 2012-04-04]
-
or_values and or_keys for finer control of filter criteria
[iElectric, chaoflow, 2012-03-24]
-
support paged searching
[iElectric, chaoflow, 2012-03-24]
0.9.1
-
added is_multivalued to properties and modified node to use this list instead
of the static list. prepare for binary attributes.
[jensens, 2012-03-19]
-
added schema_info to node.
[jensens, 2012-03-19]
-
shadowInactive
defaults to 0
.
[rnix, 2012-03-06]
-
Introduce expiresAttr
and expiresUnit
in principals config.
Considered in Users.authenticate
.
[rnix, 2012-02-11]
-
Do not throw KeyError
if secondary key set but attribute not found on
entry. In case, skip entry.
[rnix, 2012-02-10]
-
Force unicode ids and keys in UGM API.
[rnix, 2012-01-23]
-
Add unicode support for filters.
[rnix, 2012-01-23]
-
Add LDAPUsers.id_for_login
.
[rnix, 2012-01-18]
-
Implement memberOf Support for openldap memberof overlay and AD memberOf
behavior.
[rnix, 2011-11-07]
-
Add LDAPProps.escape_queries
for ActiveDirectory.
[rnix, 2011-11-06]
-
Add group object class to member attribute mapping for ActiveDirectory.
[rnix, 2011-11-06]
-
Make testlayer and testldap more flexible for usage outside this package.
[jensens, 2010-09-30]
0.9
- refactor form
bda.ldap
.
[rnix, chaoflow]
TODO
-
Consider search_st
with timeout.
-
Investigate ReconnectLDAPObject.set_cache_options
.
-
Check/implement silent sort on only the keys LDAPNode.sortonkeys
.
-
Interactive configuration showing live how many users/groups are found with
the current config and what a selected user/group would look like.
-
Configuration validation for UGM. Add some checks in Ugm.__init__
which
tries to block stupid configuration.
-
Group in group support.
-
Rework ldap testsetup to allow for multiple servers in order to test with
different overlays it would be nice to start different servers or have one
server with multiple databases. whatever feels better.
-
Rework tests and ldifs to target isolated aspects.
-
Potentially multi-valued attrs always as list.
License
Copyright (c) 2006-2021, BlueDynamics Alliance, Austria, Germany, Switzerland
Copyright (c) 2021-2022, Node Contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
-
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
-
Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.