File Coverage

blib/lib/Catalyst/Authentication/Store/LDAP/Backend.pm
Criterion Covered Total %
statement 106 164 64.6
branch 19 60 31.6
condition 16 42 38.1
subroutine 15 19 78.9
pod 10 10 100.0
total 166 295 56.2


line stmt bran cond sub pod time code
1              
2             =pod
3              
4             =head1 NAME
5              
6             Catalyst::Authentication::Store::LDAP::Backend
7             - LDAP authentication storage backend.
8              
9             =head1 SYNOPSIS
10              
11             # you probably just want Store::LDAP under most cases,
12             # but if you insist you can instantiate your own store:
13              
14             use Catalyst::Authentication::Store::LDAP::Backend;
15              
16             use Catalyst qw/
17             Authentication
18             Authentication::Credential::Password
19             /;
20              
21             my %config = (
22             'ldap_server' => 'ldap1.yourcompany.com',
23             'ldap_server_options' => {
24             'timeout' => 30,
25             },
26             'binddn' => 'anonymous',
27             'bindpw' => 'dontcarehow',
28             'start_tls' => 1,
29             'start_tls_options' => {
30             'verify' => 'none',
31             },
32             'user_basedn' => 'ou=people,dc=yourcompany,dc=com',
33             'user_filter' => '(&(objectClass=posixAccount)(uid=%s))',
34             'user_scope' => 'one', # or 'sub' for Active Directory
35             'user_field' => 'uid',
36             'user_search_options' => {
37             'deref' => 'always',
38             },
39             'user_results_filter' => sub { return shift->pop_entry },
40             'entry_class' => 'MyApp::LDAP::Entry',
41             'user_class' => 'MyUser',
42             'use_roles' => 1,
43             'role_basedn' => 'ou=groups,dc=yourcompany,dc=com',
44             'role_filter' => '(&(objectClass=posixGroup)(member=%s))',
45             'role_scope' => 'one',
46             'role_field' => 'cn',
47             'role_value' => 'dn',
48             'role_search_options' => {
49             'deref' => 'always',
50             },
51             'role_search_as_user' => 0,
52             );
53            
54             our $users = Catalyst::Authentication::Store::LDAP::Backend->new(\%config);
55              
56             =head1 DESCRIPTION
57              
58             You probably want L<Catalyst::Authentication::Store::LDAP>.
59              
60             Otherwise, this lets you create a store manually.
61              
62             See the L<Catalyst::Authentication::Store::LDAP> documentation for
63             an explanation of the configuration options.
64              
65             =head1 METHODS
66              
67             =cut
68              
69             package Catalyst::Authentication::Store::LDAP::Backend;
70 3     3   45169 use base qw( Class::Accessor::Fast );
  3         12  
  3         1897  
71              
72 3     3   8481 use strict;
  3         14  
  3         91  
73 3     3   16 use warnings;
  3         6  
  3         181  
74              
75             our $VERSION = '1.015';
76              
77 3     3   1738 use Catalyst::Authentication::Store::LDAP::User;
  3         10  
  3         64  
78 3     3   801 use Net::LDAP;
  3         110260  
  3         27  
79 3     3   1574 use Catalyst::Utils ();
  3         116539  
  3         113  
