Using LDAP Paged Controls with Python

Most LDAP servers can be set to return an unlimited number of entries on an LDAP search, however depending on the size of the LDAP database/directory this can possibly exceed your memory. Moreover if you want to write portable code, you probably should not depend on the LDAP server being able to return unlimited entries. For instance, AD’s LDAP generally defaults to 1,000 entries maximum.

Because using LDAP paging isn’t very difficult there’s not a lot of reason to not use it. Adding paging only marginally reduces performance, while certainly putting less stress on the LDAP server(s). Personally I recommend you use it on a general basis, even where not strictly necessary.

Python’s LDAP supports paging, though it isn’t well documented. I found two examples this one and this one. Both had their pluses, but neither explained what was going on too much. I melded them together, added comments, and streamlined a bit. Hopefully this will help you get the mojo…

As a final note, one of the documents I found said the paged controls did not work with OpenLDAP. That’s not what I found – pretty much the exact code above worked without issue with OpenLDAP.

UPDATE:

A GitHub “Gist” for the above can be found here.

UPDATE 2:

For users of Python LDAP 2.4, you should check out of the comment by Ilya Rumyantsev which gives a forward/backward compatible set of code snippets since the API has changed a bit. Many thanks to Ilya for the update.

UPDATE 3:

Below I took Ilya’s updates and merged them in with some minor enhancements to compare the Python LDAP version on the fly. My next stop is to take this and convert it to a generator function, which would be more ideal than using a callback. The issue with going to a generator is handling the errors, which means throwing exceptions in some sane fashion…

UPDATE 4:

It turns out that the Python “ldap” module does not follow “StrictVersion” versioning in it’s “__version__” string. I have updated the “UPDATE 3” code to use “LooseVersion” comparisons.

UPDATE 5:

I updated the above code to default to “criticality=False” for the paging control. If the LDAP service doesn’t support paging, it will yield a potentially confusing “Critical extension is unavailable” error.

Note I need to ultimately fix the exception handling as for whatever reason the exception object passed back doesn’t have a reasonable “__str__()” method and the message is left in the “desc” key.

Comments

  1. Ilya Avatar
    Ilya

    Hi thanks for the nice snippet. This only works for python-ldap<2.4 (as there were some api changes). I wrote some snippets which would work with both, maybe you can use it:

    First of all: the version independent-part:

            lc = create_controls(pagesize)
            while True:
    
                result = self.con.search_ext(root_dn, ldap.SCOPE_SUBTREE,
                                             filter_string,
                                             serverctrls=[lc])
                rtype, rdata, rmsgid, sctrls = self.con.result3(result)
                self.result.extend([LdapObject(el) for el in rdata
                                    if not el[0] is None])
                
                pctrls = get_pctrls(sctrls)
                if not pctrls:
                    print >> sys.stderr, 'Warning: Server ignores RFC 2696 control.'
                    break
    
                if not set_cookie(lc, pctrls, pagesize):
                    break
            return self.result
    

    Now the functions which depend on the library:

        def create_controls(pagesize):
            if LDAP_VERSION >= '2.4':
                return SimplePagedResultsControl(True, size=pagesize,
                                                 cookie='')
            else:
                return SimplePagedResultsControl(ldap.LDAP_CONTROL_PAGE_OID,
                                                 True,
                                                 (pagesize,''))
    					     
        def get_pctrls(serverctrls):
            if LDAP_VERSION >= 2.4:
                return [c for c in serverctrls
                        if c.controlType==SimplePagedResultsControl.controlType]
            else:
                return [c for c in serverctrls
                        if c.controlType == ldap.LDAP_CONTROL_PAGE_OID]
                
        def set_cookie(lc_object, pctrls, pagesize):
            if LDAP_VERSION >= '2.4':
                cookie = pctrls[0].cookie
                lc_object.cookie = cookie
                return cookie
            else:
                est, cookie = pctrls[0].controlValue
                lc_object.controlValue = (pagesize, cookie)
                return cookie
    
    

    Hope, it is helpful as I had some troubles finding a good documentation (as you already mentioned)

  2. Jean Avatar
    Jean

    Thanks for sharing. As a new comer to python and python-ldap and seeking for help, you made my day 🙂

  3. gv Avatar
    gv

    Thanks – I had to write my first python script which needed to read from LDAP and write to MSSQL and I basically used this script as-is 🙂

  4. Zane Zakraisek Avatar
    Zane Zakraisek

    Thank you very very much. Best implementation I’ve seen so far.

  5. BPeters504 Avatar
    BPeters504

    Very helpful! The code is so clean and readable, it made it so easy to understand and implement myself. Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *