File Coverage

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


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 10     10   524519 use base qw( Class::Accessor::Fast );
  10         27  
  10         4503  
73              
74 10     10   29190 use strict;
  10         17  
  10         170  
75 10     10   31 use warnings;
  10         18  
  10         411  
76              
77             our $VERSION = '1.016';
78              
79 10     10   4621 use Catalyst::Authentication::Store::LDAP::User;
  10         26  
  10         121  
80 10     10   2536 use Net::LDAP;
  10         121776  
  10         65  
81 10     10   4246 use Catalyst::Utils ();
  10         330622  
  10         228  
82 10     10   3688 use Catalyst::Exception;
  10         1143101  
  10         587  
83              
84             BEGIN {
85 10     10   186 __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 14     14 1 24770 my ( $class, $config ) = @_;
110              
111 14 50 33     140 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 14         22 my %config_hash = %{$config};
  14         98  
117 14   50     69 $config_hash{'binddn'} ||= 'anonymous';
118 14   50     37 $config_hash{'user_filter'} ||= '(uid=%s)';
119 14   50     32 $config_hash{'user_scope'} ||= 'sub';
120 14   50     30 $config_hash{'user_field'} ||= 'uid';
121 14   100     66 $config_hash{'role_filter'} ||= '(memberUid=%s)';
122 14   100     47 $config_hash{'role_scope'} ||= 'sub';
123 14   100     55 $config_hash{'role_field'} ||= 'cn';
124             $config_hash{'use_roles'} = '1'
125 14 100       33 unless exists $config_hash{use_roles};
126 14   50     61 $config_hash{'start_tls'} ||= '0';
127 14   100     245 $config_hash{'entry_class'} ||= 'Catalyst::Model::LDAP::Entry';
128 14   100     56 $config_hash{'user_class'}
129             ||= 'Catalyst::Authentication::Store::LDAP::User';
130 14   100     53 $config_hash{'role_search_as_user'} ||= 0;
131 14   100     56 $config_hash{'persist_in_session'} ||= 'username';
132             Catalyst::Exception->throw('persist_in_session must be either username or all')
133 14 50       89 unless $config_hash{'persist_in_session'} =~ /\A(?:username|all)\z/;
134              
135 14         76 Catalyst::Utils::ensure_class_loaded( $config_hash{'user_class'} );
136 14         1255 my $self = \%config_hash;
137 14         30 bless( $self, $class );
138 14         31 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 15     15 1 2845 my ( $self, $authinfo, $c ) = @_;
154 15   33     83 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 18     18 1 27 my ( $self, $id, $c ) = @_;
169 18         60 my $user = $self->user_class->new( $self, $self->lookup_user($id), $c );
170 18         86 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 15     15 1 27 my ($self) = shift;
182 15         17 my $ldap;
183 15 100       59 if ( defined( $self->ldap_server_options() ) ) {
184             $ldap
185             = Net::LDAP->new( $self->ldap_server,
186 4 50       32 %{ $self->ldap_server_options } )
  4         31  
187             or Catalyst::Exception->throw($@);
188             }
189             else {
190 11 50       99 $ldap = Net::LDAP->new( $self->ldap_server )
191             or Catalyst::Exception->throw($@);
192             }
193 15 50 33     15512 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 15         265 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 20     20 1 110 my ( $self, $ldap, $binddn, $bindpw ) = @_;
220 20   33     110 $ldap ||= $self->ldap_connect;
221 20 50       365 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 20 100       44 if ( !defined $binddn ) {
228 19         62 $binddn = $self->binddn;
229 19         104 $bindpw = $self->bindpw;
230             }
231              
232 20 100       86 if ( $binddn eq "anonymous" ) {
233 19         46 $self->_ldap_bind_anon($ldap);
234             }
235             else {
236 1 50       3 if ($bindpw) {
237 1         4 my $mesg = $ldap->bind( $binddn, 'password' => $bindpw );
238 1 50       41 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 20         370 return $ldap;
248             }
249              
250             sub _ldap_bind_anon {
251 19     19   27 my ( $self, $ldap, $dn ) = @_;
252 19         73 my $mesg = $ldap->bind($dn);
253 19 50       18133 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 300 my ( $self, $binddn, $bindpw ) = @_;
268 3         11 my $ldap = $self->ldap_connect;
269 3 50       97 if ( !defined $ldap ) {
270 0         0 Catalyst::Exception->throw("LDAP server undefined!");
271             }
272 3         15 my $mesg = $ldap->bind( $binddn, password => $bindpw );
273 3 50       1702 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 it's 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 18     18 1 143 my ( $self, $id ) = @_;
300              
301             # Trim trailing space or we confuse ourselves
302 18         41 $id =~ s/\s+$//;
303 18         45 my $ldap = $self->ldap_bind;
304 18         26 my @searchopts;
305 18 50       66 if ( defined( $self->user_basedn ) ) {
306 18         104 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 18         93 my $filter = $self->_replace_filter( $self->user_filter, $id );
313 18         39 push( @searchopts, 'filter' => $filter );
314 18         51 push( @searchopts, 'scope' => $self->user_scope );
315 18 50       105 if ( defined( $self->user_search_options ) ) {
316 0         0 push( @searchopts, %{ $self->user_search_options } );
  0         0  
317             }
318 18         125 my $usersearch = $ldap->search(@searchopts);
319              
320 18 100       599677 return undef if ( $usersearch->is_error );
321              
322 16         261 my $userentry;
323 16         101 my $user_field = $self->user_field;
324 16         145 my $results_filter = $self->user_results_filter;
325 16         63 my $entry;
326 16 50       50 if ( defined($results_filter) ) {
327 0         0 $entry = &$results_filter($usersearch);
328             }
329             else {
330 16         66 $entry = $usersearch->pop_entry;
331             }
332 16 50       489 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 16 50       330 if ( defined($entry) ) {
344 16 50       91 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 16         567 $userentry = $entry;
350             }
351              
352 16         82 $ldap->unbind;
353 16         9287 $ldap->disconnect;
354 16 50       1415 unless ($userentry) {
355 0         0 return undef;
356             }
357 16         31 my $attrhash;
358 16         81 foreach my $attr ( $userentry->attributes ) {
359 74         346 my @attrvalues = $userentry->get_value($attr);
360 74 100       647 if ( scalar(@attrvalues) == 1 ) {
361 60         133 $attrhash->{ lc($attr) } = $attrvalues[0];
362             }
363             else {
364 14         44 $attrhash->{ lc($attr) } = \@attrvalues;
365             }
366             }
367              
368 16         33 eval { Catalyst::Utils::ensure_class_loaded( $self->entry_class ) };
  16         77  
369 16 50       10853 if ( !$@ ) {
370 16         53 bless( $userentry, $self->entry_class );
371 16         113 $userentry->{_use_unicode}++;
372             }
373 16         62 my $rv = {
374             'ldap_entry' => $userentry,
375             'attributes' => $attrhash,
376             };
377 16         147 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 it's 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 it's criteria.
389              
390             =cut
391              
392             sub lookup_roles {
393 3     3 1 258 my ( $self, $userobj, $ldap ) = @_;
394 3 100 66     21 if ( $self->use_roles == 0 || $self->use_roles =~ /^false$/i ) {
395 1         12 return ();
396             }
397 2 100 33     26 $ldap ||= $self->role_search_as_user
398             ? $userobj->ldap_connection : $self->ldap_bind;
399 2         2 my @searchopts;
400 2 50       8 if ( defined( $self->role_basedn ) ) {
401 2         9 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         9 my $filter_value = $userobj->has_attribute( $self->role_value );
408 2 50       201 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 it's roles!" );
414             }
415 2         21 my $filter = $self->_replace_filter( $self->role_filter, $filter_value );
416 2         2 push( @searchopts, 'filter' => $filter );
417 2         9 push( @searchopts, 'scope' => $self->role_scope );
418 2         12 push( @searchopts, 'attrs' => [ $self->role_field ] );
419 2 50       11 if ( defined( $self->role_search_options ) ) {
420 0         0 push( @searchopts, %{ $self->role_search_options } );
  0         0  
421             }
422 2         13 my $rolesearch = $ldap->search(@searchopts);
423 2         84 my @roles;
424 2         7 RESULT: foreach my $entry ( $rolesearch->entries ) {
425 4         216 push( @roles, $entry->get_value( $self->role_field ) );
426             }
427 2         68 return @roles;
428             }
429              
430             sub _replace_filter {
431 20     20   86 my $self = shift;
432 20         24 my $filter = shift;
433 20         26 my $replace = shift;
434 20         50 $replace =~ s/([*()\\\x{0}])/sprintf '\\%02x', ord($1)/ge;
  4         17  
435 20         77 $filter =~ s/\%s/$replace/g;
436 20         40 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 788 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       19 if ( ref $frozenuser eq 'HASH' ) {
468             # we can rely on the existance of this key if the user is a hashref
469 2 50       8 if ( $frozenuser->{persist_in_session} eq 'all' ) {
470 2         8 return $self->user_class->new( $self, $frozenuser->{user}, $c, $frozenuser->{_roles} );
471             }
472             }
473              
474 3         8 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