File Coverage

blib/lib/Dancer2/Plugin/Auth/Extensible/Provider/LDAP.pm
Criterion Covered Total %
statement 60 73 82.1
branch 23 34 67.6
condition 5 9 55.5
subroutine 10 11 90.9
pod 3 3 100.0
total 101 130 77.6


line stmt bran cond sub pod time code
1              
2             use Carp qw/croak/;
3 1     1   945254 use Dancer2::Core::Types qw/HashRef Str/;
  1         12  
  1         59  
4 1     1   455 use Net::LDAP;
  1         200561  
  1         10  
5 1     1   3995  
  1         3  
  1         12  
6             use Moo;
7 1     1   619 with "Dancer2::Plugin::Auth::Extensible::Role::Provider";
  1         6917  
  1         6  
8             use namespace::clean;
9 1     1   1534  
  1         3  
  1         11  
10             our $VERSION = '0.706';
11              
12             =head1 NAME
13              
14             Dancer2::Plugin::Auth::Extensible::Provider::LDAP - LDAP authentication provider for Dancer2::Plugin::Auth::Extensible
15              
16             =head1 DESCRIPTION
17              
18             This class is a generic LDAP authentication provider.
19              
20             See L<Dancer2::Plugin::Auth::Extensible> for details on how to use the
21             authentication framework.
22              
23             =head1 ATTRIBUTES
24              
25             =head2 host
26              
27             The LDAP host name or IP address passed to L<Net::LDAP/CONSTRUCTOR>.
28              
29             Required.
30              
31             =cut
32              
33             has host => (
34             is => 'ro',
35             isa => Str,
36             required => 1,
37             );
38              
39             =head2 options
40              
41             Extra options to be passed to L<Net::LDAP/CONSTRUCTOR> as a hash reference.
42              
43             =cut
44              
45             has options => (
46             is => 'ro',
47             isa => HashRef,
48             default => sub { +{} },
49             );
50              
51             =head2 basedn
52              
53             The base dn for all searches (e.g. 'dc=example,dc=com').
54              
55             Required.
56              
57             =cut
58              
59             has basedn => (
60             is => 'ro',
61             isa => Str,
62             required => 1,
63             );
64              
65             =head2 binddn
66              
67             This must be the distinguished name of a user capable of binding to
68             and reading the directory (e.g. 'cn=admin,dc=example,dc=com').
69              
70             Not required, as some LDAP setups allow for anonymous binding.
71              
72             =cut
73              
74             has binddn => (
75             is => 'ro',
76             isa => Str,
77             required => 0,
78             );
79              
80             =head2 bindpw
81              
82             The password for L</binddn>.
83              
84             Not required, as some LDAP setups allow for anonymous binding.
85              
86             =cut
87              
88             has bindpw => (
89             is => 'ro',
90             isa => Str,
91             required => 0,
92             );
93              
94             =head2 ldap
95              
96             Returns a connected L<Net::LDAP> object.
97              
98             =cut
99              
100             has ldap => (
101             is => 'lazy',
102             clearer => '_clear_ldap',
103             predicate => '_has_ldap',
104             );
105              
106             my $self = shift;
107             my $ldap = Net::LDAP->new( $self->host, %{ $self->options } )
108 0     0   0 or croak "LDAP connect failed for: " . $self->host;
109 0 0       0 return $ldap;
  0         0  
