Novell is now a part of Micro Focus

How to Use Perl, Python, and PHP to Access eDirectory 8.7 via LDAP

Articles and Tips: article

Mike Richichi
Assistant Director of Academic Technology
Drew University
mrichich@drew.edu

01 May 2003


This AppNote gives an overview of configuring and using the scripting languages Perl, Python, and PHP to access Novell eDirectory 8.7 via LDAP. It shows how to authenticate to LDAP, perform queries, and do meaningful things with the results in both command-line and CGI programs.

This is an update of an AppNote originally published in the August 2001 issue (http://support.novell.com/techcenter/articles/ana20010805.html).


Topics

eDirectory, Perl, Python, PHP, LDAP, scripting languages

Products

Novell eDirectory 8.7

Audience

network application developers

Level

intermediate

Prerequisite Skills

familiarity with LDAP and eDirectory

Operating System

NetWare, Linux, Win32 systems configured to run Perl, Python, and PHP scripts

Tools

none

Sample Code

yes

Introduction

This AppNote provides an overview of configuring and using Perl, PHP, and Python to access eDirectory 8.7 via LDAP. It shows how to authenticate to LDAP, perform queries, and do meaningful things with the results in both command-line and CGI programs. Although Linux is the reference platform in this AppNote, the concepts are extensible to other Unix systems, to Cygwin on Win32 systems, and to NetWare AMP (Apache, MySQL, PHP/Perl) which will be available with NetWare 6.5.

You may have Web servers running on non-NetWare platforms that require access to the directory for Web-based applications. Also, your environment may include administrators or users who are used to programming with scripting languages on Unix- or Linux-based systems. The popularity of Novell eDirectory means that many organizations now have a flexible, scalable, replicated, and secure directory service at their disposal, to be used to provide a common information store across the enterprise. eDirectory 8.7's support of LDAP is well-integrated and exposes nearly all eDirectory functionality via the LDAP v3 specification, thus allowing any LDAP-enabled program to utilize eDirectory. This allows unprecedented flexibility in using eDirectory in your environment.

A perfect example is the university where I work. We have multiple servers running eDirectory 8.7, with all faculty, staff, and students having an eDirectory account. As befits the university environment, we have multiple platforms for servers (NetWare, Linux, Solaris, Tru64, OpenVMS, Windows NT/2000) to provide a comprehensive set of network services for all people on campus. eDirectory not only stores objects for all the people on campus, but has objects for every course offered in a term, and students and faculty have assignments to these objects. We provide many services based on that information, some of it through Web-based interfaces. In addition, we are required to create hundreds of new accounts every year by interfacing with a legacy administrative interface that provides flat files for this purpose. We have found that using LDAP-based solutions through common scripting languages is an ideal solution.

This AppNote outlines the considerations for developing directory-enabled applications with a scripting language. It also considers the concepts of LDAP that are important to working with any API, but with relevance to the methodology of accessing LDAP via a script language. Furthermore, it provides a brief overview of the installation and configuration of LDAP libraries in Perl, Python, and PHP, along with sample code in each of these languages for doing simple directory operations. Finally, it includes one additional Perl program that can be used to send e-mail warnings of upcoming password expirations to your users.

Why Use a Scripting Language?

There are distinct advantages and disadvantages to scripting versus compiled programs. These are described in the following sections.

Rapid Development

Scripting languages are popular because there's no compilation overhead in the development cycle, and because they usually have simple interfaces that would require many lines of code in a compiled language. For instance, PHP builds its LDAP support on a C SDK, and its LDAP calls abstract much of the buffer and connection handle management that you would have to do yourself if you were programming to the C SDK directly. Perl and Python behave similarly-the access methods to LDAP are closer to the native data access methods of the language, although understanding of the LDAP structures is important.

Web Integration

Perl, Python, and PHP are all very popular CGI scripting languages. There are other available modules in these languages for many common CGI operations- most notably, database access. The combination of LDAP access to eDirectory and database access to an enterprise database for dynamic Web content is an extremely powerful one. It can provide rich dynamic content backed up by the directory, and allow for things like restricting access to database data by LDAP/eDirectory authentication, as well as even some data interchange and synchronization between LDAP and the database. And of course, designing Web interfaces to eDirectory data means that data is available across operating system platforms from anywhere in the world.

Language Acceptance

Perl, Python, and PHP are well-understood, widely-accepted languages with large and loyal followings. There is a broad talent pool of programmers who are well-versed in one or even all of these scripting languages. Many of these programmers may not be familiar with the NetWare environment. Allowing these programmers to work in an environment in which they are comfortable (say, a Perl programmer on a Linux system), using LDAP (which they may have experience with, say, using OpenLDAP) means they can be utilizing eDirectory without having to learn eDirectory programming on a NetWare server or in the Win32 environment. This may mean more efficient development of applications that leverage the power of the directory-and that is the important part.

Platform Independence

You may be running eDirectory in a pure Unix environment, with no NetWare whatsoever. Thus, using standard NetWare development solutions for CGI programming, such as Novell Script or Java Server Pages, may not be possible. You may decide that you want to devote the resources of your NetWare servers to file, print, and eDirectory, and not implement application programs on them. You can use Unix or Linux systems, for instance, to do that work, but there is limited availability of native eDirectory APIs on these platforms. LDAP provides an access method into the Directory that is easily available on these systems. Again, you get the advantages of eDirectory with the benefit of working on the platforms you already have.

Of course, it's not all wine and roses with scripting languages. You will have to deal with performance and compatibility concerns, as well as the dynamic state of the codebase.

Performance

In general, scripting languages may not be as fast at runtime as compiled languages. Since you will have some overhead due to LDAP communications itself and execution time of LDAP searches and other work on the eDirectory server, the overhead of scripting may be small compared to the work LDAP itself is doing. Fortunately, the performance of scripting languages is increasing all the time (most are now precompiling into an intermediate bytecode that is actually quite fast). However, if your application requires the fastest performance, you will need to consider a compiled language such as C or C++.

SDK Issues and LDAP v2 vs. v3

You will possibly have issues with SDKs and levels of LDAP compatibility. It's possible that the API you're using in a script language won't support all the features of LDAP v3. Therefore, it may not be able to work with the LDAP extensions that eDirectory uses to expose certain features of eDirectory. Likewise, it may not be able to support SSL binds (more on that later) nor follow LDAP referrals (which could have implications in a distributed tree design).

It's important to note that although eDirectory 8.7 is fully LDAP v3 compliant, it also supports full LDAP v2 functionality as a subset of that and interoperates fully with any LDAP v2 client. All the implementations in this AppNote allow complete ability to modify, search, update, and delete all attributes and values exposed by LDAP on any eDirectory object in your directory tree, via authenticated access controlled by eDirectory access rights. However, you may not be able to do things like schema manipulation, partitioning, or other eDirectory-specific operations from your selected scripting language. I will point out these limitations where they apply. Since the first version of this AppNote, the LDAP APIs in scripting languages have taken on more LDAP v3 functionality.

Dynamic Nature of the Codebase

One last consideration about the LDAP APIs available to scripting languages is that they are open source projects developed by programmers who are contributing their time and resources to the project. These APIs may be considered by their authors to be alpha or beta quality. In practice, I have found them to be as reliable as many commercial packages, but a bit more sensitive to changing specifications as the products mature and develop. It's possible that some of the code presented here may not work as written in the near future-but that's true with commercial APIs from many vendors as well. Also, remember there is no official Novell DeveloperNet support for these products. However, the volunteer SysOps in the Developer Forums may be able to answer questions. You can also look to the many resources available on the Web for help, but opening a developer support incident is not an available option at this time.

With all that said, I hope I haven't scared you away and that you actually want to develop applications utilizing one of these languages. In this AppNote I treat Perl, Python, and PHP as different dialects of LDAP itself, so let's start with some LDAP fundamentals.

What You Need to Know About LDAP

This section does not provide a complete technical introduction to LDAP-for that, you really need to read the LDAP Developer's Guide published by Novell Press (Novell Press, ISBN 0-7645-4720-9-see http://www.novellpress.com for ordering information). It would be folly to try to redo what the authors of that book have already accomplished. So seriously, go read the whole book right now. I'll wait.

There . . . good book, wasn't it? Okay, since you've already read the book and are now just reviewing for the exam, I'll let you read my crib notes of it.

LDAP is about objects, attributes, values, and schema. An object is the fundamental unit of storage in an LDAP tree. Some objects are "container" objects-these contain other objects, possibly even other container objects. The hierarchy of container objects and their layout defines the structure of the directory tree. All objects have attributes, which are fields that store information about the objects. Attributes can have one or more values, which are data about the object. What attributes go with what objects, what classes of objects there are, what structure the values can take in an attribute, what objects can contain other objects-basically, all characteristics of the directory itself-are controlled by the schema. My friend Gary Porter likes to refer to the schema as a big cookbook- it's the reference for creating any eDirectory object, and it will tell you exactly how to do it. (He would also say, "How cool is that?", but I digress.)

While schema is important, it's also important to consider the sociopolitical and geographic implications of your directory tree design-considerations that are far beyond the scope of this AppNote, but which will make a tremendous difference in the usability of your tree and its data. However you do your eDirectory tree design, try to do it so that you can leverage the hierarchical structure to maximize the efficiency of searching for specific things.

Consistency in data entry is also important. For instance, we try to make sure all User objects have values in the attributes givenName, surname, and fullName, as well as a valid address in the Mail attribute. It's also important to us that the names be properly capitalized (no ALL CAPS) so that software doesn't have to waste time making sure the values are pretty when displayed. In addition, we need to ensure that e-mail addresses are in a valid format so you know that you can always e-mail a user by reading the value from the Mail attribute of the User object. Such consistency will make your life easier in the long run-and it is easy to enforce at the data entry level with scripting languages.

The current version of the LDAP protocol that eDirectory supports is LDAP v3, which is defined in RFC 2251 and also RFCs 2252-2256, which detail definitions for data types and for filters. Now, reading RFCs may not be your idea of a fun thing to do on a Friday night, but they are really worth reading, because you'll come to understand the fundamentals of what you're doing, and everything else builds from that foundation. Anyway, a juicy paragraph from RFC 2251 states:

"The general model adopted by this protocol is one of clients performing protocol operations against servers. In this model, a client transmits a protocol request describing the operation to be performed to a server. The server is then responsible for performing the necessary operation(s) in the directory. Upon completion of the operation(s), the server returns a response containing any results or errors to the requesting client."

This is what you'll be doing the whole time you're using LDAP. You'll send a request to a server (say, a search request, or an object change request), and then get the response. The server will take that request, do things with it, and return a response, which will either be a confirmation of an action, the data you requested, or an error because something went wrong. Your program will take that result and do things with it, possibly using the response to formulate more requests, and so on, until you've accomplished whatever it is you're trying to do.

Before you talk to an LDAP server, you should introduce yourself. Actually, as of LDAP v3, you can just start making requests. The server won't get offended, but it will treat you like it doesn't know you. In technical terms, a session with an LDAP server is started with a bind operation, where you present credentials (usually a username and password, but LDAP v3 allows for other credentials) to the server, and it authenticates you as that user with appropriate access rights. If you don't bind first, LDAP v3 will automatically perform an anonymous bind, which means the LDAP server will treat you as an unprivileged user with whatever public access has been granted to the directory. Actually, eDirectory allows anonymous binds to occur as an LDAP Proxy user in eDirectory, which can have more or fewer rights than [Root] or [Public] as appropriate.

I suggest you do anonymous binds to your tree and see what kinds of data are being exposed. You may have confidential items like Employee ID numbers, Social Security Numbers, home addresses, and the like, that should not be public information. Universities have to comply with FERPA, which limits what student information can be public and allows students to opt out of sharing directory information. Luckily, once such things are discovered, eDirectory 8.7 makes it easy to limit access with careful application of Access Control Lists (ACLs), especially inherited ACLs on specific attributes, while allowing users who need the data access to it. Again, these are considerations you must think through for any information system, and which have policy and legal implications beyond what I'm prepared to write about here.

So, you've bound to an LDAP server and now want to do "stuff." LDAP v3 defines nine operations: bind and unbind, search, modify, add, delete, modify DN, compare, and abandon. It also defines an extended operation that can let you do additional things (in the case of eDirectory, things like partitioning) in a standards-based, extensible fashion. In this AppNote, we'll mostly do searches, modifies, adds, and deletes (besides binding and unbinding). However, the other operations are important as well.

When a request is made to an LDAP server, data is formatted in a way defined by the LDAP v3 RFCs, then sent to the server where it is parsed. Data is then returned in RFC-defined formats. All LDAP APIs and script language implementations do most of this dirty work for you, so you don't have to worry about things like allocating memory to create request and result structures and parsing them. Still, being aware of how it works will help you understand LDAP in any language you use to access it.

For instance, here's the RFC 2251 definition of an LDAP v3 search operation (in ASN1 notation), which you will be using frequently to perform operations on the tree:

SearchRequest ::= [APPLICATION 3] SEQUENCE {
               baseObject      LDAPDN,
               scope           ENUMERATED {
                     baseObject              (0),
                  singleLevel             (1),
                  wholeSubtree            (2) },
            derefAliases    ENUMERATED {
                  neverDerefAliases       (0),
                  derefInSearching        (1),
                  derefFindingBaseObj     (2),
                  derefAlways             (3) },
            sizeLimit       INTEGER (0 .. maxInt),
            timeLimit       INTEGER (0 .. maxInt),
            typesOnly       BOOLEAN,
            filter          Filter,
            attributes      AttributeDescriptionList }

         Filter ::= CHOICE {
            and             [0] SET OF Filter,
            or              [1] SET OF Filter,
            not             [2] Filter,            
            equalityMatch   [3] AttributeValueAssertion,
            substrings      [4] SubstringFilter,
            greaterOrEqual  [5] AttributeValueAssertion,
            lessOrEqual     [6] AttributeValueAssertion,
            present         [7] AttributeDescription,
            approxMatch     [8] AttributeValueAssertion,
            extensibleMatch [9] MatchingRuleAssertion }

         SubstringFilter ::= SEQUENCE {
            type            AttributeDescription,
            -- at least one must be present
            substrings      SEQUENCE OF CHOICE {
               initial [0] LDAPString,
               any     [1] LDAPString,
               final   [2] LDAPString } }

         MatchingRuleAssertion ::= SEQUENCE {
            matchingRule    [1] MatchingRuleId OPTIONAL,
            type            [2] AttributeDescription OPTIONAL,
            matchValue      [3] AssertionValue,
            dnAttributes    [4] BOOLEAN DEFAULT FALSE }

What this code is telling you is that a search operation is defined by several things:

  • A base object, which defines the starting level of the search (this is usually a container object like "o=yoyodyne" or "ou=propulsion,o=yoyodyne").

  • A scope (base means the object itself, onelevel means everything at the level of the object, and subtree means the level of the object and every level below it)

  • Whether and/or how you should dereference aliases

  • Limits on the number of objects returned and the amount of time a search can take

  • Whether you actually want the values of attributes or just a list of attributes the object(s) have

  • A list of attributes you want returned from the object(s)

The reason I point this out here is that you'll see that each language will call its LDAP search operation with parameters that reflect the RFCs structure for an LDAP search object. So, while part of an API's job is to abstract the dirty work of the protocol, it's impossible to separate working in LDAP from some understanding of the protocol. Therefore, it helps to understand these definitions at the protocol level.

Another important consideration is security. Novell's LDAP implementation, as a v3 standard, allows SSL/TLS encryption of bind requests over a normal TCP connection. However, the scripting platforms involved have various states of readiness for SSL-enabled LDAP, and often take multiple steps to get the code and tools for TLS encryption. You also may be able to use the publicly available stunnel package to create an SSL/TLS tunnel to the eDirectory server, and do a normal unauthenticated bind to a LDAP port on the host where you're running the script. If you are not able to TLS-enable connections, you will have to disable the "Require TLS for all operations" checkbox in ConsoleOne and accept the risk of unencrypted passwords and data.

You can also use the SASL authentication features to allow for certificate-based or MD5-based authentication, which avoids cleartext passwords but leave data unencrypted. This risk can be minimized, but not eliminated, by having an LDAP server on the same host as the Web server. However, that may create other risks as well.

At the very least, if you're doing CGI programming that requires users to supply a username and password over the Web, that dialog should be secured by an SSL-enabled Web page. The rest you may have to deal with by having links between the CGI server and LDAP server that you can secure physically and logically by limiting user access to machines, keeping up on security patches, and following good system administration practices. In a high-security environment, any risk of plaintext communication may be unacceptable, and thus such solutions may not be available to you.

New eDirectory 8.7 Features Available via LDAP

eDirectory 8.7 has several enhancements and new features, many of which affect LDAP. Some of these are:

  • SASL authentication. This allows you to use external certificates, MD5 passwords (known in eDirectory as simple passwords) or other NMAS methods to authenticate to eDirectory.

  • Extensible matching. This allows you to use different matching rules on search filters. eDirectory 8.7 in particular allows DN matching, which means you can do searches based on the content of a DN itself.

  • Persistent Search. This allows you to initiate an LDAP search with a special control that keeps the connection open and returns results for the search when new objects match the search filter. This can be used to do things when a person is added to a group, for instance.

  • Event Publishing. LDAP can be used to track directory events like object adds and partition operations.

Many of these features depend on the LDAP SDK supporting controls and extensions. I'll show the cases where these might be relevant.

LDAP SDKs and You

Even though we're dealing with scripting languages, both Python's and PHP's LDAP implementations are built against a C SDK. So, a brief discussion of the available SDKs is relevant here.

OpenLDAP

OpenLDAP is a reference implementation of an LDAP server, clients, and libraries. When you go to their Web site at http://www.openldap.org, you will see a "stable" version and the latest version (as of this writing, 2.1.16 is the stable version and 2.1.17 is the latest release). All versions since OpenLDAP 2.0 are fully LDAP v3 compliant and can be built with SSL support. When you do this (assuming you have already built and installed OpenSSL), you'll need to run the configure with the following options:

env CPPFLAGS="-I/usr/local/ssl/include" LDFLAGS="-L/usr/local/ ssl/lib" ./configure --with-tls --enable-crypt

This will ensure that OpenLDAP will build with SSL support and find the correct libraries. You'll need OpenLDAP to build LDAP support in Python.

Mozilla

Mozilla's (formerly Netscape's) directory SDK supports LDAP v3 as well. It uses Mozilla's Network Security Services (NSS) component for SSL support. PHP supports either the OpenLDAP or Mozilla libraries. Mozilla's directory SDK is located at http://www.mozilla.org/directory. I personally do not use this SDK on a regular basis and won't be focusing on it here.

