Book Home Perl for System AdministrationSearch this book

6.4. LDAP: A Sophisticated Directory Service

LDAP and ADSI are much richer and more sophisticated directory services. LDAP stands for Lightweight Directory Access Protocol. There are two widely deployed versions of the LDAP protocol out there (Version 2 and Version 3--anything that is version specific will be clearly noted as such). This protocol is fast becoming the industry standard for directory access. System administrators have embraced LDAP because it offers them a way to centralize and make available all sorts of infrastructure information. Besides the standard "company directory," examples applications include:

LDAP is also the basis of other sophisticated directory services like Microsoft's Active Directory, which we'll see later, in the section Section 6.5, "ADSI (Active Directory Service Interfaces)".

Even if LDAP is not used in your environment to provide anything but a fancy phone book, there are still good reasons to learn how to use the protocol. LDAP servers themselves can be administered using the same protocol they serve, similar to SQL database servers being administered via SQL. To this end, Perl offers an excellent glue environment for automating LDAP administrative tasks. Before we get there, we need to be sure we understand LDAP itself.

Appendix B, "The Ten-Minute LDAP Tutorial", contains a quick introduction to LDAP for the uninitiated. The biggest barrier new system administrators encounter when they begin to learn about LDAP is the unwieldy nomenclature it inherited from its parent protocol, the X.500 Directory Service. LDAP is a simplified version of X.500, but unfortunately, the distillation process did not make the terminology any easier to swallow. Taking a few moments with Appendix B, "The Ten-Minute LDAP Tutorial" to get these terms under your belt will make understanding how to use LDAP from Perl easier.

6.4.1. LDAP Programming with Perl

Like so many other systems administration tasks in Perl, a good first step towards LDAP programming is the selection of the required Perl module. LDAP is not the most complex protocol out there, but it is not a plain text protocol. As a result, cobbling something together that speaks LDAP is not a trivial exercise. Luckily two sets of authors have already done this work for us: Graham Barr created Net::LDAP and Leif Hedstrom and Clayton Donley created Mozilla::LDAP (a.k.a. PerLDAP). Table 6-1 lists some of the ways that the two modules differ.

Table 6.1. Comparison of the Two Perl LDAP Modules

Feature

Net::LDAP

Mozilla::LDAP (PerLDAP)

Portability

Pure Perl

Requires the Mozilla/Netscape LDAP C-SDK (source is freely available). The SDK compiles on many Unixs, NT, and MacOS

SSL encrypted sessions

Yes

Yes

Asynchronous operations

Yes

Only with the non-object-oriented base-level

API

Both of these modules have the functionality to perform the simple system administration-related tasks we'll be discussing, but they take slightly different approaches in how they offer it. This creates a rare educational opportunity because it allows us to observe how two different authors implemented substantial modules to address essentially the same niche. Careful comparison between the two modules can offer insight into the module implementation process, which we'll briefly demonstrate in Chapter 10, "Security and Network Monitoring". To facilitate this comparison, most of the examples in this section show the syntax for both Perl LDAP modules. The use modulename line in each full code example will clue you in on which module we are using at the time.

For demonstration servers, we'll be using the commercial Netscape 4.0 Directory Server and the free OpenLDAP server (found at http://www.netscape.com and http://www.openldap.org) almost interchangeably. Both come with almost identical command-line utilities that you can use to prototype and crosscheck your Perl code.

6.4.2. The Initial LDAP Connection

Connecting with authentication is the usual first step in any LDAP client-server transaction. In LDAP-speak this is known as "binding to the server." Binding to a server before sending commands to it was required in LDAPv2 but this requirement was relaxed for LDAPv3.

When you bind to an LDAP server, you are said to be doing so in the context of a specific Distinguished name (DN), described as the bind DN for that session. This context is similar to logging in as a particular user on a multiuser system. On a multiuser system, your current login (for the most part) determines your level of access to data on that system. With LDAP, it is the bind DN context that determines how much data on the LDAP server you can see and modify. There is a special DN known as the root Distinguished Name (which is not given an acronym to avoid confusing it with Relative Distinguished Name). The root Distinguished Name is the DN context that has total control over the whole tree, just like being logged in as root under Unix or Administrator for NT/2000. Some servers also refer to this as the manager DN.

If a client provides no authentication information (e.g., DN and password) as part of a bind, or does not bother to bind before sending commands, this is known as anonymous authentication. Anonymously authenticated clients typically receive very restricted access to a server's data.

There are two flavors of binding in the LDAPv3 specification: simple and SASL. Simple binding uses plain-text passwords for authentication. SASL (Simple Authentication and Security Layer) is an extensible authentication framework defined in RFC2222 that allows client/server authors to plug in a number of different authentication schemes like Kerberos and One-Time Passwords. When a client connects to a server, it requests a particular authentication mechanism. If the server supports this mechanism, it will begin the challenge-response dialogue specific to that mechanism to authenticate the client. During this dialogue, the client and server may also negotiate a security layer (e.g., "all traffic between us will be encrypted using TLS") for use after the initial authentication has been completed.

Some LDAP servers and clients add one more authentication method to the standard simple and SASL choices. This method comes as a by-product of running LDAP over an encrypted channel via Secure Socket Layer (SSL). To set this channel up, LDAP servers and clients exchange public-key cryptography certificates just like a web server and browser do for HTTPS. The LDAP server can be told to use a trusted client's certificate as authentication information without having to bother with other authentication info. Of the Perl modules available, only PerLDAP offers LDAPS (SSL-encrypted sessions). To keep our examples from getting too complicated, we'll stick to simple authentication and unencrypted transport sessions.

Here's how you do a simple bind and unbind in Perl:

use Mozilla::LDAP::Conn;
# use empty $binddn and $passwd for anonymous bind
$c = new Mozilla::LDAP::Conn($server, $port, $binddn, $passwd); 
die "Unable to connect to $server" unless $c;
...
$c->close(  );

