Beefy Boxes and Bandwidth Generously Provided by pair Networks
We don't bite newbies here... much
 
PerlMonks  

comment on

( [id://3333]=superdoc: 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 = "ldap.example.com"; 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 \"example.com\" 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.


In reply to Using Net::LDAP to access and update LDAP directories by araqnid

Title:
Use:  <p> text here (a paragraph) </p>
and:  <code> code here </code>
to format your post; it's "PerlMonks-approved HTML":



  • Are you posting in the right place? Check out Where do I post X? to know for sure.
  • Posts may use any of the Perl Monks Approved HTML tags. Currently these include the following:
    <code> <a> <b> <big> <blockquote> <br /> <dd> <dl> <dt> <em> <font> <h1> <h2> <h3> <h4> <h5> <h6> <hr /> <i> <li> <nbsp> <ol> <p> <small> <strike> <strong> <sub> <sup> <table> <td> <th> <tr> <tt> <u> <ul>
  • Snippets of code should be wrapped in <code> tags not <pre> tags. In fact, <pre> tags should generally be avoided. If they must be used, extreme care should be taken to ensure that their contents do not have long lines (<70 chars), in order to prevent horizontal scrolling (and possible janitor intervention).
  • Want more info? How to link or How to display code and escape characters are good places to start.
Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others avoiding work at the Monastery: (7)
As of 2024-03-28 15:10 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found