An Introduction to perl-ldap

by Paul Dwerryhouse

As systems get larger and the number of users they support increases, it becomes more difficult to manage systems using only the old-fashioned UNIX /etc/passwd file. A common solution to this problem is to use a Lightweight Directory Access Protocol (LDAP) server. The use of an LDAP server presents a problem to the system administrator, however, in that the contents of the database are no longer available in an easy to read or modify format. Hence, new tools must be written that allow standard, everyday tasks, such as adding or deleting users, to be performed.

This is where perl-ldap comes in. perl-ldap provides the Net::LDAP perl module, which enables easy access to the data contained in LDAP directories from Perl scripts. This makes the module a useful tool for system administrators and Web developers alike. The perl-ldap home page is located at http://perl-ldap.sourceforge.net/.

For this article, I assume you have a reasonable knowledge of LDAP and are a competent Perl programmer. If not, plenty of published material is available on the Internet covering both of these topics.

Installation

If you're running one of the popular Linux distributions, chances are perl-ldap already has been packaged for you, which makes installation simple. Under Debian Linux, perl-ldap is found in the libnet-ldap-perl package. Assuming that your /etc/apt/sources.list file contains an up-to-date Debian server, the following commands should install perl-ldap:

apt-get update 
apt-get install libnet-ldap-perl

Mandrake users will find what they need in the perl-ldap package; for Mandrake 9.1, the specific package is perl-ldap-0.27.01-1mdk.noarch.rpm. If you have urpmi configured correctly, you can install perl-ldap simply by entering:

urpmi perl-ldap

This command also installs the perl-Authen-SASL and perl-XML-Parser packages, which are perl-ldap dependencies in Mandrake.

Red Hat does not appear to provide a perl-ldap package, so users of this distribution either have to obtain it from another RPM-based distribution or install it from the tar.gz package as described below.

If a pre-built package isn't available for your system, you have to download the tar.gz package from CPAN and install it yourself. As the LDAP protocol uses ASN1 encodings, you also need the Convert::ASN1 library. Although you probably can install perl-ldap without it, perl-ldap certainly won't run unless this library available. Both of these libraries are easy to install:

perl Makefile.PL
make
make test
su root
make install            
Basic Usage

As with other Perl libraries, perl-ldap is invoked with the use statement:

use Net::LDAP 

A new LDAP connection is opened using the new() function call. In the following example, we open a connection to a machine with hostname ldapserver.domain.name:

$ldap = Net::LDAP->new("ldapserver.domain.name");

Because we haven't specified which port number to use, perl-ldap assumes a default of port 389, the well-known LDAP port. If we want to use a different port, say 1389, we need to pass the port parameter:

$ldap = Net::LDAP->new("ldapserver.domain.name", port=>1389);

If the server is not reachable, the above function calls return an error after 120 seconds. You can use the timeout parameter to alter this:

$ldap = Net::LDAP->new("ldapserver.domain.name", timeout=>30);

After the connection has been initiated, you no longer need to refer explicitly to the Net::LDAP package. All of the perl-ldap functions are accessed as methods of the reference returned from the new() call. The most commonly used methods provided by perl-ldap are as follows:

$ldap->add();           # Add an entry to the server
$ldap->bind();          # Bind to a directory server
$ldap->delete();        # Delete an entry from the server
$ldap->moddn();         # Modify an entry's Distinguished Name (DN)
$ldap->modify();        # Modify the contents of an entry
$ldap->search();        # Perform a search on a directory
$ldap->unbind();        # Unbind from a server

These are described in detail below.

Binding to the Server - bind()

For this example, we assume that I have an LDAP directory with the following contents:

dn: dc=leapster,dc=org
|
-- dn: cn=admin,dc=leapster,dc=org
|
-- dn: ou=People,dc=leapster,dc=org
   |
   -- dn: uid=paul,ou=People,dc=leapster,dc=org
   |
   -- dn: uid=mike,ou=People,dc=leapster,dc=org

Put simply, my LDAP base DN is dc=leapster,dc=org. The administrative user of the system (the entry that has superuser control) is cn=admin,dc=leapster,dc=org. It also contains two user entries, uid=paul and uid=mike.