Novell

Novell provides a C SDK for LDAP for NetWare, Windows, Linux, Solaris, and AIX. These libraries come prebuilt with SSL support and, in fact, are based on the OpenLDAP libraries. Novell's libraries include source code and also come with API routines for all of the Novell extensions and Novell-supported controls such as persistent search. It may someday be possible to build PHP's and Python's LDAP support with the Novell libraries, but there won't be any more functionality than the routines already available in those languages. Therefore, while the libraries are useful for developing your own C programs to access the directory, there's no significant advantage to using them over OpenLDAP to build LDAP support in Python or PHP.

Implementations of LDAP in Perl, Python, and PHP

Now that we've seen what SDKs are out there, let's look at implementations of the LDAP protocol available in Perl, Python, and PHP.

LDAP Implementations for Perl

Of the many LDAP implementations for Perl, the one that is being actively developed-and the one I use-is perl-ldap, which is available at http://perl-ldap.sourceforge.net. Unlike the Python and PHP LDAP implementations, perl-ldap is not based on a C SDK but is written entirely in Perl. Thus it's portable to any systems with a current Perl implementation that is version 5.004 or later. Perl-ldap is a full-featured LDAP v3 implementation, although not all interfaces are fully documented.

Here's some sample code showing a bind with perl-ldap and a simple search filter.

