Examining PerLDAP

Simplifying LDAP access

By Troy Neeriemer

Troy is a systems engineer for Intraware and can be contacted at troy@ intraware.com.

LDAP (short for "Lightweight Directory Access Protocol") promises to be a central repository of information about users and corporate resources. However, if it is difficult to access or manipulate that information, then few organizations will take LDAP seriously. Programmers and administrators, in particular, need to be able to access this information through a variety of methods and tools.

From a programmer's perspective there should be several ways to access the information. A low-level C API for directory access, for those times when speed is important, needs to be embedded in a compiled application. Java is becoming more important in the corporate environment, and as a result access to LDAP from Java has become a necessity. But there are also times when the ability to generate a quick prototype in a scripting language can make the difference in a project's success.

Administrators also need several ways to get to the information in a directory. A GUI for adding and changing information easily is an absolute requirement. Command-line utilities are frequently important for doing batch updates. However, sometimes command-line utilities don't allow a fine enough degree of control, so once again access through a scripting language is important.

To address issues such as these, Netscape has released PerLDAP, which provides a mechanism for accessing directory information from Perl. This is an important tool for both programmers and administrators. In this article, I'll provide both a high-level overview of what PerLDAP does and a detailed explanation of how you can use it.

What is PerLDAP?

PerLDAP (available in source code form at http://www.mozilla.org/directory/ perldap.html) is a set of Perl functions and objects that simplify access to LDAP services. Although Netscape released PerLDAP with the intent that it be used with the Netscape Directory Server, it should work equally well with most LDAP v3-compliant directories.

Netscape now has three ways for you to access LDAP. The first of these is the C API, which is distributed in the form of the Directory Server SDK. This SDK is available for a wide variety of platforms, and would be used by anyone developing an LDAP application in either C or C++. For Java developers, Netscape has the Java LDAP SDK. As you'd expect, the Java SDK has an object-oriented approach, and as such is much easier to use than the C API. PerLDAP also has an object-oriented approach to LDAP. PerLDAP is the most approachable of these three tools for accessing the Directory Server. All three tools (and some others as well) are available at Netscape's DevEdge Online (http:// developer.netscape.com/program/ home.html).

Experienced Perl developers may already be familiar with the Net::LDAP- api package available on the CPAN, the Comprehensive Perl Archive Network (http://www .cpan.org/). Net::LDAPapi does have a Perl object-oriented interface, but does not provide a general-purpose LDAP object. In other words, Net::LDAPapi really only gives you access to the LDAP API without providing a simpler-to-use object-oriented mechanism. PerLDAP, on the other hand, gives you a more general-purpose LDAP object that behaves much like the Java LDAP classes, which are part of the Java LDAP SDK 3.0. Both PerLDAP and Net::LDAPapi require the Directory Server SDK 3.0.

PerLDAP comes with a set of Perl functions that mirror the functions in the Directory Server SDK. These functions let you connect to a Directory Server, create, modify, and delete LDAP entries. More importantly, PerLDAP includes two objects that wrap around these functions, creating a more friendly developer experience. These objects are Mozilla::LDAP::Conn and Mozilla::LDAP::Entry.

Mozilla::LDAP::Conn is a general-purpose LDAP object that is instantiated by calling the new() method with the appropriate parameters. The parameters are the host name of the directory server, the bind distinguished name (dn), the bind password, and the LDAP search base. Once a connection has been established, Mozilla::LDAP::Conn has methods for searching the directory server, adding entries, modifying entries, and deleting entries.

Mozilla::LDAP::Entry is an object that gives you access to the components of an LDAP entry. It also provides methods for modifying an entry. Many Mozilla::LDAP:: Conn methods return Mozilla::LDAP::Entry objects.

Because most LDAP entries are in textual format, Perl's strong string-processing capabilities make it a natural fit. Furthermore, the ease of using the PerLDAP objects can make it an attractive alternative to using the command-line tools such as ldapsearch and ldapmodify. PerLDAP gives you the ability to create an LDIF export without shutting down the Directory Server. It includes sample code for synchronization with PeopleSoft. It can also be used to batch import users from other user databases. Only need and imagination limit the list of possible uses.

An Example Application

It is common for an organization to set up a variety of mailing lists. The uses range from being an alias for an entire department to being a tool for collaboration. If you've ever seen an e-mail address such as "engineering-newengland@company.com," then you've seen mailing lists in action. Collaborative mailing lists frequently allow people to subscribe and unsubscribe to the mailing list on their own without the intervention of an administrator. This is usually done using a list server such as Majordomo.

The Netscape Messaging Server supports mailing lists, but doesn't provide a means for users to subscribe or unsubscribe to those mailing lists. It requires that administrators manually add users to groups. This generally encourages people to look to an external program such as Majordomo to manage their mailing lists.

The example I present here, which demonstrates the inner workings of PerLDAP, is a CGI program that can be used to manage mailing lists via Directory and Messaging Servers. I will demonstrate some techniques for connecting (or binding) to the Directory Server, processing the results of a search, and modifying LDAP entries.

Binding to the Directory Server Using PerLDAP

The demo CGI program needs to do a few basic things, such as prompt users to log in, display a list of mailing groups available on the server, and let users modify membership in those mailing groups. Listing One is the core of this program. In a nutshell, it checks the state of the application and sends back the appropriate screen. The basic approach that I've taken with this CGI program is to separate the functions that display HTML from those that process LDAP entries. This is a good CGI programming practice, and it also makes it easier to focus the discussion on functions that make use of PerLDAP.

Since users are going to be modifying LDAP entries, it is necessary to make them bind to the Directory Server. However, users should be able to simply type in their user ID and password without having to remember their entire distinguished name. Users are used to this sort of behavior, and you want to mimic it. It turns out that this is actually pretty easy to accomplish.

There are three steps to binding to the Directory Server starting with just a user ID. The first step is to bind anonymously to the Directory Server. The second step is to search for the distinguished name for that user ID. Listing Two shows the ldap_get_user_info() function that does these first two steps. This function also returns the full name of the user, but this is only for display purposes and not a necessary component to binding to the Directory Server. The third step is to use the distinguished name returned by ldap_get_user_info() to bind to the Directory Server. This is a common approach to solving this problem. In fact, the Netscape Enterprise Server uses a similar mechanism for logging into the web server using just a user ID and password.

The ldap_get_user_info() function shows the basics of using the Mozilla::LDAP::Conn package. Calling the new() method without specifying a bind dn creates an anonymous connection to the Directory Server. Once a connection has been established, a search can be conducted. The search() method is similar to using the LDAP search feature in Communicator or in the command-line tools. You simply specify a search base and an attribute to search on. The sub parameter tells the search() method to look in subtrees. From there, it's just a matter of processing the result set that search() returned.

The search() function returns a Mozilla ::LDAP::Entry if the search was successful. You can then use the Mozilla::LDAP::Entry methods to access the data in the entry. One such method is getDN(), which returns the distinguished name of that entry. Another useful method is exists(), which lets you check if a value exists for the specified attribute.

Extracting the values of attributes can be a little tricky. The most important thing to remember is that an entry can have more than one value for each attribute. In fact, it is quite common. For example, a typical entry for an individual will have the values "top," "person," "organizationalperson," and "inetorgperson" for the objectclass attribute. This is important because when you ask Mozilla::LDAP::Entry for the value of an attribute, it will return a reference to an array. The general syntax looks something like:

$value = $entry->{attribute}[array_index];

You can use the size() method to determine how many elements are in the array. For the purposes of ldap_get_user_info(), you know that there is generally only one common name per entry, so you just grab the first one after testing to make sure a value exists.

Since most searches will return more than one entry, a method is needed to iterate through the result set. That method is nextEntry(), which is a Mozilla::LDAP::Conn method. In the case of ldap_get_user_info(), getting more than one entry back indicates there is a problem with the directory information tree because Netscape requires that uids be unique for the entire directory. The last thing that needs to be done with any basic LDAP connection is to close it. To do this, simply call close().

Processing Search Results

Once you know the bind dn of the user in question, you can bind to the Directory Server as that user and search for a list of available mailing lists. Listing Three shows the ldap_get_mail_groups() function, which does exactly that. This function is a straightforward LDAP search. The primary differences between this function and ldap_get_user_info() are that you aren't binding anonymously and the function populates an array with LDAP entries that are the results of the search. After calling ldap_get_mail_groups(), you have an array full of Mozilla::LDAP::Entry objects.

Listing Four shows what you can do with that array, although this is not the only way to process the results of a search. Rather than stuffing each entry into an array, the ldap_get_mail_groups() function could have processed each entry as it was retrieved. However, I wanted to be able to reuse my primary search so I separated it into an another function.

Listing Four demonstrates how to look through each entry to see if the user is a member of that group. The group lists members by using the uniqueMember attribute. In other words, to find a user in a group, you have to step through the array of uniqueMember attributes looking for the member. This is done by finding out how many elements are in the array using the size() method. Once this is known, it's simply a matter of using a for loop to look at each entry. You exit the for loop when you find the member.

Modifying Entries

The last major piece of functionality in PerLDAP that I'll examine is how to modify an entry, specifically how to add or remove an attribute and its corresponding value. The process is similar for both actions, so I'll take a look at adding an attribute. Listing Five shows how to do this. First, connect to the LDAP server. Second, find the entry that you want to modify. Third, add the attribute value. This is done with the addValue() method of the Mozilla::LDAP::Entry object. Finally, update the entry on the server. The update() method of the Mozilla::LDAP::Conn object has a single parameter, which is the Entry that has been modified. This is an important step. If update() isn't called, then the Entry is only modified in memory and not on the server.

Making the Mailing List Manager

If you've spent any time poking around the access control mechanism for the Directory Server, you may have noticed a permission called "selfwrite." Don't be surprised if Netscape renames this permission in a future release because it doesn't do what you'd expect. The first thing that comes to mind is that setting this permission for a group of users would allow them to modify their own entry. However, what it really does is allow users to add or remove themselves from a groupofuniquenames, which is also a mailGroup.

If you are using the Netscape Messaging Server in conjunction with the Directory Server, the installer for the Messaging Server extends the default schema to support the use of groups as mailing lists. In other words, if a group is given an e-mail address, then the Messaging Server will send an e-mail addressed to all members of the group. A typical use of this functionality would be to create e-mail aliases for departments or teams. The selfwrite permission allows users to modify their membership in the group.

With a little creativity, this feature can also be used to mimic some of the functionality of a list server such as Major-domo. An important feature of Major-domo lets users self-administer their membership in mailing groups by using commands sent by e-mail. PerLDAP makes it easy to create a web-based self-administration tool. Listing One shows the basic framework of such an application. The process_login() function makes use of binding to the Directory Server, searching, and processing the result set. The process_submit() function also adds or removes attributes from Entries. These functions both call functions that display HTML as well. The complete source code is available electronically from DDJ (see "Resource Center," page 5) or by writing me at troy@intraware.com.

You must grant the selfwrite privilege to at least some groups and users for the Mailing Group administration CGI to work. Refer to the Directory Server Administrator's Guide if you need help in setting up access control on the Directory Server. Be sure to look at the comments at the beginning of the source code because they will tell you what all the necessary components are and how to point the program at your Directory Server.

There are a couple of other things about this program you should be aware of. The first is that the program passes the user's password as a parameter and as a hidden field. This means that if you use it in an extranet environment you should be sure to use SSL so that this information is encrypted. Second, you don't want to tread lightly in the area of access control. If you are working with a production Directory Server, be sure to coordinate all activities with the primary administrator because it is easy to lock everyone out.

Conclusion

I've presented just one possible use for PerLDAP. There are many ways this tool can be used to simplify Directory Server administration. The key things to remember about doing a search with PerLDAP are:

There are, of course, many variations on this theme, but if you keep these four steps in mind it should be relatively easy for even beginning Perl developers to make good use of this tool.

DDJ

Listing One

unless ($cgi->param()) {
    html_display_login();
} elsif ($cgi->param("exit") eq "Exit") {
    html_display_login();
} elsif ($cgi->param("mode") eq "login") {
    process_login();
} elsif ($cgi->param("mode") eq "submit") {
    process_submit();
}

Back to Article

Listing Two

sub ldap_get_user_info {
    my $user = $_[0]; # The user ID
    my $bind_dn;
    my $cn;
    my $anon_conn = new Mozilla::LDAP::Conn($ldap_host, 
            $ldap_port, "", "", "") || die "Can't connect to $ldap_host.\n";
    my $entry = $anon_conn->search($search_base, "sub", "(uid=$user)");

    if (! $entry) {
        return;
    } else {
        my $i = 0;
        while($entry) {
            $i++;
           $bind_dn = $entry->getDN();
            if ($entry->exists("cn")) {
                $cn = $entry->{"cn"}[0];
            } else {
                return;
            }
            $entry = $anon_conn->nextEntry();
        }
        if ($i > 1) {
            return;
        }
    }   
    $anon_conn->close();
    return ($bind_dn, $cn);
}

Back to Article

Listing Three

sub ldap_get_mail_groups {
    my $bind_dn = $_[0];  # The user's bind DN
    my $password = $_[1];  # The user's password
    my $mgref = $_[2];  # A reference to an array of Entry objects
    my $conn = new Mozilla::LDAP::Conn($ldap_host, $ldap_port, $bind_dn, 
                                     $password, "");
    my $entry;
    my $cn;
    if (! $conn) {
        html_display_error( "Couldn't connect to the directory server.");
        exit(0);
    } else {
        $entry = $conn->search($search_base, "sub", "objectclass=mailGroup");
        if (! $entry) {
            html_display_error("There aren't any mailing lists on this server.");
            exit(0);
        }
        while($entry) {
            $cn = $entry->{"cn"}[0];
            # add the entry to the array unless it's the Postmaster group
            if ($cn ne "Postmaster") {
                push(@$mgref,$entry);
            }
            $entry = $conn->nextEntry();
        }
        $conn->close();
    }
}

Back to Article

Listing Four

foreach (@mailGroup){
    $groupName = $_->{"cn"}[0];
    $description = $_->{"description"}[0];
    my $isMember = 0;
    my $ct = $_->size("uniquemember");
    for ($i = 0; $i < $ct ; $i++) {
        if ($bind_dn eq $_->{"uniquemember"}[$i]) {
            $isMember = 1;
            last;
        }
    }
    html_row($isMember, $groupName, $ct, $description);
}

Back to Article

Listing Five

sub ldap_add_to_group {
    my $bind_dn = $_[0];  # The user's bind DN
    my $password = $_[1]; # The user's password
    my $mg_cn = $_[2]; # The CN of the Group
    my $conn = new Mozilla::LDAP::Conn($ldap_host, $ldap_port, $bind_dn,              
                                      $password, "");
if (!$conn) {
        html_display_error( "Couldn't connect to the directory server.");
        exit(0);
    } else {
        my $entry = $conn->search($search_base, "sub", "(cn=$mg_cn)");
        if (!$entry) {
            html_display_error( "Couldn't find $mg_cn.");
            exit(0);
        }
        $entry->addValue("uniquemember", $bind_dn);
        $conn->update($entry);
        $conn->close();

    }
}

Back to Article


Copyright © 1999, Dr. Dobb's Journal