10.4 Updating the Directory

Searching for objects in the directory is only the beginning. The real power of scripting is that it allows you to modify the directory; you can add entries, delete entries, and modify existing entries.

10.4.1 Adding New Entries

The first script, import.pl, reads the contents of an LDIF file (specified as a command-line argument) and adds each entry in the file to the directory. Here's a starting point; it resembles the last version of your search.pl script:

#!/usr/bin/perl
##
## Usage: ./import.pl filename
##
## Author: Gerald Carter <jerry@plainjoe.org>
## 
use Net::LDAP;
use Net::LDAP::LDIF;
      
## Connect and bind to the server.
$ldap = Net::LDAP->new ("ldap.plainjoe.org", port =>389,
                        version => 3 )
or die $!;
      
## Secure data and credentials.
$result = $ldap->start_tls(  );
die $result->error(  ) if $result->code(  );
      
## Bind to the server. The account must have sufficient privileges because you will 
## be adding new entries.
$result = $ldap->bind(
        "cn=Directory Admin,ou=people,dc=plainjoe,dc=org", 
         password => "secret");
die $result->error(  ) if $result->code(  );
      
## Open the LDIF file or fail. Check for existence first.
die "$ARGV[0] not found!\n" unless ( -f $ARGV[0] );
$ldif = Net::LDAP::LDIF->new ($ARGV[0], "r")
      or die $!;

Once the script has a handle to the input file, you can begin processing the entries. Net::LDAP::LDIF has an eof( ) method for detecting the end of input. The main loop continues until this check returns true.

while ( ! $ldif->eof ) {
    ## Get next entry and process input here.
}

Retrieving the next LDIF entry in the file is extremely easy because the Net::LDAP::LDIF module does all the work, including testing the file to ensure that its syntax is correct. If the next entry in the file is valid, the read_entry( ) method returns it as a Net::LDAP::Entry object.

$entry = $ldif->read_entry(  );

If the call to read_entry( ) fails, you can retrieve the offending line by invoking the error_lines( ) routine:

if ( $ldif->error(  ) ) {
    print "Error msg: ", $ldif->error(  ), "\n";
    print "Error lines:\n", $ldif->error_lines(  ), "\n";
    next;
}

If no errors occur, the script adds the entry it has read from the file to the directory by invoking the Net::LDAP add( ) method:

$result = $ldap->add( $entry );
warn $result->error(  ) if $result->code(  );

The final version of the loop looks like:

## Loop until the end-of-file.
while ( ! $ldif->eof(  ) ) {
    $entry = $ldif->read_entry(  );
      
    ## Skip the entry if there is an error. 
    if ( $ldif->error(  ) ) {
        print "Error msg: ", $ldif->error(  ), "\n";
        print "Error lines:\n", $ldif->error_lines(  ), "\n";
        next;
    }
      
    ## Log to STDERR and continue in case of failure.
    $result = $ldap->add( $entry );
    warn $result->error(  ) if $result->code(  );
}

Note that you test for an error after adding the entry to the directory. You can't assume that the entry was added successfully on the basis of a successful return from read_entry( ). read_entry( ) guarantees that the entry was syntactically correct, and gives you a valid Net::LDAP::Entry object, but other kinds of errors can occur when you add the object to a directory. The most common cause of failure at this stage in the process is a schema violation.

Now that you've finished the main loop, unbind from the directory server and exit:

$ldap->unbind(  );
exit(0);

10.4.2 Deleting Entries

The next script complements import.pl. It gives you the ability to delete an entire subtree from the directory by specifying its base entry. The delete( ) method of Net::LDAP requires a DN specifying which entry to delete. The rmtree.pl script accepts a DN from the command line (e.g., rmtree.pl "ou=test,dc=plainjoe,dc=org") and deletes the corresponding tree.

How should you implement this script? You could simply perform a subtree search and delete entries one at a time. However, if the script exits prematurely, it could leave nodes, or entire subtrees, orphaned. A disconnected directory is very difficult to correct. A more interesting and only slightly more complex approach is to delete entries from the bottom of the tree and work your way up. This strategy eliminates the possibility of leaving orphaned entries because the tree is always contiguous: you delete only leaf entries, which have no nodes underneath them.

To implement bottom-up deletion, perform a depth-first search using recursion and allow Perl to handle the stack for you. The DeleteLdapTree( ) subroutine introduced in this script deletes an entry only after all of its children have been removed. It does a one-level search at the root of the tree to be deleted, and then calls itself on each of the entries returned by that search.