#!/usr/bin/perl

use NET::LDAP;

$ldap->Net::LDAP->new('ldap`);
#$ldap defines an LDAP connection handle, and 'ldap' is the name
# of your LDAP server

$ldap->bind(dn=>'cn=admin,o=yoyodyne', password=>'plaintext');
# By the way, did I mention that you may have scripts where the password is
# hardcoded in them?  Sure, you can prompt as well, but if you're doing
# batch processing, you'll need to be aware of this.
$result=$ldap->search(basedn=>'o=yoyodyne',
   scope=>'one',filter=>'cn=admin',
   attrs=>['surname','mail','groupMembership']);
$result->code && warn $result->error;
foreach $entry ($result->all_entries) {
   @surname=$entry->get('surname');
   @mail=$entry->get('mail');
   @groupMembership=$entry->get('groupMembership');
   $dn=$entry->dn;
   print ("Info for $dn:\n");
   print ("Surname: ");
   foreach $surnamevalue (@surname) {print "\"$surnamevalue\" ");}
   print ("\n");
   print ("email: ");
   foreach $mailvalue (@mail) {print "\"$mailvalue\" ");}
   print ("\n");
   foreach $groupmembershipvalue (@groupmembership) {print
      "\"$groupmembershipvalue\" ");}
   print ("\n\n");
   }
