http://www.developer.com/

Back to article

Searching Active Directory with Perl


November 10, 2003

In the first installment of this series, I introduced PHP's LDAP functionality, and demonstrated just how easy it was to create PHP scripts that talked to Microsoft's Active Directory product. In this installment, we'll take a look at another language offering great LDAP support: Perl.

In true TMTOWTDI (There's More Than One Way To Do It; The official motto of Perl) fashion, several LDAP packages exist for Perl. I've had the pleasure of experimenting with all of them, and have become particularly partial to perl-ldap. Created by Graham Barr and now a SourceForge project under development in conjunction with several individuals, perl-ldap offers a great collection of object-oriented modules used to communicate with an LDAP server. Because it's written entirely in Perl, it, as well as all code written using it, is 100% compatible on any platform capable of running the Perl interpreter. In the following pages, you'll learn about basic module functionality, with a special focus on its search capabilities. We'll cap off the article with a real-World example involving the use of a Perl script to create cached user contact directories based on the information stored within our directory server. You can view an example of this in action on the Fisher College of Business Web site.

Note. As was the case with the first article, although the focus of the article is on integration of Perl and Active Directory, you should be able to easily adapt these examples to almost any LDAP implementation. In fact, many of the examples will run without modification on other LDAP servers.

Prior to embarking upon an introduction of the core topics of this article, there are a few preliminary items which should be kept in mind.

Prerequisites

I'll assume that you're at least somewhat acquainted with Perl, LDAP and their respective syntax. Furthermore, in addition to a working directory server, you'll need to ensure that you have the following items in order.

Perl

For those of you interacting with the directory server via a Unix-based platform, Perl is likely already available on your system. If it isn't, head on over to the official Perl Web site and pick up the latest release. If you're planning on communicating with Active Directory from a Windows platform, ActiveState's ActivePerl is now the recommended distribution. You can download the latest version at Activestate's Web site.

A Perl LDAP Library

Regardless of the operating system, you'll need to ensure that LDAP functionality is available to your Perl installation. Again, although there are several LDAP interfaces for Perl, I find the perl-ldap package to be particularly appealing. You can learn more about perl-ldap here.

The Convert::ASN1 Module

LDAP requires this module in order to make the necessary conversions of the LDAP commands to and from the BER (Basic Encoding Rules) used to transfer the data between the server and the client. You can install this module using CPAN.

Firewall Adjustments

If you intend to work with a directory server residing outside of your local network, some firewall adjustments may be required. By default, LDAP connections take place over port 389; if your directory server supports secure connections (LDAPS. The perl-ldap package offers Net::LDAPS should you require secure communication. See the perl-ldap Web site for more information about requirements.), you'll need to open port 636. Therefore, you'll need to adjust your firewall to allow for access via at least these two ports, if not already enabled.

Key Net::LDAP Functions

In this section I introduce several of perl-ldap's key functions. Specifically, you'll learn how to initiate a new connection, authenticate, close an existing connection, and search the server, in that order.

Note. The perl-ldap distribution actually offers three modules, Net::LDAP, Net::LDAPS, and Net::LDAPI, used to communicate with an LDAP server via a standard, secure, and a UNIX domain socket, respectively. Save for a minor change to the new() method's input arguments for Net::LDAPI, the methods described in this section operate identically for each methodology.

new()
new(host [, options])

The Net::LDAP module is object-oriented, meaning that a new object must be created prior to using any of the module's functionality. This module's constructor will also establish a connection to the directory server, done by passing the address of the directory server (either by hostname or IP address) to the constructor. The constructor also takes as input one or several options which serve to modify the default behavior of the connection. A few of the more commonly used options are listed here:

  • port => N: Specifies the connection port on the remote server.
  • timeout => N: Denotes the number of seconds that will be devoted to attempting to establish a new connection.
  • debug => N: Determines the debugging level. Setting this to 1 will dump outgoing packets to STDERR, while setting it to 2 will dump incoming packets.
  • version => N: You can override the default protocol version of LDAPv3 with this option.

An example follows:

#!/usr/bin/perl

use Net::LDAP;

$ad = Net::LDAP->new(.ldap://ad.wjgilmore.com.)
          or die(.Could not connect to LDAP server..);

Assuming that all goes okay, a connection to the LDAP server will have been established, and an object referencing that connection returned. But before you can really do anything with that connection, some credentials must be furnished. This is accomplished with the bind() method, introduced next.

bind()
bind([dn[,options]])

The bind() method serves to furnish credentials, or login, to the server. Both arguments are optional; omitting them will result in an attempt to perform an anonymous bind to the directory server. Anonymous binds are rare, however, therefore you'll almost certainly need to provide a DN (distinguished name) and a password. An example follows:

$ad->bind(.CN=ad-web,DC=ad,DC=wjgilmore,DC=com., password=.secret.);

Alternatively, when talking to Active Directory you can designate the DN using a user@domain argument, like so:

$ad->bind(.ad-web@ad.wjgilmore.com., password=>.secret.);

In addition to password, several other options are available. Each is introduced here:

  • control => HASH: . CONTROL HANDLERS ARE This control must be specified as a hash, with the hash containing three items:
    • type => OID. The OID specifying the type of requested control. This is required.
    • critical => FLAG. The request criticality. This is optional.
    • value=> VALUE. If the requested control requires a value, this element should point to that value.
  • callback => CALLBACK. A callback is a function that is called for every packet received from the server. You can point to that function here.
  • noauth | anonymous => 1. This results in an attempt to bind anonymously. Use of this option is unnecessary, as bind() will by default attempt to bind with no password in the case that one isn't supplied.
  • password => PSWD. Authenticate using this password.
  • sasl => SASLOBJ. This tells the command to bind using a Simple Authentication and Security Layer (SASL) mechanism. The value must be a subclass of Authen::SASL., another module written by perl-ldap's creator.

Once a successful bind is established, you can begin carrying out the necessary tasks.

unbind()
unbind()

Once you're done communicating with the directory server, you should close the connection. This is accomplished with the unbind() method. It accepts no arguments; simply call it at the conclusion of your script:

#!/usr/bin/perl

use strict;
use Net::LDAP;

my $ad = Net::LDAP->new("ad.wjgilmore.com") or die "$0";

$ad->bind("ad-web\@ad.wjgilmore.com", password=>"secret");

# Interact with the directory server here

# Unbind from the server
$ad->unbind;

Thus far, we know how to connect to, login (bind), and logout (unbind) of the directory server. While these actions are all necessary pieces of the puzzle, but we've yet to learn how to actually do anything particularly cool; Not to worry, as the remainder of this tutorial is devoted to a thorough investigation regarding how to use perl-ldap's powerful search capabilities to navigate the directory server.

search()
search(options)

The search() method is used to search a directory, returning an object of class Net::LDAP::Search. It takes as input one or several options, each of which is defined here:

Note. The Net::LDAP::Search class inherits Net::LDAP::Message, meaning that all methods defined in that class are also available to a search object. This class offers a number of useful methods, including those pertinent to retrieving errors which may arise during communication with the directory server. Although I won't formally introduce Net::LDAP::Message, consider reviewing the perl-ldap documentation regarding this matter.

  • base => DN. This determines the distinguished name used as the search target.
  • scope => 'base' | 'one' | 'sub'. This option will override the default behavior of searching the entire directory tree residing within the search target. Setting this option to 'base' will result in the search of just the base object. Setting it to 'one' will result in a search of just the level residing directly below the base object. Setting it to 'sub' will return the behavior to the default.
  • deref => 'always' | 'find' | 'never' | 'search'.
  • sizelimit => N. This option determines the maximum number of returned entities.
  • timelimit => N. This option sets the maximum number of seconds devoted to the search. By default, this is set to infinite.
  • typesonly => 1. Enabling this option results in only the return of the attributes, sans values.
  • filter => FILTER. Including a search filter will impose certain constraints as to which entries are returned. LDAP filters are very similar to the WHERE clause in an SQL query.
  • attrs => [ATTR1, ATTR2, . ATTRN]. You can restrict which attribute->value mappings for an entry are returned. Not including this option will result in the return of all attributes deemed viewable by the bound user.

Let's consider several examples of growing complexity. The first example simply searches for staff member's last names, and returns the total found:

Displaying Number of Entries Retrieved

#!/usr/bin/perl

use strict;
use Net::LDAP;

my $ad = Net::LDAP->new("ad.wjgilmore.com")
                or die "Could not connect!";

$ad->bind("ad-web\@ad.wjgilmore.com", password=>"secret");

# Declare the necessary search variables

# What is the search base?

my $searchbase = 'OU=People,OU=staff,DC=ad,DC=wjgilmore,DC=com';

# What are we searching for?

my $filter = "memberof=CN=staff,OU=groups,DC=ad,DC=wjgilmore,DC=com";

# Which attributes should be returned?

my $attrs = "sn";

# Execute the search

my $results = $ad->search(base=>$searchbase,filter=>$filter,attrs=>$attrs);

# How many entries returned?

my $count = $results->count;

print "Total entries returned: $count\n";

# Unbind from the server

$ad->unbind;

Executing this script results in output similar to:

Total entries returned: 5

Displaying Retrieved Entries

In the next example, we'll modify the previous script to display the retrieved entries. For sake of space, I'll forego repetition of the connection and bind calls; just keep in mind that these commands are requirements of any script.

# The search base?

my $base = 'OU=People,OU=staff,DC=ad,DC=wjgilmore,DC=com';

# What are we searching for?

my $filter = "memberof=CN=staff,OU=groups,DC=ad,DC=wjgilmore,DC=com";

# Which attributes should be returned?

my $attrs = "sn, givenname";

# Execute the search

my $results = $ad->search(base=>$base,filter=>$filter,attrs=>$attrs);

# How many entries returned?

my $count = $results->count;

# Display entries

my $entry;

for (my $i=0; $i<$count; $i++) {

     $entry = $results->entry($i);

     print $entry->get_value('sn').", ".
           $entry->get_value('givenname')."\n";

}

Returning:

Gilmore, Jason
Javascript, Jackie
Perl, Peter
Python, Paul
Mysql, Mary

Looking for a Specific User

In the final introductory example, let's retrieve information specific to a particular user. Note the use of the Time::Local module to convert the number of seconds since the user has last changed his password.

use Time::Local;
# . . .

my $base = 'OU=People,OU=staff,DC=ad,DC=wjgilmore,DC=com';

# What are we searching for?

my $filter = "CN=Jason Gilmore";

# Which attributes should be returned?

my $attrs = "sn, givenname, telephonenumber, mail, pwdLastSet";

# Execute the search

my $results = $ad->search(base=>$base,filter=>$filter,attrs=>$attrs);

# Display entries

my $entry;

$entry = $results->entry(0);

print $entry->get_value('sn').", ".$entry->get_value('givenname')."\n";
print "Tel: ".$entry->get_value('telephonenumber')."\n";
print "Email: ".$entry->get_value('mail')."\n";
print "Password last changed: ".
       localtime($entry->get_value('pwdLastSet'))."\n";

Returning:

Gilmore, Jason
Tel: 614-999-9999
Email: jason@wjgilmore.com
Password last changed: Sat Nov 1 12:45:10 2003

Building Static Contact Directories

I always like to incorporate a real-world examples into my tutorials. In this tutorial, I'll show you how to create static Web-based copies of the staff directory. Why would you want to do this, you ask? Even though LDAP is read-optimized, it just doesn't make sense to constantly query the directory server for rarely changing contact information. Nonetheless, the Web-based version of the directory should always contain the latest information. The answer? A Perl script which generates these directories nightly!

Note. You can view a live example of the results of this script on the Fisher College of Business Web site.

#!/usr/bin/perl

use strict;
use Net::LDAP;

# Connect and bind

my $ad = Net::LDAP->new("ad.wjgilmore.com")
         or die "Could not connect!";

$ad->bind("ad-web\@ad.wjgilmore.com", password=>"secret");

# build the alphabetical toc

my $letter = 'a';

my $toc = "

"; for(my $index=0; $index<26; $index++) { $toc .= "$letter "; $letter++; } $toc .= "

"; # Perform LDAP queries, build directory pages $letter = 'a'; my $base = 'OU=People,OU=staff,DC=ad,DC=wjgilmore,DC=com'; for(my $index=0; $index<26; $index++) { # Filter on the staff membership and # first letter of samaccountname attribute my $filter = "(& (memberof=CN=staff,OU=groups,DC=ad,DC=wjgilmore,DC=com) (samaccountname=$letter*))"; # Which attributes should be returned? my $attrs = "sn, givenname, mail"; # Execute the search my $results = $ad->search(base=>$base,filter=>$filter,attrs=>$attrs); # How many entries returned? # Build the directory my $count = $results->count; my $entry; my $directory; for (my $i=0; $i<$count; $i++) { $entry = $results->entry($i); $directory .= $entry->get_value('givenname')." ". $entry->get_value('sn'). " (".$entry->get_value('mail').")". "
\n"; } # Write the file open(FILE, "> /www/wjgilmore/directory/$letter.html"); print FILE $toc; print FILE $directory; close FILE; $letter++; } # Unbind from the server $ad->unbind;

Sample output is shown in Figure 1-1:

Conclusion

I welcome questions and comments! E-mail me at jason@wjgilmore.com . I'd also like to hear more about your experiences integrating Microsoft and Open Source technologies!

About the Author

W. Jason Gilmore (http://www.wjgilmore.com/ ) is an Internet application developer for the Fisher College of Business. He's the author of the upcoming book, PHP 5 and MySQL: Novice to Pro, due out by Apress in 2004. His work has been featured within many of the computing industry's leading publications, including Linux Magazine, O'Reillynet, Devshed, Zend.com, and Webreview. Jason is also the author of A Programmer's Introduction to PHP 4.0 (453pp., Apress). Along with colleague Jon Shoberg, he's co-author of "Out in the Open," a monthly column published within Linux magazine.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date