#!/usr/bin/perl
##
## Usage: ./rmtree.pl DN
##
## Author: Gerald Carter <jerry@plainjoe.org>
## 
use Net::LDAP;
      
#######################################################
## Perform a depth-first search on the $dn, deleting entries from the bottom up.
## Parameters: $handle (handle to Net::LDAP object) 
##             $dn       (DN of entry to remove)  
sub DeleteLdapTree {
    my ( $handle, $dn ) = @_;
    my ( $result );
      
    $msg = $handle->search( base => $dn,
                            scope => one,
                            filter => "(objectclass=*)" );
    if ( $msg->code(  ) ) {
        $msg->error(  );
        return;
    }
      
    foreach $entry in ( $msg->all_entries ) {
        DeleteLdapTree( $handle, $entry->dn(  ) );
    }
      
    $result = $handle->delete( $dn );
    warn $result->error(  ) if $result->code(  );
      
    print "Removed $dn\n";
      
    return;
}

The driver for this script begins by connecting to a directory server and binding to the server as a specific user with appropriate privileges. By now, this code should be familiar:

## Connect and bind to the server.
$ldap = Net::LDAP->new ("ldap.plainjoe.org", port =>389,
                        version => 3 )
or die $!;
      
## Secure data and credentials.
$result = $ldap->start_tls(  );
die $result->error(  ) if $result->code(  );
      
## Bind to the server. The account must have sufficient privileges because you will
## be adding new entries.
$result = $ldap->bind(
        "cn=Directory Admin,ou=people,dc=plainjoe,dc=org", 
         password => "secret");
die $result->error(  ) if $result->code(  );

To begin the deletion process, the script verifies that the DN specified on the command line points to a valid directory entry:

$msg = $ldap->search( base => $ARGV[0],
                      scope => base,
                      filter => "(objectclass=*)" );
die $msg->error(  ) if $msg->code(  );

Once assured that the entry does in fact exist, the script makes a single call to the recursive DeleteLdapTree( ) routine, which does all the work:

DeleteLdapTree( $ldap, $ARGV[0] );

After the subtree is deleted, the script unbinds from the server and exits:

$ldap->unbind(  );
exit(0);

10.4.3 Modifying Entries

Now that you can add and delete entries, let's look at modifying data that already exists in the LDAP tree. There are two routines for making changes to entries in the directory. The update( ) method of Net::LDAP pushes an Entry object to the directory; to use this method, get a local copy of the Net::LDAP::Entry object you want to modify, make your changes, and then push the change to the server. The modify( ) method allows you to specify a list of changes, and performs those changes directly on the server, eliminating the need to start by obtaining a copy of the entry. Each mechanism has its own advantages and disadvantages. Pushing local changes to the directory is more intuitive, but not as efficient. However, before discussing the pros and cons of these approaches, you must become acquainted with the routines for manipulating a Net::LDAP::Entry client object.

10.4.3.1 Net::LDAP::Entry

The most common way to instantiate a Net::LDAP::Entry object is to call the search( ) method of Net::LDAP. If you need a blank entry, you can create one by invoking the Net::LDAP::Entry constructor (i.e., new). You can print the contents of an Entry by calling its dump( ) method, but you can also create a custom printing method by using various methods from the Net::LDAP::Entry and Net::LDAP::LDIF modules.

We'll start this new exercise by writing a custom printing function. The new function, named DumpEntry( ), accepts a Net::LDAP::Entry object as its only parameter. It then prints the entry's DN followed by each value of each attribute that it contains. Here's a complete listing of DumpEntry( ):

sub DumpEntry {
    my ( $entry ) = @_;
    my ( $attrib, $val );
       
    print $entry->dn(  ), "\n";
       
    foreach $attrib in ( $entry->attributes(  ) ) {
        foreach $val in ( $entry->get_value( $attrib ) ) {
            print $attrib, ": ", $val, "\n";
        }
    }
}

This code introduces three new methods:

dn( )

When called with no arguments, the dn( ) method returns the distinguished name of the entry as a character string. If you pass it a parameter, that parameter is used to set the entry's DN.

attributes( )

This method returns an array containing the entry's attributes.

get_value( )

In its most basic form, the get_value( ) routine accepts an attribute name and returns an array of values for that attribute.

To find out more about the Entry methods, type the following command at a shell prompt:

$ perldoc Net::LDAP::Entry