or:

use Net::LDAP;
$c = Net::LDAP->new($server, port => $port) or 
    die "Unable to connect to $server: $@\n";
# use no parameters to bind(  ) for anonymous bind
$c->bind($binddn, password => $passwd) or
    die "Unable to bind: $@\n"; 
...
$c->unbind(  );

With Mozilla::LDAP::Conn, the creation of a new connection object also binds to the server. In Net::LDAP this is a two-step process. To initialize a connection without performing a bind in Mozilla::LDAP, you'll need to use a function (ldap_init( )) from the non-object-oriented Mozilla::LDAP::API module.

TIP

Be Prepared to Carefully Quote Attribute Values

A quick tip before we do any more Perl programming: if you have an attribute in your Relative Distinguished Name with a value that contains one of the characters "+", "(space)," ",", "'", ">", "<", or ";", you must specify the value surrounded by quotation marks or with the offending character escaped by a backslash (\). If the value contains quotation marks, those marks must be escaped using backslashes. Backslashes in values are also escaped with more backslashes.

Insufficient quoting will bite you if you are not careful.

6.4.3. Performing LDAP Searches

The D in LDAP stands for Directory, and the one operation you perform most on a directory is a search. Let's start our exploration of LDAP functionality by learning how to find information. An LDAP search is specified in terms of:

Where to begin the search

This is called the base DN or search base. A base DN is simply the DN of an entry in the directory tree where the search should begin.

Where to look

This is known as the search scope. The scope can be: base (search just the base DN), one (search everything one level below the base DN, not including the base DN itself), or sub (search the base DN and all of the parts of the tree below it).

What to look for

This is called the search filter. We'll discuss filters and how they are specified in just a moment.

What to return

To speed up the search operation, you can select which attributes are returned for each entry found by the search filter. It is also possible to request that only attribute names and not their values are returned. This is useful for those times when you want to know which entries have a certain attribute, but you don't care what that attribute contains.

In Perl, a search looks like this (the bind step has been replaced with an ellipsis):

use Mozilla::LDAP::Conn;
...
$entry = $c->search($basedn, $scope, $filter);
die "Bad search: ". $c->getErrorString()."\n" if $c->getErrorCode(  );

or:

use Net::LDAP;
...
$searchobj = $c->search(base => $basedn, scope => $scope, 
                        filter => $filter); 
die "Bad search, errorcode #".$searchobj->code() if $searchobj->code(  );

Let's talk about the mysterious $filter parameter before we see a fully fleshed-out code example. Simple search filters are of the form:

<attribute name> <comparison operator> <attribute value>

where <comparison operator> is specified in RFC2254 as one of the operators listed in Table 6-2.

Table 6.2. LDAP Comparison Operators

Operator

Means

=

Exact value match. Can also be a partial value match if * is used in the <attribute value> specification (e.g., cn=Tim O*).

=*

Match all entries that have values for <attribute name>, independent of what the values are. By specifying * instead of <attribute value>, we test for presence of that particular attribute in an entry (e.g., cn=* would select entries that have cn attributes).

~=

Approximate value match.

>=

Greater than or equal to value.

<=

Less than or equal to value.

These look Perlish, but don't be deceived. Two misleading constructs to a Perl person are ~= and =*. The first has nothing to do with regular expression matches; it finds matches that approximate the stated value. The definition of "approximate" in this case is server dependent. Most servers use an algorithm originally invented for census-taking called soundex to determine the matching values, to find words that "sound like" the given value (in English) but are spelled differently.[2]

[2]If you want to play with the soundex algorithm, Mark Mielke's Text::Soundex module provides a Perl implementation.

The other construct that may clash with your Perl knowledge is the = operator. In addition to testing for exact value matches (both string and numeric), = can also be used with prefix and suffix asterisks as wildcard characters, similar to shell globbing. For example, cn=a* will yield all of the entries that have a common name that begins with the letter "a". cn=*a* performs just as you would suspect, finding all entries whose common name attribute has a letter "a" in it.

We can take two or more of these <attribute name> <comparison operator> <attribute value> simple search forms and string them together with Boolean operators to make a more complex filter. This takes the form:

(<boolean operator> (<simple1>) (<simple2>) (<simple3>) ... )

People with LISP experience will have no problem with this sort of syntax; everyone else will just have to remember that the operator that combines the simple search forms is written first. To filter entries that match both criteria A and B, you would use (&(A)(B)). For entries that match criteria A or B or C, you would use (|(A)(B)(C)). The exclamation mark negates a specific criterion: A and not B is (&(A)(!B)). Compound filters can be compounded themselves to make arbitrarily complex search filters. Here is an example of a compound search filter that finds all of the Finkelsteins who work in Boston:

(&(sn=Finkelstein)(l=Boston))

To find anyone with the last name Finkelstein or Hinds:

(|(sn=Finkelstein)(sn=Hinds))

To find all of the Finkelsteins who do not work in Boston:

(&(sn=Finkelstein)(!(l=Boston)))

To find all the Finkelsteins or Hinds who do not work in Boston:

(&(|(sn=Finkelstein)(sn=Hinds))(!l=Boston))

Here are two code examples that take an LDAP server name and an LDAP filter and return the results of the query:

use Mozilla::LDAP::Conn;

$server = $ARGV[0];
$port   = getservbyname("ldap","tcp") || "389";
$basedn = "c=US";
$scope  = "sub";  

$c = new Mozilla::LDAP::Conn($server, $port, "",""); # anonymous bind
die "Unable to bind to $server\n" unless $c; 

$entry = $c->search($basedn, $scope, $ARGV[1]);
die "Error in search: ". $c->getErrorString()."\n" if $c->getErrorCode(  );

