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.
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 <[email protected]>
##
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 " "; } }
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 "; $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
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 [email protected] 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 => '[email protected]' } );
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.
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: $@ ";
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 [email protected]:
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: [email protected] Valid starting Expires Service principal 06/27/02 18:27:04 06/28/02 04:27:04 krbtgt/[email protected]
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: [email protected]
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.
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 ([email protected]), discussing how to implement the Password Modify extension:[3]
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" );
Many controls also end up being implemented as Net::LDAP classes. The following controls are included in perl-ldap 0.26:
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.
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.
Implementation of the Server Side Sorting control for search results described in RFC 2891.
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] "; $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.