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 - <local: 10.3.9.227:17296 - remote: 209.132.178.99:389> - 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 - <local: 10.3.9.227:27370 - remote: 209.132.178.99:389> - 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 - <local: 10.3.9.227:27438 - remote: 209.132.178.99:389> - 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 - <local: 10.3.9.227:29283 - remote: 209.132.178.99:389> - 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)
<class 'datetime.datetime'>

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)
<class 'int'>
>>> 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.