Python LDAP Applications: Part 4 – LDAP SchemaAhren Stevens-Taylor
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',
>>> subschema_entry = ldaphelper.get_search_results(res)
>>> 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  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' )
'RFC2256: common name(s) for which the entity is known by'
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 ‘220.127.116.11’ 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
>>> may_attrs = oc_attrs
>>> for ( oid, attr_obj ) in must_attrs.iteritems():
... print "Must have %s" % attr_obj.names
Must have userPassword
Must have objectClass
Must have uid
>>> for ( oid, attr_obj ) in may_attrs.iteritems():
... print "May have %s" % attr_obj.names
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:
['18.104.22.168', '22.214.171.124', '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:
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' )
('description', 'seeAlso', 'localityName',
'organizationName', 'organizationalUnitName', 'host')
>>> oc_obj = subschema.get_obj( ldap.schema.ObjectClass,
... 'simpleSecurityObject' )
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.
list of strings specifying object class names or OIDs
list of 2-tuples containing lists of class attributes
which has to be matched
All KeyError exceptions for non-existent schema elements
A DIT content rule governing the structural object class
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.