# process the return values from search(  )
while ($entry) {
    $entry->printLDIF(  );
    $entry = $c->nextEntry(  );
}
$c->close(  );

use Net::LDAP;
use Net::LDAP::LDIF;

$server = $ARGV[0];
$port   = getservbyname("ldap","tcp") || "389";
$basedn = "c=US";
$scope  = "sub";

$c = new Net::LDAP($server, port=>$port) or 
    die "Unable to connect to $server: $@\n";
$c->bind(  ) or die "Unable to bind: $@\n"; # anonymous bind

$searchobj = $c->search(base => $basedn, scope => $scope, 
                        filter => $ARGV[1]);
die "Bad search, errorcode #".$searchobj->code() if $searchobj->code(  );

# process the return values from search(  )
if ($searchobj){
    $ldif = new Net::LDAP::LDIF("-");
    $ldif->write($searchobj->entries(  ));
    $ldif->done(  );
}

Here's an excerpt from some sample output:

$ ldapsrch ldap.bigfoot.com '(sn=Pooh)'
...
dn: cn="bear pooh",mail=poohbear219@hotmail.com,c=US,o=hotmail.com
mail: poohbear219@hotmail.com
cn: bear pooh
o: hotmail.com
givenname: bear
surname: pooh
...

Before we develop this example any further, let's explore the code that processes the results returned by search( ). This is one place where the two modules diverge in their programming model. Both of these code samples will output the same information in LDIF (LDAP Data Interchange Format), a format we'll explore later, but they get there in two very different ways.

The Mozilla::LDAP model holds true to the search parsing routines described in the RFC1823 C API specification. If a search succeeds, the first entry found is returned. To view the results, you ask for the subsequent entries one at a time. For each entry retrieved, the printLDIF( ) method is used to dump its contents.

Net::LDAP's programming model more closely resembles the protocol definition of RFC2251. LDAP search results are returned in LDAP Message objects. The code we just saw calls the entries( ) method to return a list of all of the entries in these packets. We use a method from the adjunct module Net::LDAP::LDIF to dump these entries out en masse. The same method, write( ), can also be used in a manner similar to printLDIF( ) in the first example to print entries one at a time, but the call shown above is more efficient.

Let's tweak our previous example a little bit. Earlier in this chapter I mentioned that we could construct speedier searches by limiting the attributes that are returned by a search. With the Mozilla::LDAP module this is as simple as adding extra parameters to our search( ) method call:

use Mozilla::LDAP::Conn;
...
$entry = $c->search($basedn,$scope,$ARGV[1],0,@attr);

The first additional parameter is a Boolean flag that dictates whether attribute values are omitted from search results. The default is false (0), since most of the time we are interested in more than just the names of the attributes.

The subsequent additional parameters are a list of names of attributes to be returned. Perl-savvy readers will note that lists within lists auto-interpolate, so the last line of the above code is equivalent to (and can be written as):

$entry = 
  $c->search($basedn,$scope,$ARGV[1],0,$attr[0],$attr[1],$attr[2],...);

If we change our original example code from:

$entry = $c->search($basedn,$scope,$ARGV[1]);

to:

@attr = qw(mail);
$entry = $c->search($basedn,$scope,$ARGV[1],0,@attr);

we get this output instead, which only shows the entry's DN and mail attribute:

...
dn: cn="bear pooh",mail=poohbear219@hotmail.com,c=US,o=hotmail.com
mail: poohbear219@hotmail.com
...

The change made to return specific attributes via Net::LDAP is similarly easy:

use Net::LDAP;
...
# could also add "typesonly => 1" to return just attribute types like
# optional 1st param above
$searchobj = $c->search(base => $basedn, filter => $ARGV[1],
                        attrs => \@attr);

Note that Net::LDAP takes a reference to an array, not values in the array like Mozilla::LDAP.

6.4.4. Entry Representation in Perl

These code samples may provoke some questions about entry representation and manipulation -- that is, how are entries themselves stored and manipulated in a Perl program? Let's answer a few of those questions as a follow-up to our LDAP searching discussion, even though we will discuss them more thoroughly in the upcoming sections on addition and modification of entries.

If you've just done a search using Mozilla::LDAP and have an entry object instance in hand, you can access the individual attributes in that entry using the Perl hash-of-lists syntax. $entry->{attributename} is a list of the values for that attribute name. I emphasize "list" because even single-valued attributes are stored in an anonymous list referenced through this hash key. To get at the value in a single-valued attribute like this, you would use $entry->{attributename}->[0]. Table 6-3 lists some more methods from Mozilla::LDAP::Entry that come in handy when retrieving entry attributes.

Table 6.3. Mozilla::LDAP::Entry Methods

Method Call

Returns

$entry->exists($attrname)

true if that entry has an attribute with this name

$entry->hasValue($attrname,$attrvalue)

true if that entry has a named attribute with this value

$entry->matchValue($attrname,$attrvalue)

Same as above, except performs a regular expression match for attribute value

$entry->size($attrname)

The number of values for that attribute (usually 1 unless the attribute is multivalued)

Some of these methods have additional parameters; see the documentation on Mozilla::LDAP::Entry for more information.

You could probably tell from the sample search code that the method for accessing Net::LDAP entry attributes is a bit different. After conducting a search, all of the results are available encapsulated by a single object. To get at the individual attributes for the entries in this object, you can take one of two approaches.

First, you could ask the module to convert all of the returned entries into one large user-accessible data structure. $searchobj->as_struct( ) returns a hash-of-hash-of-lists data structure. It returns a reference to a hash whose keys are the DNs of the returned entries. The values for these keys are references to anonymous hashes keyed on the attribute names. These keys yield references to anonymous arrays that hold the actual values for those attributes. Figure 6-1 makes this clearer.

figure

Figure 6.1. Data structure returned by as_struct( )

To print the first value for the cn attributes for all of the entries in this data structure, you could use code like this:

