Friday, November 27, 2009

10.5 Advanced Net::LDAP Scripting



[ Team LiB ]










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 ]



    No comments: