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 entryin 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 atomicthat 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

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
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;
}
•
Table of Contents
•
Index
•
Reviews
•
Reader Reviews
•
Errata
LDAP System Administration
By
Gerald Carter
Publisher
: O''''Reilly
Pub Date
: March 2003
ISBN
: 1-56592-491-6
Pages
: 308
If you want to be a master of your domain, LDAP
System Administration will help you get up and
running quickly regardless of which LDAP version you use.
After reading this book, even with no previous LDAP
experience, you''''ll be able to integrate a directory server
into essential network services such as mail, DNS, HTTP, and
SMB/CIFS.