Once you have created a connection to an LDAP server, you need to bind to it. If you're writing a program to talk to public LDAP directories, chances are you need to use only an anonymous bind:

$mesg = $ldap->bind;

However, if you're writing scripts to manage the directory of a server used for storing the account information of local users or customers, you are likely to allow only write access to specific, high-privilege users. In this case, you need to give the DN of the LDAP entry which has these privileges, as well as the password. For example:

$mesg = $ldap->bind("cn=admin,dc=leapster,dc=org", password=>"secret");

In this case, I use the following privileged user on my system: cn=admin,dc=leapster,dc=org. If I'd bound to one of the unprivileged users (for example, uid=paul,dc=leapster,dc=org), I may not have had any access to read or write options on the system at all, depending on how the server was configured.

The return value, which we store in $mesg, is an object of class New::LDAP::Message. It is discussed later in this article.

If you wish to close a connection, you must unbind from it:

$ldap->unbind;
Adding Entries - add()

If you have a large number of users on your system, it's likely you do not want to add each new user to the system one by one, through a GUI. So, one of the first things you write is a script that quickly adds lots of users in bulk. Or, perhaps you'll write a Web-based system where users can enter their personal information for themselves, with LDAP entries being added automatically. The add() method is used for adding an entry to the database:

$result = $ldap->add("uid=john,ou=People,dc=leapster,dc=org", 
                attr => [ 'cn' => 'John Smith',
                          'sn' => 'Smith',
                          'uid' => 'john',
                          'givenName' => 'John',
                          'homePhone' => '555-2020',
                          'mail' => 'john@domain.name',
                          'objectclass' => [ 'person', 'inetOrgPerson']
                        ]
           );

The above snippet of code adds a user named John Smith to our database. As you can see, the attributes are provided as a list to the attr parameter. Any attribute to which you wish to give multiple values should have them supplied as a list (in our example above, objectclass is such an attribute).

We're now at the point where we can write a simple script to add our large number of users in bulk, a script I've called ldap_addusers.

#!/usr/bin/perl
use Net::LDAP;
$ldap = Net::LDAP->new("localhost");
$ldap->bind("cn=admin,dc=leapster,dc=org", password=>"secret");
while(<>) {
        chomp $_;
        ($uid,$givenName,$sn,$mail) = split(/:/,$_);
        $cn="$givenName $sn";
        $dn="uid=$uid,ou=People,dc=leapster,dc=org";
        $result = $ldap->add($dn,
                attr => [ 'uid' => $uid,
                          'cn' => $cn,
                          'sn' => $sn,
                          'mail' => $mail,
                          'givenName' => $givenName,
                          'objectclass' => [ 'person', 'inetOrgPerson']
                        ]
           );
        $result->code && warn "error: ", $result->error;
}

The above script takes a colon-separated file of users, one per line, on stdin:

tom:Tom:Jones:tom@domain.name
dick:Dick:Tracy:dick@domain.name
harry:Harry:Windsor:harry@domain.name

So, if these names are stored in a file called userlist, we can enter them into our LDAP database with the ldap_addusers script, as follows:

cat userlist | ./ldap_addusers

There's one line in the ldap_addusers script that we haven't seen before:

$result->code && warn "error: ", $result->error;

As mentioned earlier, the add() method returns an object of type Net::LDAP::Message. Here, this object is referenced by $result. $result->code is the code value returned from the LDAP server in the result message after a query (in this case, the request to add an entry). Generally, when the request is successful, a zero is returned. Hence, in our above statement, the warning is issued only when $result->code is not zero.

Some other useful methods in Net::LDAP::Message are:

$result->dn        The DN contained in the result message
$result->error     The error message in the result (only if there was an error)
$result->done      True if the request was completed
$result->is_error  True if the particular result is an error for the operation

For a full description of the other methods, see the perldoc manual for Net::LDAP::Message.

perl-ldap also provides the delete() method for deleting entries, when given their DN:

$dn="uid=paul,ou=People,dc=leapster,dc=org";
$ldap->delete($dn);

