LDAP System Administration
Page 28
$result = $ldap->bind("cn=Gerald Carter,ou=people,dc=plainjoe,dc=org",
password => "hello");
die $result->error( ) if $result->code( );
If there is no error, the script is free to search the directory. The search( ) method accepts the standard parameters that are expected from an LDAP query tool. At this point, we're interested only in the base, scope, and filter parameters. To make the script more flexible, use the first argument passed in from the command line (i.e., search.pl "Gerald Carter") to build a filter string that searches for the user's common name (cn):
$msg = $ldap->search(
base => "ou=people,dc=plainjoe,dc=org",
scope => "sub",
filter => "(cn=$ARGV[0])" );
The return value of the search is an instance of the Net::LDAP::Search object. You can manipulate this object to retrieve any entries that matched the search. This object has a count( ) method that returns the number of entries, and an all_entries( ) method that returns the results as an array of Net::LDAP::Entry objects, each of which represents information associated with a single directory node. You can view the results of this query by dumping each entry from the array:
if ( $msg->count( ) > 0 ) {
print $msg->count( ), " entries returned.n";
foreach $entry ( $msg->all_entries( ) ) {
$entry->dump( );
}
}
The output for a single entry looks like this:
dn:cn=Gerald Carter,ou=people,dc=plainjoe,dc=org
objectClass: inetOrgPerson
cn: Gerald Carter
sn: Carter
givenName: Gerald
o: Hewlett-Packard
mobile: 256.555.5309
mail: jerry@plainjoe.org
postalAddress: 103 Somewhere Street
l: Some Town
st: AL
postalCode: 55555-5555
The dump( ) routine is not meant to generate valid LDIF output, as can be seen from the extra whitespace added to center the attribute/value pairs; another module, aptly named Net::LDAP::LDIF, handles that feature. We'll discuss working with LDIF files later in this chapter. For now, just printing the attribute/value pairs in any form is good enough.
What if you're interested only in a person's email address? Some entries contain many attributes, and asking a user to look through all this output in search of an email address could qualify as cruel and unusual punishment. How can you modify the script so that it displays only the attributes you want? The search( ) function has an optional parameter that allows the caller to define an array of attribute names. The search returns only the values of attributes that match names in the list. Here's how to modify the script so that it retrieves only the mail and cn attributes:
$msg = $ldap->search(
base => "ou=people,dc=plainjoe,dc=org",
scope => "sub",
filter => "(cn=$ARGV[0])",
attrs => [ "cn", "mail" ] );
And here's what you get when you dump the entry returned by the modified query:
dn:cn=Gerald Carter,ou=people,dc=plainjoe,dc=org
cn: Gerald Carter
mail: jerry@plainjoe.org
The last line of the script invokes the unbind( ) method to disconnect from the directory:
$ldap->unbind( );
This routine effectively destroys the connection. The most portable means to rebind to an LDAP server using a new set of credentials is to call bind( ) again with the new DN and password (but only when using LDAPv3). Once the unbind( ) subroutine has been invoked, the connection should be thrown away and a new one created if needed.
The following listing shows the search.pl script in its entirety:
#!/usr/bin/perl
##
## Usage: ./search.pl name
##
## Author: Gerald Carter
##
use Net::LDAP;
## Connect and bind to the server.
$ldap = Net::LDAP->new ("ldap.plainjoe.org", port =>389,
version => 3 )
or die $!;
$result = $ldap->start_tls( );
die $result->error( ) if $result->code( );
$result = $ldap->bind(
"cn=Gerald Carter,ou=people,dc=plainjoe,dc=org",
password => "hello");
die $result->error( ) if $result->code( );
## Query for the cn and mail attributes.
$msg = $ldap->search(
base => "ou=people,dc=plainjoe,dc=org",
scope => "sub",
filter => "(cn=$ARGV[0])",
attrs => [ "cn", "mail" ] );
## Print resulting entries to standard output.
if ( $msg->count( ) > 0 ) {
print $msg->count( ), " entries returned.n";
foreach $entry ( $msg->all_entries( ) ) {
$entry->dump( );
}
}
## Unbind and exit.
$ldap->unbind( );
Working with Net::LDAP::LDIF
The search.pl script provided a simple introduction to retrieving data from an LDAP directory. However, the query results represented the state of the directory at a single point in time. The script has no good way to save the search results, and the way in which it prints the information is useful for humans, but not useful to any other LDAP tools. You need the ability to save the results in a format that can be parsed by other LDAP tools: in other words, you need to be able to read and write LDIF files directly from Perl code.
The Net::LDAP::LDIF module provides the ability to work with LDIF files. To introduce Net::LDAP::LDIF, we'll revisit search.pl and replace the call to dump( ) with code to produce valid LDIF output. Your first modification to the script is to add a second use pragma that imports the LDIF module:
use Net::LDAP::LDIF;
Next, the script must create a new instance of a Net::LDAP::LDIF object. Output from this object can be linked to an existing file handle such as STDOUT, as shown here:
$ldif = Net::LDAP::LDIF->new (scalar
or die $!;
It is possible to pass a filename to the new( ) method, as well as inform the module how this file will be used ("r" for read, "w" for write + truncate, and "a" for write + append). This line of code creates an LDIF output stream named result.ldif in the current directory:
$ldif = Net::LDAP::LDIF->new ("./result.ldif", "w")
or die $!;
It is best to use this code after you've run the search and confirmed that it produced some results. So, you open the file after the script has tested that $msg->count( ) > 0:
if ( $msg->count( ) > 0 ) {
print $msg->count( ), " entries returned.n";
$ldif = Net::LDAP::LDIF->new (scalar
or die $!;
Finally, replace the entire foreach loop that calls dump( ) on each entry with a single call to the write_entry( ) method of Net::LDAP::LDIF:
$ldif->write_entry($msg->all_entries( ));
write_entry( ) accepts either a single Net::LDAP::Entry or a one-dimensional array of these objects. The new loop is:
if ( $msg->count( ) > 0 ) {
print $msg->count( ), " entries returned.n";
$ldif = Net::LDAP::LDIF->new (scalar
or die $!;
$ldif->write_entry($msg->all_entries( ));
}
Now the output of the script looks like this:
dn: cn=Gerald Carter,ou=contacts,dc=plainjoe,dc=org
cn: Gerald Carter
mail: jerry@samba.org
This doesn't look like a big change, but it's an important one. Because the data is now in LDIF format, other tools such as ldapmodify can parse your script's output.
Once the script has created the LDIF output file, you can explicitly close the file by executing the done( ) method.
$ldif->done( );
This method is implicitly called whenever a Net::LDAP::LDIF object goes out of scope
.
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.
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
##
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);
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
##
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 $dnn";
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 deleting 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);
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 f
or 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.
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";
}