110             }
111 0         0  
112             =head2 username_attribute
113              
114             The attribute to match when searching for a username.
115              
116             Defaults to 'cn'.
117              
118             =cut
119              
120             has username_attribute => (
121             is => 'ro',
122             isa => Str,
123             default => 'cn',
124             );
125              
126             =head2 name_attribute
127              
128             The attribute which contains the full name of the user. See also:
129              
130             L<Dancer2::Plugin::Auth::Extensible::Role::User/name>.
131              
132             Defaults to 'displayName'.
133              
134             =cut
135              
136             has name_attribute => (
137             is => 'ro',
138             isa => Str,
139             default => 'displayName',
140             );
141              
142             =head2 user_filter
143              
144             Filter used when searching for users.
145              
146             Defaults to '(objectClass=person)'.
147              
148             =cut
149              
150             has user_filter => (
151             is => 'ro',
152             isa => Str,
153             default => '(objectClass=person)',
154             );
155              
156             =head2 role_attribute
157              
158             The attribute used when searching for role names.
159              
160             Defaults to 'cn'.
161              
162             =cut
163              
164             has role_attribute => (
165             is => 'ro',
166             isa => Str,
167             default => 'cn',
168             );
169              
170             =head2 role_filter
171              
172             Filter used when searching for roles.
173              
174             Defaults to '(objectClass=groupOfNames)'
175              
176             =cut
177              
178             has role_filter => (
179             is => 'ro',
180             isa => Str,
181             default => '(objectClass=groupOfNames)',
182             );
183              
184             =head2 role_member_attribute_name
185              
186             The attribute of a user object who's value should be the value used to identify
187             which roles a specific user is a member of.
188              
189             Defaults to 'dn'
190              
191             =cut
192              
193             has role_member_attribute_name => (
194             is => 'ro',
195             isa => Str,
196             default => 'dn',
197             );
198              
199             =head2 role_member_attribute
200              
201             The attribute of a role object who's value should be the value of a user's
202             L</role_member_attribute_name> attribute to look up which roles a user is a
203             member of.
204              
205             Defaults to 'member'.
206              
207             =cut
208              
209             has role_member_attribute => (
210             is => 'ro',
211             isa => Str,
212             default => 'member',
213             );
214              
215             my ($self) = @_;
216              
217             return
218 105     105   268 unless $self->_has_ldap;
219              
220             my $ldap = $self->ldap;
221 105 50       444  
222             $ldap->unbind;
223 0         0 $ldap->disconnect;
224             $self->_clear_ldap;
225 0         0 }
226 0         0  
227 0         0 my ( $self, $username, $dummy, $password ) = @_;
228              
229             my $ldap = $self->ldap or return;
230              
231 111     111   376 # If either username or password is defined, ensure we have both,
232             # otherwise we cannot bind to LDAP. Otherwise, assume we are going
233 111 50       343 # to anonymously bind.
234             my $mesg;
235             if( !defined $username && !defined $password ) {
236             $self->plugin->app->log( debug => "Binding to LDAP anonymously" );
237             $mesg = $ldap->bind;
238 111         570 }
239 111 50 33     412 else {
240 0         0 croak "username and password must be defined"
241 0         0 unless defined $username && defined $password;
242              
243             $self->plugin->app->log( debug => "Binding to LDAP with credentials" );
244 111 50 33     609 $mesg = $ldap->bind( $username, password => $password );
245             }
246              
247 111         696 return $mesg;
248 111         60768 }
249              
250             =head1 METHODS
251 111         42814  
252             =head2 authenticate_user $username, $password
253              
254             =cut
255              
256             my ( $self, $username, $password ) = @_;
257              
258             croak "username and password must be defined"
259             unless defined $username && defined $password;
260              
261 52     52 1 1526879 my $user = $self->get_user_details($username) or return;
262              
263 52 100 100     729 my $ldap = $self->ldap or return;
264              
265             my $mesg = $self->_bind_ldap( $user->{dn}, password => $password );
266 49 100       204  
267             $self->_unbind_ldap;
268 14 50       60  
269             return not $mesg->is_error;
270 14         120 }
271              
272 14         60 =head2 get_user_details $username
273              
274 14         53 =cut
275              
276             my ( $self, $username ) = @_;
277              
278             croak "username must be defined"
279             unless defined $username;
280              
281             my $ldap = $self->ldap or return;
282 98     98 1 401338  
283             my $mesg = $self->_bind_ldap( $self->binddn, password => $self->bindpw );
284 98 100       528  
285             if ( $mesg->is_error ) {
286             croak "LDAP bind error: " . $mesg->error;
287 97 50       433 }
288              
289 97         1024 $mesg = $ldap->search(
290             base => $self->basedn,
291 97 50       509 sizelimit => 1,
292 0         0 filter => '(&'
293             . $self->user_filter
294             . '(' . $self->username_attribute . '=' . $username . '))',
295 97         2053 );
296              
297             if ( $mesg->is_error ) {
298             croak "LDAP search error: " . $mesg->error;
299             }
300              
301             my $user;
302             if ( $mesg->count > 0 ) {
303 97 100       241248 my $entry = $mesg->entry(0);
304 6         73 $self->plugin->app->log(
305             debug => "User $username found with DN: ",
306             $entry->dn
307 91         1102 );
308 91 100       424  
309 52         828 # now get the roles
310 52         3691  
311             my $role_member_attribute_value;
312             if ( $self->role_member_attribute_name eq 'dn' ) {
313             $role_member_attribute_value = $entry->dn;
314             } else {
315             $role_member_attribute_value = $entry->get_value( $self->role_member_attribute_name );
316             }
317 52         29100 $mesg = $ldap->search(
318 52 50       314 base => $self->basedn,
319 52         216 filter => '(&'
320             . $self->role_filter . '('
321 0         0 . $self->role_member_attribute . '='
322             . $role_member_attribute_value . '))',
323 52         798 );
324              
325             if ( $mesg->is_error ) {
326             $self->plugin->app->log(
327             warning => "LDAP search error: " . $mesg->error );
328             }
329              
330             my @roles =
331 52 50       137932 map { $_->get_value( $self->role_attribute ) } $mesg->entries;
332 0         0  
333             $user = {
334             username => $username,
335             name => $entry->get_value( $self->name_attribute ),
336             dn => $entry->dn,
337 52         748 roles => \@roles,
  86         1218  
338             map { $_ => scalar $entry->get_value($_) } $entry->attributes,
339             };
340             }
341             else {
342             $self->plugin->app->log(
343             debug => "User not found via LDAP: $username" );
344 52         959 }
  216         4112  
345              
346             $self->_unbind_ldap;
347              
348 39         770 return $user;
349             }
350              
351             =head2 get_user_roles
352 91         23173  
353             =cut
354 91         390  
355             my ( $self, $username ) = @_;
356              
357             croak "username must be defined"
358             unless defined $username;
359              
360             my $user = $self->get_user_details($username) or return;
361              
362 17     17 1 35056 return $user->{roles};
363             }
364 17 100       195  
365             1;
366