This would, for example, allow you to write a script to do a bulk delete of expired users from your system:

#!/usr/bin/perl
use Net::LDAP;
$ldap = Net::LDAP->new("localhost");
$ldap->bind("cn=admin,dc=leapster,dc=org", password=>"secret");
while(<>) {
        chomp $_;
        $dn="uid=$_,ou=People,dc=leapster,dc=org";
        $ldap->delete($dn);
}

This script deletes all users whose uids are fed to it on standard input, one per line. Be careful when using this script on production servers; if you accidentally feed it the wrong file, you may find yourself with no users left in your directory.

Searching for Entries - search()

It wouldn't be much use only to be able to write to our LDAP server; we need to be able to read from it also. The perl-ldap search command is used to perform lookups on an LDAP server.

$mesg = $ldap->search(filter=>"(uid=paul)", base=>"dc=leapster,dc=org");

The base parameter specifies the base object entry from which the search will be made. In the example above, it searches the entire LDAP tree. The search could be confined to only the ou=People branch with:

$mesg = $ldap->search(  filter=>"(uid=paul)", 
                        base=>"ou=People,dc=leapster,dc=org");

A number of other optional parameters are available to the search() method:

  • scope : This can be one of the following:

    • base: Search the base object only

    • one: Search only the entries one level below the base

    • sub: Search the entire subtree below the base

    If I knew no subtrees were below ou=People--or if there were, but I didn't want results returned from them--I could use:

    $mesg = $ldap->search(  filter=>"(uid=paul)", 
                            base=>"ou=People,dc=leapster,dc=org"
                            scope=>"one");
    
  • timelimit: Set a limit in seconds on the amount of time that a request may take. The default is 0, which signals that the time is unlimited.

  • attrs: Set the attributes (as a reference to an array) that should be returned in the search. If not provided, the search returns all of the attributes. For example, the following search returns only the uid, sn and givenName attributes:

    $mesg = $ldap->search(  filter=>"(uid=paul)", 
                            base=>"ou=People,dc=leapster,dc=org",
                            attrs=> ['uid', 'sn', 'givenName'] );
    
  • filter: The filter may be a string, in standard LDAP filter format (see the ldap_search(3) man page for a description of this), or it may be a Net::LDAP::Filter object (see the Net::LDAP:Filter man page for further information).

The search() method returns Net::LDAP::Search objects. The easiest way to get at the contents of this object is to use its entries() method, which returns an array of Net::LDAP::Entry objects (see below):

@entries = $mesg->entries;

The Net::LDAP::Search object also has a number of other useful methods:

$mesg->count;           The number of entries returned in the search
$mesg->entry(n);        Returns the n'th entry (initial entry is 0)
$mesg->sorted([list])   Returns a list of entry objects sorted by attr list

Now, we can write a small script to list every entry in the directory:

#!/usr/bin/perl
use Net::LDAP;
$ldap = Net::LDAP->new("localhost");
$ldap->bind("cn=admin,dc=leapster,dc=org", password=>"secret");
$mesg = $ldap->search(filter=>"(objectClass=*)", base=>"dc=leapster,dc=org");
@entries = $mesg->entries;
foreach $entry (@entries) {
        $entry->dump;
}

I've cheated a little in the above script and used the dump() method of Net::LDAP::Entry, in order to make things clearer. dump() primarily is used for debugging; it merely dumps the DN and contents of an entry straight to standard output, without allowing for any manipulation of the results.

The most commonly used methods of the Net::LDAP::Entry object are:

  • attributes: returns the list of attributes contained in this entry.

    @attrs = $entry->attributes();
    
  • dn: Returns the DN of the current entry. If given with a parameter, it sets the DN of the entry:

    $dn = "uid=pbd,ou=Users,dc=leapster,dc=org";
    $entry->dn($dn);
    
  • get_value: obtains the value or values for the attribute, given as a parameter. If this method is used to assign to a scalar variable, it returns only the first value for the attribute; if used with an array, it returns all of the attributes.

    $phone = $entry->get_value("homePhone");  # returns only one phone number
    @phone = $entry->get_value("homePhone");  # returns all phone numbers for entry
    
  • add, delete, modify: These methods allow changes to be made to the entry and are discussed further in the next section.

  • update: pushes any changes made to the entry to the LDAP server (whose object is given as a parameter):

    $entry->add(homePhone => "555 3034");
    $entry->update($ldap);
    

