|
|
BOOK ![]() Mastering OpenLDAP: Configuring, Securing and Integrating Directory Services See More This article mini-series by Matt Butcher will look at the Python application programmers interface (API) for the LDAP libraries, and using this API, we will connect to our OpenLDAP server and manipulate the directory information tree. More specifically, we will cover the following in this article series:
This first part will deal with installation and configuration of the Python-LDAP library. We will then see how the binding operation is performed. See More |
Python LDAP Applications: Part 2 - LDAP Operations
The LDAP Compare OperationOne 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' 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' 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): 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 ) 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 ) 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 ) 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 OperationLDAP 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:
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:
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:
Now we can perform our search in the Python interpreter: >>> import ldap 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 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 ModuleTo 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 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:
This object doesn't add much to the original returned data. It just makes it a little easier to access. Attribute Names The Case Sensitivity GotchaThere 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:
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
For more information, please visit: www.PacktPub.com/OpenLDAP-Developers-Server-Open-Source-Linux/book Getting LDIF Data from the LDAPSearchResult ObjectWhile 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): 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.pyNow 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 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: 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: Now we are ready to look at the main LDAP code for this program. l.bind_s(user_dn, user_pw) 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' 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. Other Search MethodsSo 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' Notice that this time, we used five parameters with the search_st() method:
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: 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: 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:
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' 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 SearchingThere 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 ) 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 PasswordThe 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 The passwd_s() method (and its asynchronous counterpart, passwd()) takes three arguments:
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. Mastering OpenLDAP: Configuring, Securing and Integrating Directory Services
For more information, please visit: www.PacktPub.com/OpenLDAP-Developers-Server-Open-Source-Linux/book About The AuthorMatt Butcher is the principal consultant for Aleph-Null, Inc., a systems integrator that specializes in Free and Open Source solutions.Matt has worked on a wide variety of projects, including embedding Linux in set-top boxes and developing advanced search engines based on artificial intelligence and medical informatics technologies. Matt is involved in several Open Source communities. He is also a member of the Emerging Technologies Lab at Loyola University Chicago, where he is currently finishing a Ph.D. in philosophy. Matt has written two other books for Packt: Managing and Customizing OpenCms 6, and Building Websites with OpenCms. Matt has also contributed articles to Newsforge.com, TheServerSide.com, and LinuxDevices.com.
Tuesday, December 18, 2007 | Open Source
This is the third article in the article mini-series on Python LDAP applications by Matt Butcher. The first part deals with the installation and configuration of Python-LDAP library, and the binding-unbinding operations, and changing of the LDAP password. The second article takes a look at some of LDAP operations. In this article we will see some more LDAP operations such as add operation, delete operation etc. Then we will take a look at LDAP URL Library. See More |
See More |
| ||||||