$searchstruct = $searchobj->as_struct;
for (keys %$searchstruct){
    print $searchstruct->{$_}{cn}[0],"\n";
}

Alternatively, you can first use any one of these methods to unload an individual entry object from the object returned by a search:

# return a specific entry number
$entry   = $searchobj->entry($entrynum);

# acts like Perl shift(  ) on entry list
$entry   = $searchobj->shift_entry;      

# acts like Perl pop(  ) on entry list
$entry   = $searchobj->pop_entry;        

# return all of the entries as a list 
@entries = $searchobj->entries;

Once you have an entry object, you can use one of the method calls in Table 6-4.

Table 6.4. Net::LDAP Entry Methods

Method Call

Returns

$entry->get($attrname)

The value of that attribute in the given entry

$entry->attributes( )

The list of attribute names for that entry

It is possible to chain these method calls together in a fairly legible fashion. For instance, this line of code will retrieve the value of the cn attribute in the first returned entry:

$value = $searchobj->entry(1)->get(cn)

Now that you know how to access individual attributes and values returned by a search, let's look at how to get this sort of data into a directory server in the first place.

6.4.5. Adding Entries with LDIF

Before we look at the generic methods for adding entries to an LDAP directory, let's stay true to the title of this book and look at a technique useful mostly to system and directory administrators. This technique uses a data format that helps you to bulk load data into a directory server. We're going to look at ways of writing and reading LDIF.

LDIF, defined in a set of pre-RFC draft standards at the time of this writing, offers a simple text representation of a directory entry. Here's a simple LDIF example taken from the latest draft standard by Gordon Good:

version: 1 
   dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com   
   objectclass: top
   objectclass: person
   objectclass: organizationalPerson
   cn: Barbara Jensen
   cn: Barbara J Jensen
   cn: Babs Jensen                 
   sn: Jensen       
   uid: bjensen
   telephonenumber: +1 408 555 1212
   description: A big sailing fan.

   dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com
   objectclass: top   
   objectclass: person
   objectclass: organizationalPerson
   cn: Bjorn Jensen  
   sn: Jensen 
   telephonenumber: +1 408 555 1212

The format should be almost self-explanatory to you by now. After the LDIF version number, each entry's DN, objectClass definitions, and attributes are listed. A line separator alone on a line (i.e., a blank line) separates individual entries.

Our first task is to learn how to write LDIF files from extant directory entries. In addition to giving us practice data for the next section (where we read LDIF files), this functionality is useful because once we have an LDIF file, we can massage it any way we like using Perl's usual text manipulation idioms.

You've already seen how to print out entries in LDIF format during our discussion of LDAP searches. Let's change the code we used in that example so it writes to a file instead:

use Mozilla::LDAP::Conn;
use Mozilla::LDAP::LDIF;

<perform bind & search>

open(LDIF,">$LDIFfile") or die "Unable to write to $LDIFfile:$!\n";
# create new LDIF object and pass in destination filehandle
$ldif = new Mozilla::LDAP::LDIF(\*LDIF); 

while ($entry) {
    $ldif->writeOneEntry($entry);
    $entry = $c->nextEntry(  );
}

$c->close(  );
close(LDIF);

Mozilla::LDAP also has a writeEntries( ) method that can take an array of entries instead of just a single entry and write them out in a similar fashion.

For Net::LDAP, changing our original code is even simpler. Instead of:

$ldif = new Net::LDAP::LDIF("-");

we use:

$ldif = new Net::LDAP::LDIF($filename, "w");

to print the output to the specified filename instead of the standard output channel.

Let's work in the opposite direction now, reading LDIF files instead of writing them. The module object methods we're about to explore will allow us to easily add entries to a directory.[3]

[3]LDIF files can also contain a special changetype: directive that instructs the LDIF reader to delete or modify entry information rather than just add it. Of the two modules we've been working with, only Net::LDAP has direct support for changetype: via its Net::LDAP::LDIF::read_cmd( ) method.

When you read LDIF data in via Perl, the process is exactly the reverse of what we used in the previous LDIF-writing examples. Each entry listing in the data gets read in and converted to an entry object instance that is later fed to the appropriate directory modification method. Both modules handle the data reading and parsing for you, so this is a relatively painless process. For instance, with Mozilla::LDAP, we can use code like the following:

use Mozilla::LDAP::Conn;
use Mozilla::LDAP::LDIF;

$server   = $ARGV[0];
$LDIFfile = $ARGV[1];
$port     = getservbyname("ldap","tcp") || "389";
$rootdn   = "cn=Manager, ou=Systems, dc=ccs, dc=hogwarts, dc=edu";
$pw       = "secret";

# read in an LDIF file specified as the second argument 
# on the command line 
open(LDIF,"$LDIFfile") or die "Unable to open $LDIFfile:$!\n";
$ldif = new Mozilla::LDAP::LDIF(\*LDIF);

# parse all of the entries, store in @entries
@entries = $ldif->readEntries(  ); 
close(LDIF);

# non-anonymous bind
$c = new Mozilla::LDAP::Conn($server,$port,$rootdn,$pw); 
die "Unable to bind to $server\n" unless $c;

# iterate through our parsed entry list, attempting to add one at a time
for (@entries){
    $c->add($_); # add this entry to the directory
    warn "Error in add for ". $_->getDN().": ".$c->getErrorString(  )."\n"
      if $c->getErrorCode(  );
}
$c->close(  );

This sample also demonstrates the use of the getErrorCode( ) and getErrorString( ) methods to retrieve and report any errors that occur during the data load. Errors can manifest for any number of reasons, including DN/RDN duplication, schema violations, hierarchy problems, etc., so it is important to check for them as part of any entry modification.

