Beefy Boxes and Bandwidth Generously Provided by pair Networks
go ahead... be a heretic
 
PerlMonks  

RFC: Net::LDAP::Simple

by bronto (Priest)
on Apr 14, 2003 at 16:11 UTC ( #250325=perlquestion: print w/replies, xml ) Need Help??

bronto has asked for the wisdom of the Perl Monks concerning the following question:

Hello all

I am writing some different applications that I'll use to manage entries on a directory server. Each application will manage a subtree (mostly), and each application will do about the same job on different subtrees, with different objectclasses and attributes but in the same way.

The module of choice for doing this is, of course, Net::LDAP, but it's interface seems to me a bit verbose in an environment like this one, so I'm planning to write a subclass, which I would like to call Net::LDAP::Simple, that simplifies the Net::LDAP interface.

Net::LDAP::Simple would offer a simpler interface to directory searches, giving back the matching entries directly (no $mesg->code checks); the add and delete methods will work on arrays of entries and some more shortcut methods will be added

That said, I'd like to hear from you about the module documentation below. I'd like to have suggestions about it before beginning to write code, so I'm looking forward to hear from you. Thanks in advance.

Ciao!
--bronto

NAME Net::LDAP::Simple - Simplified interface for Net::LDAP SYNOPSIS use Net::LDAP::Simple; # connect and bind to a directory server # bindDN and bindpw are optional, if you don't specify them an # anonymous bind is performed # base is the base subtree for searches, it is an optional param +eter # searchattrs are the attributes that are used by the simplesear +ch() # method. eval { my $ldap = Net::LDAP::Simple->new(host => 'localhost', bindDN => 'cn=admin,ou=People,dc=me', bindpw => 'secret', base => 'ou=People,dc=me', searchattrs => [qw(cn uid loginname)] +, %parms) ; # params for Net::LDAP::new } ; if ($@) { die "Can't connect to ldap server: $@" ; } my $filter = '(|(loginname=~bronto)((cn=~bronto)(uid=~bronto)))' + ; my $entries ; # These all return the same array of Net::LDAP::Entry objects $entries = $ldap->search(filter => $filter) ; # uses new()'s bas +e $entries = $ldap->search(base => 'ou=People,dc=me', filter => $filter) ; $entries = $ldap->simplesearch('bronto') ; # uses new()'s search +attrs # Now elaborate results: foreach my $entry (@$entries) { modify_something_in_this($entry) ; } # You often want to rewrite the entire entry (slow, but if it's +just # what you want...) foreach my $entry (@$entries) { die "Error rewriting entry" unless defined $ldap->rewrite($ent +ry) ; } # but you also can do this: my @result = $ldap->rewrite(@$entries) ; unless (@result == @$entries) { print "Error rewriting entries: ",$ldap->error, "; code ",$ldap->errcode,".\n\n" ; } # Add an entry, or an array of them, works as above: die $ldap->error unless $ldap->add($entry) ; # rename an entry: sometimes you simply want to change a name # and nothing else... $ldap->rename($entry,$newrdn) ; # Sometime you want to copy an entry in a new one # in the same subtree... $ldap->copy($entry,$newrdn) ; # or on another subtree... $ldap->copy($entry,$newrdn,$subtree) ; # And you can even move it with $ldap->move, the syntax is the s +ame. DESCRIPTION Net::LDAP::Simple is a simplified interface to the fantastic Graha +m Barr's Net::LDAP. Net::LDAP is a great module for working with dir +ectory servers, but it's a bit overkill when you want to do simple short scripts or have big programs that always do the same job again and again, say: open an authenticated connection to a directory server +, search entries against the same attributes each time and in the sa +me way (e.g.: approx search against the three attributes cn, uid and loginname). With Net::LDAP this would mean: * connect to the directory server using new(); * authenticate with bind() ; * compose a search filter, and pass it to search(), along with t +he base subtree; * perform the search getting a Net::LDAP::Search object; * verify that the search was successful using the code() or is_e +rror() method on the search object; * if the search was successful, extract the entries from the Sea +rch object, for example with entries or shift_entry. With Net::LDAP::Simple this is done with: * connect, authenticate, define default search subtree and simple-search attributes with the new() method; * pass the simplesearch method a search string to be matched aga +inst the attributes defined with searchattrs in new() and check the return value: if it was successful you have a reference to an +array of Net::LDAP::Entry objects, if it was unsuccessful you get un +def, and you can check what the error was with the error() method ( +or the error code with errcode) ; CONSTRUCTOR new(%parms) Creates a Net::LDAP::Simple object. Accepts all the parameters + that are legal to Net::LDAP::new but the directory server name/addr +ess is specified via the "host" parameter. Specific Net::LDAP::Simple parameters are therefore: host the name or IP address of the directory server we are conn +ecting to. Mandatory. bindDN bind DN in case of authenticated bind bindpw bind password in case of authenticated bind base base subtree for searches. ***(Mandatory or optional?) searchattrs attributes to use for simple searches (see the simplesearc +h method); searchbool boolean operator in case that more than one attribute is specified with searchattrs; default is '|' (boolean or); a +llowed boolean operators are | and &. searchmatch By default, an 'approx' search is performed by simplesearc +h(); for those directory servers that doesn't support the ~= op +erator it is possible to request a substring search specifying th +e value 'substr' for the searchmatch parameter. searchextras A list of attributes that should be returned in addition o +f the default ones. REDEFINED METHODS All Net::LDAP methods are supported via inheritance. Method specif +ic in Net::LDAP::Simple or that override inherited methods are documente +d below. add You can use it the same way you'd use Net::LDAP::add, or you c +an pass it an array of Net::LDAP::Entry objects; returns a list o +f Net::LDAP::Entry objects that successfully made it on the dire +ctory server. You can check if every entry has been added by compari +ng the length of the input list against the length of the output list +. Use the error and/or errorcode methods to see what went wrong. delete Works the same way as "add", but it deletes entries instead search search works exactly as Net::LDAP::search() does, however it t +akes advantage of the defaults set with new(): uses new()'s base parameter if you don't specify another base, and adds searchex +tras to default attributes unless you specify an "attrs" parameter. Another change in the search() interface is the return value: +now search() returns a reference to an array of entries for succes +s, or undef on error. Passing a simple string to "search" simply proxyies the call t +o the method "simplesearch". NEW METHODS rename($entry,$newrdn) Renames an entry; $entry can be a Net::LDAP::Entry or a DN, $n +ewrdn is a new value for the RDN. copy($entry,$newrdn [,$subtree]) Copies an entry; if you specify an optional third parameter th +e entry is copied on the specified subtree. move($entry,$newrdn [,$subtree]) Moves an entry; if you specify an optional third parameter the + entry is moved to the specified subtree. rewrite(@entries) Rewrites an entry entirely. Every attribute value is rewritten + even if it is unchanged. rewrite takes a list of Net::LDAP::Entry o +bjects as arguments. simplesearch($searchstring) Searches entries using the new()'s search* and base parameters +. Takes a search string as argument. AUTHOR Marco Marongiu, <bronto@CENSORED> SEE ALSO the Net::LDAP manpage.

The very nature of Perl to be like natural language--inconsistant and full of dwim and special cases--makes it impossible to know it all without simply memorizing the documentation (which is not complete or totally correct anyway).
--John M. Dlugosz

Replies are listed 'Best First'.
Re: RFC: Net::LDAP::Simple
by submersible_toaster (Chaplain) on Apr 15, 2003 at 06:09 UTC

    I am uncertain how much simpler you could make Net::LDAP to cope with, with perhaps the exception of the $mesg->code && die $mesg->error dance. I have some existing Net::LDAP code, from a CGI application I am prototyping, which bundles the LDAP access politely away from the main code. I have added more verbose comments to describe it, and it appears thus.

    sub authenticate { # instance method, $self is a blessed hash holding various # important details like ldaphost etc. my $self = shift; # $q , a CGI->new query object my $q = shift; # Open an anonymous ldap session (anon reads are allowed) my $ldap = Net::LDAP->new( $ldaphost ) or die "LDAP Connection error" +; $ldap->bind; my $user = $q->param('username'); my $mesg = $ldap->search ( base=>$self->{ldap}{userbase}, filter=>"(&(cn=$user))" ); # one day these dies will be calls to pretty printed # html . mymodule::error->database_error() $mesg->code && die $mesg->error; $ldap->unbind; # Dodgy, I admit : we only expect one account with the uid eq $user my $entry = $mesg->shift_entry; # Bailout if user does not exist in LDAP. return undef unless ($entry); my $ldaphash = $entry->get_value('userPassword'); my $ldapuser = $entry->get_value('uid'); # hash the CGI supplied password to compare with LDAP userPassword my $md5 = Digest::MD5->new; $md5->add( $q->param('phrase') ); my $hash = '{MD5}' . encode_base64($md5->digest, ''); if ( ( $q->param('username') eq $ldapuser) and ($hash eq $ldaphash) ) { my $sessionid = $self->start_session( $q ); return $sessionid; } else { return undef } }

    I can see where you're coming from , however rewriting this to use Net::LDAP::Simple , feels more like I'm shuffling the args to different methods rather than simplifying the code. I think search paramaters belong with search methods, not in the constructor.

    sub authenticate { my $self = shift; my $q = shift; # Using Net::LDAP::Simple my $ldap = Net::LDAP::Simple->new( host=>$ldaphost , base=>$self->{ldap}{userbase} , searchattrs=>'uid' ) or die "LDAP Connection error"; my $user = $q->param('username'); my $result = $ldap->simplesearch( $user ); die $ldap->error unless $result; $ldap->unbind; my $entry = shift @{$result}; # Bailout if user does not exist in LDAP. return undef unless ($entry); my $ldaphash = $entry->get_value('userPassword'); my $ldapuser = $entry->get_value('uid'); my $md5 = Digest::MD5->new; $md5->add( $q->param('phrase') ); my $hash = '{MD5}' . encode_base64($md5->digest, ''); if ( ( $q->param('username') eq $ldapuser) and ($hash eq $ldaphash) ) { my $sessionid = $self->start_session( $q ); return $sessionid; } else { return undef } }

    Please forgive me if I have misunderstood your approach, and for goodness sake keep working on the idea. Collecting peoples ideas RE what would make LDAP simpler to use for them might be a good start. I have considered writing some meta-methods to do commonplace things like move and rename, I reckon your ideas there are spot on. I watch with interest


    -toaster

    I can't believe it's not psellchecked

      First of all, thanks for your comments!

      I can see where you're coming from , however rewriting this to use Net::LDAP::Simple , feels more like I'm shuffling the args to different methods rather than simplifying the code. I think search paramaters belong with search methods, not in the constructor.

      The greatest benefits of this approach come when you are performing many operations on array of entries over the same connection. I'm not sure what to put on an example, since the concept of simplicity is different from person to person, but I'll try anyway.

      Compare this two snippets: you are doing similar searches on the same attributes but with different search strings, then adding an objectclass to each entry and storing the entries back again.

      use strict ; use warnings ; use Net::LDAP::Simple ; eval { my $ldap = Net::LDAP::Simple->new(host => 'x.it', bindDN => 'cn=admin,ou=People,dc=x,dc=it', bindpw => 'secret', base => 'ou=People,dc=x,dc=it', searchattrs => [qw(cn uid loginname)]) ; } ; die "Can't connect: $@" unless defined $ldap ; my @users ; # I won't preload all entries in production code, # in fact this is just an example :-) foreach my $user (qw(pinco pallino caro bellino)) { my $res = $ldap->simplesearch($user) ; die $ldap->error unless defined $res ; push @users,@$res ; } my $update = $ldap->rewrite(map($_->add(objectclass => 'posixAccount') +)) ; unless (@$update == @users) { my $entry = pop @$update ; warn "Cannot modify ".$entry->dn.", giving up!" ; }

      with this:

      use strict ; use warnings ; use Net::LDAP ; sub makefilter { return qq/(|(uid~=$_[0])(|(cn~=$_[0])(loginname~=$_[0])))/ } my $ldap = Net::LDAP->new('x.it') ; { my $msg = $ldap->bind('cn=admin,ou=People,dc=x,dc=it', password => 'secret') ; die "Cannot bind: ".$msg->error if $msg->is_error ; } my $base = 'ou=People,dc=x,dc=it' ; my @users ; # I won't preload all entries in production code, # in fact this is just an example :-) foreach my $user (qw(pinco pallino caro bellino)) { my $filter = makefilter($user) ; my $msg = $ldap->search(base => $base, filter => $filter) ; die $msg->error if $msg->is_error ; push @users,$ldap->entries ; } foreach my $entry (@users) { my $msg = $ldap->modify($entry, add => { objectclass => 'posixAccount' }) ; if ($msg->is_error) { warn "Cannot modify ".$entry->dn.", giving up!" ; last ; } }

      In the Net::LDAP::Simple code you don't need to define a makefilter sub, the module takes care of it; you don't need to check $msg->is_error at every call: you get an array reference or undef for every method that works on entries; you don't need to iterate over an array of @entries: the module takes care of it. Some application do exactly that, and those applications are the target of the module... er, class.

      Again, thanks for your feedback!

      Ciao!
      --bronto


      The very nature of Perl to be like natural language--inconsistant and full of dwim and special cases--makes it impossible to know it all without simply memorizing the documentation (which is not complete or totally correct anyway).
      --John M. Dlugosz
      This question is OT from the original post.

      I've a question about the authentication method you describe. What were the reasons to retrieve the password using an anonymous bind versus trying to bind with the username/password pair given? I'm doing similar work but our dir server does not allow an anonymous bind to retrieve the userPassword attribute.

        Still a good question!
        The main reason (although it is not obvious from the code) is that there are many OUs beneath the userbase DN, for reasons too lengthy to explain here. Hence I cannot explicitly bind the given user as

        $ldap->bind( "cn=$user,".$self->{ldap}{userbase} , password=>$password )
        Since that user may be in any of a number of sub OUs to the userbase. I admit that there was much "umm" and "err" about using an anonymous bind to find the user entry, then rebind with that DN and the supplied password. The directory in question is accessible only from 127.0.0.1 , and it is not involved in any way in storing system accounts. My concerns about userPassword hashes being stolen are largely moot, if they can only be accessed locally, if a malicious user is already local - I have more problems than them having anon read access to LDAP!.

        Please post some code if you can, or in the least read/comment my meditation that more fully explains what I am stabbing in the dark at.


        I can't believe it's not psellchecked
Re: RFC: Net::LDAP::Simple
by bronto (Priest) on Apr 14, 2003 at 16:13 UTC

    ACK! Where are the readmore tags I put around the docs?

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://250325]
Approved by sschneid
Front-paged by valdez
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others lurking in the Monastery: (3)
As of 2020-08-07 23:56 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    Which rocket would you take to Mars?










    Results (51 votes). Check out past polls.

    Notices?