Now that we've examined the Net::LDAP::Entry object, we can expand the above script further. We can write out the contents of the entries ourselves:

#!/usr/bin/perl
use Net::LDAP;
$ldap = Net::LDAP->new("localhost");
$ldap->bind("cn=admin,dc=leapster,dc=org", password=>"secret");
$mesg = $ldap->search(filter=>"(objectClass=*)", base=>"dc=leapster,dc=org");
@entries = $mesg->entries;
foreach $entry (@entries) {
        print "dn: " . $entry->dn() . "\n";
        @attrs = $entry->attributes();
        foreach $attr (@attrs) {
                printf("\t%s: %s\n", $attr, $entry->get_value($attr));
        }
}
Modifying Entries - modify()

It would be unusual for entries in a directory to be static. Various attributes probably change over time, as users change phone numbers, addresses or even names. At the very least, you would hope your users change their passwords regularly. perl-ldap provides the modify() method to handle such changes.

The three main modification actions that can be performed upon an LDAP entry are:

  • add: add one on more attributes to an entry

  • delete: delete one or more attributes from an entry

  • replace: replace one or more attributes with different values.

Examples:

$dn = "uid=paul,ou=People,dc=leapster,dc=org";
# add a 'homePhone' attribute and a 'mail' attribute
$mesg = $ldap->modify($dn, add => { "homePhone" => "555 3030",
                                    "mail" => "paul\@mail.home"} );
# add two more 'homePhone' attributes
$mesg = $ldap->modify($dn, add => { "homePhone" => ["555 3031", "555 3032"] });
# delete the mobile and pager attributes
$mesg = $ldap->modify($dn, delete => [ 'mobile', 'pager' ] );
# change the mail attribute to 'paul@domain.name'
$mesg = $ldap->modify($dn, replace => { "mail" => "paul\@domain.name" } );

If you have an attribute with multiple values and wish to delete only one of those values, you can give delete a specific attribute/value hash to delete:

$mesg = $ldap->modify($dn, delete => { 'homePhone' => "555 3031" } );

If you wish to do a number of changes at once, modify also provides the changes parameter, which takes a list of add, delete and replace operations:

# Add an employeenumber and delete 
$mesg = $ldap->modify($dn, changes => [
                                       add => [ employeeNumber => "4321" ],
                                       delete => [ mail => [] ]
                                      ]);

As with most other perl-ldap methods, modify() returns a Net::LDAP::Message object. Therefore, you can use $mesg-code to check whether an error was returned.

It's also possible to modify local copies of LDAP entries directly and then push the changes through to the server afterwards. Net::LDAP::Entry has a number of methods for doing this. Each method takes a list of attribute/value hashes (delete also accepts a simple list of attribute names):

  • add: adds one or more attributes to an entry.

  • delete: deletes one or more attributes from an entry.

  • replace: replaces one or more attributes in an entry.

None of these changes are propagated to the directory server until the update() method is called.

$base = "ou=People,dc=leapster,dc=org";
$mesg = $ldap->search(  filter=>"(uid=paul)", base=>$base);
$entry = $mesg->entry(0);
$entry->add(homePhone => "555 3035", pager => "555 4040");
$entry->delete("suburb");
$entry->replace(fax => "555 5050");
$entry->update($ldap);
Summary

To summarise, perl-ldap is a convenient and straightforward library for accessing LDAP servers with Perl scripts. Thus it provides a simple method for a system administrator to perform maintenance on systems serving large numbers of users, in much the same manner as they have been doing on existing flat-file /etc/passwd systems. Last winter, I used perl-ldap in scripts to help transfer 1.2 million users from our old Netscape Messaging Server system to our new, custom-built QmailLDAP servers. perl-ldap continues to prove invaluable for day-to-day maintenance of the same system.

Load Disqus comments