Tutorial: ldap3 Abstraction Layer - Reading data ################################################ Reading entries --------------- Let's define a Reader cursor to get all the entries of class 'inetOrgPerson' in the 'ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org' context:: >>> obj_inetorgperson = ObjectDef('inetOrgPerson', conn) >>> r = Reader(conn, obj_inetorgperson, 'ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org') >>> r CURSOR : Reader CONN : ldap://ipa.demo1.freeipa.org:389 - cleartext - user: uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - not lazy - bound - open - - tls not started - listening - SyncStrategy - internal decoder DEFS : ['inetOrgPerson'] [audio, businessCategory, carLicense, cn, departmentNumber, description, destinationIndicator, displayName, employeeNumber, employeeType, facsimileTelephoneNumber, givenName, homePhone, homePostalAddress, initials, internationalISDNNumber, jpegPhoto, l, labeledURI, mail, manager, mobile, o, objectClass, ou, pager, photo, physicalDeliveryOfficeName, postOfficeBox, postalAddress, postalCode, preferredDeliveryMethod, preferredLanguage, registeredAddress, roomNumber, secretary, seeAlso, sn, st, street, telephoneNumber, teletexTerminalIdentifier, telexNumber, title, uid, userCertificate, userPKCS12, userPassword, userSMIMECertificate, x121Address, x500UniqueIdentifier] ATTRS : ['audio', 'businessCategory', 'carLicense', 'cn', 'departmentNumber', 'description', 'destinationIndicator', 'displayName', 'employeeNumber', 'employeeType', 'facsimileTelephoneNumber', 'givenName', 'homePhone', 'homePostalAddress', 'initials', 'internationalISDNNumber', 'jpegPhoto', 'l', 'labeledURI', 'mail', 'manager', 'mobile', 'o', 'objectClass', 'ou', 'pager', 'photo', 'physicalDeliveryOfficeName', 'postOfficeBox', 'postalAddress', 'postalCode', 'preferredDeliveryMethod', 'preferredLanguage', 'registeredAddress', 'roomNumber', 'secretary', 'seeAlso', 'sn', 'st', 'street', 'telephoneNumber', 'teletexTerminalIdentifier', 'telexNumber', 'title', 'uid', 'userCertificate', 'userPKCS12', 'userPassword', 'userSMIMECertificate', 'x121Address', 'x500UniqueIdentifier'] BASE : 'ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org' [SUB] FILTER : '(objectClass=inetOrgPerson)' We didn't provide any filter, but the Reader automatically uses the ObjectDef class to read entries of the requested object class. Now you can ask the Reader to execute the search, fetching the results in its ``entries`` property:: >>> r.search() >>> r CURSOR : Reader CONN : ldap://ipa.demo1.freeipa.org:389 - cleartext - user: uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - not lazy - bound - open - - tls not started - listening - SyncStrategy - internal decoder DEFS : ['inetOrgPerson'] [audio, businessCategory, carLicense, cn, departmentNumber, description, destinationIndicator, displayName, employeeNumber, employeeType, facsimileTelephoneNumber, givenName, homePhone, homePostalAddress, initials, internationalISDNNumber, jpegPhoto, l, labeledURI, mail, manager, mobile, o, objectClass, ou, pager, photo, physicalDeliveryOfficeName, postOfficeBox, postalAddress, postalCode, preferredDeliveryMethod, preferredLanguage, registeredAddress, roomNumber, secretary, seeAlso, sn, st, street, telephoneNumber, teletexTerminalIdentifier, telexNumber, title, uid, userCertificate, userPKCS12, userPassword, userSMIMECertificate, x121Address, x500UniqueIdentifier] ATTRS : ['audio', 'businessCategory', 'carLicense', 'cn', 'departmentNumber', 'description', 'destinationIndicator', 'displayName', 'employeeNumber', 'employeeType', 'facsimileTelephoneNumber', 'givenName', 'homePhone', 'homePostalAddress', 'initials', 'internationalISDNNumber', 'jpegPhoto', 'l', 'labeledURI', 'mail', 'manager', 'mobile', 'o', 'objectClass', 'ou', 'pager', 'photo', 'physicalDeliveryOfficeName', 'postOfficeBox', 'postalAddress', 'postalCode', 'preferredDeliveryMethod', 'preferredLanguage', 'registeredAddress', 'roomNumber', 'secretary', 'seeAlso', 'sn', 'st', 'street', 'telephoneNumber', 'teletexTerminalIdentifier', 'telexNumber', 'title', 'uid', 'userCertificate', 'userPKCS12', 'userPassword', 'userSMIMECertificate', 'x121Address', 'x500UniqueIdentifier'] BASE : 'ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org' [SUB] FILTER : '(objectClass=inetOrgPerson)' ENTRIES: 3 [executed at: 2016-11-09T09:33:00.342762] There are now three Entries in the Reader. An Entry has some interesting features accessible from its properties and methods. Because Attribute names are used as Entry properties, all the "operational" properties and methods of an Entry start with the **entry_** prefix (the underscore is an invalid character in an attribute name, so there can't be an attribute with that name). It's easy to get a useful representation of an Entry:: >>> r[0] DN: cn=b.young,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2016-11-09T09:35:02.739203 cn: b.young departmentNumber: DEV givenName: Beatrix objectClass: inetOrgPerson organizationalPerson person top sn: Young telephoneNumber: 1111 Let's explore some of them:: >>> # get the DN of an entry >>> r[0].entry_dn 'cn=b.young,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org' >>> # query the attributes in the Entry as a list of names >>> r[0].entry_attributes ['destinationIndicator', 'x500UniqueIdentifier', 'audio', 'photo', 'uid', 'l', 'pager', 'carLicense', 'street', 'teletexTerminalIdentifier', 'o', 'st', 'homePostalAddress', 'preferredDeliveryMethod', 'roomNumber', 'sn', 'homePhone', 'x121Address', 'displayName', 'userSMIMECertificate', 'userPassword', 'title', 'physicalDeliveryOfficeName', 'mail', 'initials', 'ou', 'businessCategory', 'seeAlso', 'jpegPhoto', 'registeredAddress', 'facsimileTelephoneNumber', 'postalAddress', 'telephoneNumber', 'mobile', 'labeledURI', 'postalCode', 'objectClass', 'employeeNumber', 'secretary', 'employeeType', 'description', 'cn', 'userCertificate', 'userPKCS12', 'postOfficeBox', 'departmentNumber', 'givenName', 'internationalISDNNumber', 'preferredLanguage', 'telexNumber', 'manager'] >>> # query the attributes in the Entry as a dict of key/value pairs >>> r[0].entry_attributes_as_dict {'destinationIndicator': [], 'x500UniqueIdentifier': [], 'audio': [], 'photo': [], 'uid': [], 'l': [], 'pager': [], 'carLicense': [], 'street': [], 'teletexTerminalIdentifier': [], 'o': [], 'homePostalAddress': [], 'preferredDeliveryMethod': [], 'roomNumber': [], 'st': [], 'homePhone': [], 'x121Address': [], 'displayName': [], 'userSMIMECertificate': [], 'userPassword': [], 'title': [], 'physicalDeliveryOfficeName': [], 'mail': [], 'preferredLanguage': [], 'initials': [], 'internationalISDNNumber': [], 'ou': [], 'businessCategory': [], 'seeAlso': [], 'jpegPhoto': [], 'registeredAddress': [], 'facsimileTelephoneNumber': [], 'postalAddress': [], 'telephoneNumber': ['1111'], 'mobile': [], 'labeledURI': [], 'postalCode': [], 'objectClass': ['inetOrgPerson', 'organizationalPerson', 'person', 'top'], 'employeeNumber': [], 'description': [], 'employeeType': [], 'secretary': [], 'cn': ['b.young'], 'userPKCS12': [], 'postOfficeBox': [], 'departmentNumber': ['DEV'], 'givenName': ['Beatrix'], 'sn': ['Young'], 'userCertificate': [], 'telexNumber': [], 'manager': []} >>> # let's check which attributes are mandatory >>> r[0].entry_mandatory_attributes ['sn', 'objectClass', 'cn'] >>> # convert the Entry to LDIF >>> print(r[0].entry_to_ldif()) version: 1 dn: cn=b.young,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org objectClass: inetOrgPerson objectClass: organizationalPerson objectClass: person objectClass: top sn: Young telephoneNumber: 1111 cn: b.young departmentNumber: DEV givenName: Beatrix # total number of entries: 1 >>> print(r[0].entry_to_json(include_empty=False)) # Use include_empty=True to include empty attributes { "attributes": { "cn": [ "b.young" ], "departmentNumber": [ "DEV" ], "givenName": [ "Beatrix" ], "objectClass": [ "inetOrgPerson", "organizationalPerson", "person", "top" ], "sn": [ "Young" ], "telephoneNumber": [ "1111" ] }, "dn": "cn=b.young,ou=ldap3-tutorial,dc=demo1,dc=freeipa,dc=org" } If you search for the uid=admin entry, there are some auxiliary classes attached to it. The uid=admin entry is not an *inetOrgPerson* but a *person*, so you must use the ``obj_person`` defined in the previous chapter of this tutorial:: >>> obj_person OBJ : person [person (Structural) 2.5.6.6, top (Abstract) 2.5.6.0] MUST: cn, objectClass, sn MAY : description, seeAlso, telephoneNumber, userPassword This ObjectDef lacks the *uid* attribute, used for naming the admin entry, so we must add it to the Object definition: >>> obj_person += 'uid' # implicitly creates a new AttrDef >>> obj_person OBJ : person [person (Structural) 2.5.6.6, top (Abstract) 2.5.6.0] MUST: cn, objectClass, sn MAY : description, seeAlso, telephoneNumber, uid, userPassword Now let's build the Reader cursor, using the Simplified Query Language; note how the filter is converted: >>> r = Reader(conn, obj_person, 'cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org', 'uid:=admin') >>> r CURSOR : Reader CONN : ldap://ipa.demo1.freeipa.org:389 - cleartext - user: uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - not lazy - bound - open - - tls not started - listening - SyncStrategy - internal decoder DEFS : ['person'] [cn, description, objectClass, seeAlso, sn, telephoneNumber, uid, userPassword] ATTRS : ['cn', 'description', 'objectClass', 'seeAlso', 'sn', 'telephoneNumber', 'uid', 'userPassword'] BASE : 'cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org' [SUB] QUERY : 'uid:=admin' [AND] PARSED : 'uid: =admin' [AND] FILTER : '(&(objectClass=person)(uid=admin))' And finally perform the search operation:: >>> r.search() [DN: uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2016-11-09T09:59:56.393112 cn: Administrator objectClass: top person posixaccount krbprincipalaux krbticketpolicyaux inetuser ipaobject ipasshuser ipaSshGroupOfPubKeys ipaNTUserAttrs sn: Administrator uid: admin] Only one entry is found. As you can see this Entry has additional auxiliary object classes attached. This means that there can be other attributes stored in the entry. Let's define an ObjectDef that also requests the 'posixAccount' and the 'krbprincipalaux' object classes:: >>> obj_person = ObjectDef(['person', 'posixAccount', 'krbprincipalaux'], conn) OBJ : person, posixAccount, krbPrincipalAux [person (Structural) 2.5.6.6, top (Abstract) 2.5.6.0, posixAccount (Auxiliary) 1.3.6.1.1.1.2.0, top (Abstract) 2.5.6.0, krbPrincipalAux (Auxiliary) 2.16.840.1.113719.1.301.6.8.1] MUST: cn, gidNumber, homeDirectory, objectClass, sn, uid, uidNumber MAY : description, gecos, krbAllowedToDelegateTo, krbCanonicalName, krbExtraData, krbLastAdminUnlock, krbLastFailedAuth, krbLastPwdChange, krbLastSuccessfulAuth, krbLoginFailedCount, krbPasswordExpiration, krbPrincipalAliases, krbPrincipalAuthInd, krbPrincipalExpiration, krbPrincipalKey, krbPrincipalName, krbPrincipalType, krbPwdHistory, krbPwdPolicyReference, krbTicketPolicyReference, krbUPEnabled, loginShell, seeAlso, telephoneNumber, userPassword As you can see, the ObjectDef now includes all Attributes from the *person*, *top*, *posixAccount* and *krbPrincipalAux* classes. Now create a new Reader, and you will see that its filter will automatically include all the requested object classes:: >>> r = Reader(conn, obj_person, 'dc=demo1,dc=freeipa,dc=org', 'uid:=admin') >>> r CURSOR : Reader CONN : ldap://ipa.demo1.freeipa.org:389 - cleartext - user: uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - not lazy - bound - open - - tls not started - listening - SyncStrategy - internal decoder DEFS : ['person', 'posixAccount', 'krbPrincipalAux'] [cn, description, gecos, gidNumber, homeDirectory, krbAllowedToDelegateTo, krbCanonicalName, krbExtraData, krbLastAdminUnlock, krbLastFailedAuth, krbLastPwdChange, krbLastSuccessfulAuth, krbLoginFailedCount, krbPasswordExpiration, krbPrincipalAliases, krbPrincipalAuthInd, krbPrincipalExpiration, krbPrincipalKey, krbPrincipalName, krbPrincipalType, krbPwdHistory, krbPwdPolicyReference, krbTicketPolicyReference, krbUPEnabled, loginShell, objectClass, seeAlso, sn, telephoneNumber, uid, uidNumber, userPassword] ATTRS : ['cn', 'description', 'gecos', 'gidNumber', 'homeDirectory', 'krbAllowedToDelegateTo', 'krbCanonicalName', 'krbExtraData', 'krbLastAdminUnlock', 'krbLastFailedAuth', 'krbLastPwdChange', 'krbLastSuccessfulAuth', 'krbLoginFailedCount', 'krbPasswordExpiration', 'krbPrincipalAliases', 'krbPrincipalAuthInd', 'krbPrincipalExpiration', 'krbPrincipalKey', 'krbPrincipalName', 'krbPrincipalType', 'krbPwdHistory', 'krbPwdPolicyReference', 'krbTicketPolicyReference', 'krbUPEnabled', 'loginShell', 'objectClass', 'seeAlso', 'sn', 'telephoneNumber', 'uid', 'uidNumber', 'userPassword'] BASE : 'dc=demo1,dc=freeipa,dc=org' [SUB] QUERY : 'uid:=admin' [AND] PARSED : 'uid: =admin' [AND] FILTER : '(&(&(objectClass=person)(objectClass=posixAccount)(objectClass=krbPrincipalAux))(uid=admin))' >>> r.search() >>> r[0] DN: uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org - STATUS: Read - READ TIME: 2016-11-09T10:03:47.741382 cn: Administrator gecos: Administrator gidNumber: 1120000000 homeDirectory: /home/admin krbExtraData: b'\x00\x02\xd2\xad"Xroot/admin@DEMO1.FREEIPA.ORG\x00' krbLastFailedAuth: 2016-11-09 06:22:15+00:00 krbLastPwdChange: 2016-11-09 05:02:10+00:00 krbLastSuccessfulAuth: 2016-11-09 09:03:49+00:00 krbLoginFailedCount: 0 krbPasswordExpiration: 2017-11-09 05:02:10+00:00 krbPrincipalName: admin@DEMO1.FREEIPA.ORG loginShell: /bin/bash objectClass: top person posixaccount krbprincipalaux krbticketpolicyaux inetuser ipaobject ipasshuser ipaSshGroupOfPubKeys ipaNTUserAttrs sn: Administrator uid: admin uidNumber: 1120000000 Note that attributes are properly formatted thanks to the information read from the server schema. For example, the krbLastPwdChange is stored as a date (Generalized Time, a standard LDAP data type):: >>> obj_person.krblastpwdchange ATTR: krbLastPwdChange - mandatory: False - single_value: True Attribute type: 2.16.840.1.113719.1.301.4.45.1 Short name: krbLastPwdChange Single value: True Equality rule: generalizedTimeMatch Syntax: 1.3.6.1.4.1.1466.115.121.1.24 [('1.3.6.1.4.1.1466.115.121.1.24', 'LDAP_SYNTAX', 'Generalized Time', 'RFC4517')] Optional in: krbPrincipalAux So the ldap3 library returns it as a DateTime object (with time zone info):: >>> type(r[0].krblastpwdchange.value) .. warning:: The ldap3 library returns dates with time tone info. These dates can be compared only with dates including time zones. You can't compare them with a "naive" date object. .. note:: Attributes have three properties for getting their values: the ``values`` property returns always a list containing all values, even in a single-valued attribute; the ``value`` property returns the very same list in a multi-valued attribute or the value in a single-valued attribute. ``raw_attributes`` always returns a list of the binary values received in the LDAP response. When the schema is available, the ``values`` and ``value`` properties are properly formatted as standard Python types. You can add additional custom formatters with the ``formatter`` parameter of the Server object. If you look at the raw data read from the server, you get the values actually stored in the DIT:: >>> r[0].krblastpwdchange.raw_values [b'20161109050210Z'] Similar formatting is applied to other well-known attribute types, for example GUID or SID in Active Directory. Numbers are returned as ``int``:: >>> e[0].krbloginfailedcount.value krbLoginFailedCount: 0 >>> type(e[0].krbloginfailedcount.value) >>> e[0].krbloginfailedcount.raw_values [b'0'] Search scope ------------ By default the Reader searches the whole sub-tree starting from the specified base. If you want to search entries only in the base, you can pass the ``sub_tree=False`` parameter in the Reader definition. You can also override the default scope with the ``search_level()``, ``search_object()`` and ``search_subtree()`` methods of the Reader object:: >>> r.search_level() # search only at the 'dc=demo1,dc=freeipa,dc=org' context >>> print(len(r)) # the admin entry in in the cn=users,cn=account container, so no entry is found 0 >>> r.search_subtree() # search walking down from the 'dc=demo1,dc=freeipa,dc=org' context >>> print(len(r)) 1 Matching entries in cursor results ---------------------------------- Once a cursor is populated with entries, you can get a specific entry with the standard index feature of List object: ``r.entries[0]`` returns the first entry found, ``r.entries[1]`` returns the second one and any subsequent entry is returned by the relevant index number. The Cursor object has a shortcut for this operation: you can use ``r[0]``, ``r[1]`` (and so on) to perform the same operation. Furthermore, the Cursor object has a useful feature that helps you to find a specific entry without knowing its index: when you use a string as the Cursor index, the text will be searched in all entry DNs. If only one entry matches, it will be returned, but if there is more than one match, a KeyError exception is raised. You can also use the ``r.match_dn(dn)`` method to return all entries with the specified text in the DN and ``r.match(attributes, value)`` to return all entries that contain the ``value`` in any of the specified ``attributes`` where you can pass a single attribute name or a list of attribute names. When searching for values, both the formatted attribute and the raw value are checked.