Package: node.ext.ldap
node.ext.ldapOverviewnode.ext.ldapis a LDAP convenience library for LDAP communication based onpython-ldap(version 2.4 or later)
andnode.The package contains base configuration and communication objects, a LDAP node
object and a LDAP node based user and group management implementation utilizingnode.ext.ugm.This package is the successor ofbda.ldap.Contentsnode.ext.ldapOverviewAPI changes compared to 0.9.xUsageLDAP PropertiesLDAP ConnectionLDAP CommunicationLDAP SessionLDAP NodesSearching LDAPJSON SerializationUser and Group managementCharacter EncodingCaching SupportDependenciesContributorsHistory1.2 (2022-12-05)1.1 (2022-10-06)1.0 (2022-03-19)1.0rc2 (2022-03-01)1.0rc1 (2021-11-08)1.0b12 (2020-05-28)1.0b11 (2019-09-08)1.0b10 (2019-06-30)1.0b9 (2019-05-07)1.0b8 (2018-10-22)1.0b7 (2017-12-15)1.0b6 (2017-10-27)1.0b5 (2017-10-27)1.0b4 (2017-06-07)1.0b3 (2016-10-18)1.0b2 (2016-09-09)1.0b1 (31.12.2015)0.9.70.9.60.9.50.9.40.9.30.9.20.9.10.9TODOLicenseAPI changes compared to 0.9.xLDAPNodeinstances cannot have direct children of subtree any longer.
This was a design flaw because of possible duplicate RDN’s.LDAPNode.searchreturns DN’s instead of RDN’s by default.Secondary keys and alternative key attribute features have been removed
entirely fromLDAPNode.LDAPProps.check_duplicatessetting has been removed.UsageLDAP PropertiesTo define connection properties for LDAP usenode.ext.ldap.LDAPPropsobject:>>>fromnode.ext.ldapimportLDAPProps>>>props=LDAPProps(...uri='ldap://localhost:12345/',...user='cn=Manager,dc=my-domain,dc=com',...password='secret',...cache=False...)Test server connectivity withnode.ext.ldap.testLDAPConnectivity:>>>fromnode.ext.ldapimporttestLDAPConnectivity>>>asserttestLDAPConnectivity(props=props)=='success'LDAP ConnectionFor handling LDAP connections,node.ext.ldap.LDAPConnectoris used. It
expects aLDAPPropsinstance in the constructor. Normally there is no
need to instantiate this object directly, this happens during creation of
higher abstractions, see below:>>>fromnode.ext.ldapimportLDAPConnector>>>importldap>>>connector=LDAPConnector(props=props)Callingbindcreates and returns the LDAP connection:>>>conn=connector.bind()>>>assertisinstance(conn,ldap.ldapobject.ReconnectLDAPObject)Callingunbinddestroys the connection:>>>connector.unbind()LDAP CommunicationFor communicating with an LDAP server,node.ext.ldap.LDAPCommunicatoris
used. It provides all the basic functions needed to search and modify the
directory.LDAPCommunicatorexpects aLDAPConnectorinstance at creation time:>>>fromnode.ext.ldapimportLDAPCommunicator>>>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:>>>importnode.ext.ldap>>>res=communicator.search(...'(objectClass=person)',...node.ext.ldap.SUBTREE...)>>>assertres==[(...'cn=foo,ou=demo,dc=my-domain,dc=com',...{...'objectClass':['person'],...'userPassword':['secret'],...'cn':['foo'],...'sn':['Mustermann']...}...)]Modify directory entry:>>>fromldapimportMOD_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']...)>>>assertres==[('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']...)>>>assertres==[(...'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...)>>>assertres==[]Close connection:>>>communicator.unbind()LDAP SessionA more convenient way for dealing with LDAP is provided bynode.ext.ldap.LDAPSession. It basically provides the same functionality
asLDAPCommunicator, but automatically creates the connectivity objects
and checks the connection state before performing actions.InstantiateLDAPSessionobject. ExpectsLDAPPropsinstance:>>>fromnode.ext.ldapimportLDAPSession>>>session=LDAPSession(props)LDAP session has a convenience to check given properties:>>>res=session.checkServerProperties()>>>assertres==(True,'OK')Set default search DN for session:>>>session.baseDN='ou=demo,dc=my-domain,dc=com'Search in directory:>>>res=session.search()>>>assertres==[...('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')>>>assertresisTrueModify 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']...)>>>assertres==[(...'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)>>>assertres==[]Close session:>>>session.unbind()LDAP NodesOne can deal with LDAP entries as node objects. Therefornode.ext.ldap.LDAPNodeis used. To get a clue of the complete
node API, seenodepackage.Create a LDAP node. The root Node expects the base DN and aLDAPPropsinstance:>>>fromnode.ext.ldapimportLDAPNode>>>root=LDAPNode('ou=demo,dc=my-domain,dc=com',props=props)Every LDAP node has a DN and a RDN:>>>root.DNu'ou=demo,dc=my-domain,dc=com'>>>root.rdn_attru'ou'Check whether created node exists in the database:>>>root.existsTrueDirectory 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']=personIf 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 itschangedflag. If changed isTrueit means either that the node attributes or node
children has changed:>>>root.changedTrue>>>root()>>>root.changedFalseModify 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:>>>delperson.attrs['description']>>>person()Delete LDAP node:>>>delroot['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 LDAPAdd some users and groups we’ll search for:>>>foriinrange(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 ‘|’:>>>fromnode.ext.ldapimportLDAPFilter>>>filter=LDAPFilter('(objectClass=person)')>>>filter|=LDAPFilter('(objectClass=groupOfNames)')>>>res=sorted(root.search(queryFilter=filter))>>>assertres==[...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:>>>fromnode.ext.ldapimportLDAPDictFilter>>>filter=LDAPDictFilter({...'objectClass':['person'],...'cn':'person1'...})>>>res=root.search(queryFilter=filter)>>>assertres==[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’:>>>fromnode.ext.ldapimportLDAPRelationFilter>>>filter=LDAPRelationFilter(root['cn=group1'],'cn:businessCategory')>>>res=root.search(queryFilter=filter)>>>assertres==[...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 byLDAPNode.search. If
multiple keywords are used, combine search criteria with ‘&’ where appropriate.Ifattrlistis given, the result items consists of 2-tuples with a dict
containing requested attributes at position 1:queryFilterEither a LDAP filter instance or a string. If given argument is string type,
aLDAPFilterinstance is created.criteriaA dictionary containing search criteria. ALDAPDictFilterinstance is
created.attrlistList of attribute names to return. Special attributesrdnanddnare allowed.relationEitherLDAPRelationFilterinstance or a string defining the relation.
If given argument is string type, aLDAPRelationFilterinstance is
created.relation_nodeIn combination withrelationargument, when given as string, userelation_nodeinstead of self for filter creation.exact_matchFlag whether 1-length result is expected. Raises an error if empty result
or more than one entry found.or_searchIn combination withcriteria, this parameter is passed to the creation
of LDAPDictFilter. This flag controls whether to combine criteriakeysandvalueswith ‘&’ or ‘|’.or_keysIn combination withcriteria, this parameter is passed to the creation
of LDAPDictFilter. This flag controls whether criteriakeysare
combined with ‘|’ instead of ‘&’.or_valuesIn combination withcriteria, this parameter is passed to the creation
of LDAPDictFilter. This flag controls whether criteriavaluesare
combined with ‘|’ instead of ‘&’.page_sizeUsed in conjunction withcookiefor querying paged results.cookieUsed in conjunction withpage_sizefor querying paged results.get_nodesIfTrueresult containsLDAPNodeinstances instead of DN’sYou can define search defaults on the node which are always considered when
callingsearchon this node. If set, they are always ‘&’ combined with
any (optional) passed filters.Define the default search scope:>>>fromnode.ext.ldapimportSUBTREE>>>root.search_scope=SUBTREEDefine default search filter, could be of type LDAPFilter, LDAPDictFilter,
LDAPRelationFilter or string:>>>root.search_filter=LDAPFilter('objectClass=groupOfNames')>>>res=root.search()>>>assertres==[...u'cn=group1,ou=demo,dc=my-domain,dc=com',...u'cn=group2,ou=demo,dc=my-domain,dc=com'...]>>>root.search_filter=NoneDefine default search criteria as dict:>>>root.search_criteria={'objectClass':'person'}>>>res=root.search()>>>assertres==[...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()>>>assertres==[...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()>>>assertres==[]JSON SerializationSerialize and deserialize LDAP nodes:>>>root=LDAPNode('ou=demo,dc=my-domain,dc=com',props=props)Serialize children:>>>fromnode.serializerimportserialize>>>json_dump=serialize(root.values())Clear and persist root:>>>root.clear()>>>root()Deserialize JSON dump:>>>fromnode.serializerimportdeserialize>>>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)>>>assertserialized==(...'{'...'"__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)>>>assertserialized==(...'{'...'"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 managementLDAP is often used to manage Authentication, thusnode.ext.ldapprovides
an API for User and Group management. The API follows the contract ofnode.ext.ugm:>>>fromnode.ext.ldapimportONELEVEL>>>fromnode.ext.ldap.ugmimportUsersConfig>>>fromnode.ext.ldap.ugmimportGroupsConfig>>>fromnode.ext.ldap.ugmimportRolesConfig>>>fromnode.ext.ldap.ugmimportUgmInstantiate users, groups and roles configuration. They are based onPrincipalsConfigclass and expect this settings:baseDNPrincipals container base DN.attrmapPrincipals Attribute map asodict.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.scopeSearch scope for principals.queryFilterSearch Query filter for principalsobjectClassesObject 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.defaultsDict 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.strictDefine whether all available principal attributes must be declared in attmap,
or only reserved ones. Defaults to True.memberOfSupportFlag whether to use ‘memberOf’ attribute (AD) or memberOf overlay
(openldap) for Group membership resolution where appropriate.Reserved attrmap keys for Users, Groups and roles:idThe attribute containing the user id (mandatory).rdnThe attribute representing the RDN of the node (mandatory)
XXX: get rid of, should be detected automaticallyReserved attrmap keys for Users:loginAlternative 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,...)InstantiateUgmobject:>>>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 onusersrespectivegroupsattribute:>>>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')TrueChange user password:>>>user.passwd('secret','newsecret')>>>user.authenticate('newsecret')TrueGroups 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()>>>assertres==[...u'person1',...u'person2',...u'person3',...u'person4',...u'person5',...u'person99'...]Delete User:>>>delugm.users['person99']>>>ugm.users()>>>res=ugm.users.keys()>>>assertres==[...u'person1',...u'person2',...u'person3',...u'person4',...u'person5'...]Fetch Group:>>>group=ugm.groups['group1']Group members:>>>res=group.member_ids>>>assertres==[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>>>assertmember_ids==[u'person1',u'person2',u'person3']Delete group member:>>>delgroup['person3']>>>member_ids=group.member_ids>>>assertmember_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))>>>assertroles==['editor','viewer']Query roles directly:>>>roles=sorted(user.roles)>>>assertroles==['editor','viewer']Call UGM to persist roles:>>>ugm()Delete role via ugm:>>>ugm.remove_role('viewer',user)>>>roles=user.roles>>>assertroles==['editor']Delete role directly:>>>user.remove_role('editor')>>>roles=user.roles>>>assertroles==[]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))>>>assertroles==['editor','viewer']>>>roles=sorted(group.roles)>>>assertroles==['editor','viewer']>>>ugm()Remove roles:>>>ugm.remove_role('viewer',group)>>>group.remove_role('editor')>>>roles=group.roles>>>assertroles==[]>>>ugm()Character EncodingLDAP (v3 at least,RFC 2251) usesutf-8string encoding only.LDAPNodedoes the encoding for you. Consider it a bug, if you receive
anything else than unicode fromLDAPNode, except attributes configured as
binary. TheLDAPSession,LDAPConnectorandLDAPCommunicatorare
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-patchnode.ext.ldap._node.CHARACTER_ENCODING.Caching Supportnode.ext.ldapcan cache LDAP searches usingbda.cache. You need
to provide a cache factory utility in you application in order to make caching
work. If you don’t,node.ext.ldapfalls back to usebda.cache.NullCache,
which does not cache anything and is just an API placeholder.To provide a cache based onMemcachedinstall memcached server and
configure it. Then you need to provide the factory utility:>>>fromzope.interfaceimportregistry>>>components=registry.Components('comps')>>>fromnode.ext.ldap.cacheimportMemcachedProviderFactory>>>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)Dependenciespython-ldappasslibargparseplumbernodenode.ext.ugmbda.cacheContributorsRobert NiederreiterFlorian FriesdorfJens KleinGeorg BernhardJohannes RaggamAlexander PilzDomen KožarDaniel WiderinAsko SoukkaAlex Milosz SielickiManuel ReinhardtPhilip BauerHistory1.2 (2022-12-05)Implementexpiresandexpiredproperties onnode.ext.ldap.ugm._api.LDAPUseras introduced onnode.ext.ugm.interfaces.IUseras of node.ext.ugm 1.1.
[rnix]Introducenode.ext.ldap.ugm.expires.AccountExpirationand use it for
account expiration management.
[rnix]Removenode.ext.ldap.ugm._api.AccountExpiredsingleton.LDAPUsers.authenticatealways returnsFalseif authentication fails.
[rnix]node >= 1.1 is required bynode.behaviors.suppress_lifecycle_eventssupport
[mamico]Backward compatibility with pas.plugins.ldap <= 1.8.1 where LdapProps does not have
timeout properties.
[mamico]1.1 (2022-10-06)Add propertiesconn_timeoutandop_timeout(both not set by default)
to configureReconnectLDAPObject.
[mamico]Adopt lifecycle related changes fromnode1.1.
[rnix]Moveensure_connectionfromLDAPSessiontoLDAPCommunicatorto
prevent binds on searches that return cached results.
[enfold-josh]1.0 (2022-03-19)Callensure_connectioninLDAPSession.delete.
[rnix]Remove usage ofNodespacesbehavior.
[rnix]Replace deprecated use ofStoragebyMappingStorage.
[rnix]Replace deprecated use ofIStoragebyIMappingStorage.
[rnix]Replace deprecated use ofNodifybyMappingNode.
[rnix]Replace deprecated use ofNodeChildValidatebyMappingConstraints.
[rnix]Replace deprecated use ofAdoptbyMappingAdopt.
[rnix]Replace deprecated use ofallow_non_node_childrenbychild_constraints.
[rnix]1.0rc2 (2022-03-01)Fix #61: Close open connections to LDAP on GC.
[jensens]1.0rc1 (2021-11-08)Rename deprecatedallow_non_node_childstoallow_non_node_childrenonPrincipalAliasedAttributes.
[rnix]Allow to generate MD5 hashes in FIPS enabled environments.
[frapell]Fix DN comparison inLDAPStorage.node_by_dnto ignore case sensitivity.
[rnix]1.0b12 (2020-05-28)Make sureLDAPPrincipals._login_attrhas a value. This wayLDAPUsers.id_for_loginalways returns the principal id as stored in the
database.
[rnix]Improve value comparison inLDAPAttributesBehavior.__setitem__to avoid
unicode warnings.
[rnix]Implementinvalidateonnode.ext.ldap.ugm._api.Ugm.
[rnix]Support for group DNs inmemberOfattribute that are outside of the UGMs configured group.
[jensens]1.0b11 (2019-09-08)Return empty search result list when an LDAP error occurs.
Fixesissue #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 tonode.utils.UNSETor
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]Removesmbpasswddependency. Usepasslibinstead.
[rnix]Usebytes_mode=Falsewhen usingpython-ldap. This is the default
behavior in python 3 and handles everything as unicode/text except
entry attribute values.
For more details seehttps://www.python-ldap.org/en/latest/bytes_mode.html[rnix]Addensure_bytes_py2innode.ext.ldap.base.
[rnix]Renamedecode_utf8toensure_textinnode.ext.ldap.base.
[rnix]Renameencode_utf8toensure_bytesinnode.ext.ldap.base.
[rnix]Python 3 Support.
[rnix, reinhardt]Convert doctests to unittests.
[rnix]1.0b8 (2018-10-22)Useldap.ldapobject.ReconnectLDAPObjectinstead ofSimpleLDAPObjectto create
the connection object. This makes the connection more robust.
Add propertiesretry_max(default 1) andretry_delay(default 10) tonode.ext.ldap.properties.LDAPServerPropertiesto configureReconnectLDAPObject.
[joka]Useexplode_dninLDAPPrincipals.__getitem__to preventKeyErrorif DN contains comma.
[dmunicio]1.0b7 (2017-12-15)Do not catchValueErrorinnode.ext.ldap._node.LDAPStorage.batched_search.
[rnix]Use property decorators fornode.ext.ldap._node.LDAPStorage.changedandnode.ext.ldap.session.LDAPSession.baseDN.
[rnix]Fix signature ofnode.ext.ldap.interfaces.ILDAPStorage.searchto match
the actual implementation innode.ext.ldap._node.LDAPStorage.search.
[rnix]Fix signature ofnode.ext.ldap.ugm.LDAPPrincipals.searchaccording tonode.ext.ugm.interfaces.IPrincipals.search. The implementation exposed
LDAP related arguments and has been renamed toraw_search.
[rnix]Addexistsproperty toLDAPStorage.
[rnix]AddobjectSidandobjectGUIDfrom Active Directory schema toproperties.BINARY_DEFAULTS.
[rnix]Fix default value ofLDAPStorage._multivalued_attributesandLDAPStorage._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 abatched_searchgenerator function, which do the actual batching for us.
Use this function internally too.
[jensens, rnix]In testing set size_limit to 3 inslapd.confin order to catch problems with batching.
[jensens, rnix]Fix missing paging in UGM group mapping methodmember_ids.
[jensens]1.0b2 (2016-09-09)Minor code cleanup
[jensens]Paginate LDAP node__iter__.
[jensens, rnix]1.0b1 (31.12.2015)RemoveILDAPProps.check_duplicatesrespectiveLDAPProps.check_duplicates.
[rnix]rdncan be queried viaattrlistinLDAPNode.searchexplicitely.
[rnix]Introduceget_nodeskeyword argument inLDAPNode.search. When set,
search result containsLDAPNodeinstances instead of DN’s in result.
[rnix]LDAPNode.searchreturns DN’s instead of RDN’s in result. This fixes
searches with scope SUBTREE where result items can potentially contain
duplicate RDN’s.
[rnix]Introducenode_by_dnonLDAPNode.
[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_attrand_rdn_attr.
child.LDAPNode does not provide secondary keys any longer. Removed_seckey_attrs.
[rnix]Deprecatenode.ext.ldap._node.AttributesBehaviorin favor ofnode.ext.ldap._node.LDAPAttributesBehavior.
[rnix]Remove deprecatednode.ext.ldap._node.AttributesPart.
[rnix]Don’t fail onUNWILLING_TO_PERFORMexceptions when authenticating. That
might be thrown, if the LDAP server disallows us to authenticate anadminuser, while we are interested in the localadminuser.
[thet]Addignore_certoption to ignore TLS/SSL certificate errors for self
signed certificates when using theldapsuri schema.
[thet]Housekeeping.
[rnix]0.9.7Added possibility to hook external LDIF layer for testldap server via
buildout configuration.
[rnix]Update openldap version in buildout configs.
[rnix]0.9.6Add new property to allow disablecheck_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.5Addexpiredproperty tonode.ext.ldap.ugm._api.LDAPUser.
[rnix, 2012-12-17]Introducenode.ext.ldap.ugm._api.calculate_expiredhelper function.
[rnix, 2012-12-17]Lookupexpiredattribut from LDAP innode.ext.ldap.ugm._api.LDAPUser.authenticate.
[rnix, 2012-12-17]0.9.4Encode DN innode.ext.ldap._node.LDAPStorage._ldap_modify.
[rnix, 2012-11-08]Encode DN innode.ext.ldap._node.LDAPStorage._ldap_delete.
[rnix, 2012-11-08]Encode DN innode.ext.ldap.ugm._api.LDAPUsers.passwd.
[rnix, 2012-11-08]Encode DN innode.ext.ldap.ugm._api.LDAPUsers.authenticate.
[rnix, 2012-11-07]EncodebaseDNinLDAPPrincipal.member_of_attr.
[rnix, 2012-11-06]EncodebaseDNinAttributesBehavior.load.
[rnix, 2012-11-06]Python 2.7 compatibility.
[rnix, 2012-10-16]PEP-8.
[rnix, 2012-10-16]FixLDAPPrincipals.idbydnhandling UTF-8 DN’s properly.
[rnix, 2012-10-16]Rename parts to behaviors.
[rnix, 2012-07-29]adopt tonode0.9.8.
[rnix, 2012-07-29]Adopt toplumber1.2.
[rnix, 2012-07-29]Do not convert cookie to unicode inLDAPSession.search. Cookie value is
no utf-8 string but octet string as described inhttp://tools.ietf.org/html/rfc2696.html.
[rnix, 2012-07-27]AddUser.group_ids.
[rnix, 2012-07-26]0.9.3Fix schema to not bind to test BaseDN only and make binding deferred.
[jensens, 2012-05-30]0.9.2Removeescape_queriesproperty fromnode.ext.ldap.properties.LDAPProps.
[rnix, 2012-05-18]Usezope.interface.implementerinstead ofzope.interface.implements.
[rnix, 2012-05-18]Structural object classinetOrgPersoninstead ofaccounton 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.1added 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]shadowInactivedefaults to0.
[rnix, 2012-03-06]IntroduceexpiresAttrandexpiresUnitin principals config.
Considered inUsers.authenticate.
[rnix, 2012-02-11]Do not throwKeyErrorif 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]AddLDAPUsers.id_for_login.
[rnix, 2012-01-18]Implement memberOf Support for openldap memberof overlay and AD memberOf
behavior.
[rnix, 2011-11-07]AddLDAPProps.escape_queriesfor 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.9refactor formbda.ldap.
[rnix, chaoflow]TODOConsidersearch_stwith timeout.InvestigateReconnectLDAPObject.set_cache_options.Check/implement silent sort on only the keysLDAPNode.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 inUgm.__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.LicenseCopyright (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.