Python LDAP Applications: Part 2 - LDAP Opearations

Exclusive offer: get 50% off this eBook here
Mastering OpenLDAP: Configuring, Securing and Integrating Directory Services

Mastering OpenLDAP: Configuring, Securing and Integrating Directory Services — Save 50%

Install, Configure, Build, and Integrate Secure Directory Services with OpenLDAP server in a networked environment

£18.99    £9.50
by Matt Butcher | December 2007 | Architecture & Analysis Linux Servers Open Source

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 "<stdin>", line 1, in <module>
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.
  • The scope, which indicates how deeply the search should delve into the directory information tree.
  • The search filter, which indicates which entries should be considered matches.
  • The attribute list, 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:

  1. search()
  2. search_s()
  3. 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.

Mastering OpenLDAP: Configuring, Securing and Integrating Directory Services Install, Configure, Build, and Integrate Secure Directory Services with OpenLDAP server in a networked environment
Published: August 2007
eBook Price: £18.99
Book Price: £30.99
See more
Select your format and quantity:

Getting LDIF Data from the LDAPSearchResult Object

While most of the methods are simple and straightforward Python code, the to_ldif() method uses another piece of the Python-LDAP API that we have not looked at, yet: the ldif module.

The ldif module provides tools for converting to and from the LDIF format. Here, we use the LDIFWriter object to create LDIF output.

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()

The LDIFWriter always writes to an output stream. Since we want the results as a string, we use the StringIO object (part of the standard Python distribution) to mediate.