$ldap->unbind;

This uses standard Perl object-oriented syntax. You start an LDAP connection by creating an $ldap variable to act as a connection handle by invoking the new method of Net::LDAP, with a parameter of the server name (there are other parameters you can throw at it, but you can get it started just with that). You then bind to the LDAP server with the bind method, passing a DN of the User object and a password. Be aware that if you don't prompt for the password, it will just be there in your file.

You then perform a search by invoking the search method and assigning the result to another variable; I called it $result. By the way, you can also get the result codes of the new and bind operations-it may, in fact, be a good idea because you can then test the error codes and do some error handling if something goes wrong. For instance, after the search, I look at $result->code, and if it's non-zero, I print the error result. It's pretty useful.

Anyway, you perform a search, and the $result->all_entries array has LDAP::Entry objects for each DN found. You then iterate over each entry object with the get method, which returns arrays for each returned attribute containing all the values of an object. If you access these arrays in a scalar context, you will get the first attribute value; however, since many LDAP fields are multivalued, you may want to access it as an array, and then iterate over the array for all of its values. Here, I just print the values, but you may want to do more useful things with them.

Installing perl-ldap.There are a few ways to install perl-ldap. On a Unix or Linux system, you can go to http://perl-ldap.sourceforge.net and download the latest gzipped tar file, unpack it, do a "perl Makefile.PL" then a "make" and a "make install." Also, if you have CPAN.pm installed, you can do a "perl -MCPAN -eshell" and then an "install Bundle::Net::LDAP" and it will do it automatically, including installing SSL support. However, to install the IO::Socket::SSL that Bundle::Net::LDAP needs, you will also need the latest version of OpenSSL available at http://www.openssl.org.

