13 min read

This is the second article in the article mini-series on Python LDAP applications by Matt Butcher. For first part please visit this link.

In this article we will see some of the LDAP operations such as compare operation, search operation. We will also see how to change an LDAP password.

The LDAP Compare Operation

One of the simplest LDAP operations to perform is the compare operation.

The LDAP compare operation takes a DN, an attribute name, and an attribute value and checks the directory to see if the given DN has an attribute with the given attribute name, and the given attribute value. If it returns true then there is a match, and if false then otherwise.

The Python-LDAP API supports LDAP compare operations through the LDAPObject’s compare() and compare_s() functions. The synchronous function is simple. It takes three string parameters (DN, attribute name, and asserted value), and returns 0 for false, and 1 for true:

>>> dn = 'uid=matt,ou=users,dc=example,dc=com'

>>> attr_name = 'sn'

>>> attr_val = 'Butcher'

>>> con.compare_s(dn, attr_name, attr_val)

1

In this case, we check the DN uid=matt,ou=user,dc=example,dc=com to see if the surname (sn) has the value Butcher. It does, so the method returns 1.

But let’s set the attr_val to a different surname, one that the record does not contain:

>>> attr_val = 'Smith'

>>> con.compare_s(dn, attr_name, attr_val)

0

>>>

Since the record identified by the DN uid=matt,ou=users,dc=example,dc=com does not have an SN attribute with the value Smith, this method returns 0, false.

Historically, Python has treated the boolean value False with 0, and numeric values greater than zero as boolean True. So it is possible to use a compare like this:

if con.compare_s(dn, attr_name, attr_val):

print "Match"

else:

print "No match."

If compare_s() returns 1, this will print Match. If it returns 0, it will print No match.

Let’s take a quick look, now, at the asynchronous version of the compare operation, compare(). As we saw in the section on binding, the asynchronous version starts the operation in a new thread, and then immediately returns control to the program, not waiting for the operation to complete. Later, the result of the operation can be examined using the LDAPObject’s result() method.

Running the compare() method is almost identical to the synchronized version, with the difference being the value returned:

>>> retval = con.compare( dn, attr_name, attr_val )

>>> print retval

15

Here, we run a compare() method, storing the identification number for the returned information in the variable retval.

Finding out the value of the returned information is a little trickier than one might guess. Any attempt to retrieve the result of a compare operation using the result() method will raise an exception. But, this is not a sign that the application has encountered an error. Instead, the exception itself indicates whether the compare operation returned true or false. For example, let’s fetch the result for the previous operation in the way we might expect:

>>> print con.result( retval )

Traceback (most recent call last):

File " ", line 1, in

File "/usr/lib/python2.5/site-packages/ldap/ldapobject.py",

line 405, in result

res_type,res_data,res_msgid = self.result2(msgid,all,timeout)

File "/usr/lib/python2.5/site-packages/ldap/ldapobject.py",

line 409, in result2

res_type, res_data, res_msgid, srv_ctrls = self.result3

(msgid,all,timeout)

File "/usr/lib/python2.5/site-packages/ldap/ldapobject.py",

line 415, in result3

rtype, rdata, rmsgid, serverctrls = self._ldap_call

(self._l.result3,msgid,all,timeout)

File "/usr/lib/python2.5/site-packages/ldap/ldapobject.py",

line 94, in _ldap_call

result = func(*args,**kwargs)

ldap.COMPARE_TRUE: {'info': '', 'desc': 'Compare True'}

What is going on here? Attempting to retrieve the value resulted in an exception being thrown. As we can see from the last line, the exception raised was COMPARE_TRUE. Why?

The developers of the Python-LDAP API worked around a difficulty in the standard LDAP C API by providing the results of the compare operation in the form of raised exceptions. Thus, the way to retrieve information from the asynchronous form of compare is with a try/except block:

>>> retval = con.compare( dn, attr_name, attr_val )

>>> try:

... con.result( retval )

...

... except ldap.COMPARE_TRUE:

... print "Returned TRUE."

...

... except ldap.COMPARE_FALSE:

... print "Returned FALSE."

...

Returned TRUE.

In this example, we use the raised exception to determine whether the compare returned true, which raises the COMPARE_TRUE exception, or returned false, which raises COMPARE_FALSE.

Performing compare operations is fairly straightforward, even with the nuances of the asynchronous version. The next operation we will examine is search.

The Search Operation

LDAP servers are intended as high read, low write databases, which means that it is expected that most operations that the server handles will be “read” operations that do not modify the contents of the directory information tree. And the main operation for reading a directory, as we have seen throughout this book, is the LDAP search operation.

As a reminder, the LDAP search operation typically requires five parameters:

The base DN , which indicates where in the directory information tree the search should start.

, which indicates where in the directory information tree the search should start. The scope , which indicates how deeply the search should delve into the directory information tree.

, which indicates how deeply the search should delve into the directory information tree. The search filter , which indicates which entries should be considered matches.

, which indicates which entries should be considered matches. The attribute list , which indicates which attributes of a matching record should be returned.

, which indicates which attributes of a matching record should be returned. A flag indicating whether attribute values should be returned (the Attrs Only flag).

There are other additional parameters, such as time and size limits, and special client or server controls, but those are less frequently used.

Once a search is processed, the server will return a bundle of information including the status of the search, all of the matching records (with the appropriate attributes), and, occasionally, error messages indicate some outstanding condition on the server.

When writing Python-LDAP code to perform searches, we will need to handle all of these issues.

In the Python-LDAP API, there are three (functional) variations of the search function:

search() search_s() search_st()

The first is the asynchronous form, and the second is the synchronous form. The third is a special form of the synchronous form that allows the programmer to add on a hard time limit in which the client must respond.

There are two other versions of the search method, search_ext() and search_ext_s(). These two provide parameter placeholders for passing client and server extension mechanisms, but such extension handling is not yet functional, so neither of these functions is performatively different than the three above.

We will begin by looking at the second method, search_s().

The search_s() function of the LDAPObject has two required parameters (Base DN and scope), and three optional parameters (search filter, attribute list, and the attrs only flag).

Here, we will do a simple search for a list of surnames for all of the users in our directory information tree. For this, we will not need to set the attrs only flag (which is off by default, and, when turned on, will not return the attribute values). But we will need the other four parameters:

Base DN: The users branch, ou=users,dc=example,dc=com

Scope: Subtree (ldap.SCOPE_SUBTREE)

Filter: Any person objects, (objectclass=person)

Attributes: Surname (sn)

Now we can perform our search in the Python interpreter:

>>> import ldap

>>> dn = "uid=matt,ou=users,dc=example,dc=com"

>>> pw = "secret"

>>>

>>> con = ldap.initialize('ldap://localhost')

>>> con.simple_bind_s( dn, pw )

(97, [])

>>>

>>> base_dn = 'ou=users,dc=example,dc=com'

>>> filter = '(objectclass=person)'

>>> attrs = ['sn']>>>

>>> con.search_s( base_dn, ldap.SCOPE_SUBTREE, filter, attrs )

[('uid=matt,ou=Users,dc=example,dc=com', {'sn': ['Butcher']}),

('uid=barbara,ou=Users,dc=example,dc=com', {'sn': ['Jensen']}),

('uid=adam,ou=Users,dc=example,dc=com', {'sn': ['Smith']}),

('uid=dave,ou=Users,dc=example,dc=com', {'sn': ['Hume']}),

('uid=manny,ou=Users,dc=example,dc=com', {'sn': ['Kant']}),

('uid=cicero,ou=Users,dc=example,dc=com', {'sn': ['Tullius']}),

('uid=mary,ou=Users,dc=example,dc=com', {'sn': ['Wollstonecraft']}),

('uid=thomas,ou=Users,dc=example,dc=com', {'sn': ['Hobbes']})]>>>

The first seven lines should look familiar – there is nothing in these lines not covered in the previous sections.

Next, we declare variables for the Base DN (base_dn), filter (filter), and attributes (attrs). While base_dn and filter are strings, attrs requires a list. In our case, it is a list with one member: [‘sn’].

Safe Filters

If you are generating the LDAP filter dynamically (or letting users specify the filter), then you may want to use the escape_filter_chars() and filter_format() functions in the ldap.filter module to keep your filter strings safely escaped.

We don’t need to create a variable for the scope, since all of the available scopes (subtree, base, and onelevel) are available as constants in the ldap module: ldap.SCOPE_SUBTREE, ldap.SCOPE_BASE, and ldap.SCOPE_ONELEVEL.

The line highlighted above shows the search, and the lines following – that big long messy conglomeration of tuples, dicts, and lists – is the result returned from the server.

Strictly speaking, the result returned from search_s() is a list of tuples, where each tuple contains a DN string, and a dict of attributes. Each dict of attributes has a string key (the attribute name), and a list of string values.

While this data structure is compact, it is not particularly easy to work with. For a complex data structure like this, it can be useful to create some wrapper objects to make use of this information a little more intuitive.

The ldaphelper Helper Module

To better work with LDAP results, we will create a simple package with just one class. This will be our ldaphelper module, stored in ldaphelper.py:

import ldif

from StringIO import StringIO

from ldap.cidict import cidict



def get_search_results(results):

"""Given a set of results, return a list of LDAPSearchResult

objects.

"""

res = []

if type(results) == tuple and len(results) == 2 :

(code, arr) = results

elif type(results) == list:

arr = results



if len(results) == 0:

return res



for item in arr:

res.append( LDAPSearchResult(item) )



return res



class LDAPSearchResult:

"""A class to model LDAP results.

"""



dn = ''



def __init__(self, entry_tuple):

"""Create a new LDAPSearchResult object."""

(dn, attrs) = entry_tuple

if dn:

self.dn = dn

else:

return



self.attrs = cidict(attrs)



def get_attributes(self):

"""Get a dictionary of all attributes.

get_attributes()->{'name1':['value1','value2',...],

'name2: [value1...]}

"""

return self.attrs



def set_attributes(self, attr_dict):

"""Set the list of attributes for this record.



The format of the dictionary should be string key, list of

string alues. e.g. {'cn': ['M Butcher','Matt Butcher']}



set_attributes(attr_dictionary)

"""



self.attrs = cidict(attr_dict)



def has_attribute(self, attr_name):

"""Returns true if there is an attribute by this name in the

record.



has_attribute(string attr_name)->boolean

"""

return self.attrs.has_key( attr_name )



def get_attr_values(self, key):

"""Get a list of attribute values.

get_attr_values(string key)->['value1','value2'] """

return self.attrs[key]

def get_attr_names(self):

"""Get a list of attribute names.

get_attr_names()->['name1','name2',...] """

return self.attrs.keys()



def get_dn(self):

"""Get the DN string for the record.

get_dn()->string dn

"""

return self.dn





def pretty_print(self):

"""Create a nice string representation of this object.



pretty_print()->string

"""

str = "DN: " + self.dn + "n"

for a, v_list in self.attrs.iteritems():

str = str + "Name: " + a + "n"

for v in v_list:

str = str + " Value: " + v + "n"

str = str + "========"

return str



def to_ldif(self):

"""Get an LDIF representation of this record.



to_ldif()->string

"""

out = StringIO()

ldif_out = ldif.LDIFWriter(out)

ldif_out.unparse(self.dn, self.attrs)

return out.getvalue()

This is a large chunk of code to take in at once, but the function of it is easy to describe.

Remember, to use a Python module, you must make sure that the module is in the interpreter’s path. See the official Python documentation (http://python.org) for more information.

The package has two main components: the get_search_results() function, and the LDAPSearchResult class.

The get_search_results() function simply takes the results from a search (either the synchronous ones, or the results from an asynchronous one, fetched with result()) and converts the results to a list of LDAPSearchResult objects.

An LDAPSearchResults object provides some convenience methods for getting information about a record. The get_dn() method returns the record’s DN, and the following methods all provide access to the attributes or the record:

get_dn(): return the string DN for this record.

get_attributes(): get a dictionary of all of the attributes. The keys are attribute name strings, and the values are lists of attribute value strings.

set_attributes(): takes a dictionary with attribute names for keys, and lists of attribute values for the value field.

has_attribute(): takes a string attribute name and returns true if that attribute name is in the dict of attributes returned.

get_attr_values(): given an attribute name, this returns all of the values for that attribute (or none if that attribute does not exist).

get_attr_names(): returns a list of all of the attribute names for this record.

pretty_print(): returns a formatted string presentation of the record.

to_ldif(): returns an LDIF formatted representation of the record.

This object doesn’t add much to the original returned data. It just makes it a little easier to access.

Attribute Names

LDAP attributes can have multiple names. The attribute for surnames has two names: surname and sn (though most LDAP directory entries use sn). Either one might be returned by the server. To make your application aware of this difference, you can use the ldap.schema package to get schema information.

The Case Sensitivity Gotcha

There is one noteworthy detail in the code above. The search operation returns the attributes in a dictionary. The Python dictionary is case sensitive; the key TEST is different than the key test.

This exemplifies a minor problem in dealing with LDAP information. Standards-compliant LDAP implementations treat some information in a case-insensitive way. The following items are, as a rule, treated as case-insensitive:

Object class names: inetorgperson is treated as being the same as inetOrgPerson.

Attribute Names: givenName is treated as being the same as givenname.

Distinguished Names: DNs are case-insensitive, though the all-lower-case version of a DN is called Normalized Form.

The main area where this problem surfaces is in retrieving information from a search. Since the attributes are returned in a dict, they are, by default, treated as case-sensitive. For example, attrs.has_key(‘objectclass’) will return False if the object class attribute name is spelled objectClass.

To resolve this problem, the Python-LDAP developers created a case-insensitive dictionary implementation (ldap.cidict.cidict). This cidict class is used above to wrap the returned attribute dictionary.

Make sure you do something similar in your own code, or you may end up with false misses when you look for attributes in a case-sensitive way, e.g. when you look for givenName in an entry where the attribute name is in the form givenname.