LDAP System Administration

Home > Other > LDAP System Administration > Page 30
LDAP System Administration Page 30

by Gerald Carter


  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

  ##

  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 "$urln";

  $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

  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 [email protected]
RG 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: $@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 [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]

  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.

  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 ([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
  user [1] STRING OPTIONAL,

  oldpasswd [2] STRING OPTIONAL,

  newpasswd [3] STRING OPTIONAL

  }>);

  my $passwdModRes = Convert::ASN1->new;

  $passwdModRes->prepare(q
  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" );

  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 =>

‹ Prev