DumpEntry( ) acts just like the dump( ) method, in that it prints only the attributes and values that are stored in the local copy of the Net::LDAP::Entry object. Additional attributes may be stored in the directory.

Three methods manipulate an entry's attributes and values: add( ), delete( ), and replace( ). The add( ) method inserts a new attribute or value into an entry object. The following line of code adds a new email address for the entry represented by the scalar $e. If the attribute does not currently exist in the entry, it is added. If it does exist, the new value is added to any previous values.

$e->add ( "mail" => "jerry@plainjoe.org" );

The add( ) method does not perform any schema checking because it is working only with a local copy of the entry. If the mail attribute is not supported by the object classes assigned to the entry, you won't find out until you push the entry back to the directory server. Likewise, add( ) also allows you to assign multiple values to an attribute that allows only a single value (for example, the uidNumber attribute included in a posixAccount).

Multiple values can be assigned to a single attribute by using an array:

$e->add( "mail" => [ "jerry@plainjoe.org",  
                     "jerry@samba.org"] );

The add( ) method also supports adding multiple attributes with a single call:

$e->add( "mail" => "jerry@plainjoe.org", 
          "cn"  => "Gerald Carter" );

To erase an attribute from a local entry, call delete( ). This method accepts the attribute names that should be removed, either as a scalar value or as an array.

$e->delete ( [ "mail", "cn" ] );

It is possible to delete individual values from a multivalued attribute by passing an array of items to be removed. Here, I remove only jerry@samba.org from the entry's email addresses:

$e->delete( mail => [ "jerry@samba.org" ] );

Finally, you can delete an attribute (and all its associated values) and re-add it by calling replace( ). This method accepts attribute/value pairs in a similar fashion as add( ). The following line of code replaces all values assigned to the mail attribute with the new address jerry@plainjoe.org. If the attribute does not exist, it is inserted into the entry, just as if you had called add( ).

$e->replace( "mail" => "jerry@plainjoe.org" );

When working with a Net::LDAP::Entry object, remember that the client instance is only a copy, and that any changes you make affect only the local copy of the entry. The next section explains how to propagate these changes to the directory.

10.4.3.2 Pushing an updated entry back to the server

No changes made to a local copy of a Net::LDAP::Entry object are reflected in the directory until its update( ) method is called. To show how to update a directory, we will develop a simple script that allows a user to change her password. The script makes two assumptions:

  • Every user has an entry in the directory; a user's Unix login name matches the value of the uid attribute (e.g., a posixAccount object).

  • Every user can update their userPassword attribute values.

You need two additional modules for this program. Term::ReadKey allows you to read the user types without displaying them on the screen. Digest::MD5 provides a routine to generate a Base64-encoded md5 digest hash of a string. Here's how the script starts:

#!/usr/bin/perl 
      
use Net::LDAP;
use Term::ReadKey;
use Digest::MD5 qw(md5_base64);

You obtain the user's login name by looking up the UID of the running process (i.e., $<):

$username = getpwuid($>);
print "Changing password for user ", $username, "\n";

The script then performs some familiar LDAP connection setup:

$ldap = Net::LDAP->new( "ldap.plainjoe.org",
                        version => 3)
      or die $!;
$result = $ldap->start_tls(  );
die $result->error(  ) if $result->code(  );

Next, the program implicitly binds to the directory anonymously and attempts to locate the entry for the current user. The query is a subtree search using the filter (uid=$username). If the search finds multiple matches, it returns only the first entry. If no entry is found, the script complains loudly and exits.

$msg = $ldap->search( 
                base => "ou=people,dc=plainjoe,dc=org",
                scope => "sub",
                filter => "(uid=$username)" );
die $msg->error(  ) if $msg->code(  );
die "No such user in directory [$username]!\n" 
     if !$msg->count;

When you know that the user exists in the LDAP directory, prompt the user to type the old and new password strings. Ask for the new string twice, and then ensure that the user typed the same thing both times:

## Read old and new password strings. Use ReadMode to prevent the passwords from 
## being echoed to the screen.
ReadMode( 'noecho' );
print "Enter Old Password: ";
$old_passwd = chomp( ReadLine(0) );
print "\nEnter New Password: ";
$new_passwd = chomp( ReadLine(0) );
print "\nEnter New Password again: ";
$new_passwd2 = chomp( ReadLine(0) );
print "\n";
ReadMode( 'restore' );
      
## Check that new password was typed correctly.
if ( "$new_passwd" ne "$new_passwd2" ) {
        print "New passwords do not match!\n";
        exit (1);
}

