Python LDAP Applications: Part 4 - LDAP Schema

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

$29.99    $15.00
by Matt Butcher | December 2007 | Architecture & Analysis Linux Servers Open Source

Welcome to the fourth and the last article in the Python LDAP applications series by Matt Butcher. In previous three articles we have seen the installation and configuration of Python-LDAP library, and the binding-unbinding operations, and changing of the LDAP password as well as LDAP operations, and LDAP URL library and some more LDAP operations.

 

In this article, we will take a brief look at what might be the most complex module in the Python-LDAP API, the ldap.schema module.

Using Schema Information

As with most LDAP servers, OpenLDAP provides schema access to LDAP clients. An LDAP schema defines object classes, attributes, matching rules, and other LDAP structures.

In this article, we will take a brief look at what might be the most complex module in the Python-LDAP API, the ldap.schema module.

This module provides programmatic access to the schema, using the LDAP subschema record, and the subschema's subentries obtained from the LDAP server.

The module has two major components. The first is the SubSchema object, which contains the schema definition, and provides numerous functions for navigating through the definitions stored in the schema.

The second component is the model, which contains classes that describe structural components (Schema Elements) of the schema. For example, the model contains classes like ObjectClass, AttributeType, MatchingRule, and DITContentRule.

Getting the Schema from the LDAP Server

The ldap.schema module does not automatically retrieve the schema information. It must be fetched from the server with an LDAP search operation. The schema is always stored in a specific entry, almost always accessible with the DN cn=subschema. (If it is elsewhere, and is accessible, the Root DSE will note the location.) We can retrieve the record by doing a search with a base scope:

>>> res = l.search_s('cn=subschema',
... ldap.SCOPE_BASE,
... '(objectclass=*)',
... ['*','+']
... )
>>> subschema_entry = ldaphelper.get_search_results(res)[0]
>>> subschema_subentry = subschema_entry.get_attributes()
>>>

The search configuration above should return only one record – the record for cn=subschema. Because most of the schema attributes are operational attributes, we need to specify, in the list of attributes, both * for all regular attributes and + for all operational attributes.

The ldaphelper.get_search_results() function we created early in this series returns a list of LDAPSearchResult objects. Since we know that we want the first one (in a list of one), we can use the [0] notation at the end to return just the first item in the resulting list.

Now, schema_entry contains the LDAPSearchResult object for the cn=subschema record. We need the list of attributes – namely, the schema-defining attributes, usually called the subschema subentry. We can use the get_attributes() method to retrieve the dict of attributes.

Now we have the information necessary for creating a new SubSchema object.

The SubSchema Object

The SubSchema object provides access to the details of the schema definitions. The SubSchema() constructor takes one parameter: a dictionary of attributes that contains the subschema subentry information. This is the information we retrieved and stored in the subschema_subentry variable above. Creating a new SubSchema object is done like this:

>>> subschema = ldap.schema.SubSchema( subschema_subentry )
>>>

Now we can access the schema information. We can, for instance, get the schema information for the cn attribute:

>>> cn_attr = subschema.get_obj( ldap.schema.AttributeType, 'cn' )
>>> cn_attr.names
('cn', 'commonName')
>>> cn_attr.desc
'RFC2256: common name(s) for which the entity is known by'
>>> cn_attr.oid
'2.5.4.3'
>>>

The first line employs the get_obj() method to retrieve an AttributeType object. The call to get_obj() above uses two parameters.

The first is the class (a subclass of SchemaElement) that represents an attribute. This is ldap.schema.AttributeType. If we were getting an object class instead of an attribute, we would use the same method, but pass an ldap.schema.ObjectClass as the first parameter.

The second parameter is a string name (or OID) of the attribute. We could have used 'commonName' or '2.5.4.3' and attained the same result.

The cn_attr object (an instance of an AttributeType class) has a number of properties representing schema statements. For example, in the example above, the names property contains a tuple of the attribute names for that attribute, and the desc property contains the value of the description, as specified in the schema. The oid attribute contains the Object Identifier (OID) for the CN attribute.

Let's look at one more method of the SubSchema class before moving on to the final script in this article. Using the attribute_types() method of the SubSchema class, we can find out what attributes are required for an record, and what attributes are allowed.

For example, consider a record that has the object classes account and simpleSecurityObject. The uid=authenticate,ou=system,dc=example,dc=com entry in our directory information tree is an example of such a user. We can use the attribute_types() method to get information about what attributes this record can or must have:

>>> oc_list = ['account', 'simpleSecurityObject']
>>> oc_attrs = subschema.attribute_types( oc_list )
>>> must_attrs = oc_attrs[0]
>>> may_attrs = oc_attrs[1]
>>>
>>> for ( oid, attr_obj ) in must_attrs.iteritems():
... print "Must have %s" % attr_obj.names[0]
...
Must have userPassword
Must have objectClass
Must have uid
>>> for ( oid, attr_obj ) in may_attrs.iteritems():
... print "May have %s" % attr_obj.names[0]
...
May have o
May have ou
May have seeAlso
May have description
May have l
May have host
>>>

The oc_list list has the names of the two object classes in which we are interested: account and simpleSecurityObject. Passing this list to the attribute_types() method, we get a two-item tuple.

The first item in the tuple is a dictionary of required attributes. The key in the dictionary is the OID:

>>> must_attrs.keys()
['2.5.4.35', '2.5.4.0', '0.9.2342.19200300.100.1.1']
>>>

The value in the dictionary is an AttributeType object corresponding to the attribute defined for the OID key:

>>> must_attrs['2.5.4.35'].oid
'2.5.4.35'
>>>

In the code snippet above, we assigned each value in the two-item tuple to a different variable: must_attrs contains the first item in the tuple – the dictionary of must-have attributes. The may_attrs contains a dictionary of the attributes that are allowed, but not required.

Iterating through the dictionaries and printing the output, we can see that the required attributes for a record that used both the account and the simpleSecurityObject object classes would be userPassword, objectclass, and uid.

Several other attributes are allowed, but not required: o, ou, seeAlso, description, l, and host.

We could find out which object class definitions required or allowed which of these attributes using the get_obj() method we looked at above:

>>> oc_obj = subschema.get_obj( ldap.schema.ObjectClass, 'account' )
>>> oc_obj.may
('description', 'seeAlso', 'localityName',
'organizationName', 'organizationalUnitName', 'host')
>>> oc_obj.must
('userid',)
>>>
>>> oc_obj = subschema.get_obj( ldap.schema.ObjectClass,
... 'simpleSecurityObject' )
>>> oc_obj.must
('userPassword',)
>>> oc_obj.may
()
>>>

From the above, we can see that most of the required and optional attributes come from the account definition, while only userPassword comes from the simpleSecurityObject definition. The requirement of the objectClass attribute comes from the top object class, the ultimate ancestor of all structural object classes.

The schema support offered by the Python-LDAP API makes it possible to program schema-aware clients that can, for instance, perform client-side schema checking, dynamically build forms for creating records, or compare definitions between different LDAP servers on a network.

Unfortunately, the ldap.schema module is poorly documented. With most of the module, the best source of information is the __doc__ strings embedded in the code:

>>> print ldap.schema.SubSchema.attribute_types.__doc__

Returns a 2-tuple of all must and may attributes including
all inherited attributes of superior object classes
by walking up classes along the SUP attribute.

The attributes are stored in a ldap.cidict.cidict dictionary.

object_class_list
list of strings specifying object class names or OIDs
attr_type_filter
list of 2-tuples containing lists of class attributes
which has to be matched
raise_keyerror
All KeyError exceptions for non-existent schema elements
are ignored
ignore_dit_content_rule
A DIT content rule governing the structural object class
is ignored

>>>

In some cases, though, the best source of documentation is the code itself.

The last script in this article will provide an example of how the schema information can be used.

An Example Script: suggest_attributes.py

This example script compares the attributes in a user-specified record with the possible attributes, and prints out an annotated list of “suggested” available attributes.

This script is longer than the other scripts in this article, but it makes use of similar techniques, and we will be able to move through it quickly.

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: $29.99
Book Price: $49.99
See more
Select your format and quantity:

Here is the suggest_attributes.py script in its entirety.

#!/usr/bin/env python
import ldap.schema
import ldap, ldaphelper, sys

user_dn = ''
user_pw = ''
server = 'ldap://localhost'

usage="""Usage: %s dn

Print out a list of all of the attributes that the record
for the given DN could use.n""" % sys.argv[0]

if len(sys.argv) != 2:
sys.stderr.write("You must provide a DN.nn")
sys.stdout.write(usage)
sys.exit(1)

dn = sys.argv[1]

def get_record( ldap_con, dn, operational_attrs = False ):
"""Get the complete record for the given DN.

ldap_con:
LDAP connection, after bind.
dn:
Complete DN.
operational_attrs:
If True, then operational attributes will be fetched, too.

Returns an LDAPSearchResult object, or None if no object was
found."""
filter = '(objectclass=*)'
attrs = ['*']
if operational_attrs: attrs.append('+')

raw_res = l.search_s( dn, ldap.SCOPE_BASE, filter, attrs )
if len(raw_res) == 0: return None

return ldaphelper.get_search_results( raw_res )[0]

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

schema_record = get_record( l, 'cn=subschema', True )
if schema_record == None:
sys.stderr.write("Error: Cannot access subschema.n")
sys.exit(1)

