node.ext.ldap
is a LDAP convenience library for LDAP communication based on
python-ldap (version 2.4 or later)
and 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.
This package is the successor of bda.ldap.
Contents
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.
To define connection properties for LDAP use node.ext.ldap.LDAPProps
object:
>>> 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
:
>>> from node.ext.ldap import testLDAPConnectivity
>>> assert testLDAPConnectivity(props=props) == 'success'
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:
>>> from node.ext.ldap import LDAPConnector
>>> import ldap
>>> connector = LDAPConnector(props=props)
Calling bind
creates and returns the LDAP connection:
>>> conn = connector.bind()
>>> assert isinstance(conn, ldap.ldapobject.ReconnectLDAPObject)
Calling unbind
destroys the connection:
>>> connector.unbind()
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:
>>> from node.ext.ldap import LDAPCommunicator
>>> communicator = LDAPCommunicator(connector)
Bind to server:
>>> communicator.bind()
Adding directory entry:
>>> communicator.add(
... 'cn=foo,ou=demo,dc=my-domain,dc=com',
... {
... 'cn': 'foo',
... 'sn': 'Mustermann',
... 'userPassword': 'secret',
... 'objectClass': ['person'],
... }
... )
Set default search DN:
>>> communicator.baseDN = 'ou=demo,dc=my-domain,dc=com'
Search in directory:
>>> 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:
>>> 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:
>>> 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:
>>> communicator.delete('cn=foo,ou=demo,dc=my-domain,dc=com')
>>> res = communicator.search(
... '(objectClass=person)',
... node.ext.ldap.SUBTREE
... )
>>> assert res == []
Close connection:
>>> communicator.unbind()
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:
>>> from node.ext.ldap import LDAPSession
>>> session = LDAPSession(props)
LDAP session has a convenience to check given properties:
>>> res = session.checkServerProperties()
>>> assert res == (True, 'OK')
Set default search DN for session:
>>> session.baseDN = 'ou=demo,dc=my-domain,dc=com'
Search in directory:
>>> res = session.search()
>>> assert res == [
... ('ou=demo,dc=my-domain,dc=com',
... {
... 'objectClass': ['top', 'organizationalUnit'],
... 'ou': ['demo'],
... 'description': ['Demo organizational unit']
... }
... )]
Add directory entry:
>>> 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:
>>> session.passwd('cn=foo,ou=demo,dc=my-domain,dc=com', 'secret', '12345')
Authenticate a specific user:
>>> res = session.authenticate('cn=foo,ou=demo,dc=my-domain,dc=com', '12345')
>>> assert res is True
Modify directory entry:
>>> 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:
>>> session.delete('cn=foo,ou=demo,dc=my-domain,dc=com')
>>> res = session.search('(objectClass=person)', node.ext.ldap.SUBTREE)
>>> assert res == []
Close session:
>>> session.unbind()
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 package.
Create a LDAP node. The root Node expects the base DN and a LDAPProps
instance:
>>> 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:
>>> root.DN
u'ou=demo,dc=my-domain,dc=com'
>>> root.rdn_attr
u'ou'
Check whether created node exists in the database:
>>> root.exists
True
Directory entry has no children yet:
>>> root.keys()
[]
Add children to root node:
>>> 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:
>>> person.attrs['cn']
u'person2'
Fetch children DN by key from LDAP node:
>>> root.child_dn('cn=person1')
u'cn=person1,ou=demo,dc=my-domain,dc=com'
Have a look at the tree:
>>> 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:
>>> root.changed
True
>>> root()
>>> root.changed
False
Modify a LDAP node:
>>> person = root['cn=person1']
Modify existing attribute:
>>> person.attrs['sn'] = 'Mustermensch'
Add new attribute:
>>> person.attrs['description'] = 'Mustermensch description'
>>> person()
Delete an attribute:
>>> del person.attrs['description']
>>> person()
Delete LDAP node:
>>> 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>
Add some users and groups we'll search for:
>>> 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 '|':
>>> 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:
>>> 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':
>>> 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:
>>> 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
anddn
are allowed. - relation
- Either
LDAPRelationFilter
instance or a string defining the relation. If given argument is string type, aLDAPRelationFilter
instance is created. - relation_node
- In combination with
relation
argument, when given as string, userelation_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 containsLDAPNode
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:
>>> from node.ext.ldap import SUBTREE
>>> root.search_scope = SUBTREE
Define default search filter, could be of type LDAPFilter, LDAPDictFilter, LDAPRelationFilter or string:
>>> 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:
>>> 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:
>>> 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:
# empty result, there are no groups with group 'cn' as 'description'
>>> root.search_criteria = {'objectClass': 'group'}
>>> res = root.search()
>>> assert res == []
Serialize and deserialize LDAP nodes:
>>> root = LDAPNode('ou=demo,dc=my-domain,dc=com', props=props)
Serialize children:
>>> from node.serializer import serialize
>>> json_dump = serialize(root.values())
Clear and persist root:
>>> root.clear()
>>> root()
Deserialize JSON dump:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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"'
... '}'
... '}]'
... '}'
... )
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:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> ugm.keys()
['users', 'groups']
>>> ugm.users
<Users object 'users' at ...>
>>> ugm.groups
<Groups object 'groups' at ...>
Fetch user:
>>> user = ugm.users['person1']
>>> user
<User object 'person1' at ...>
User attributes. Reserved keys are available on user attributes:
>>> user.attrs['id']
u'person1'
>>> user.attrs['login']
u'Mustermensch'
'login' maps to 'sn':
>>> user.attrs['sn']
u'Mustermensch'
>>> user.attrs['login'] = u'Mustermensch1'
>>> user.attrs['sn']
u'Mustermensch1'
>>> user.attrs['description'] = 'Some description'
>>> user()
Check user credentials:
>>> user.authenticate('secret')
True
Change user password:
>>> user.passwd('secret', 'newsecret')
>>> user.authenticate('newsecret')
True
Groups user is member of:
>>> user.groups
[<Group object 'group1' at ...>]
Add new User:
>>> 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:
>>> del ugm.users['person99']
>>> ugm.users()
>>> res = ugm.users.keys()
>>> assert res == [
... u'person1',
... u'person2',
... u'person3',
... u'person4',
... u'person5'
... ]
Fetch Group:
>>> group = ugm.groups['group1']
Group members:
>>> res = group.member_ids
>>> assert res == [u'person1', u'person2']
>>> group.users
[<User object 'person1' at ...>, <User object 'person2' at ...>]
Add group member:
>>> group.add('person3')
>>> member_ids = group.member_ids
>>> assert member_ids == [u'person1', u'person2', u'person3']
Delete group member:
>>> 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:
>>> user = ugm.users['person1']
Add role for user via ugm:
>>> ugm.add_role('viewer', user)
Add role for user directly:
>>> user.add_role('editor')
Query roles for user via ugm:
>>> roles = sorted(ugm.roles(user))
>>> assert roles == ['editor', 'viewer']
Query roles directly:
>>> roles = sorted(user.roles)
>>> assert roles == ['editor', 'viewer']
Call UGM to persist roles:
>>> ugm()
Delete role via ugm:
>>> ugm.remove_role('viewer', user)
>>> roles = user.roles
>>> assert roles == ['editor']
Delete role directly:
>>> user.remove_role('editor')
>>> roles = user.roles
>>> assert roles == []
Call UGM to persist roles:
>>> ugm()
Same with group. Fetch a group:
>>> group = ugm.groups['group1']
Add roles:
>>> 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:
>>> ugm.remove_role('viewer', group)
>>> group.remove_role('editor')
>>> roles = group.roles
>>> assert roles == []
>>> ugm()
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
.
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:
>>> 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:
>>> components = registry.Components('comps')
>>> cache_factory = MemcachedProviderFactory(servers=[
... '10.0.0.10:22122',
... '10.0.0.11:22322'
... ])
>>> components.registerUtility(cache_factory)
- python-ldap
- passlib
- argparse
- plumber
- node
- node.ext.ugm
- bda.cache
- 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