LDAP System Administration
Page 29
}
}
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.
* * *
Note
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.
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);
}
* * *
Tip
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 i
s 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 successfullyn";
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
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
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.
&nbs
p; $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;
}
Advanced Net::LDAP Scripting
At this point, we've covered all the basics: binding to a server, reading, writing, and modifying entries. The remainder of the chapter covers more advanced programming techniques. We'll start by discussing how to handle referrals and references returned from a search operation.
References and Referrals
It's important for both software developers and administrators to understand the difference between a reference and a referral. These terms are often confused, probably because the term "referral" is overused or misused. As defined in RFC 2251, an LDAP server returns a reference when a search request cannot be completed without the help of another directory server. I have called this reference a "subordinate knowledge reference" earlier in this book. In contrast, a referral is issued when the server cannot service the request at all and instead points the client to another directory that may have more knowledge about the base search suffix. I have called this link a "superior knowledge reference" because it points the client to a directory server that has superior knowledge, compared to the present LDAP server. These knowledge references will be returned only if the client has connected to the server using LDAPv3; they aren't defined by LDAPv2.