80              
81             BEGIN {
82 3     3   50 __PACKAGE__->mk_accessors(
83             qw( ldap_server ldap_server_options binddn
84             bindpw entry_class user_search_options
85             user_filter user_basedn user_scope
86             user_attrs user_field use_roles role_basedn
87             role_filter role_scope role_field role_value
88             role_search_options start_tls start_tls_options
89             user_results_filter user_class role_search_as_user
90             )
91             );
92             }
93              
94             =head2 new($config)
95              
96             Creates a new L<Catalyst::Authentication::Store::LDAP::Backend> object.
97             $config should be a hashref, which should contain the configuration options
98             listed in L<Catalyst::Authentication::Store::LDAP>'s documentation.
99              
100             Also sets a few sensible defaults.
101              
102             =cut
103              
104             sub new {
105 2     2 1 1558 my ( $class, $config ) = @_;
106              
107 2 50 33     28 unless ( defined($config) && ref($config) eq "HASH" ) {
108 0         0 Catalyst::Exception->throw(
109             "Catalyst::Authentication::Store::LDAP::Backend needs to be configured with a hashref."
110             );
111             }
112 2         3 my %config_hash = %{$config};
  2         11  
113 2   50     11 $config_hash{'binddn'} ||= 'anonymous';
114 2   50     5 $config_hash{'user_filter'} ||= '(uid=%s)';
115 2   50     5 $config_hash{'user_scope'} ||= 'sub';
116 2   50     5 $config_hash{'user_field'} ||= 'uid';
117 2   50     10 $config_hash{'role_filter'} ||= '(memberUid=%s)';
118 2   50     7 $config_hash{'role_scope'} ||= 'sub';
119 2   50     8 $config_hash{'role_field'} ||= 'cn';
120 2   50     10 $config_hash{'use_roles'} ||= '1';
121 2   50     13 $config_hash{'start_tls'} ||= '0';
122 2   50     8 $config_hash{'entry_class'} ||= 'Catalyst::Model::LDAP::Entry';
123 2   50     9 $config_hash{'user_class'}
124             ||= 'Catalyst::Authentication::Store::LDAP::User';
125 2   50     11 $config_hash{'role_search_as_user'} ||= 0;
126              
127 2         14 Catalyst::Utils::ensure_class_loaded( $config_hash{'user_class'} );
128 2         48 my $self = \%config_hash;
129 2         37 bless( $self, $class );
130 2         4 return $self;
131             }
132              
133             =head2 find_user( I<authinfo>, $c )
134              
135             Creates a L<Catalyst::Authentication::Store::LDAP::User> object
136             for the given User ID. This is the preferred mechanism for getting a
137             given User out of the Store.
138              
139             I<authinfo> should be a hashref with a key of either C<id> or
140             C<username>. The value will be compared against the LDAP C<user_field> field.
141              
142             =cut
143              
144             sub find_user {
145 4     4 1 788 my ( $self, $authinfo, $c ) = @_;
146 4   33     30 return $self->get_user( $authinfo->{id} || $authinfo->{username}, $c );
147             }
148              
149             =head2 get_user( I<id>, $c)
150              
151             Creates a L<Catalyst::Authentication::Store::LDAP::User> object
152             for the given User ID, or calls C<new> on the class specified in
153             C<user_class>. This instance of the store object, the results of
154             C<lookup_user> and $c are passed as arguments (in that order) to C<new>.
155             This is the preferred mechanism for getting a given User out of the Store.
156              
157             =cut
158              
159             sub get_user {
160 4     4 1 7 my ( $self, $id, $c ) = @_;
161 4         16 my $user = $self->user_class->new( $self, $self->lookup_user($id), $c );
162 4         71 return $user;
163             }
164              
165             =head2 ldap_connect
166              
167             Returns a L<Net::LDAP> object, connected to your LDAP server. (According
168             to how you configured the Backend, of course)
169              
170             =cut
171              
172             sub ldap_connect {
173 4     4 1 5 my ($self) = shift;
174 4         6 my $ldap;
175 4 50       20 if ( defined( $self->ldap_server_options() ) ) {
176 4         21 $ldap
177             = Net::LDAP->new( $self->ldap_server,
178 4 50       42 %{ $self->ldap_server_options } )
179             or Catalyst::Exception->throw($@);
180             }
181             else {
182 0 0       0 $ldap = Net::LDAP->new( $self->ldap_server )
183             or Catalyst::Exception->throw($@);
184             }
185 4 50 33     5248 if ( defined( $self->start_tls ) && $self->start_tls =~ /(1|true)/i ) {
186 0         0 my $mesg;
187 0 0       0 if ( defined( $self->start_tls_options ) ) {
188 0         0 $mesg = $ldap->start_tls( %{ $self->start_tls_options } );
  0         0  
189             }
190             else {
191 0         0 $mesg = $ldap->start_tls;
192             }
193 0 0       0 if ( $mesg->is_error ) {
194 0         0 Catalyst::Exception->throw( "TLS Error: " . $mesg->error );
195             }
196             }
197 4         94 return $ldap;
198             }
199              
200             =head2 ldap_bind($ldap, $binddn, $bindpw)
201              
202             Bind's to the directory. If $ldap is undef, it will connect to the
203             LDAP server first. $binddn should be the DN of the object you wish
204             to bind as, and $bindpw the password.
205              
206             If $binddn is "anonymous", an anonymous bind will be performed.
207              
208             =cut
209              
210             sub ldap_bind {
211 4     4 1 7 my ( $self, $ldap, $binddn, $bindpw ) = @_;
212 4   33     17 $ldap ||= $self->ldap_connect;
213 4 50       15 if ( !defined($ldap) ) {
214 0         0 Catalyst::Exception->throw("LDAP Server undefined!");
215             }
216              
217             # if username is present, make sure password is present too.
218             # see https://rt.cpan.org/Ticket/Display.html?id=81908
219 4 50       11 if ( !defined $binddn ) {
220 4         17 $binddn = $self->binddn;
221 4         33 $bindpw = $self->bindpw;
222             }
223              
224 4 50       28 if ( $binddn eq "anonymous" ) {
225 4         14 $self->_ldap_bind_anon($ldap);
226             }
227             else {
228 0 0       0 if ($bindpw) {
229 0         0 my $mesg = $ldap->bind( $binddn, 'password' => $bindpw );
230 0 0       0 if ( $mesg->is_error ) {
231 0         0 Catalyst::Exception->throw(
232             "Error on Initial Bind: " . $mesg->error );
233             }
234             }
235             else {
236 0         0 $self->_ldap_bind_anon( $ldap, $binddn );
237             }
238             }
239 4         78 return $ldap;
240             }
241              
242             sub _ldap_bind_anon {
243 4     4   8 my ( $self, $ldap, $dn ) = @_;
244 4         20 my $mesg = $ldap->bind($dn);
245 4 50       7265 if ( $mesg->is_error ) {
246 0         0 Catalyst::Exception->throw( "Error on Bind: " . $mesg->error );
247             }
248             }
249              
250             =head2 ldap_auth( $binddn, $bindpw )
251              
252             Connect to the LDAP server and do an authenticated bind against the
253             directory. Throws an exception if connecting to the LDAP server fails.
254             Returns 1 if binding succeeds, 0 if it fails.
255              
256             =cut
257              
258             sub ldap_auth {
259 0     0 1 0 my ( $self, $binddn, $bindpw ) = @_;
260 0         0 my $ldap = $self->ldap_connect;
261 0 0       0 if ( !defined $ldap ) {
262 0         0 Catalyst::Exception->throw("LDAP server undefined!");
263             }
264 0         0 my $mesg = $ldap->bind( $binddn, password => $bindpw );
265 0 0       0 return $mesg->is_error ? 0 : 1;
266             }
267              
268             =head2 lookup_user($id)
269              
270             Given a User ID, this method will:
271              
272             A) Bind to the directory using the configured binddn and bindpw
273             B) Perform a search for the User Object in the directory, using
274             user_basedn, user_filter, and user_scope.
275             C) Assuming we found the object, we will walk it's attributes
276             using L<Net::LDAP::Entry>'s get_value method. We store the
277             results in a hashref. If we do not find the object, then
278             undef is returned.
279             D) Return a hashref that looks like:
280              
281             $results = {
282             'ldap_entry' => $entry, # The Net::LDAP::Entry object
283             'attributes' => $attributes,
284             }
285              
286             This method is usually only called by find_user().
287              
288             =cut
289              
290             sub lookup_user {
291 4     4 1 45 my ( $self, $id ) = @_;
292              
293             # Trim trailing space or we confuse ourselves
294 4         12 $id =~ s/\s+$//;
295 4         12 my $ldap = $self->ldap_bind;
296 4         8 my @searchopts;
297 4 50       21 if ( defined( $self->user_basedn ) ) {
298 4         37 push( @searchopts, 'base' => $self->user_basedn );
299             }
300             else {
301 0         0 Catalyst::Exception->throw(
302             "You must set user_basedn before looking up users!");
303             }
304 4         30 my $filter = $self->_replace_filter( $self->user_filter, $id );
305 4         13 push( @searchopts, 'filter' => $filter );
306 4         19 push( @searchopts, 'scope' => $self->user_scope );
307 4 50       33 if ( defined( $self->user_search_options ) ) {
308 0         0 push( @searchopts, %{ $self->user_search_options } );
  0         0  
309             }
310 4         38 my $usersearch = $ldap->search(@searchopts);
311              
312 4 50       177965 return undef if ( $usersearch->is_error );
313              
314 4         69 my $userentry;
315 4         27 my $user_field = $self->user_field;
316 4         57 my $results_filter = $self->user_results_filter;
317 4         22 my $entry;
318 4 50       16 if ( defined($results_filter) ) {
319 0         0 $entry = &$results_filter($usersearch);
320             }
321             else {
322 4         19 $entry = $usersearch->pop_entry;
323             }
324 4 50       199 if ( $usersearch->pop_entry ) {
325 0         0 Catalyst::Exception->throw(
326             "More than one entry matches user search.\n"
327             . "Consider defining a user_results_filter sub." );
328             }
329              
330             # a little extra sanity check with the 'eq' since LDAP already
331             # says it matches.
332             # NOTE that Net::LDAP returns exactly what you asked for, but
333             # because LDAP is often case insensitive, FoO can match foo
334             # and so we normalize with lc().
335 4 50       137 if ( defined($entry) ) {
336 4 50       36 unless ( lc( $entry->get_value($user_field) ) eq lc($id) ) {
337 0         0 Catalyst::Exception->throw(
338             "LDAP claims '$user_field' equals '$id' but results entry does not match."
339             );
340             }
341 4         240 $userentry = $entry;
342             }
343              
344 4         70 $ldap->unbind;
345 4         4150 $ldap->disconnect;
346 4 50       513 unless ($userentry) {
347 0         0 return undef;
348             }
349 4         7 my $attrhash;
350 4         36 foreach my $attr ( $userentry->attributes ) {
351 20         132 my @attrvalues = $userentry->get_value($attr);
352 20 100       249 if ( scalar(@attrvalues) == 1 ) {
353 16         49 $attrhash->{ lc($attr) } = $attrvalues[0];
354             }
355             else {
356 4         17 $attrhash->{ lc($attr) } = \@attrvalues;
357             }
358             }
359              
360 4         13 eval { Catalyst::Utils::ensure_class_loaded( $self->entry_class ) };
  4         30  
361 4 50       1493 if ( !$@ ) {
362 0         0 bless( $userentry, $self->entry_class );
363 0         0 $userentry->{_use_unicode}++;
364             }
365 4         112 my $rv = {
366             'ldap_entry' => $userentry,
367             'attributes' => $attrhash,
368             };
369 4         96 return $rv;
370             }
371              
372             =head2 lookup_roles($userobj, [$ldap])
373              
374             This method looks up the roles for a given user. It takes a
375             L<Catalyst::Authentication::Store::LDAP::User> object
376             as it's first argument, and can optionally take a I<Net::LDAP> object which
377             is used rather than the default binding if supplied.
378              
379             It returns an array containing the role_field attribute from all the
380             objects that match it's criteria.
381              
382             =cut
383              
384             sub lookup_roles {
385 0     0 1 0 my ( $self, $userobj, $ldap ) = @_;
386 0 0 0     0 if ( $self->use_roles == 0 || $self->use_roles =~ /^false$/i ) {
387 0         0 return undef;
388             }
389 0 0 0     0 $ldap ||= $self->role_search_as_user
390             ? $userobj->ldap_connection : $self->ldap_bind;
391 0         0 my @searchopts;
392 0 0       0 if ( defined( $self->role_basedn ) ) {
393 0         0 push( @searchopts, 'base' => $self->role_basedn );
394             }
395             else {
396 0         0 Catalyst::Exception->throw(
397             "You must set up role_basedn before looking up roles!");
398             }
399 0         0 my $filter_value = $userobj->has_attribute( $self->role_value );
400 0 0       0 if ( !defined($filter_value) ) {
401 0         0 Catalyst::Exception->throw( "User object "
402             . $userobj->username
403             . " has no "
404             . $self->role_value
405             . " attribute, so I can't look up it's roles!" );
406             }
407 0         0 my $filter = $self->_replace_filter( $self->role_filter, $filter_value );
408 0         0 push( @searchopts, 'filter' => $filter );
409 0         0 push( @searchopts, 'scope' => $self->role_scope );
410 0         0 push( @searchopts, 'attrs' => [ $self->role_field ] );
411 0 0       0 if ( defined( $self->role_search_options ) ) {
412 0         0 push( @searchopts, %{ $self->role_search_options } );
  0         0  
413             }
414 0         0 my $rolesearch = $ldap->search(@searchopts);
415 0         0 my @roles;
416 0         0 RESULT: foreach my $entry ( $rolesearch->entries ) {
417 0         0 push( @roles, $entry->get_value( $self->role_field ) );
418             }
419 0         0 return @roles;
420             }
421              
422             sub _replace_filter {
423 4     4   27 my $self = shift;
424 4         6 my $filter = shift;
425 4         7 my $replace = shift;
426 4         31 $replace =~ s/([*()\\\x{0}])/sprintf '\\%02x', ord($1)/ge;
  4         31  
427 4         22 $filter =~ s/\%s/$replace/g;
428 4         15 return $filter;
429             }
430              
431             =head2 user_supports
432              
433             Returns the value of
434             Catalyst::Authentication::Store::LDAP::User->supports(@_).
435              
436             =cut
437              
438             sub user_supports {
439 0     0 1   my $self = shift;
440              
441             # this can work as a class method
442 0           Catalyst::Authentication::Store::LDAP::User->supports(@_);
443             }
444              
445             =head2 from_session( I<id>, I<$c> )
446              
447             Returns get_user() for I<id>.
448              
449             =cut
450              
451             sub from_session {
452 0     0 1   my ( $self, $c, $id ) = @_;
453 0           $self->get_user( $id, $c );
454             }
455              
456             1;
457              
458             __END__
459              
460             =head1 AUTHORS
461              
462             Adam Jacob <holoway@cpan.org>
463              
464             Some parts stolen shamelessly and entirely from
465             L<Catalyst::Plugin::Authentication::Store::Htpasswd>.
466              
467             Currently maintained by Peter Karman <karman@cpan.org>.
468              
469             =head1 THANKS
470              
471             To nothingmuch, ghenry, castaway and the rest of #catalyst for the help. :)
472              
473             =head1 SEE ALSO
474              
475             L<Catalyst::Authentication::Store::LDAP>, L<Catalyst::Authentication::Store::LDAP::User>, L<Catalyst::Plugin::Authentication>, L<Net::LDAP>
476              
477             =head1 COPYRIGHT & LICENSE
478              
479             Copyright (c) 2005 the aforementioned authors. All rights
480             reserved. This program is free software; you can redistribute
481             it and/or modify it under the same terms as Perl itself.
482              
483             =cut
484