[ Team LiB ] Previous Section Next Section

10.5 Advanced Net::LDAP Scripting

At this point, we've covered all the basics: binding to a server, reading, writing, and modifying entries. The remainder of the chapter covers more advanced programming techniques. We'll start by discussing how to handle referrals and references returned from a search operation.

10.5.1 References and Referrals

It's important for both software developers and administrators to understand the difference between a reference and a referral. These terms are often confused, probably because the term "referral" is overused or misused. As defined in RFC 2251, an LDAP server returns a reference when a search request cannot be completed without the help of another directory server. I have called this reference a "subordinate knowledge reference" earlier in this book. In contrast, a referral is issued when the server cannot service the request at all and instead points the client to another directory that may have more knowledge about the base search suffix. I have called this link a "superior knowledge reference" because it points the client to a directory server that has superior knowledge, compared to the present LDAP server. These knowledge references will be returned only if the client has connected to the server using LDAPv3; they aren't defined by LDAPv2.

A Net::LDAP search returns a Net::LDAP::Reference object if the search can't be completed, but must be continued on another server. In this case, the reference is returned along with Net::LDAP::Entry objects. If a search requires a referral, it doesn't return any Entry objects, but instead issues the LDAP_REFERRAL return code. Both references and referrals are returned in the form of an LDAP URL. To illustrate these new concepts and their use, we will now modify the original search.pl script to follow both types of redirection. As of Version 0.26, the Net::LDAP module does not help you follow references or referrals—you have to do this yourself.

To aid in parsing an LDAP URL, use the URI::ldap module. If the URI module is not installed on your system, you can obtain it from http://search.cpan.org/. LDAP_REFERRAL is a constant from Net::LDAP::Constant that lets you check return codes from the Net::LDAP search( ) method.

#!/usr/bin/perl
## Usage: ./fullsearch.pl name
##
## Author: Gerald Carter <jerry@plainjoe.org>
## 
use Net::LDAP qw(LDAP_REFERRAL);
use URI::ldap;

The script then connects to the directory server:

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

To simplify the example, we will omit the bind( ) call (from the original version of search.pl) and bind to the directory anonymously. We'll also request all attributes for an entry rather than just the cn and mail values. The callback parameter is new. Its value is a reference to the subroutine that should process each entry or reference returned by the search:

$msg = $ldap->search(
                base => "ou=people,dc=plainjoe,dc=org",
                scope => "sub",
                filter => "(cn=$ARGV[0])",
                callback => \&ProcessSearch );
      
ProcessReferral( $msg->referrals(  ) ) 
    if $msg->code(  ) =  = LDAP_REFERRAL;

This code does two things: it registers ProcessSearch( ) as the callback routine for each entry or reference returned from the search and calls ProcessReferral( ) if the server replies with a referral. Both of these subroutines will be examined in turn.

All callback routines are passed two parameters: a Net::LDAP::Message object and a Net::LDAP::Entry object. ProcessSearch( ) has two responsibilities: it prints the contents of any Net::LDAP::Entry object and follows the LDAP URL in the case of a Net::LDAP::Reference object. The ProcessSearch( ) subroutine begins by assigning values to $msg and $result. If $result is not defined, as in the case of a failed search, ProcessSearch( ) can return without performing any work.

sub ProcessSearch {
    my ( $msg, $result ) = @_;
      
    ## Nothing to do
    return if ( ! defined($result) );

If $result exists, it must be either a Reference or an Entry. First, check whether it is a Net::LDAP::Reference. If it is, the URL is passed to the FollowURL( ) routine to continue the search. The Net::LDAP::Reference references( ) method returns a list of URLs, so you will follow them one by one:

if ( $result->isa("Net::LDAP::Reference") ) {
    foreach $link ( $result->refererences(  ) ){
        FollowURL( $link );
    }
}

If $result is defined and is not a Net::LDAP::Reference, it must be a Net::LDAP::Entry. In this case, the routine simply prints its contents to standard output using the dump( ) method:

    else {
        $result->dump(  );
        print "\n";  
    }
}

The FollowURL( ) routine merits some discussion of its own. It expects to receive a single LDAP URL as a parameter. This URL is stored in a local variable named $url:

