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
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 | [reply] [Watch: Dir/Any] [d/l] [select] |
|
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 | [reply] [Watch: Dir/Any] [d/l] [select] |
|
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.
| [reply] [Watch: Dir/Any] |
|
$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 | [reply] [Watch: Dir/Any] [d/l] |
|
Re: RFC: Net::LDAP::Simple
by bronto (Priest) on Apr 14, 2003 at 16:13 UTC
|
| [reply] [Watch: Dir/Any] |
|
| [reply] [Watch: Dir/Any] |
|
|