Since I assume you want to be secure, I'll outline the steps using CPAN:

  1. Download and install Open SSL.

    This involves exploding the newest .tar.gz file, changing to that directory, running "./Configure", then a "make" and "make install" as root. Then do "perl -MCPAN -eshell" again and enter "install IO::Socket::SSL." If anything goes wrong, you'll have to dig a little deeper, but this should work.

  2. Do an SSL bind to a server.

    To do this, replace "use Net::LDAP" with "use Net::LDAPS" in your script. Optionally, if you've exported the server certificate and have it in a local file, you can verify it with optional parameters to the bind: "verify=>'require'" and "capath='/usr/local/certs'." This will verify that the server is who it says it is (or at least who you think you say it is.)

Sample Perl Script.So, what do you do with all this cool stuff? Well, I decided to solve a problem, which was this: We have setups where people can use their eDirectory password to authenticate without being notified that their grace logins are being used, resulting in potential password problems. I have written a simple Perl script that scans the tree for users whose passwords are less than one week from expiring and sends them an e-mail. I run this script as a cron job on one of our Linux servers, and thus users are notified before their passwords expire and are encouraged to change them. We also have ways for them to change their password over the Web, and the e-mail has links to all of that. Here's the script:

#!/usr/bin/perl

use Net::LDAP;
use Time::Local;
use Net::SMTP;

#set up some constants we'll use, and be able to configure