sub FollowURL {
    my ( $url) = @_;
    my ( $ldap, $msg, $link );

Next, FollowURL( ) creates a new URI::ldap object using the character string stored in $url:

    print "$url\n";
    $link = URI::ldap->new( $url );

A URI::ldap object has several methods for obtaining the URL's components. We are interested in the host( ), port( ), and dn( ) methods, which tell us the LDAP server's hostname, the port to use in the new connection, and the base search suffix to use when contacting the directory server. With this new information, you can create a Net::LDAP object that is connected to the new server:

$ldap = Net::LDAP->new( $link->host(  ), 
                        port => $link->port(  ), 
                        version => 3 )
or { warn $!; return; };

The most convenient way to continue the query to the new server is to call search( ) again, passing ProcessSearch( ) as the callback routine. Note that this new search uses the same filter as the original search, since the intent of the query has not changed.

    $msg = $ldap->search( base => $link->dn(  ),
                          scope => "sub",
                          filter => "(cn=$ARGV[0])",
                          callback => \&ProcessSearch );
    $msg->error(  ) if $msg->code(  );
}

The first time you called search( ), you tested to see whether the search returned a referral. Don't perform this test within FollowLink( ) because the LDAP reference should send you to a server that can process the query. If the new server sends you a referral, choose not to follow it. Be aware that there are no implicit or explicit checks in this code for loops caused by chains of referrals or references.

Now let's go back and look at the implementation of ProcessReferral( ). Net::LDAP::Message provides several methods for handling error conditions. In the case of an LDAP_REFERRAL, the referrals( ) routine can be used to obtain a list of LDAP URLs returned from the server. The implementation of ProcessReferral( ) is simple because you've already done most of the work in FollowURL( ); it's simply a wrapper function that unpacks the list of URLs, and then calls FollowURL( ) for each item:

sub ProcessReferral {
    my ( @links ) = @_;
      
    foreach $link ( @links ) {
        FollowURL($link);
    }
}

When executed, fullsearch.pl produces output such as:

$ ./fullsearch.pl "test*"
--------------------------------------------------------
dn:uid=testuser,ou=people,dc=plainjoe,dc=org
      
  objectClass: posixAccount
          uid: testuser
    uidNumber: 1013
    gidNumber: 1000
homeDirectory: /home/tashtego/testuser
   loginShell: /bin/bash
           cn: testuser
      
ldap://tashtego.plainjoe.org/ou=test1,dc=plainjoe,dc=org
--------------------------------------------------------
dn:cn=test user,ou=test1,dc=plainjoe,dc=org
      
objectClass: person
         sn: user
         cn: test user

10.5.2 Scripting Authentication with SASL

In previous releases, the Authen::SASL package was bundled inside the perl-ldap distribution. Beginning in January of 2002, the Authen::SASL code became a separate module, supporting mechanisms such as ANONYMOUS, CRAM-MD5, and EXTERNAL. There is another SASL Perl module also available on CPAN, Authen::SASL::Cyrus by Mark Adamson, that uses the Cyrus SASL library. This is the one you will need if you are interested in the GSSAPI mechanism. Both modules use the same Authen::SASL framework and can be installed on a system without any conflict.

Probably the most common use of the GSSAPI SASL mechanism is to interoperate with Microsoft's implementation of Windows Active Directory. Chapter 9 discussed several interoperability issues between this server and non-Windows clients.

Updating the search script that I've developed throughout this chapter provides an excellent means of illustrating the GSSAPI package and Perl-ldap's SASL support. The only piece of code that needs to be modified is the code that binds to the directory server. Assume that you need to bind to a Windows domain with a domain controller named windc.ad.plainjoe.org. The Kerberos realm is named AD.PLAINJOE.ORG, and you'll use the principal jerry@AD.PLAINJOE.ORG for authentication and authorization.

First, the revised script must include the Authen::SASL package along with the familiar Net::LDAP module:

use Net::LDAP;
use Authen::SASL;

To bind to the Active Directory server using SASL, the script must create an Authen::SASL object and specify the authentication mechanism:

$sasl = Authen::SASL->new( 'GSSAPI',
          callback => { user => 'jerry@AD.PLAINJOE.ORG' } );