One other quick note before we move on to the Net::LDAP equivalent: in this and the rest of the examples we're using the root or manager DN user context for demonstration purposes. In general, if you can avoid using this context for everyday work, you should. Good practice for setting up an LDAP server includes creating a powerful account or account group (which is not the root DN) for directory management. Keep this security tip in mind as you code your own applications.

With Net::LDAP, the LDIF entry addition code is similar:

use Net::LDAP;
use Net::LDAP::LDIF;

$server   = $ARGV[0];
$LDIFfile = $ARGV[1];
$port     = getservbyname("ldap","tcp") or "389";
$rootdn   = "cn=Manager, ou=Systems, dc=ccs, dc=hogwarts, dc=edu";
$pw       = "secret";

# read in an LDIF file specified as the second argument
# on the command line
# last parameter is "r" for open for read, "w" for write
$ldif = new Net::LDAP::LDIF($LDIFfile,"r"); 
@entries = $ldif->read(  );

$c = new Net::LDAP($server, port => $port) or 
    die "Unable to connect to $server: $@\n";
$c->bind(dn => $rootdn, password => $pw) or die "Error in bind: $@\n";

for (@entries){
    $res = $c->add($_);
    warn "Error in add for ". $_->dn(  ).": error code ".$res->code."\n"
      if $res->code(  );
}

$c->unbind(  );

A few notes about this code sample:

@entries = new Net::LDAP::LDIF($LDIFfile,"r")->read;
sub LDAP_ALREADY_EXISTS            (  ) { 0x44 }

Now we know we've attempted to add an entry from an LDIF file that already exists in the directory.

6.4.6. Adding Entries with Standard LDAP Operations

It's time we look under the hood of the entry addition process so we can learn how to create and populate entries manually, instead of just reading them from a file like we did in the last subsection. Our two available modules handle this process quite differently, so we'll have to deal with them separately. Mozilla::LDAP is closer to classic object-oriented programming style. We create a new object instance:

use Mozilla::LDAP::Entry;
$e = new Mozilla::LDAP::Entry(  )

and then we begin to populate it. The next step is to give the entry a DN. This accomplished with the setDN( ) method:

$e->setDN("uid=jay, ou=systems, ou=people, dc=ccs, dc=hogwarts, dc=edu");

To populate the other attributes like objectClass we can take one of two tacks. We can make some assumptions about the underlying data structure used to represent the entry (essentially a hash of lists) and populate the data structure directly:

$e->{cn} = ['Jay Sekora'];

Here we're using the attribute name for the hash key and a reference to an anonymous array that holds the data. The Mozilla::LDAP module expects an array reference as the hash value, not the data itself, so the following, as tempting as it might be, would be incorrect:

# evil incarnate (or at the very least, just wrong)
$e->{cn} = 'Jay Sekora';

Alternatively, we can play it safe and use an object method to add the data:

$e->addValue('cn', 'Jay Sekora');

To add multiple values to an attribute, you simply call addValue( ) repeatedly:

$e->addValue('title', 'Unix SysAdmin');
$e->addValue('title', 'Part-time Lecturer');

I'm partial to the second method because your code is less likely to break if future versions of the module change the underlying data representation.

Once you've populated your entry, you call the add( ) method to add this entry to the directory. Here's a small script that will add an entry to a directory. It takes a server, a user ID (to be used as part of a DN), and a common name as commandline arguments:

use Mozilla::LDAP::Conn;

$server   = $ARGV[0];
$port     = getservbyname("ldap","tcp") || "389";
$suffix   = "ou=People, ou=Systems, dc=ccs, dc=hogwarts, dc=edu";
$rootdn   = "cn=Manager, ou=Systems, dc=ccs, dc=hogwarts, dc=edu";
$pw       = "secret";

# non-anonymous bind 
$c = new Mozilla::LDAP::Conn($server,$port,$rootdn,$pw); 
die "Unable to bind to $server\n" unless $c;

$e = new Mozilla::LDAP::Entry;
# DN is uid plus a suffix detailing where to put this 
# in the directory tree
$e->setDN("uid=$ARGV[1],$suffix"); 
$e->addValue('uid', $ARGV[1]);
$e->addValue('cn', $ARGV[2]);
$c->add($e);
die "Error in add: ". $c->getErrorString()."\n" if $c->getErrorCode(  );

Note that this code does no input error checking. If you are putting together a script that might really be used interactively, you'll need to check your input to make sure it does not include non-escaped special characters like commas. See the owl tip on attribute value quoting earlier in this section for more details.

Let's turn our attention to Net::LDAP. The entry addition process for Net::LDAP can be less object-oriented if you are so inclined. It too has an Entry module (Net::LDAP::Entry) and a constructor for an entry object instance. But it also contains an add( ) function that can take a naked data structure for single-step entry addition:

$res = $c->add(
    dn => 'uid=jay, ou=systems, ou=people, dc=ccs, dc=hogwarts, dc=edu',
    attr => [ 'cn'   => 'Jay Sekora',
              'sn    => 'Sekora',
              'mail' => 'jayguy@ccs.hogwarts.edu',
              'title'=> ['Sysadmin','Part-time Lecturer'],
              'uid'  => 'jayguy',
             ]
             );
die "unable to add, errorcode #".$res->code() if $res->code(  );

Here we are passing two arguments to add( ). The first is a DN for the entry; the second is a reference to an anonymous array of attribute-value pairs. You'll notice that multivalued attributes like title are specified using a nested anonymous array. If you are used to working with Perl data structures and have an aversion to the object-oriented programming style, this may be a more comfortable idiom.

6.4.7. Deleting Entries

Deleting entries from a directory is easy (and irrevocable, so be careful). Here's some code snippets, again with the bind code left out for brevity's sake:

use Mozilla::LDAP::Conn;
...
# if you have an entry in hand, you can use 
# $c->delete($entry->getDN(  )) instead
$c->delete($dn) or 
    die "unable to delete entry: ". $c->getErrorString(  )."\n";

use Net::LDAP;
...
$res = $c->delete($dn);
die "unable to delete, errorcode #".$res->code() if $res->code(  );

It is important to note that delete( ) in both modules operates on a single entry at a time. If you want to delete an entire sub-tree, you will need to first search for all of the child entries of that sub-tree using a scope of sub or one and then iterate through the return values, deleting as you go. Once the children have been deleted, then you can remove the top of that sub-tree.

6.4.8. Modifying Entry Names

For our final look at LDAP operations, we will focus on two kinds of modifications to LDAP entries. The first kind of modification we'll consider is a change of DN or RDN. Changing the RDN for an entry is easy and supported by both of our modules. Here's the Mozilla::LDAP version:

use Mozilla::LDAP::Conn;
...
$c->modifyRDN($newRDN,$oldDN,$delold) or
    die "unable to rename entry:". $c->getErrorString(  )."\n";

This code should be self-explanatory with the exception of the $delold parameter to modifyRDN( ). When true, this instructs the LDAP libraries to remove the values in the entry that match the values changed in the RDN. For example, if an entry's RDN included the attribute l (for location), and the RDN was changed, the old l attribute in the entry itself would be deleted, leaving only the new value.

Here's the equivalent Net::LDAP code to rename an entry:

use Net::LDAP;
...
$res = $c->moddn($oldDN, 
                 newrdn       => $newRDN, 
                 deleteoldrdn => 1);
die "unable to rename, errorcode #".$res->code() if $res->code(  );

Net::LDAP's moddn( ) is actually more powerful than this example shows. So far we've only changed an entry's RDN, a change that does not have an effect on that entry's location in the directory tree hierarchy. Version 3 of LDAP introduces a more powerful rename operation that allows arbitrary entry relocations within the directory tree hierarchy. moddn( ) gives you access to this capability when called with the additional parameter newsuperior. If we add it like so:

$result = $c->moddn($oldDN, 
                    newrdn       => $newRDN,
                    deleteoldrdn => 1, 
                    newsuperior  => $parentDN);
die "unable to rename, errorcode #".$res->code() if $res->code(  );

then the entry located at $oldDN will be moved to become the child of the DN specified in $parentDN. Using this method to move entries in a directory tree is more efficient than the add( ) or delete( ) sequence previously required by the protocol, but is not supported by all LDAP servers. In any case, if you've carefully designed your directory tree structure, you'll have to relocate entries less often.

6.4.9. Modifying Entry Attributes

Let's move on to the more common operation of modifying the attributes and attribute values in an entry. Here too we see a significant difference between Mozilla::LDAP and Net::LDAP. With Mozilla::LDAP, we use one of the method calls in Table 6-5 to change the attributes in an entry.

Table 6.5. Mozilla::LDAP Entry Modification Methods

Method Call

Effect

$entry->addValue($attrname,$attrvalue)

Adds the specified value to the named attribute in that entry.

$entry-> removeValue($attrname,$attrvalue)

Removes the specified value from the named attribute in that entry. If this value is the only value set for that attribute, the whole attribute is removed.

$entry-> setValue($attrname,$attrvalue1,...)

Changes the values for the named attribute to the specified value or values.

$entry-> remove($attrname)

Removes the named attribute (values and all) from that entry.

Once you've made all of your changes to an entry using these method calls, you must call the update( ) method for that LDAP connection to propagate your changes to the directory server. update( ) is called with a reference to the entry as an argument (i.e., $c->update($entry)).

Let's see these method calls in action as part of a global search and replace. Here's the scenario: one of the facilities at your company is being forced to move from Boston to Indiana. This code will change all of the entries with a Boston location:

use Mozilla::LDAP::Conn;

$server = $ARGV[0];
$port   = getservbyname("ldap","tcp") || "389";
$basedn = "dc=ccs,dc=hogwarts,dc=edu";
$scope  = "sub";
$rootdn = "cn=Manager, ou=Systems, dc=ccs, dc=hogwarts, dc=edu";
$pw     = "secret";

# non-anonymous bind 
$c = new Mozilla::LDAP::Conn($server,$port,$rootdn,$pw); 
die "Unable to bind to $server\n" unless $c;

# notice that we ask for the least amount of info 
# possible for a speedy search
$entry = $c->search($basedn,$scope,"(l=Boston)",1,'');
die "Error in search:". $c->getErrorString()."\n" if $c->getErrorCode(  );

if ($entry){
    while($entry){
        $entry->removeValue("l","Boston");
        $entry->addValue("l","Indiana");
        $c->update($entry);
        die "Error in update:" . $c->getErrorString(  ) . "\n" 
          if $c->getErrorCode(  );
        $entry = $c->nextEntry(  );
    };
}
$c->close(  );

Net::LDAP takes a different approach for entry modification. It crams all of the separate Mozilla::LDAP method calls we just saw into one mega-method called modify( ). The parameters passed to this method determine its functionality. Table 6-6 lists the possible choices.

Table 6.6. Net::LDAP Entry Modification Methods

Parameter

Effect

add => {$attrname => $attrvalue}

Adds a named attribute with the given value.

add => {$attrname => [$attrvalue1,
$attrvalue2...]}

Adds a named attribute with the specified set of values.

delete => {$attrname => $attrvalue}

Deletes a named attribute with a specific value.

delete => {$attrname => []}
delete => [$attrname1,$attrname2...]

Deletes an attribute or set of attributes independent of their value or values.

replace => {$attrname => $attrvalue}

Like add, but replaces the current named attribute value. If $attrvalue is a reference to an empty anonymous list ([]), this becomes a synonym for the delete operation above.

Be sure to pay attention to the punctuation in the previous table. Some parameters call for a reference to an anonymous hash, others call for a reference to an anonymous array. Mixing the two will cause problems.