Once we create our new LDIFWriter, named ldif_out, we use the unparse() method to take a DN and a dictionary of attributes (of the form {'attribute_name1': ['value1','value2',...]} and convert it to LDIF output.

Once this is done, we can retrieve a string from the StringIO object using the getvalue() method.

A Script to Dump an LDIF Record: dump_record.py

Now we can put together another simple script. This time, we will write one that will retrieve a single record and display it as LDIF data.

Here is the script:

#!/usr/bin/env python

import ldap, ldaphelper, sys, getpass

user_dn = None
user_pw = None
dump_dn = None

server = 'ldap://localhost'
filter = '(objectclass=*)'
attrs = ['*']

usage="""Usage: %s user_dn dn

Log in as user_dn and dump the record for person dn.n""" % sys.argv[0]

if len(sys.argv) != 3:
sys.stderr.write("Error: expected user_dn and dn.nn")
sys.stdout.write(usage)
sys.exit(1)

user_dn = sys.argv[1]
dump_dn = sys.argv[2]
user_pw = getpass.getpass("Password for %s:" % user_dn)
# Add a blank line...
sys.stdout.write("n")

try:
l = ldap.initialize(server)
try:
#l.start_tls_s()
l.bind_s(user_dn, user_pw)

raw_res = l.search_s( dump_dn, ldap.SCOPE_BASE, filter,
attrs )

res = ldaphelper.get_search_results( raw_res )

for record in res:
sys.stdout.write(record.to_ldif())

except ldap.INVALID_CREDENTIALS:
print "Your username or password is incorrect."
sys.exit()
except ldap.LDAPError, e:
if type(e.message) == dict:
for (k, v) in e.message.iteritems():
sys.stderr.write("%s: %sn" % (k, v) )
else:
sys.stderr.write(e.message)
sys.exit(1)

finally:
l.unbind()

Again, the first sixteen lines are boilerplate Python. We import the ldap module, and our own custom ldaphelper module to do the LDAP work. the sys and getpass modules are used for standard interaction with the system. We initialize some variables, and create a helpful usage message.

Next, we need to get some information from the command line:

if len(sys.argv) != 3:
sys.stderr.write("Error: expected user_dn and dn.nn")
sys.stdout.write(usage)
sys.exit(1)

user_dn = sys.argv[1]
dump_dn = sys.argv[2]
user_pw = getpass.getpass("Password for %s:" % user_dn)
# Add a blank line...
sys.stdout.write("n")

If there are too few arguments, usage information gets printed, and the program terminates. Otherwise, the user's DN and the DN for the record to retrieve get stored in their appropriate variables, and the getpass.getpass() function prompts the user to enter a password.

Just as last time, we want to wrap all LDAP code in a try/finally block to make sure that the unbind() method gets called. And we also wrap the LDAP calls inside of a try/except block to catch any exceptions that are raised:

try:
l = ldap.initialize(server)
try:
# Some stuff omitted...

except ldap.INVALID_CREDENTIALS:
print "Your username or password is incorrect."
sys.exit()

except ldap.LDAPError, e:
if type(e.message) == dict:
for (k, v) in e.message.iteritems():
sys.stderr.write("%s: %sn" % (k, v) )
else:
sys.stderr.write(e.message)
sys.exit(1)

finally:
l.unbind()

Now we are ready to look at the main LDAP code for this program.

l.bind_s(user_dn, user_pw)

raw_res = l.search_s( dump_dn, ldap.SCOPE_BASE, filter,
attrs )

res = ldaphelper.get_search_results( raw_res )

for record in res:
sys.stdout.write(record.to_ldif())

All of the LDAP code here is synchronous. First, we bind with the DN and password given by the user.

Next, we perform a search which should retrieve either one or zero records. To do this, we perform a regular search, but set the base DN to the DN of the record we want to retrieve, and then set the search scope to ldap.SCOPE_BASE, which restricts the search scope to only the DN given as the base DN.

Since filter ((objectclass=*)) will match any record, and attrs (['*']) will retrieve all standard (non-operational) attributes, this should match any record, and return all the desired attributes.

We could fetch both standard and operational attributes by setting the filter to ['*','+']

If no records are found that match the search request (and this might happen if there is no record with the DN dump_dn), then an exception is thrown.

An ACL can grant access to a record, but not to any of its attributes. In such a case, this program will complete successfully, but will print no record.

Otherwise, the results (and there should be only one) are converted into LDAPSearchResult objects, and then the to_ldif() method is called on the objects.

What does this look like from the user's perspective? Here's an example.

$ ./dump_record.py 'uid=matt,ou=users,dc=example,dc=com' 
'uid=manny,ou=users,dc=example,dc=com'

Password for uid=matt,ou=users,dc=example,dc=com:

dn: uid=manny,ou=Users,dc=example,dc=com
cn: Manny Kant
givenName: Manny
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
ou: Users
sn: Kant
uid: immanuel
uid: manny

In this example, the user uid=matt,ou=users,dc=example,dc=com is used to log into the directory server and search for the record uid=manny,ou=users,dc=example,dc=com. The user is prompted for uid=matt's password, and then the program connects to the server and retrieves the record.

The entire record is then formatted as LDIF and printed to the standard output.

Matplotlib for Python Developers Build remarkable publication-quality plots the easy way
Published: November 2009
eBook Price: £16.99
Book Price: £27.99
See more
Select your format and quantity:

Other Search Methods

So far, we have been using the search_s() method. But this method has some drawbacks. The other LDAP operations we have looked at, bind and compare, return only a small amount of information. And it is usually expected that these will execute in a short amount of time.

But searches may take a lot longer to execute, either because the server does not return them quickly, or because network latency slows the transmission of the data.

With searching, then, it is more important to manage the connection. One way of doing this is to perform asynchronous searches, and we will look at this in just a moment. But if the only worry is in leaving a connection open and waiting for too long, the search_st() variant may be sufficient to address the concern.

The search_st() method of LDAPObject takes one additional parameter over search_s(): an integer that specifies how long, at most, the client ought to wait for the search operation to complete.

It is used like this:

>>> base_dn = 'ou=users,dc=example,dc=com'
>>> filter = '(objectclass=person)'
>>> attrs = ['cn','uid','mail']
>>> attrs_only = 0
>>> timeout = 3
>>>
>>> r = con.search_st( base_dn, ldap.SCOPE_SUBTREE, filter, attrs,
... attrs_only, timeout )
>>>

Notice that this time, we used five parameters with the search_st() method:

  • Base DN
  • Scope
  • Filter
  • Attribute list
  • The Attrs Only flag (set to false)
  • The timeout

Since we need to use the time out feature, and since we can't pass in None as a value in lieu of the Attrs Only flag, we must pass in either 0/False or 1/True.

With the search_st() method, if the server takes longer than is specified in timeout, then an ldap.TIMEOUT exception is raised, and the search is aborted.

This method can be useful for keeping a simple application from hanging indefinitely. But if you want more control over what the application does while waiting, you may want to use the search() method, an asynchronous version of the LDAP search operation.

One thing we can do with the asynchronous version is poll the connection until the results are completely returned. In between checks, we can notify the user that we are waiting for the server to respond.

A variation of the pertinent portion of our dump_record.py script might look like this:

try:
#l.start_tls_s()
l.bind_s(user_dn, user_pw)

rid = l.search( dump_dn, ldap.SCOPE_BASE, filter, attrs )

raw_res = (None, None)
while raw_res[0] == None:
sys.stdout.write("Polling...n")
raw_res = l.result( rid, True, 0)

res = ldaphelper.get_search_results( raw_res )

for record in res:
sys.stdout.write(record.to_ldif())

except ldap.INVALID_CREDENTIALS:
print "Your username or password is incorrect."
sys.exit()
except ldap.LDAPError, e:
if type(e.message) == dict:
for (k, v) in e.message.iteritems():
sys.stderr.write("%s: %sn" % (k, v) )
else:
sys.stderr.write(e.message)
sys.exit(1)

The highlighted lines above are the modifications for this asynchronous version.

The search() method returns an ID for the results. We can use this ID to get the results.

Next, the results will be in the form of a two-item tuple, with a result code as the first element, and the list of messages as the second item.

We initialize this as (None, None). Then we use a while loop to perform the polling:

while raw_res[0] == None:
sys.stdout.write("Polling...n")
raw_res = l.result( rid, True, 0)

As long as the first element in the raw_res tuple is None, the while loop will do two things.

First, it will print a message to standard output: Polling....

Second, it will poll the results queue to see if the operation has returned. This is done with the result() method – using a few parameters we have not seen before. This version of the result() method has the following three parameters:

  • The ID of the returned response, as returned from search().
  • A flag indicating whether the result() function should return only after all of the items have been fetched (True, in this case). Setting this to False will result in records being fetched one at a time.
  • A timeout, in seconds. If this is set to something greater than 0, then this behaves like search_st(). If it is set to -1 (the default), then it will wait indefinitely. But if it is set to 0, then the library will poll.

So, as we have it configured, result() will check to see if the entire result has been returned. If so, it will return the entire results. If not, it will return (None, None), and return control of the program back to the current thread.

Practically speaking, then, this new bit of code will start the search, and then periodically check on the results, printing Polling... each time it checks until the server retrieves all results. Then, since raw_res[0] will contain something other than None, the program will continue on, printing the results in LDIF format.

Running this variant of our script will create output like this:

$ ./dump_record_async.py 'uid=matt,ou=users,dc=example,dc=com'  
'uid=manny,ou=users,dc=example,dc=com'

Password for uid=matt,ou=users,dc=example,dc=com:

Polling...
Polling...
Polling...
dn: uid=manny,ou=Users,dc=example,dc=com
cn: Manny Kant
givenName: Manny
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
ou: Users
sn: Kant
uid: immanuel
uid: manny

This method of polling makes it possible to keep the application going (and the user notified) even while the application is waiting for a large result to return.

Advanced Asynchronous Searching

There is a design flaw in the script we created above. There is nothing to ever interrupt the polling process. If something goes wrong, the client could continue polling indefinitely.

One thing we could do is code an upper bound to the number of times it polls. This may not be the ideal solution (a better one would be to give the client control of interrupting), but it will serve as an illustration for us.

rid = l.search( dump_dn, ldap.SCOPE_BASE, filter, attrs )

max_poll = 300
poll_num = 0
raw_res = (None, None)
while raw_res[0] == None and poll_num < max_poll:
sys.stdout.write("Polling %d...n" % poll_num)
raw_res = l.result( rid, True, 0)
poll_num = poll_num + 1

if raw_res[0] == None:
l.abandon(rid)
raise ldap.TIMEOUT("The server took too long to respond")

res = ldaphelper.get_search_results( raw_res )

for record in res:
sys.stdout.write(record.to_ldif())

This minor revision of our above script puts an upper limit on the number of times the client will poll before giving up. The limit (which is too low for a production environment) is 300.

For this sort of interrupt, a timer is usually better than counting polling instances. Timers are more predictable.

In this script, if the limit is exceeded, then the abandon() method is called, which terminates the search, and a TIMEOUT exception is thrown.

The Python-LDAP API also has the tools necessary to gain more advanced control of the returned results. By changing the second flag of the search() method to False, you can cause result() to return one record at a time, blocking when it doesn't have a record. The ldap.async package contains a special object, List, that can be used to asynchronously retrieve partial results.

These advanced features are documented in the official Python-LDAP documentation. But we are going to move on and look at some of the operations that can be used for writing to the directory information tree.

Changing an LDAP Password

The next batch of LDAP operations we will examine are those that change the directory information tree – writing operations. We will start with a very simple one, and then move on to the more complex operations.

As we have seen already, the Python-LDAP library has a rich API. It implements a handful of LDAP extensions, such as the LDAP Who Am I Extended Operation. One such operation is the Password Modify Extended Operation (RFC 3062).

Since the Python-LDAP API includes support for the Password Modify Extended Operation, no special encryption and encoding need be done on the client. Changing a password can be done in one call, using either the passwd() method or the passwd_s() method of the LDAPObject class.

>>> import ldap
>>> dn = 'uid=manny,ou=users,dc=example,dc=com'
>>> l = ldap.initialize('ldap://localhost')
>>> l.simple_bind_s( dn, 'secret' )
(97, [])
>>> l.passwd_s( dn, 'secret', 'super_secret' )
(120, [])
>>>

The passwd_s() method (and its asynchronous counterpart, passwd()) takes three arguments:

  • The DN of the record to change.
  • The old password
  • The new password

If the change is successful, it returns a tuple with the status code (ldap.RES_EXTENDED, which is the integer 120), and an empty list.

If an error occurs, an exception will be raised. If, for example, the value of the old password is wrong, the server will raise an UNWILLING_TO_PERFORM exception.

The asynchronous passwd() method takes the same arguments, but returns, as would be expected, a result ID code. The results returned by the result() method are the same: a tuple with RES_EXTENDED as the first item, and an empty list ([]) as the second item.

In our next article we will some more operations and LDAP URL Library.

About the Author :


Matt Butcher

Matt is a web developer and author. He has previously written five other books for Packt, including two others on Drupal. He is a senior developer for the New York Times Company, where he works on ConsumerSearch.com, one of the most traffic-heavy Drupal sites in the world. He is the maintainer of multiple Drupal modules and also heads QueryPath – a jQuery-like PHP library. He blogs occasionally athttp://technosophos.com.

 

Books From Packt

Expert Python Programming
Expert Python Programming

Python Testing: Beginner's Guide
Python Testing: Beginner's Guide

Cacti 0.8 Network Monitoring
Cacti 0.8 Network Monitoring

Django 1.0 Website Development
Django 1.0 Website Development

Grok 1.0 Web Development
Grok 1.0 Web Development

Beginning OpenVPN 2.0.9
Beginning OpenVPN 2.0.9

Linux Email
Linux Email

Asterisk 1.4 – the Professional’s Guide
Asterisk 1.4 – the Professional’s Guide

 

 

Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software