File Coverage

blib/lib/Catalyst/Authentication/Store/LDAP/Backend.pm
Criterion Covered Total %
statement 144 172 83.7
branch 41 68 60.2
condition 26 42 61.9
subroutine 19 20 95.0
pod 10 10 100.0
total 240 312 76.9


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