New Authen::SASL objects require a mechanism name (or list of mechanisms to choose from) and possibly a set of callbacks. These callbacks are used to provide information to the SASL layer during the authentication process. The GSSAPI mechanism will be handled by Adamson's module, which currently supports a limited set of predefined callback names.[2] The user callback used here is very simple; you just return the string containing the name of the account used for authentication. More information on callbacks can be found in the Authen::SASL documentation.

[2] The callback names supported in Authen::SASL::Cyrus-0.06 are user, auth, and language.

The code to create a new LDAP connection to the server is identical to the previous scripts that used simple binds for authentication. Remember that SASL requires the use of LDAPv3; hence the version => 3 parameter.

$ldap = Net::LDAP->new( 'windc.ad.plainjoe.org',
                        port => 389,
                        version => 3 )
or die "LDAP error: $@\n";

At this point, you can bind to the directory server. There is no need to specify a DN to use when binding because authentication is handled by the KDC and Kerberos client libraries.

$msg = $ldap->bind( "", sasl => $sasl );
$msg->code && die "[",$msg->code(  ), "] ", $msg->error;

You also need to modify the search script to use the base suffix that Active Directory uses for storing user accounts. In this case, the required suffix is cn=users,dc=ad,dc=plainjoe,dc=org. If you try running the SASL-enabled search script, chances are that the result will be a less-than-helpful error message about a decoding failure:

$ ./saslsearch.pl 'Gerald*'
[84] decode error 28 144 at /usr/lib/perl5/site_perl/5.6.1/Convert/ASN1/_decode.pm 
line 230.

The most common cause of this failure is the lack of a TGT from the Kerberos KDC. A quick check using the klist utility proves that you have not established your initial credentials:

$ klist -5
klist: No credentials cache file found (ticket cache FILE:/tmp/krb5cc_780)

If klist shows that a TGT has been obtained for the principal@REALM, another frequent cause of failure is clock skew between the Kerberos client and server. The clocks on the client and server must be synchronized to within five minutes.

Assuming that the failure occurred because you didn't establish your credentials, you need to run kinit to create the credentials file:

$ kinit
Password for jerry@AD.PLAINJOE.ORG:

Now when klist is executed, it shows that you have a TGT for the Windows domain:

$ klist -5
Ticket cache: FILE:/tmp/krb5cc_780
Default principal: jerry@AD.PLAINJOE.ORG
      
Valid starting     Expires            Service principal
06/27/02 18:27:04  06/28/02 04:27:04   
               krbtgt/AD.PLAINJOE.ORG@AD.PLAINJOE.ORG

This time, saslsearch.pl returns information about a user. I've trimmed the search output to save space.

$ ./saslsearch.pl 'Gerald*'
------------------------------------------------------------
dn:CN=Gerald W. Carter,CN=Users,DC=ad,DC=plainjoe,DC=org
      
                cn: Gerald W. Carter
       objectClass: top
                    person
                    organizationalPerson
                    user
    primaryGroupID: 513
        pwdLastSet: 126696214196660064
              name: Gerald W. Carter
    sAMAccountName: jerry
                sn: Carter
userAccountControl: 66048
 userPrincipalName: jerry@ad.plainjoe.org

10.5.3 Extensions and Controls

As mentioned in previous chapters, controls and extensions are means by which new functionality can be added to the LDAP protocol. Remember that LDAP controls behave more like adverbs, describing a specific request, such as a sorted search or a sliding view of the results. Extensions act more like verbs, creating a new LDAP operation. It is now time to examine how these two LDAPv3 features can be used in conjunction with the Net::LDAP module.

10.5.3.1 Extensions

The Net::LDAP::Extension and the Net::LDAP::Control classes provide a way to implement new extended operations. Past experience indicates that new LDAP extensions that are published in an RFC have a good chance of being included as a package or method in future versions of the Net::LDAP module. The Net::LDAP start_tls( ) routine is a good example. Therefore, you may never need to implement an extension from scratch. However, it is worthwhile to know how it can be done.

Graham Barr posted this listing on the perl-ldap development list (perl-ldap-dev@sourceforge.net), discussing how to implement the Password Modify extension:[3]

[3] For more information on the Password Modify extension and how it works, refer to RFC 3062.

package Net::LDAP::Extension::SetPassword;
      
require Net::LDAP::Extension;
@ISA = qw(Net::LDAP::Extension);
      
use Convert::ASN1;
my $passwdModReq = Convert::ASN1->new;
$passwdModReq->prepare(q<SEQUENCE {
                       user         [1] STRING OPTIONAL,
                       oldpasswd    [2] STRING OPTIONAL,
                       newpasswd    [3] STRING OPTIONAL
                       }>);
      
my $passwdModRes = Convert::ASN1->new;
$passwdModRes->prepare(q<SEQUENCE {
                       genPasswd    [0] STRING OPTIONAL
                       }>);
      
sub Net::LDAP::set_password {
    my $ldap = shift;
    my %opt = @_;
      
    my $res = $ldap->extension(
        name => '1.3.6.1.4.1.4203.1.11.1',
        value => $passwdModReq->encode(\%opt) );
      
    bless $res; # Naughty :-)
}
      
sub gen_password {
    my $self = shift;
      
    my $out = $passwdModRes->decode($self->response);
    $out->{genPasswd};
}
      
1;

The Net::LDAP extension( ) method requires two parameters: the OID of the extended request (e.g., 1.3.6.1.4.1.4203.1.11.1) and the octet string encoding of any parameters defined by the operation. In this case, the value parameter contains the user identifier, the old string, and the new password string.

The $passwordModReq and $passwordModRes variables are instances of the Convert::ASN1 class and contain the encoding rules for the extension request and response packets. The encoding rule specified in this example was taken directly from the Password Modify specification in RFC 3062. The Convert::ASN1 module generates encodings compatible with LBER, even though it uses ASN.1. For more information on Convert::ASN, refer to the module's installed documentation.

The good news is that it's easy to invoke the extension by executing:

$msg = $ldap->set_password( user => "username",
                            oldpassword => "old",
                            newpassword => "new" );
10.5.3.2 Controls

Many controls also end up being implemented as Net::LDAP classes. The following controls are included in perl-ldap 0.26:

Net::LDAP::Control::Paged

Implementation of the Paged Results control used to partition the results of an LDAP search into manageable chunks. This control is described in RFC 2696.

Net::LDAP::Control::ProxyAuth

Implementation of the Proxy Authentication mechanism described by the Internet-Draft draft-weltman-ldapv3-proxy-XX.txt. This control, supported by Netscape's Directory Server v4.1 and later, allows a client to bind as one entity and perform operations as another.

Net::LDAP::Control::Sort, Net::LDAP::Control::SortResult

Implementation of the Server Side Sorting control for search results described in RFC 2891.

Net::LDAP::Control::VLV, Net::LDAP::Control::VLVResponse

Implementation of the Virtual List View control described in draft-ietf-ldapext-ldapv3-vlv-XX.txt. This control can be used to view a sliding window of search results. This feature is often used by address book applications.

Using the built-in controls is really just a matter of reading the documentation and following the right syntax. To show how to use these Control classes, we will extend the saslsearch.pl script used to search a Windows AD server.

In order to work around the size limits for searches and return large numbers of entries in response to queries, AD servers (and several other LDAP servers) support the Paged Results control, which is implemented by the Net::LDAP::Control::Paged class. The idea behind this control is to pass a pointer, or cookie, between the client and server to keep track of which results have been returned and which are left to process. To help make the implementation a little easier to swallow, we'll break the search operation into a separate function. The subroutine, called DoSearch( ), expects two input parameters: a handle to a valid Net::LDAP object already connected to the server, and a DN that will be used as the base suffix for the search:

sub DoSearch {
    my ( $ldap, $dn ) = @_;
    my ( $page, $ctrl, $cookie, $i );

The Paged Results control requires a single parameter: the maximum number of entries that can be present in a single page. In this example, you'll set the number of entries set to 4, which is more convenient for demonstration; a production script would want more entries per page:

$page = Net::LDAP::Control::Paged->new( size => 4 );

To verify that the search is being done in pages, maintain a counter and print its value at the end of each iteration (i.e., every time you read a page of results). The loop will run until all entries have been returned from the server, or there is an error.

$i = 1;
while (1) {

After the Net::LDAP::Control::Paged object has been initialized, it must be included in the call to the Net::LDAP search( ) method. The control parameter accepts an array of control objects to be applied to the request.

$msg = $ldap->search( base => $dn,
                      scope => "sub",
                      filter => "(cn=$ARGV[0])",
                      callback => \&ProcessSearch,
                      control => [ $page ] );

The use of an LDAP control in the search does not affect the search return codes, so it is still necessary to process any referrals or protocol errors:

## Check for a referral.
if ($msg->code(  ) =  = LDAP_REFERRAL) {
    ProcessReferral($msg->referrals(  ));
}
## Any other errors?
elsif ($msg->code(  )) {
    $msg->error(  );
    last;
}

Finally, you need to obtain the cookie returned from the server as part of the previous search response. This value must be included in the next search request so the server will know at what point the client wants to continue in the entry list.

## Handle the next set of paged entries.
( $ctrl ) = $msg->control( LDAP_CONTROL_PAGED )
    or last;
$cookie = $ctrl->cookie(  )
    or last;
$page->cookie( $cookie );

At the end of the loop, print the page number:

         print "Paged Set [$i]\n";
         $i++;
    }
}

Here's what the output looks like:

$ ./pagedsearch.pl '*' | egrep '(dn|Paged)'
dn:CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=Gerald W. Carter,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=TelnetClients,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=Administrator,CN=Users,DC=ad,DC=plainjoe,DC=org
Paged Set [1]
dn:CN=Guest,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=TsInternetUser,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=krbtgt,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=Domain Computers,CN=Users,DC=ad,DC=plainjoe,DC=org
Paged Set [2]
dn:CN=Domain Controllers,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=Schema Admins,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=Enterprise Admins,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=Cert Publishers,CN=Users,DC=ad,DC=plainjoe,DC=org
Paged Set [3]
dn:CN=Domain Admins,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=Domain Users,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=Domain Guests,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=Group Policy Creator Owners,CN=Users,DC=ad,DC=plainjoe,DC=org
Paged Set [4]
dn:CN=RAS and IAS Servers,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=DnsAdmins,CN=Users,DC=ad,DC=plainjoe,DC=org
dn:CN=DnsUpdateProxy,CN=Users,DC=ad,DC=plainjoe,DC=org

At some point in the future, it might be necessary to implement a new control. The constructor for a generic Net::LDAP::Control object can take three parameters:

type

A character string representing the control's OID.

critical

A Boolean value that indicates whether the operation should fail if the server does not support the control. If this parameter is not specified, it is assumed to be FALSE, and the server is free to process the request in spite of the unimplemented control.

value

Optional information required by the control. The format of this parameter value is unique to each control and is defined by the control's designer. It is possible that no extra information is needed by the control.

The most common use of a raw Net::LDAP::Control object is to delete a referral object within the directory. By default, the directory server denies an attempt to delete or modify a referral object and sends the client the URL of the LDAP reference. The actual control needed to update or remove a referral entry is vendor-dependent.

OpenLDAP servers support the Manage DSA IT control described in RFC 3088. This control informs the server that the client intends to manipulate the referrals as though they were normal entries. There is no requirement that it be a critical or noncritical action. That behavior is left to the client using the control.

Creating a Net::LDAP::Control object representing ManageDSAIT simply involves specifying the OID. We'll specify that the server support the control; no optional information is required:

$manage_dsa = Net::LDAP::Control->( 
                  type => "2.16.840.1.113730.3.4.2",
                  critical => 1 );

Net::LDAP::Constant defines a number of names that you can use as shorthand for long and unmemorable OIDs; be sure to check this module before writing code such as the lines above. These lines can be rewritten as:

$manage_dsa = Net::LDAP::Control->( 
                  type => LDAP_CONTROL_MANAGEDSAIT, 
                  critical => 1 );

This control can now be included in a modify operation:

$msg = $ldap->modify( 
    "ou=department,dc=plainjoe,dc=org",
    replace => 
     { ref => "ldap://ldap2.plainjoe.org/ou=dept,dc=plainjoe,dc=org" },
    control => $manage_dsa );

It's difficult to discuss LDAP controls in detail because they are often tied to a specific server. A good place to look for new controls and possible uses is the server vendor's documentation. It is also a good idea to monitor the IETF's LDAP working groups to keep abreast of any controls that are on the track to standardization.

    [ Team LiB ] Previous Section Next Section