More tidbits and code samples using the Term::ReadKey and other Perl modules can be found in Perl Cookbook by Tom Christiansen and Nathan Torkington (O'Reilly).

To convert the Net::LDAP::Search results to a single Net::LDAP::Entry object, the script calls the former's entry( ) method. This subroutine accepts an integer index to the array of entries produced by the previous search. In this case, we are concerned only with the first entry?in fact, we are assuming that the search returns only one entry:

$entry = $msg->entry(0);

The array of entries is not sorted in any particular order, so if you're dealing with multiple entries, this method call could conceivably return a different entry every time it is run. The best way to avoid this ambiguity is to choose an attribute that is unique within the directory subtree rooted at the search base.

You now have both the DN of the user's entry and the old password value. At this point, you can authenticate the user by binding to the directory server. If the bind fails, the script informs the user that the old password was incorrect, and exits:

$result = $ldap->bind( $entry->dn(  ), 
                       password => $old_passwd );
die "Old Password is invalid!\n" if $result->code(  );

All that remains is to update the user's password in the directory. This code is pretty trivial. The script uses the md5_base64( ) function from the Digest::MD5 module to generate the new password hash:

## Generate Base64 md5 hash of the new passwd.
$md5_pw = "{MD5}" . md5_base64($new_passwd) . "=  =";

The "= =" is appended to the password hash to pad the digest string so that its length is a multiple of four bytes. This is necessary for interoperability with other Base64 md5 digest strings and is described in the Digest::MD5 documentation. Next, overwrite the old password value by calling replace( ):

$entry->replace( userPassword => $md5_pw );

To propagate the change to the directory, call the update( ) method. This method accepts a handle to the Net::LDAP object representing the directory server on which the update will be performed.

$result = $entry->update( $ldap );
die $result->error(  ) if $result->code(  );

Now inform the user that her password has been updated, and exit:

print "Password updated successfully\n";
exit (0);

When executed, the output of passwd.pl looks similar to the standard Unix passwd utility:

$ ./passwd.pl
Changing password for user jerry
Enter Old Password: secret
Enter New Password: new-secret
Enter New Password again: new-secret
Password updated successfully
10.4.3.3 Modifying directory entries

Although LDAPv3 does not specify support for transactions across multiple entries, the RFCs indicate that changes to a single entry must be made atomically. When and why would you care about atomic updates? Assume that, on your network, all user accounts are created in a central LDAP directory using the posixAccount object class. Since it's a large network, you have several administrators, each of which may need to perform user management tasks at any time. You need to guarantee that their user management tool always obtains the next available numeric UID and GID without having to be concerned that two scripts running concurrently obtain the same ID number.

At this point, using the directory to store the currently available UID and GID values is the proverbial "no-brainer." What you need is a subroutine to retrieve the next free ID number and then store the newly incremented value. This operation must be atomic?that is, there must be no way for some other script to sneak in after you've read a value and read the same (unincremented) value. To support this, you need to introduce two new object classes, one for the uidPool and one for the gidPool. The schema for these two objects is illustrated in Figure 10-1.

Figure 10-1. uidPool and gidPool object classes
figs/ldap_1001.gif

Here's the implementation of the get_next_uid( ) function. It requires a handle to a Net::LDAP object as its only parameter. get_next_gid( ) is almost identical; I'll leave it to you to make the necessary modifications.

#########################################################
## Get the next available UID from the idPool. Spin until you get one.
##
sub get_next_uid {
    my ( $ldap ) = @_;
    my ( $uid, $msg, $entry );
    my ( @Add, @Delete, @Changes );

The logic of the function is:

  • Retrieve the next available uidNumber value from the uidPool entry.

  • Issue an LDAP modify request that attempts to delete the original uidNumber value, and store the old value incremented by 1 as the new uidNumber.

  • If the update fails, repeat the entire process until the modification succeeds.

The search and update steps are wrapped in a do . . . while loop to ensure that you have a valid UID upon exit. You perform a one-level search because the uidPool object is assumed to be stored directly under the search base (e.g., dc=plainjoe,dc=org). The actual location of the pool in the directory is an arbitrary choice, of course. If the search fails, either by returning an error or because of an empty list, get_next_uid( ) fails and returns an invalid UID value (-1):

do {
    $msg = $ldap->search( 
                base => "dc=plainjoe,dc=org", 
                scope => "one",
                filter => "(objectclass=uidPool)" );
    if ($msg->code ) {
        warn $msg->error;
        return -1;
    }
      
    if ( ! $msg->count ) {
        warn "Unable to locate uidPool entry!";
        return -1;
    }

To obtain the next available ID number, the function grabs the uidNumber attribute from the first entry returned by the search( ) call. The uidNumber attribute defined by the RFC 2307 schema is single-valued, so get_value( ) always returns a scalar value in this context:

$entry = $msg->entry(0);
$uid = $entry->get_value( 'uidNumber' );

The Net::LDAP modify( ) method requires the DN of the entry to be changed as the first parameter:

modify( DN, options );

The options specify which type of update to perform: add, delete, replace, or changes. The first three options accept a reference to a hash table of attributes and values. For example, this call deletes the mail attribute value jerry@plainjoe.org:

$ldap->modify( $entry->dn(  ), 
         delete => [ 'mail' => 'jerry@plainjoe.org' ] );

A single modify( ) call can make multiple changes of different types. Here, you delete an email address and add a phone number:

$ldap->modify( $entry->dn(  ), 
         delete => { 'mail' => 'jerry@plainjoe.org' },
         add    => { 'telephoneNumber' => '555-1234' } );

Using separate add and delete parameters, there are no guarantees about which update will be applied first, only that all the updates will be combined into a single LDAP modify message. The ordering of changes is important to get_next_uid( ) because the delete must precede the add. For this reason, get_next_uid( ) uses the changes parameter instead because it allows the programmer to specify how the modifications will be applied.

The changes option specifies a nested array of updates. At the top dimension of the array is a pair of items: the first is the modification type (add, delete, or replace), and the second is a reference to an array composed of attribute/value pairs. The add and delete options in the previous example can be represented using the changes option like so:

$ldap->modify( $entry->dn(  ), changes => 
          [ 'delete, [ 'mail', 'jerry@plainjoe.org' ],
            'add', ['telephoneNumber', '555-1234' ] ] );

It is often easier to understand these updates if they are placed in an actual array, rather than using an anonymous reference. The following code from get_next_uid( ) uses three arrays to store the changes. The first stores the delete request, the second stores the add request, and the third stores references to the previous two after indicating the type of change:

push ( @Delete, 'uidNumber', $uid );
push ( @Add, 'uidNumber', $uid+1 );
push ( @Changes, 'delete', \@Delete );
push ( @Changes, 'add', \@Add );
      
$result = $ldap->modify( $entry->dn(  ),
                  'changes' => [ @Changes ] );

If the modify( ) call fails, the script assumes that the delete operation failed because the uidNumber value did not match. Therefore, the $uid variable is set to -1 so that the loop will repeat:

    if ( $result->code ) { $uid = -1 }
      
} while ( $uid =  = -1 );

Finally, the routine returns the valid numeric UID to the caller:

    return $uid;
}

To wrap things up, here is the get_next_uid( ) function in its entirety:

########################################################
## Get the next available UID from the idPool. Spin until you get one.
##
sub get_next_uid {
    my ( $ldap ) = @_;
    my ( $uid, $msg, $entry );
    my ( @Add, @Delete, @Changes );
      
    do {
        ## Get the uidPool entry and perform error checking.
        $msg = $ldap->search( 
                    base => "dc=plainjoe,dc=org", 
                    scope => "one",
                    filter => "(objectclass=uidPool)" );
        if ($msg->code ) {
            warn $msg->error;
            return -1;
        }
      
        if ( ! $msg->count ) {
            warn "Unable to locate uidPool entry!";
            return -1;
        }
      
        ## Get the next UID. 
        $entry = $msg->entry(0);
        $uid = $entry->get_value( 'uidNumber' );
      
        ## Put the changes together to update the next UID in the directory.
        push ( @Delete, 'uidNumber', $uid );
        push ( @Add, 'uidNumber', $uid+1 );
        push ( @Changes, 'delete', \@Delete );
        push ( @Changes, 'add', \@Add );
      
        ## Update the directory.
        $result = $ldap->modify( $entry->dn(  ),
                          'changes' => [ @Changes ] );
        if ( $result->code ) { $uid = -1 }
      
    ## Do you need another round?
    } while ( $uid =  = -1 );
      
    ## All done
    return $uid;
}

This function would be invoked in a fashion similar to:

if ( ($nextuid=get_next_uid( $ldap )) =  = -1) {
    print "Unable to generate new uid!\n";
    exit 1;
}