[ Team LiB ] Previous Section Next Section

10.2 Connecting, Binding, and Searching

To get started with the Net::LDAP module, we will write a basic LDAP query script named search.pl. This script illustrates the methods used to connect to a directory server and retrieve information. It begins by importing the Net::LDAP symbols via the use pragma:

#!/usr/bin/perl
use Net::LDAP;

After the module has been included, you can create a new instance of a Net::LDAP object. To create a new Net::LDAP instance, you need the hostname of the LDAP server to which the script should connect. The constructor allows several optional arguments, of which the most common and useful are:

port

The TCP port on which the directory server is listening. If this parameter is not defined, it defaults to the well-known LDAP port (389).

version

The LDAP version to be used when connecting to the server. The default is Version 2 in the 0.26 release. However, this is likely to change in the future. Always explicitly set the version parameter if your Perl program replies with LDAPv3 features (such as SASL or referrals).

timeout

The time in seconds that the module should wait when contacting the directory server. The default value of 120 seconds is sufficient for most situations, but for more complex searches or when communicating with a very large directory, it may be necessary to increase this value.

The next line of code establishes a connection to the host ldap.plainjoe.org on port 389 using Version 3 of the protocol. The returned value is a handle to a Net::LDAP object that can be used to retrieve and modify data in the directory.

$ldap = Net::LDAP->new ("ldap.plainjoe.org", port =>389,
                        version => 3 );

The script can bind to the directory after it obtains a handle to the LDAP server. By default, Net::LDAP uses an implicit anonymous bind, but it supports all the standard binds defined by the LDAPv3 RFCs (anonymous, simple, and SASL). For now, we only examine how to use a simple bind.

However, before binding to the server, call start_tls( ) to encrypt the connection; you don't want to send the user DN and password across the network in clear text. In its simplest form, the start_tls( ) method requires no parameters and appears as:

$ldap->start_tls(  );

Checking for Errors

Most of the Net::LDAP methods return an object with two methods for obtaining the function's return status. The code( ) method retrieves the integer return value from the method call that created the object, and the error( ) method returns a descriptive character string associated with the numeric code. The constants for the various LDAP errors are contained in the Net::LDAP::Constant module. Specific error codes can be included in your code by adding a line similar to the following one:

use Net::LDAP::Constant qw(LDAP_SUCCESS);

The following code tests for an error condition after some arbitrary LDAP call:

if ($result->code(  ) != LDAP_SUCCESS) {
        die $result->error(  );
   }

Because most methods indicate success with a return code of zero, this error check can be shortened to:

die $result->error(  ) if $result->code(  );

The Net::LDAP::Util module contains a few extra functions for obtaining more error information. The ldap_error_text function returns the descriptive POD text for the error code passed in as a parameter, and ldap_error_name returns the constant name for an integer (for example, if it is passed the integer 0, it returns the string LDAP_SUCCESS).

It is a good idea to check for errors after attempting to establish a secure communication channel; if start_tls( ) fails, and the script continues blindly, it might inadvertently transmit sensitive account information in the clear. To do so, save the result object returned by start_tls( ), and then use the code( ) method to find out whether start_tls( ) succeeded:

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

If the script tries to establish transport layer security with a server that does not support this extended operation, the error check displays an error message and exits:

Operations error at ./search.pl line XXX.

The actual error from Net::LDAP::Constant is LDAP_OPERATIONS_ERROR.

Now you can safely send the sensitive data to the server. A simple authenticated bind requires only a DN and a password. If neither are provided, the call attempts to establish an explicit anonymous binding (as opposed to the implicit bind used when bind( ) is not called at all). The following line seeks to bind your client to the directory as the entry cn=Gerald Carter,ou=people,dc=plainjoe,dc=org using the password hello. Once again, you use error( ) and code( ) to check the return status:

$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: [email protected]
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: [email protected]

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 <[email protected]>
## 
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(  );
    [ Team LiB ] Previous Section Next Section