We can combine several of these parameters in the same call to modify( ), but there's a potential problem. When you modify( ) with a set of these parameters like so:

$c->modify($dn,replace => {'l' => "Medford"},
               add     => {'l' => "Boston"},
               add     => {'l' => "Cambridge"});

there's no guarantee the additions specified will take place after the replacement. If you need your operations to take place in a specific order, you can use a similar syntax to the one we've just introduced. Instead of using a set of discrete parameters, pass in a single array containing a queue of commands. Here's how it works: modify( ) will take a changes parameter whose value is a list. This list is treated as a set of pairs. The first half of the pair is the operation to be performed; the second half is a reference to an anonymous array of data for that operation. For instance, if we wanted to insure that the operations in the previous code snippet happened in order, we could write:

$c->modify($dn, changes =>
                  [ replace => ['l' => "Medford"],
                    add     => ['l' => "Boston"],
                    add     => ['l' => "Cambridge"]
                  ]);

Take careful note of the punctuation: it is different from the other parameters we saw before.

Given this information on modify( ), we can write the Net::LDAP version of our previous Boston-to-Indiana code like this:

use Net::LDAP;

$server   = $ARGV[0];
$port     = getservbyname("ldap","tcp") || "389";
$basedn   = "dc=ccs,dc=hogwarts,dc=edu";
$scope    = "sub";
$rootdn   = "cn=Manager, ou=Systems, dc=ccs, dc=hogwarts, dc=edu";
$pw       = "secret";

$c = new Net::LDAP($server, port => $port) or
  die "Unable to init for $server: $@\n";
$c->bind(dn => $rootdn, password => $pw) or die "Error in bind: $@\n";

$searchobj = $c->search(base  => $basedn, filter => "(l=Boston)",
                        scope => $scope,  attrs  => [''], 
                        typesonly => 1);
die "Error in search: ".$searchobj->error()."\n" if ($searchobj->code(  ));

if ($searchobj){
    @entries = $searchobj->entries;
    for (@entries){
        $res=$c->modify($_->dn(), # dn(  ) yields the DN of that entry
                   delete => {"l" => "Boston"},
                   add    => {"l" => "Indiana"});
        die "unable to modify, errorcode #".$res->code() if $res->code(  );
    }
}

$c->unbind(  );

6.4.10. Putting It All Together

Now that we've toured all of the major LDAP functions, let's write some small system administration-related scripts. We'll import our machine database from Chapter 5, "TCP/IP Name Services", into an LDAP server and then generate some useful output based on LDAP queries. Here are a couple of listings from that flat file, just to remind you of the format:

name: shimmer
address: 192.168.1.11
aliases: shim shimmy shimmydoodles
owner: David Davis
department: software
building: main
room: 909
manufacturer: Sun
model: Ultra60
-=-
name: bendir
address: 192.168.1.3
aliases: ben bendoodles
owner: Cindy Coltrane
department: IT
building: west
room: 143
manufacturer: Apple
model: 7500/100
-=-

The first thing we need to do is prepare the directory server to receive this data. We're going to use non-standard attributes, so we'll need to update the server's schema. Different servers handle this process in different ways. For instance, the Netscape Directory server has a pleasant Directory Server Console GUI for changing details like this. Other servers require modifications to a text configuration file. With OpenLDAP, you could use something like this in a file included by the master configuration file to define your own object class for a machine:

objectclass machine
        requires
                objectClass,
                cn
        allows
                address,
                aliases,
                owner,
                department,
                building,
                room,
                manufacturer,
                model

Once the server is configured properly, we can think about importing the data. One approach would be to bulk load it using LDIF. If the sample from our flat-file database shown above reminded you of the LDIF format, you were right on target. This similarity makes the translation easy. Still, we'll have to watch out for a few snares:

Continuation lines

Our flat-file database does not have any entries with values spanning several lines, but if it did we'd need to make sure that output conformed to the LDIF standard. The LDIF standard dictates that all continuation lines must begin with exactly one space.

Entry separators

Our database uses the adorable character sequence -=- between each entry. Two line separators (i.e., a blank line) must separate LDIF entries, so we'll need to axe this character sequence when we see it in the input.

Attribute separators

Right now our data has only one multivalued attribute: aliases. LDIF deals with multivalued attributes by listing each value on a separate line. If we encounter multiple aliases, we'll need special code to print out a separate line for each. If it weren't for this misfeature in our data format, the code to go from our format to LDIF would be a single line of Perl.

Even with these snares, the conversion program is still pretty simple:

$datafile = "database";
$recordsep = "-=-\n";
$suffix   = "ou=data, ou=systems, dc=ccs, dc=hogwarts, dc=edu";
$objectclass = <<EOC;
objectclass: top
objectclass: machine
EOC

open(DATA,$datafile) or die "unable to open $datafile:$!\n";

# Perl modules break with this, even if it is in the spec 
# print "version: 1\n"; # 

while (<DATA>) {
    # print the header for each entry
    if (/name:\s*(.*)/){ 
        print "dn: cn=$1, $suffix\n";
        print $objectclass;
        print "cn: $1\n";
        next;
    }
    # handle the multi-valued aliases attribute
    if (s/^aliases:\s*//){
        @aliases = split;
        foreach $name (@aliases){
            print "aliases: $name\n";
        }
        next;
    }
    # handle the end of record separator
    if ($_ eq $recordsep){
        print "\n";
        next;
    }
    # otherwise, just print the attribute as we found it
    print;
}

close(DATA);

If we run this code, it prints an LDIF file that looks (in part) like this:

dn: cn=shimmer, ou=data, ou=systems, dc=ccs, dc=hogwarts, dc=edu
objectclass: top
objectclass: machine
cn: shimmer
address: 192.168.1.11
aliases: shim
aliases: shimmy
aliases: shimmydoodles
owner: David Davis
department: software
building: main
room: 909
manufacturer: Sun
model: Ultra60

dn: cn=bendir, ou=data, ou=systems, dc=ccs, dc=hogwarts, dc=edu
objectclass: top
objectclass: machine
cn: bendir
address: 192.168.1.3
aliases: ben
aliases: bendoodles
owner: Cindy Coltrane
department: IT
building: west
room: 143
manufacturer: Apple
model: 7500/100
...

With this LDIF file, we can use one of the bulk-load programs that come with our servers to load our data into the server. For instance, ldif2ldbm, packaged with both the OpenLDAP and the Netscape Directory Servers, reads an LDIF file and directly imports it into the directory server's native backend format without having to go through LDAP. Though you can only use this program while the server is not running, it can provide the quickest way to get lots of data into a server. If you can't take the server down, we can use the LDIF-reading Perl code we developed earlier to feed a file like this to an LDAP server.

To throw one more option into the mix, here's some code that skips the intermediary step of creating an LDIF file and imports our data directly into an LDAP server:

use Net::LDAP;
use Net::LDAP::Entry;

$datafile  = "database";
$recordsep = "-=-";
$server    = $ARGV[0];
$port      = getservbyname("ldap","tcp") || "389";
$suffix    = "ou=data, ou=systems, dc=ccs, dc=hogwarts, dc=edu";
$rootdn    = "cn=Manager, o=University of Michigan, c=US";
$pw        = "secret";

$c = new Net::LDAP($server,port => $port) or 
  die "Unable to init for $server: $@\n";
$c->bind(dn => $rootdn,password => $pw) or die "Error in bind: $@\n";

open(DATA,$datafile) or die "unable to open $datafile:$!\n";

while (<DATA>) {
    chomp;
    # at the start of a new record, create a new entry object instance
    if (/^name:\s*(.*)/){
        $dn="cn=$1, $suffix";
        $entry = new Net::LDAP::Entry;
        $entry->add("cn",$1);
        next;
    }
    # special case for multivalued attribute
    if (s/^aliases:\s*//){
        $entry->add('aliases',[split(  )]);
        next;
    }

    # if we've hit the end of the record, add it to the server
    if ($_ eq $recordsep){
        $entry->add("objectclass",["top","machine"]);
        $entry->dn($dn);
        $res = $c->add($entry);
        warn "Error in add for " . $entry->dn(  ) . ": error code " . 
              $res->code."\n" 
          if $res->code(  );
        undef $entry;
        next;
    }

    # add all of the other attributes
    $entry->add(split(':\s*')); # assume single valued attributes
}

close(DATA);
$c->unbind(  );

Now that we've imported the data into a server, we can start to do some interesting things. For the following examples, we'll flip-flop between the two LDAP modules. To save space, the header at the top of each sample that sets our configuration variables and the code that binds us to a server will not be repeated for each example.

So what can you do with this data when it resides in an LDAP server? You can generate a hosts file on the fly:

use Mozilla::LDAP;

...
$entry = $c->search($basedn,'one','(objectclass=machine)',0,
                    'cn','address','aliases');
die "Error in search:". $c->getErrorString()."\n" if $c->getErrorCode(  );

if ($entry){
    print "#\n\# host file - GENERATED BY $0\n
           # DO NOT EDIT BY HAND!\n#\n";
    while($entry){
        print $entry->{address}[0],"\t",
              $entry->{cn}[0]," ",
              join(' ',@{$entry->{aliases}}),"\n";
        $entry = $c->nextEntry(  );
    };
}
$c->close(  );

Here's the output:

#
# host file - GENERATED BY ldap2hosts
# DO NOT EDIT BY HAND!
#
192.168.1.11    shimmer shim shimmy shimmydoodles
192.168.1.3     bendir ben bendoodles
192.168.1.12    sulawesi sula su-lee
192.168.1.55    sander sandy mickey mickeydoo

You can find the names of all of our machines made by Apple:

use Net::LDAP;
...
$searchobj = $c->search(base  => $basedn, 
                        filter => "(manufacturer=Apple)",
                        scope => 'one', attrs => ['cn']);
die "Error in search: ".$searchobj->error()."\n" if ($searchobj->code(  ));

if ($searchobj){
    for ($searchobj->entries){
        print $_->get('cn'),"\n";
    }
}

$c->unbind(  );

Here's the output:

bendir
sulawesi

You can generate a list of machine owners:

use Mozilla::LDAP;
...
$entry = $c->search($basedn,'one','(objectclass=machine)',0,
                    'cn','owner');
die "Error in search:". $c->getErrorString()."\n" if $c->getErrorCode(  );

if ($entry){
    while($entry){
        push(@{$owners{$entry->{owner}[0]}},$entry->{cn}[0]);
        $entry = $c->nextEntry(  );
    };
}
$c->close(  );
for (sort keys %owners){
    print $_.":\t".join(' ',@{$owners{$_}})."\n";
}

Here's the output:

Alex Rollins:   sander
Cindy Coltrane: bendir
David Davis:    shimmer
Ellen Monk:     sulawesi

Or you can check to see if the current user ID is the owner of the current Unix machine (pseudo-authentication):

use Mozilla::LDAP::Conn;
use Sys::Hostname;

$user = (getpwuid($<))[6];

$hostname = hostname;
$hostname =~ s/^([^.]+)\..*/$1/; # strip domain name off of host
...
$entry = $c->search("cn=$hostname,$suffix",'base',"(owner=$user)",1,'');

if ($entry){
    print "Owner ($user) logged on to machine $hostname.\n";
}
else {
    print "$user is not the owner of this machine ($hostname)\n.";
}
$c->close(  );

These snippets should give you an idea of some of the system administration uses for LDAP access through Perl, and provide inspiration to write your own. In the next section we'll take these ideas to the next level and see a whole administration framework based on the conceptual groundwork laid by LDAP.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.