$ldapuser="cn=admin,o=drew";
$ldappassword="thisisabadpassword";
$advance_warning=86400*7;

$ldap_filter="(\&(!(networkAddressRestriction=\\0A\\00\\15\\55\\33\\66))(cn=*)
   (objectclass=inetOrgPerson))"
@attribs=["passwordExpirationTime","mail"];
$baseDN="o=yoyodyne";
$passadmin="changeyourpassword@yoyodyne.not";

#create an object

$ldap=Net::LDAP->new('ldap');

#bind

$ldap->bind(dn=>$ldapuser, password=>$ldappassword);

$smtp=Net::SMTP->new('email');

print scalar localtime, "\n";
$result=$ldap->search(base=>$basedn,
               filter=>$ldapfilter,
               attrs=> @attribs);
foreach $entry ($result->all_entries) {
      $userentry = $result->pop_entry;
      @passexp = $userentry->get('passwordExpirationTime');
      @email = $userentry->get('mail');
      $mail = $email[0];
      $dn= $userentry->dn;
      $dn =~ s/,?(cn|ou|o)=/\./g;
      foreach $pwexp (@passexp) {
      ($yr,$mo,$dt,$hr,$min,$sec) = ($pwexp =~ /(\d{4})(\d{2})(\d{2})(\d{2})
         (\d{2})(\d{2})/);
      $mo2=$mo-1; $yr2=$yr-1900;
      $exptime=timegm($sec,$min,$hr,$dt,$mo2,$yr2) || warn "timegm: $!\n";
      if ($exptime < time + $advance_warning) {
      if ($exptime > time) {
         $secsleft = $exptime -time;
         $daysleft = int($secsleft/86400) + 1;
         $smtp->mail("$passadmin");
         $smtp->to("$mail");
            $smtp->data();
            $smtp->datasend("Subject:  $dn: Your eDirectory password expires
               within $daysleft days");
            $smtp->datasend("\n");

            $smtp->datasend("The network password for account $dn expires on\n
               $mo/$dt/$yr at $hr:$min:$sec.\n");
            $smtp->datasend(`cat passinfo.txt`);
            $smtp->dataend();

print "$dn $dt/$mo/$yr $hr:$min:$sec -- $daysleft days\n";
}
else {print STDERR "dn: $dn, password expired ", time- $exptime, " seconds
   ago\n";}
}
      }
}
print "\n\n";
$ldap->unbind;
$smtp->quit;

Keep in mind that this code wants a few things-namely, that every User object should have a fullName and Mail attribute, with the latter being a valid e-mail address. (By the way, in LDAP you identify User objects by objects that have the objectClass of inetOrgPerson.) Also, note my search filter. LDAP search filters are incredibly powerful. Here's a fairly complicated one:

(&(!(networkAddressRestriction=\\0A\\00\\15\\55\\33\\66))
(objectclass=inetOrgPerson))

This filter basically says, "Give me all the objects of class inetOrgPerson that do not have a Network Address Restriction of \\0A\\00\\15\\55\\33\\66 set." This is necessary in our environment to exclude certain utility accounts that aren't really users. The following would actually be a binary value, which is how address restrictions are stored. You may have all kinds of other restrictions: for example, you may want to run different scripts to find users whose last names start with A (surname=A*) or who have "outie" belly buttons (bellybuttonType="outie", if you've extended your schema appropriately). Search filters can be constructed with multiple AND, OR, and NOT statements as well.

What if you want to modify objects in the directory? Well, here's an add statement:

$result=$ldap->add (
      dn => $dn,
      attr => [  'sn' => $lname,
         'cn' => $user,
         'objectClass' => ['top','person','inetOrgPerson',
               'organizationalPerson'],
            'givenName' => $fname,
            'fullname' => "$fname $lname",
            'mail' => "$user\@drew.edu",
            'description' => $description,
            'messageServer' => $ms,
            'ou' => [$department, "STU"],
            'telephoneNumber' => $ext,
            'LoginScript' => "write \"Hello.\"",

            'passwordAllowChange' => 'TRUE',
            'passwordRequired' => 'TRUE',
            'passwordMinimumLength' => '8',
            'passwordExpirationInterval' => '10368000',
            'passwordUniqueRequired' => 'TRUE',
            'loginGraceLimit' => '12',
            'loginGraceRemaining' => '12',
            'ndsHomeDirectory' => "$home$user",
            'userPassword' => $pass
            ]
         );
$result->code && warn "Create failed: ", $result->code, $result->error;

Notice that you use the attribute names as field names in the add statement, and you must explicitly define the relevant objectClass values for the User object. Also note the special userPassword attribute. This is an attribute that can only be written to or deleted, never read from. You can change a password with LDAP by first doing a delete on the userPassword with a value of the old password, and then an add with the new password-something like this:

$result=$ldap->modify($dn
      changes => [
            delete => [ userPassword => "$oldpass"],
            add => [userPassword => "$newpass"],
      ]
      );

These must occur as one operation to work correctly and ensure the user changing the password knows it, unless the bound user has supervisory rights to $dn.

Persistent Search with perl-ldap.You can pass controls to many LDAP operations with perl-ldap, so you can take advantage of things like persistent search. Here's a script that implements persistent search:

#!/usr/bin/perl
#
#persist.pl -- Persistent search with Net::LDAP
#

use Net::LDAP;
use Net::LDAP::Control;
use Convert::ASN1;

$ldap=Net::LDAP->new("localhost");

$ldap->bind("cn=admin,o=novell",password=>"novell");

$asn=Convert::ASN1->new;

$asn->prepare(q<
      PersistentSearch ::= SEQUENCE {
         changeTypes INTEGER,
         changesOnly BOOLEAN,
         returnECs BOOLEAN
   }

>);

$params=$asn->encode(                         changeTypes=>15, 
            changesOnly=>0, 
            returnECs=>0);

$persistctl=Net::LDAP::Control->new(
            critical=>true,
            type=>"2.16.840.1.113730.3.4.3",
            value=>$params);

$result=$ldap>search(
            base=>"o=novell",
            filter=>"(&(groupMembership=cn=users,o=novell)(ou:dn:=appnotes))",
            control=>$persistctl,
            callback=>\&get_result);

if ($result->code) {
   $error = $result->code;
   $errstr = $result->error;
   print "Error: $error ($errstr)\n";
   }

sub get_result {
   my ($message,$entry) = @_;
   if ( !defined($entry)) {
      print "No records found.\n";
      return;
   }
   my $dn=$entry->dn;

   my $surname=$entry->get_value('surname');
   print "added to group:  $dn, $surname\n";
   }

$result->pop_entry;

A few things are different here. First, you need to import Net::LDAP::Control and Convert::ASN1; the former is part of perl-ldap, and the latter is required by it, so they should both be there. You then have to build the control by using Convert::ASN to encode the value parameter to the control. You can do this by using the ASN1 definition of the PersistentSearch control's data fields, and then calling the encode method to set the values. (These are defined in draft-smith-ldap-psearch; for the latest revision of this IETF draft, do a search at http://search.ietf.org.)

The values I've set say to return values on object adds, deletes, modifies, and modDN operations (changeTypes=15), to return all values currently matched when I initialize and then all changes (changesOnly=0), and to not return entry controls with the results, which would tell me what operation caused a result to be returned.

You then need to build the control structure with NET::LDAP::Control. I'm setting critical to True, which means the search will fail if persistent search is unavailable. I then pass the OID of persistent search (which is documented in the draft referenced above), and the parameters I encoded with the ASN1 module.

Finally, note that I'm using search with a callback function.This makes the search function call a subroutine each time an entry is returned. If you don't do this, you'll never get any results, because the server will not finish the search operation because it is persistent. I also included another eDirectory 8.7 feature in the search filter: extensible matching. The filter term "ou:dn:=appnotes" means "Match when the DN of the object has an OU of appnotes'." You could also do ":dn:=appnotes" which means match when any attribute of the DN is "appnotes".

When you run the script, it first returns all the current members of the group in question. As you add people to the group, it will print out the DN and the surname of the persons added. However, it will not tell you if a person is removed-you'd have to write a different persistent search for that operation.

Perl-ldap also supports SASL authentication with the Authen::SASL module, but modules for the Novell-supported SASL methods (DIGEST-MD5, EXTERNAL, and NMAS_LOGIN) have not been written.

LDAP Implementations for Python

In stark contrast to Perl's laissez-faire style of program design, Python is a language that enforces structure and readability. Python has become very popular both as a CGI provider and a command-line interpreted language.

The currently available LDAP interface for is python-ldap 2.0.0pre06. Version 2.x now has full LDAP v3 support and can be built with SSL support, as well as supporting more controls and extensions (although this support is not well documented). Python-ldap also has SASL support and should work with DIGEST-MD5 authentication. For current updates and the latest code, check the project Web site at http://python-ldap.sourceforge.net.

Here's the first example from the Perl section, now written in Python:

#!/usr/bin/env python

import ldap

l=ldap.open('ldap')
l.simple_bind_s('cn=admin,o=yoyodyne','plaintext')
res = l.search_s('o=yoyodyne', ldap.SCOPE_ONELEVEL, "cn=admin", ["sn", "mail", "groupMembership"])
for entry in res:
         attrs=entry[1]
      surname=attrs["sn"]
      mail=attrs["mail"]
      groupmembership=attrs["groupMembership"]
      dn=entry[0]
         print "Info for %s:\n" % (dn,)
         for surnamevalue in surname:
            print "\"%s\" " % (surnamevalue,)
         print "\n"
         for mailvalue in mail:
            print "\"%s\" " % (mailvalue,)
         print "\n"
         for groupmembershipvalue in groupmembership:
            print "\"%s\" " % (groupmembershipvalue,)
         print "\n"

l.unbind()

Notice the similarities and differences. The scripts are similar because you open a connection, bind to it, perform a search operation, and then iterate over the results. The parameters passed to the search are identical to the Perl case (base DN, scope, search filter, attribute list), and the results are a list of values. Each of these values contains a DN and then a dictionary (similar to a Perl hash) of the attributes, which are themselves lists (Python-ese for array) which you can iterate over to get individual attribute values. Notice that if you look beyond the language-specific syntax issues, the Perl and Python versions of this script are nearly identical.

Installing python-ldap.Python-ldap is located on the SourceForge Web site at http://python-ldap.sourceforge.net. As of this writing, the current version is 2.00pre6 and is available both as source and as a RPM package for Linux. It supports Python version 1.5.2 and later, but is recommended for Python 2.0 or newer. The source requires OpenLDAP 2.x. You use the module by doing an "import ldap" at the beginning of your program, along with whatever other LDAP modules you want to use.

For more information on python-ldap, refer to the documentation and other resources available at the above-referenced Web site.

LDAP Implementations for PHP

PHP was designed to be a simple, embedded, CGI scripting language with features of Perl, ASP, and Basic. Its syntax has elements of all of these. Unlike Perl and Python, PHP is rarely used from the command line. In fact, PHP will generate its own HTTP headers, unlike Perl or Python which require headers to be generated manually.

The current version of PHP is 4.3.1, and its LDAP support is built on the Netscape or OpenLDAP libraries. As is the case with Python and Perl, PHP supports more LDAP v3 functionality, based on the OpenLDAP 2.x support for LDAP v3. Refer to the PHP Web site at http://www.php.net for details on the LDAP calls themselves.

Since PHP has been working well for me, I'll give you the same code as in the Perl and Python sections, now in PHP:

<?php

$ldap = ldap_connect("ldap");

ldap_bind($ldap, "cn=admin,o=yoyodyne", "plaintext");

$result = ldap_search($ldap, "o=yoyodyne", "cn=admin", array( "surname", 
   "mail" ,"groupMembership"));

foreach (ldap_get_values($ldap) as $entry) {
      $dn=$entry["dn"];
      $surname=$entry["surname"];      
      $mail=$entry["mail"];
      $groupmembership=$entry["groupmembership"];
      print "Info for $dn\n";
      foreach ($surname as $surnamevalue) {
         print "\"$surnamevalue\" ";
         }
      print "\n";
      foreach ($mail as $mailvalue) {
         print "\"$mailvalue\" ";
         }
      print "\n";
      foreach ($groupmembership as $groupmembershipvalue {
         print "\"$groupmembershipvalue\" ";
         }
      print "\n";
      }
ldap_unbind($ldap);

?>

This code will only work with PHP 4. Some Linux systems may still have PHP 3 on them. If you have such a system, I strongly recommend upgrading to PHP 4. The language has much more functionality and is significantly faster.

Installing PHP 4.PHP 4 can be found at http://www.php.net. Download the latest .tar.gz file, unpack it, do a configure, and make. When you configure, make sure you add the "-with-ldap" parameter to supply LDAP support. This should work with installed OpenLDAP as is. With another LDAP library, you may need to specify the directory like "-with-ldap=/usr/local/ldapcsdk/sdk/usr/lib" or something similar to get it installed. Also, it's likely you'll have other configure options for PHP-check the INSTALL doc in the PHP build directory.

Additionally, many Linux distribution vendors (Red Hat, for instance) provide precompiled PHP modules with support for LDAP built in. Check their distribution or update sites. You may find these packages easier to configure and support in your environment. It's your call as to what you're comfortable with.

Conclusion

This AppNote has introduced the concepts of LDAP in scripting languages and provided information about how to obtain and build these languages with LDAP support to get you started in your own explorations. With a little practice, you'll find that working with LDAP in your favorite scripting language is easy, flexible, and fun. Keep in mind, though, that we've really just scratched the surface here- there's a lot more to learn about LDAP and the scripting platforms discussed in this AppNote.

Here are links to additional help for using LDAP with each of these languages, along with other some useful references:

  • Perl: http://perl-ldap.sourceforge.net

  • Python: http://python-ldap.sourceforge.net/doc/lib/

  • PHP: http://www.php.net/manual/en/ref.ldap.php

  • OpenLDAP: http://www.openldap.org

  • LDAP v3 RFCs: search for RFCs 2251-2256 at http://www.ietf.org

  • OpenSSL: http://www.openssl.org

  • Novell C SDK for Linux: http://developer.novell.com/ndk/doc_cldaplin.htm

  • Novell eDirectory/LDAP page: http://developer.novell.com/edirectory/ndsldap.htm (lists various eDirectory and LDAP references, with some good links to packages and open source tools)

  • LDAP Zone: http://www.ldapzone.com (Novell-sponsored LDAP information site, designed to be vendor-neutral; full of links and resources, as well as an open forum for discussion of all LDAP issues)

* Originally published in Novell AppNotes


Disclaimer

The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.

© Copyright Micro Focus or one of its affiliates