Beefy Boxes and Bandwidth Generously Provided by pair Networks
good chemistry is complicated,
and a little bit messy -LW

Using Net::LDAP to access and update LDAP directories

by araqnid (Beadle)
on Sep 13, 2000 at 01:19 UTC ( #32196=perltutorial: print w/replies, xml ) Need Help??

LDAP - a relatively low-profile buzzword, but becoming more and more popular. "What is it?" and "How do we do it with Perl?" are question I'll attempt to tackle in this article.

I won't really cover how to deisgn an LDAP directory- I'm concentrating on showing the bits of Perl you use to search/update one.

What is it?

(Skip this bit if you want to go right to the Net::LDAP bit)

LDAP is a way of providing a hierarchial directory service. It's actually somewhat similar to DNS and/or NIS/NIS+ in some ways. The comparison is by no means exact, but it might help you get a grasp of what it's for and give you some ideas on how it can be used. In fact, LDAP can be used as an alternative to NIS+ as a naming/authentication service using nss-ldap and pam-ldap.

So, just as a DNS domain is a collection of domain names, an LDAP directory is a collection of entries, each indentified by a Distinguished Name (DN).

A DN specfies a sequence of elements, each having a type and value. For example: "o=Example, c=GB" means "Organisation called \"Example\" in Country called \"GB\"". Like DNs, the hierarchy is written right-to-left as you descend from the root.

Any particular entry in an LDAP directory is uniquely identified by a DN. The entry that has a list of attributes, each of which has a list of values. So the LDAP entry for a particular user may have an attribute "userPassword", which a single value containing their encrypted password.

There are a bunch of standards that LDAP is built on that are handy to know about, but not necessary for basic use. e.g. ASN.1, BER (Basic Encoding Rules). This is where a lot of the terminology and conventions come from.

How to use it

In order to access an LDAP directory, you have to connect to an LDAP server that has the directory you want. A connection to an LDAP server is identified by a Net::LDAP object- the connection is made like this:

my $hostname = ""; my $ldap = Net::LDAP->new($hostname) or die "Unable to connect to LDAP server $hostname: $@\n";

Basic operations: bind

"Binding" means identifying yourself to the LDAP server. This may be necessary in order to get any data from the server, or may only be necessary to update the server.

You bind my calling the "bind" method of your Net::LDAP object:

my $result = $ldap->bind();

This is an example of an "anonymous" bind- no identification. This is a "guest logon", and often give you read-only access with perhaps some attributes elided (e.g. passwords, confidential information).

In order to actually do some identification, you supply parameters to the bind() method:

my $binddn = "uid=jblow, ou=People, dc=Example, dc=Com"; my $password = readpassword(); my $result = $ldap->bind(dn => $binddn, password => $password);

Notice that you identify yourself using a DN. In this case, the DN is read as: "User id is \"jblow\", in the \"People\" organisational unit, in the \"\" LDAP directory".

LDAP result codes

What is the result of the bind() call, $result ? The connection to the LDAP server is by default asynchronous. Instead of waiting for the bind operation to complete, the bind() method merely sends the request to the server. The return value is an object that identifies the message you sent (an LDAP::Message object).

So how can you tell whether the bind was successful or not if we haven't got a response from the server yet? Well, if you call the "code" method on the LDAP::Message object, then Net::LDAP will block until the reply for that message has been received from the server. So you do this:

my $result = $ldap->bind(); if ($result->code) { # This makes Net::LDAP get the server response die "An error occurred binding to the LDAP server\n"; }

BTW the code for "success" is zero, so checking for a non-zero value of "code" will check for an error.

Of course, in the above example, we don't print a very helpful error message if something goes wrong. There is a module to help out here: Net::LDAP::Util contains functions for converting an error code into a message. So try this:

use Net::LDAP::Util qw(ldap_error_text); my $result = $ldap->bind(); if ($result->code) { die "An error occurred binding to the LDAP server: " .ldap_error_text($result->code)."\n"; }

Often, you will want to check every call for an error as soon as you can, so you can save yourself some code duplication by writting a wrapper subroutine to check the result code of an ldap call and die()ing as applicable:

sub ldapassert { my $mesg = shift; my $action = shift; if ($mesg->code) { die "An error occurred $action: " .ldap_error_text($mesg->code)."\n"; } return $mesg; } my $result = ldapassert($ldap->bind(), "binding to the server");

This routine returns the LDAP::Message object, so you can still get any actual data that is returned as part of the message.

Basic operations: search

The most common operation, after binding, is searching. This is done with the "search" method of the Net::LDAP object. There are four parameters for a search:

  • base DN: This is the DN at which to start the search.
  • scope: How far to recurse down through the directory. The possible scopes are:
    • sub: Search all DNs under the base DN.
    • one: Search only DNs one level below the base DN. e.g. if the base DN is "ou=People, dc=Example, dc=Com", then "uid=shaslam, ou=People, dc=Example, dc=Com" will be examined, but not "nstype=Bookmarks, uid=shaslam, ou=People, dc=Example, dc=Com".
    • base: Only examine the base DN.
  • attributes: A list of attributes to return for the matching entries
  • filter: The filter to determine which entries are to be returned.

The filter is crucial. It can be either a string or a Net::LDAP::Filter object. The format for writing LDAP filters as strings is described in RFC-2254. Briefly, you write "(attribute=value)" to search for one of the values of "attribute" being "value". You then combine these expressions together in prefix form rather like LISP:

  • (& (attr1=val1) (attr2=val2) ... ): logical AND
  • (| (attr1=val1) (attr2=val2) ... ): logical OR
  • (!(....)): negate

When matching a value you can replace "=" with "~=" to mean "approximately equal", or with ">=" or "<=" for greater-equal or less-than-eqal respectively.

You can also prepend or append "*" characters to the value to represent intial, final or substring matches.

Some examples now:

  • (&(uid=jblow)(objectClass=posixAccount))
    Find an entry with a "uid" of "jblow" and an "objectClass" of "posixAccount"
  • (&(jpegPhoto=*)(!(accountStatus=disabled))(objectClass=posixAccount))
    Find an entry with a "jpegPhoto" attribute (regardless of value), with an "objectClass" of "posixAccount" that does not have an "accountStatus" of "disabled"

So how, does the search work? You just feed the parameters to the "search()" method and get an LDAP::Search (which is a subclass of LDAP::Message) back.

my $searchresult = $ldap->search(base => "ou=People, dc=Example, dc= +Com", filter => "(objectClass=posixAccoun +t)", scope => "one", attrs => ['cn', 'accountStatus'] );

By default, the scope will be "sub", and all attributes of matching entries will be returned. You can find other parameters describe in the Net::LDAP(3) manpage.

Of course, since the result is an LDAP::Message, you can wrap the search call with ldapassert().

Now, to get the entries that matched your search, you have several options. Perhaps the simplest is to use the "entries" method of the Net::LDAP::Search object, which returns an array of Net::LDAP::Entry objects:

my $searchresult = do_search($ldap); foreach my $entry ($searchresult->entries) { print "Matched: ", $entry->dn, "\n"; }

With the Net::LDAP::Entry object you have these basic methods for getting their data:

  • dn(): returns the DN of the entry, as a string
  • <g>get(attr): returns a ref to a list of values for a particular attribute

So here's a full example of how I could print out everyone's name ("cn" is a standard attribute for Common Name- i.e. someone's real name):

my $sr = ldapassert($ldap->search(base => "ou=People, $ourdn", filter => "(objectClass=person)" scope => "one"), "searching the LD +AP server"); foreach my $entry ($sr->entries) { # Getting the first value of these attributes # If they don't exist, we may be trying to unref undef here... my $cn = ${$entry->get('cn')}[0]; my $uid = ${$entry->get('uid')}[0]; print "$uid: $cn\n"; }

Modifying an entry

Net::LDAP::Entry has methods for modifying the data of an entry. For example, if I wanted to change someones name:

sub changename { my $entry = shift; my $newname = shift; $entry->replace(cn => $newname); }

replace() takes a hash of "attribute => value" pairs. The value may be an array reference to multiple values.

You can also call the "add()" method to add a single value to an existing list of values for a particular attribute, which takes parameters in the same format. And there is the "delete()" method which takes a single attribute name as a parameter and deletes it.

Note that all these methods only affect the representation of the entry kept by the Perl script. In order to update the entry on the directory server, you must call the "update()" method and pass it the Net::LDAP object to tell it which connection to update on. e.g.:

# Add "extraClass" to everyone's "objectClass" attribute. my $sr = ldapassert($ldap->search(base => $ourdn, filter => "(object +Class=person)"), "searching the LDAP server"); foreach ($sr->entries) { $_->add(objectClass => "extraClass"); ldapassert($_->update($ldap), "updating the LDAP server"); }

add() is different to replace(). This could have been written:

$_->replace(objectClass => [@{$_->get("objectClass")}, "extraClass" +]

But as well as being more messy this would introduce a race condition- if someone else modified the entry between your search and modify commands, you would lose.

Wrapping up

I strongly recommend reading the Net::LDAP(3), Net::LDAP::Search(3) and Net::LDAP::Entry(3) manpages.

also, try the OpenLDAP website (although Net::LDAP is purely in Perl, and doesn't use the OpenLDAP library)

Other links to good online LDAP resources should go here, but I don't really know any.

Replies are listed 'Best First'.
RE: Using Net::LDAP to access and update LDAP directories
by t0mas (Priest) on Sep 13, 2000 at 10:49 UTC
Re: Using Net::LDAP to access and update LDAP directories
by greenFox (Vicar) on Sep 19, 2002 at 02:25 UTC
Re: Using Net::LDAP to access and update LDAP directories
by LazerRed (Pilgrim) on Aug 24, 2003 at 23:50 UTC
    Here's a link to the Linux Journal's beginners Net::LDAP tutorial. Very good read for learning the basics of the module (Covers opening/closing connection/add/delete/modify and general tips).

    Whip me, Beat me, Make me use Y-ModemG.
Re: Using Net::LDAP to access and update LDAP directories
by naChoZ (Curate) on Aug 25, 2003 at 01:31 UTC
    Another link worth adding is It's fairly low traffic still, but it may catch on more as ldap continues to gain headway. There's some useful resources listed there, too.

    "I just read perlman:perlboot," said Tom, objectively.

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: perltutorial [id://32196]
[oiskuu]: The useful bits that relate to your process can be found under /proc/self. What information are you thinking of? Tty name?
[tye]: I just daemonized and getlogin() still knew who I had been.
[tye]: perhaps loginuid ? Not that I concede that something not being in /proc means it is not useful.
[Corion]: tye: That's really interesting, but maybe it is because getlogin() returns the name, or the uid, so if that user has been replaced by another user with the same uid in the meantime, that's no problem to the system...
[davido]: or on ubuntu /var/run/utmp
[Corion]: Otherwise, I would imagine that a user with a process still alive would lock that information in memory.
[davido]: so last -f /var/run/utmp on ubuntu provides similar (though more verbose) info
[oiskuu]: glibc getlogin just does ttyname() and falls back on getutline(); it's not security related at all. (reminds me of sendmail and remote finger services of the naive early spam era)
[Corion]: But yes, "who started this process" is interesting information :)
[tye]: no, I really believe that "login user" was added as a fundamental bit of info about each process in order to enhance the usefulness of auditing

How do I use this? | Other CB clients
Other Users?
Others pondering the Monastery: (8)
As of 2017-06-23 19:36 GMT
Find Nodes?
    Voting Booth?
    How many monitors do you use while coding?

    Results (554 votes). Check out past polls.