File Coverage

blib/lib/Dancer2/Plugin/Auth/Extensible/Provider/LDAP.pm
Criterion Covered Total %
statement 60 65 92.3
branch 22 30 73.3
condition 5 9 55.5
subroutine 9 9 100.0
pod 3 3 100.0
total 99 116 85.3


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