subschema_subentry = schema_record.get_attributes()
subschema = ldap.schema.SubSchema( subschema_subentry )

record = get_record( l, dn )
if record == None:
sys.stderr.write("Entry for %s not found.n" % dn)
sys.exit(1)

oc_list = record.get_attr_values( 'objectclass' )

attr_types = subschema.attribute_types( oc_list )
may_attrs = attr_types[1]

sys.stdout.write("Suggested Attributes:n");
suggestion_count = 0;
for attr in may_attrs.itervalues():
has_this_attr = False

for name in attr.names:
if record.has_attribute( name ):
has_this_attr = True

if not has_this_attr:
suggestion_count = suggestion_count + 1
sys.stdout.write("%d. " % suggestion_count)
for n in attr.names:
sys.stdout.write( n + ' ')
sys.stdout.write( '(' + attr.oid + ')' )
sys.stdout.write("nt" + attr.desc + "n")

if suggestion_count == 0:
sys.out.write("There are no unused attributes.n")

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)
sys.exit(1)

finally:
try:
l.unbind()
except Exception:
pass

Before looking at the particulars, let's get a basic idea of how this program works.

The program takes one command-line parameter: the DN of a record in the directory. For simplicity's sake, the program binds to ldap://localhost as the anonymous user (a more robust version of this program would allow the user pass credentials).

Once a connection with the directory server is established and the bind is complete, the script retrieves two records.

The first is the subschema record, which contains all of the schema information for the directory server. The second is the record for the DN supplied by the user.

The program then compares the attributes in this second record with the attributes that, according to the schema, such a record is allowed to have.

Then, it prints a list of all of the attributes that the record is allowed to have, but does not presently have. Here's an example of the script's output:

$ ./suggest_attributes.py 'ou=users,dc=example,dc=com'

Suggested Attributes:
1. seeAlso (2.5.4.34)
RFC2256: DN of related object
2. userPassword (2.5.4.35)
RFC2256/2307: password of user
3. searchGuide (2.5.4.14)
RFC2256: search guide, deprecated by enhancedSearchGuide
4. businessCategory (2.5.4.15)
RFC2256: business category
5. postalAddress (2.5.4.16)
RFC2256: postal address
6. street streetAddress (2.5.4.9)
RFC2256: street address of this object
# Several more attributes omitted

For each attribute, all of the possible attribute names are printed. Thus, street and streetAddress are both given for the attribute on line six. Also, the OID for each attribute is printed in parentheses after the list of attribute names. On the line beneath each attribute is the attribute description from the schema.

Now that we have a basic idea of how the program functions, let's walk through the code.

The first section of the code performes basic setup:

#!/usr/bin/env python
import ldap.schema
import ldap, ldaphelper, sys

user_dn = ''
user_pw = ''
server = 'ldap://localhost'

usage="""Usage: %s dn

Print out a list of all of the attributes that the record
for the given DN could use.n""" % sys.argv[0]

if len(sys.argv) != 2:
sys.stderr.write("You must provide a DN.nn")
sys.stdout.write(usage)
sys.exit(1)

dn = sys.argv[1]

After imports and variable declarations, a simple test is done to make sure that a DN was provided on the command line, and the DN is stored in the variable dn.

Next, we define a convenience function:

def get_record( ldap_con, dn, operational_attrs = False ):
"""Get the complete record for the given DN.

ldap_con:
LDAP connection, after bind.
dn:
Complete DN.
operational_attrs:
If True, then operational attributes will be fetched, too.

Returns an LDAPSearchResult object, or None if no object was
found."""

filter = '(objectclass=*)'
attrs = ['*']
if operational_attrs: attrs.append('+')

raw_res = l.search_s( dn, ldap.SCOPE_BASE, filter, attrs )
if len(raw_res) == 0: return None

return ldaphelper.get_search_results( raw_res )[0]

This function performs a search for a specific DN, returning an LDAPSearchResult object. If no such object is found, this function returns None. It takes three parameters, two of which are required.

  • An LDAPObject with a connection that is initialized and has already performed a bind. This connection is used to perform a search_s().
  • A DN string. The record with this DN will be retrieved from the directory.
  • An optional flag indicating whether or not operational attributes should be retrieved. By default, all non-operational attributes are returned. But if this flag is set to True, then operational attributes will be returned, too.

The workings of this function should be familiar by now, as they do not differ from the techniques introduced in the discussion of the search operation. We will move onto the next section of code.

As usual, the main LDAP transactions are nested inside of the appropriate try/finally and try/except blocks.

Inside these blocks, a bind is done, and the subschema record is retrieved and used to create a new ldap.schema.SubSchema object:

l.bind_s(user_dn, user_pw)

schema_record = get_record( l, 'cn=subschema', True )
if schema_record == None:
sys.stderr.write("Error: Cannot access subschema.n")
sys.exit(1)

subschema_subentry = schema_record.get_attributes()
subschema = ldap.schema.SubSchema( subschema_subentry )

Using the get_record() function defined above, the script retrieves the cn=subschema record, and then uses the attribute dictionary of that record to create a new instance of the SubSchema class.

A similar process is used for fetching the record with the user-supplied DN:

record = get_record( l, dn )
if record == None:
sys.stderr.write("Entry for %s not found.n" % dn)
sys.exit(1)

oc_list = record.get_attr_values( 'objectclass' )

First, the record is retrieved with the get_record() function. If the record is not found, then the script will terminate.

Once we have the desired record, we need to get the list of object classes for this record. The object classes specify what attributes the entry can have.

The values in the objectclass attribute can be used to retrieve information about allowed attributes from the schema:

attr_types = subschema.attribute_types( oc_list )
may_attrs = attr_types[1]

The attribute_types() method of the SubSchema class takes a list of object classes and returns a two-item tuple containing, first, a dictionary of all mandatory attributes, and second, a dictionary of allowed attributes.

Since we can assume that a record already has all of the mandatory attributes, all we are really concerned with are the optional attributes, so we only need the second item in the attr_types tuple. And that dictionary is assigned to the may_attrs variable.

At this point, we have the information we need. The may_attrs dictionary contains the necessary schema information, and record, an LDAPSearchResult object, has the necessary attribute information for the desired LDAP entry.

Now, all we need to do is compare the allowed fields with those actually present, and print our suggestions:

sys.stdout.write("Suggested Attributes:n");
suggestion_count = 0;
for attr in may_attrs.itervalues():
has_this_attr = False

for name in attr.names :
if record.has_attribute( name ):
has_this_attr = True

if not has_this_attr:
suggestion_count = suggestion_count + 1
sys.stdout.write("%d. " % suggestion_count)
for n in attr.names:
sys.stdout.write( n + ' ')
sys.stdout.write( '(' + attr.oid + ')' )
sys.stdout.write("nt" + attr.desc + "n")

if suggestion_count == 0:
sys.out.write("There are no unused attributes.n")

First, this snippet of code writes Suggested Attributes: to standard output. Then, it initializes a counter, suggestion_count, which will be used both for display purposes, and to print a message if there are no suggested attributes.

There are a pair of nested for loops in this segment of code. The outer one loops through all of the attributes retrieved from the schema. The may_attrs dictionary uses the attribute's OID as a key, and an AttributeType object as a value. The AttributeType object contains the information we need, so we retrieve just the values of the dictionary using the itervalues() method.

Why use a second for loop? Each attribute may have multiple attribute names, like cn and commonName, so we need to check all attribute names for each attribute.

The has_this_attr flag is used to indicate that an attribute is used, regardless of which attribute name is used to refer to the attribute. The inner for loop compares checks the record using each attribute name in the schema, setting has_this_attr to True if one of the attribute names is present in the record's attributes.

Based on the has_this_attr flag, we can now tell whether this attribute appears in the target record. If it does not, then we print a helpful suggestion to the system output, using information from attr, the AttributeType schema element object.

Finally, if no suggestions were found, then suggestion_count will be 0. If that is the case, the script prints a short message saying that no suggestions were found.

This script provides an example of how schema information can be used to enhance a program. Having the schema accessible to the client, and having a library sophisticated enough to make use of the schema, can make developing rich LDAP applications much easier.

Summary

We have quickly examined the Python-LDAP library. While we have covered the major operations, and looked at many of the modules, there are other features of this API that I have, for the sake of brevity, not discussed here. Many of these features are documented in the official online documentation (http://python-ldap.sourceforge.net/doc/python-ldap/), and even more are documented in the source code (using __doc__ strings).

In the course of this mini-series, we have covered installing the Python-LDAP library, connecting to and binding to a directory server, running many read and write operations, and even making use of the LDAP URL and LDAP schema modules.

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: $29.99
Book Price: $49.99
See more
Select your format and quantity:

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

Mastering OpenLDAP: Configuring, Securing and Integrating Directory Services
Mastering OpenLDAP: Configuring, Securing and Integrating Directory Services

Liferay Portal Enterprise Intranets
Liferay Portal Enterprise Intranets

Professional Plone Development
Professional Plone Development

Learning Website Development with Django
Learning Website Development with Django

Catalyst
Catalyst

Building Telephony Systems with OpenSER
Building Telephony Systems with OpenSER

AsteriskNOW
AsteriskNOW

Building Websites with Joomla! 1.5
Building Websites with Joomla! 1.5


 

 

No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
d
k
U
P
7
f
Enter the code without spaces and pay attention to upper/lower case.
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