node.ext.ldap


Namenode.ext.ldap JSON
Version 1.2 PyPI version JSON
download
home_pagehttps://github.com/conestack/node.ext.ldap
SummaryLDAP/AD convenience with Node-trees based on python-ldap
upload_time2022-12-05 12:13:06
maintainer
docs_urlNone
authorNode Contributors
requires_python
licenseSimplified BSD
keywords ldap authentication node tree access users groups
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            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)
------------------

- Minor code cleanup
  [jensens]

- Paginate LDAP node ``__iter__``.
  [jensens, rnix]


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
-----

- Added possibility to hook external LDIF layer for testldap server via
  buildout configuration.
  [rnix]

- Update openldap version in buildout configs.
  [rnix]


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: <key>='<value>'.

- 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.



            

Raw data

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