...making Linux just a little more fun!
By Ron Peterson
As Ron notes in his article, the adoption of
LDAP has been rather slow despite its broad range of benefits; in
my experience as a consultant, much of the problem stems from lack
of general familiarity with the protocol, or even awareness of its
existence. To be fair, today's endless alphabet soup of protocols
is enough to bewilder and stump anyone - so it's unsurprising that
something this useful can get lost in the noise. However, if you
need an organization-wide authentication system, or a secure,
highly-scalable "White Pages" service for your company - i.e., the
ability to browse directories of structured shareable information
(user names, contact info, e-mail addresses, security certificates,
etc.) - then LDAP is custom-made for you, and Ron's introduction
just might make your initial adaptation a bit easier. Enjoy.
-- Ben Okopnik
For some reason, in spite of LDAP's ubiquity, many of us are still unfamiliar with all of its benefits. We have a Free LDAP server in OpenLDAP, and LDAP-enabled applications are everywhere; so what's the hang up? Among other things, the standard set of attribute types and object classes included in the default OpenLDAP schema files are maladapted to the requirements of many applications. Fortunately, you can easily create attribute types and object classes to fit your particular requirements, once you get the hang of it. This article will attempt to get you started.
I'm going to organize this article around a practical example. Both Outlook Express and Mozilla Thunderbird can do address book look-ups against an LDAP database. The trick is to figure out what their respective LDAP look-ups expect to see when they perform their queries. Once we know that, we can construct our own custom LDAP object which will contain a set of attributes suitable for both applications.
For the purpose of this article, I'm going to assume you have a basic grasp of how to configure and run OpenLDAP. If not, there are a number of resources available to help you brush up.
If you've never seen an OpenLDAP schema file, now would be a good time to take a look. They typically reside in a sub-directory called 'schema' within your OpenLDAP installation's configuration directory, e.g. /etc/ldap/schema. The schema file names typically end with a '.schema' extension. If you have any trouble finding them, try "locate" or "slocate":
slocate schema | grep schema$
This is also a handy way to discover schema files belonging to other packages on your system that you might not have known existed. Look around a bit, but before we start attempting to emulate the definitions in these files, we must deal with an important preliminary issue: we have to get our hands on some OIDs.
The LDAP attributetype and objectclass definitions you find in these schema files are each assigned a unique object identifier called an OID. OIDs look like decimal delimited sets of integers, e.g. 1.3.6.1 (which happens to be the Internet OID).
There is a lot that can be said about OIDs, but for our current purposes, you must understand that you can't just use them willy-nilly. There are a small number of registration authorities responsible for allocating portions of the OID hierarchy. Hijacking someone else's OID space is very bad form at best. Improper use of the OID namespace could cause namespace collisions that break things.
So then, the first hurdle we must deal with is getting our hands on a portion of the OID space that we can use. We have a couple of options:
I have my own OID namespace registered under the name Yellowbank (the name of a creek on the farm where I grew up). Yellowbank has registered the following OID base with IANA: 1.3.6.1.4.1.25948. It's up to me to decide how to use the OID namespace hierarchy under this. I have elected to not use the 1.3.6.1.4.1.25948.1 branch of my hierarchy (see the yNonUnique OID Macro below) for anything important, for example. That means that if you were to happen to reconnoiter the use of OIDs under 1.3.6.1.4.1.25948.1, you would never run the risk of colliding with a production Yellowbank OID. Of course, the second you start thinking you might like to disseminate your work, or put it into production, you would of course immediately register for your own OID. I do not condone customizing any portion of the Yellowbank OID space for any production purpose whatsoever. Ahem.
Now that we have our OID preliminaries out of the way, let's get to work. The first thing we need to do is to figure out exactly what our application expects from LDAP. How? Simple: watch OpenLDAP's log file. First, make sure your OpenLDAP installation is configured to log at a level sufficient to see the queries. The slapd.conf configuration directive is 'loglevel'. For the examples below, I have my loglevel set to 256. I also direct my syslog daemon to put my slapd log in it's own file, by adding the following directive to /etc/sysklogd.conf:
local4.* /var/log/slapd.log
As the man page for slapd says, local4 corresponds to slapd's default logging facility. You may need to adjust this directive to match your installation.
Let's configure Thunderbird to use our LDAP server for address book queries: start Thunderbird, open the address book (Menu: Tools/Address Book), and create a new LDAP directory (Menu: File/New/LDAP Directory). What you enter here depends on your local LDAP installation. It goes without saying, but I'll say it anyway, that you need to be authorized to query the Base DN that you enter. If you don't enter a Bind DN, then of course the Base DN will be queried anonymously. If none of this makes sense to you, you need to consult with your LDAP administrator. If none of this makes sense to you and you are the LDAP administrator, you should spend some quality time with some of the aforementioned LDAP resources.
Once you've added your LDAP directory to Thunderbird's address book, we can query the database (Menu: Edit/Search Addresses). Create a query that includes every possible field, with a unique search string for each field (click the '+' symbol to add additional query conditions). I set up my query like this:
Anyname | contains | MyAnyname Display Name | contains | MyDisplayName etc.
When you're all done, click the 'Search' button. We don't care so much about the results returned, as we do about what the slapd process logged. Open your log file in your favorite pager, so we can see what happened. On my system, my log looks like this (reformatted a bit to be browser friendly):
Jun 9 11:08:12 ahostname slapd[13144]: conn=349441 op=1 SRCH base="ou=my,dc=base,dc=dn" scope=2 deref=0 filter="(|(cn=*myanyname*) (givenName=*myanyname*) (sn=*myanyname*) (?=undefined) (?=undefined) (cn=*mydisplayname*) (?=undefined) (?=undefined) (mail=*myemail*) (?=undefined) (homePhone=*MyAnyNumber*) (telephoneNumber=*MyAnyNumber*) (?=undefined) (pager=*MyAnyNumber*) (mobile=*MyAnyNumber*) (telephoneNumber=*MyWorkPhone*) (homePhone=*MyHomePhone*) (?=undefined) (pager=*MyPager*) (mobile=*MyMobile*) (l=*mycity*) (street=*mystreet*) (title=*mytitle*) (?=undefined) (?=undefined)) "Jun 9 11:08:12 ahostname slapd[13144]: conn=349441 op=1 SRCH attr=company o title modifytimestamp mozillaCustom4 custom4 mozillaHomeUrl homeurl mozillaCustom2 custom2 mozillaHomeCountryName department departmentnumber ou orgunit mobile cellphone carphone mozillaHomeState mozillaCustom1 custom1mozillaNickname xmozillanickname mozillaWorkUrl workurl fax facsimiletelephonenumber st region telephoneNumber mozillaHomeStreet mozillaSecondEmail xmozillasecondemail nsAIMid nscpaimscreenname street streetaddress postOfficeBox l locality homePhone description notes cn commonname givenName mozillaHomePostalCode mozillaHomeLocalityName mozillaCustom3 custom3 mozillaWorkStreet2 mozillaUseHtmlMail xmozillausehtmlmail mozillaHomeStreet2 postalCode zip c countryname pager pagerphone mail sn surname birthyear
The slapd log clearly shows us what attributes Thunderbird wants to see returned. It also shows us which attributes correspond to the fields in the Thunderbird query form. For example, the value of the field labeled 'Any Name' in the query form is compared against the 'cn', 'givenName', and 'sn' attributes.
Now we do the same thing for Outlook Express. First add the directory service (Menu: Tools/Accounts/Add/Directory Service). Again, you'll either need to be familiar with your LDAP setup, or consult with your LDAP administrator to ascertain the proper settings.
To perform a query, open the Address Book, click 'Find People', and select your previously configured LDAP provider in the pull down menu. In the dialog that appears, click the advanced tab, and enter:
Name contains MyName E-mail contains MyEmail First Name contains MyFirst Last Name contains MyLast Organization contains MyOrg
Again, search your log file to determine what attributes are requested, and to see how the filter expression corresponds to the search dialog.
My fancy ASCII table below summarizes what I discovered. The leftmost column contains the intersection of the attributes that Outlook Express and Thunderbird requested. I took the liberty of doubling up attribute names like 'cn' and 'commonName' which mean the same thing. The other two columns indicate how the search dialogs map to those attributes. So we see that the 'E-mail' box in the Outlook Express search form is queried against the 'mail' attribute, for example.
Common Attributes | Outlook Express | Thunderbird ======================================================================== c | | cn, commonName | Name | department | | facsimileTelephoneNumber | | givenName | First Name | Any Name homePhone | | Any Number, Home Phone l | | City mail | E-mail | Email mobile | | Any Number, Mobile o | Organization | ou | | pager | | Any Number, Pager postalCode | | sn, surname | Last Name | Any Name st, street | | Street streetAddress | | telephoneNumber | | Any Number, Work Phone title | | Title
Wouldn't it be nice if our LDAP directory were populated by user objects that would make sense to both Outlook Express and Thunderbird? Well, we now have enough information to do that. It's time to put all the pieces together.
Your schema file can live anywhere your LDAP daemon can read it. Edit the LDAP daemon's configuration file. On my system, it's /etc/ldap/slapd.conf. The 'include' directives in this configuration file load the schema files used by your LDAP installation. These schema files typically reside in a sub-directory of your configuration directory. On my system, they are in in /etc/ldap/schema. We can put our new custom schema file anywhere the LDAP daemon can read it. For the sake of consistency, we'll put it in the same place as the rest of the schema files. Verify your installation's schema file location by reviewing your configuration file. Add an 'include' directive to your configuration file as below. Don't kill -SIGHUP your server until our new schema file is fleshed out, of course.
include /etc/ldap/schema/yellowbank.sample.schema
While we're in the slapd.conf file, let's update our indices. Add indices on whatever attributes you think you might query frequently. Use the table above to assist you. Something like the following might suffice, perhaps.
index objectClass pres,eq index cn,sn,uid,memberUid pres,eq,approx,sub index givenName,mail pres,eq,approx,sub
You can add indices later too, of course. Just be aware that LDAP will not use new indices on an existing database until you use the the 'slapindex' command.
Guess what? It's finally time to actually start editing our schema file! What follows is basically just a colloquial summary of the schema specification information available in the OpenLDAP Software Administrator's Guide. The examples I give will be a partial representation of what I have in my full 'yellowbank.schema'. You can use the examples as they are, if you like; or you can adapt them to your own OID space.
We'll begin by fleshing out a rudimentary OID hierarchy, and giving these OIDs symbolic names, so that we don't have to remember really long strings of integers. This is not strictly necessary, you can use labels like 1.3.6.1.4.1.25948.4.2.1.1.2006.08.20.1 if you like, but I prefer names I can recognize. We'll do this using 'OID Macros', like so:
# OID Macros # # Yellowbank's IANA Assigned OID objectIdentifier yOID 1.3.6.1.4.1.25948 # objectIdentifier yNonUnique yOID:1 objectIdentifier yPedagogy yOID:2 objectIdentifier yProduction yOID:3 # # Note that OID's are used for more than just LDAP. objectIdentifier ypSNMP yPedagogy:1 objectIdentifier ypLDAP yPedagogy:2 # objectIdentifier ypAttributeType ypLDAP:1 objectIdentifier ypObjectClass ypLDAP:2 # objectIdentifier ypPersonAttribute ypAttributeType:1 objectIdentifier ypAssetAttribute ypAttributeType:2 objectIdentifier ypApplicationAttribute ypAttributeType:3 objectIdentifier ypFacilityAttribute ypAttributeType:4 objectIdentifier ypOrganizationAttribute ypAttributeType:5 # objectIdentifier ypSakaiAttribute ypApplicationAttribute:1 # objectIdentifier ypPersonObject ypObjectClass:1
Both Thunderbird and Outlook Express would like to see an attribute called 'department' which, interestingly enough, is not included in any of OpenLDAP's standard schema files. So let's create it. Add the following to our new schema file after the OID Macro section.
attributetype ( ypOrganizationAttribute:1.2006.08.20.1 NAME 'department' DESC 'Department Name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch ORDERING caseIgnoreOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} SINGLE-VALUE )
What do we see here? For starters, I'm appending a unique suffix to one of my defined OID Macros. There are no rules about how you create OIDs, but I've adopted the convention of appending a date.version to the unique integer string that identifies my attributeType. In other words, my 'department' attributetype is uniquely yOrganizationAttribute:1, and I follow this with a date.version string which corresponds to when I edited my definition. The main thing is that each OID must be unique, and that the OID string is created by appending a colon and an integer string suffix to our OID macro.
The Backus-Naur Form (BNF) grammar for attributetype definitions can be found in the appropriate section of the OpenLDAP Manual. I'll briefly summarize the definition above. Hopefully 'NAME' and 'DESC' are fairly self-explanatory. The 'EQUALITY' line defines the matching rule used in equality comparisons (case and space insensitive). The 'SUBSTR' line defines the matching rule used in substring matching (case and space insensitive). The 'ORDERING' lines defines the matching rule used in (you guessed it) ordering comparisons. The 'SYNTAX' line delimits valid input, where the given OID means 'Unicode string', and the {128} at the end limits input to 128 characters. The 'SINGLE-VALUE' line means that there can only be one instance of this attribute in an object; in other words, this is not a multi-valued attribute. Attributes are multi-valued by default, so to create an attribute type which can contain multiple values, simply omit this line.
Assuming our slapd.conf includes any other requisite schema files, which contain our other standard attribute type definitions, we can now define our own objectClass.
objectclass ( ypPersonObject:1.2006.08.20.1 NAME 'ypContact' DESC 'Yellowbank Contact' SUP top STRUCTURAL MUST ( cn $ sn $ mail ) MAY ( c $ department $ facsimileTelephoneNumber $ givenName $ homePhone $ l $ mobile $ o $ ou $ pager $ postalCode $ st $ streetAddress $ telephoneNumber $ title $ description ) )
Again, we see that our definition begins by assigning a unique OID. Again, I hope that 'NAME' and 'DESC' are fairly self-explanatory. I'll defer discussion of the 'SUP' line until a bit later. The 'MUST' line indicates, as you might guess, the attributes which must be present in objects defined with this objectClass. The 'MAY' line describes optional attributes. Let's create one more objectClass.
objectclass ( ypPersonObject:2.2006.08.20.1 NAME 'ypLoginAccount' DESC 'Yellowbank Contact' SUP ypContact STRUCTURAL MAY ( userPassword $ memberUid ) )
Notice that the 'SUP' line for this objectClass points to our previously defined objectClass. In other words, our ypLoginAccount objectClass extends our previous definition to include a couple of new attributes. We have defined a chain of inheritance that looks like this:
top <- ypContact <- ypLoginAccount
When we define objects, the objectClasses we use can only include one STRUCTURAL inheritance chain going back to 'top'. We can mix in other objectClasses as well, but only if they are defined as 'AUXILIARY'. The LDAP Manual includes the following objectClass definition, for example.
objectclass ( 1.1.2.2.1 NAME 'myPhotoObject' DESC 'mixin myPhoto' AUXILIARY MAY myPhoto )
I could define an object which uses myPhotoObject to supplement the attributes available to an object like this:
dn: blah blah objectClass: top objectClass: ypLoginAccount objectClass: myPhotoObject cn: aname sn: lname ...
But the following definition would give me trouble, because the ypLoginAccount and inetOrgPerson objectClasses are part of different STRUCTURAL inheritance chains.
dn: blah blah objectClass: top objectClass: ypLoginAccount objectClass: inetOrgPerson cn: aname sn: lname ...
The linuxlaboratory.org site has a nice LDAP Article which might provide additional illumination.
Here's a snippet of LDIF you can use to try our new schema.
dn: cn=wile,ou=someplace,ou=meaningful,dc=to,dc=you cn: wile userPassword: {SHA}Wfi6Kd1nmjpbtmzcFqCnvEqIPzs= givenName: Wile mail: wile.coyote@acme.com l: Los Lobos Hermanos o: ACME sn: Coyote description: Malnourished but dogged memberUid: coyote memberUid: mammal memberUid: emaciated objectClass: top objectClass: ypLoginAccount
Modify the dn as appropriate to your installation, and upload with your favorite incarnation of the ldapadd command - after reloading your slapd.conf file, of course, which will read in the new schema file. If all goes well, you can now query this user from either Outlook Express or Thunderbird, assuming you've taken care of all the configuration niceties.
I find knowing how to customize OpenLDAP indispensable. For example, I recently encountered an application vendor that considered their product "LDAP compliant" because it worked with one particular vendor's directory implementation. Although I tend to cynically attribute this kind of mischievous marketing malapropism to clever salesmanship, my world view may just be the simple result of ensconcing myself in an intellectually gated community. Who knows? In any case, it was a simple problem to remedy: I just used OpenLDAP to emulate the other directory implementation, thereby avoiding pointless additions to our infrastructure.
May the new-found depth of your LDAP knowledge allow you to overcome all of life's adversities, get a big raise, and earn a well-deserved promotion.
Talkback: Discuss this article with The Answer Gang
Ron Peterson is a Network & Systems Manager at Mount Holyoke College in the happy hills of western Massachusetts. He enjoys lecturing his three small children about the maleficent influence of proprietary media codecs while they watch Homestar Runner cartoons together.