File Coverage

blib/lib/Dancer2/Plugin/HTTP/Auth/Extensible.pm
Criterion Covered Total %
statement 162 192 84.3
branch 70 98 71.4
condition 11 25 44.0
subroutine 25 27 92.5
pod 8 15 53.3
total 276 357 77.3


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::HTTP::Auth::Extensible;
2              
3 5     5   1491905 use warnings;
  5         10  
  5         153  
4 5     5   18 use strict;
  5         5  
  5         124  
5              
6 5     5   17 use Carp;
  5         9  
  5         289  
7 5     5   2310 use Dancer2::Plugin;
  5         89537  
  5         29  
8 5     5   1506 use Class::Load qw(try_load_class);
  5         7  
  5         230  
9              
10 5     5   2482 use HTTP::Headers::ActionPack::Authorization;
  5         101081  
  5         166  
11 5     5   2531 use HTTP::Headers::ActionPack::WWWAuthenticate;
  5         848  
  5         10349  
12              
13             our $VERSION = '0.100';
14              
15              
16              
17             sub http_require_authentication {
18 9     9 1 203075 my $dsl = shift;
19 9 100       48 my $realm = (@_ == 2) ? shift : http_default_realm($dsl);
20 9         456 my $coderef = shift;
21              
22             return sub {
23 11 50 33 11   284938 if (!$coderef || ref $coderef ne 'CODE') {
24 0         0 warn "Invalid http_require_authentication usage, please see docs";
25             }
26            
27 11         103 my $user = http_authenticated_user($dsl, $realm);
28 10 100       92 if (!$user) {
29             # $dsl->execute_hook('http_authentication_required', $coderef);
30             # # TODO: see if any code executed by that hook set up a response
31 5         9 $dsl->header('WWW-Authenticate' =>
32 5         13 qq|@{[ http_default_scheme($dsl) ]} realm="$realm"|
33             );
34 5         533 $dsl->status(401); # Unauthorized
35             return
36 5         1462 qq|Authentication required to access realm: |
37             . qq|'$realm'|;
38             }
39 5         22 return $coderef->($dsl);
40 9         73 };
41             }
42              
43             register http_require_authentication => \&http_require_authentication;
44             register http_requires_authentication => \&http_require_authentication;
45              
46              
47             sub http_require_role {
48 5     5 1 33 return _build_wrapper(@_, 'single');
49             }
50              
51             register http_require_role => \&http_require_role;
52             register http_requires_role => \&http_require_role;
53              
54              
55             sub http_require_any_role {
56 1     1 1 18 return _build_wrapper(@_, 'any');
57             }
58              
59             register http_require_any_role => \&http_require_any_role;
60             register http_requires_any_role => \&http_require_any_role;
61              
62              
63             sub http_require_all_roles {
64 2     2 1 19 return _build_wrapper(@_, 'all');
65             }
66              
67             register http_require_all_roles => \&http_require_all_roles;
68             register http_requires_all_roles => \&http_require_all_roles;
69              
70              
71             sub _build_wrapper {
72 8     8   10 my $dsl = shift;
73 8         9 my $require_role = shift;
74 8 50       25 my $realm = (@_ == 3) ? shift : http_default_realm($dsl);
75 8         517 my $coderef = shift;
76 8         10 my $mode = shift;
77              
78             return sub {
79 8 50 33 8   73079 if (!$coderef || ref $coderef ne 'CODE') {
80 0         0 warn "Invalid http_require_authentication usage, please see docs";
81             }
82            
83 8         31 my $user = http_authenticated_user($dsl, $realm);
84 8 50       131 if (!$user) {
85             # $dsl->execute_hook('http_authentication_required', $coderef);
86             # # TODO: see if any code executed by that hook set up a response
87 0         0 $dsl->header('WWW-Authenticate' =>
88 0         0 qq|@{[ http_default_scheme($dsl) ]} realm="$realm"|
89             );
90 0         0 $dsl->status(401); # Unauthorized
91             return
92 0         0 qq|Authentication required to access realm: |
93             . qq|'$realm'|;
94             }
95            
96 8 100       37 my @role_list = ref $require_role eq 'ARRAY'
97             ? @$require_role
98             : $require_role;
99 8         12 my $role_match;
100 8 100       33 if ($mode eq 'single') {
    100          
    50          
101 5         13 for (user_roles($dsl)) {
102 10 100 100     22 $role_match++ and last if _smart_match($_, $require_role);
103             }
104             } elsif ($mode eq 'any') {
105 1         3 my %role_ok = map { $_ => 1 } @role_list;
  2         7  
106 1         5 for (user_roles($dsl)) {
107 2 100 50     10 $role_match++ and last if $role_ok{$_};
108             }
109             } elsif ($mode eq 'all') {
110 2         3 $role_match++;
111 2         5 for my $role (@role_list) {
112 4 100       8 if (!user_has_role($dsl, $role)) {
113 1         1 $role_match = 0;
114 1         3 last;
115             }
116             }
117             }
118 8 100       24 if (!$role_match) {
119              
120             # $dsl->execute_hook('http_permission_denied', $coderef);
121             # # TODO: see if any code executed by that hook set up a response
122 3         11 $dsl->status(403); # Forbidden
123             return
124 3         420 qq|Permission denied for resource: |
125 3         10 . qq|'@{[ $dsl->request->path ]}'|;
126             }
127            
128             # We're happy with their roles, so go head and execute the route
129             # handler coderef.
130 5         18 return $coderef->($dsl);
131              
132 8         91 }; # return sub
133             } # _build_wrapper
134              
135              
136              
137              
138             sub http_authenticated_user {
139 19     19 0 35 my $dsl = shift;
140 19   33     59 my $realm = shift || http_default_realm($dsl);
141            
142 19 100       51 if ( http_authenticate_user($dsl, $realm) ) { # undef unless http_authenticate_user
143 13         38 my $provider = auth_provider($dsl, http_realm($dsl));
144 13         41 return $provider->get_user_details(
145             http_username($dsl),
146             http_realm($dsl)
147             );
148             } else {
149 5         9 return;
150             }
151             }
152             register http_authenticated_user => \&http_authenticated_user;
153              
154              
155             sub user_has_role {
156 4     4 1 6 my $dsl = shift;
157 4         91 my $session = $dsl->app->session;
158              
159 4         1189 my ($username, $want_role);
160 4 50       11 if (@_ == 2) {
161 0         0 ($username, $want_role) = @_;
162             } else {
163 4         8 $username = http_username($dsl);
164 4         20 $want_role = shift;
165             }
166              
167 4 50       7 return unless defined $username;
168              
169 4         9 my $roles = user_roles($dsl, $username);
170              
171 4         37 for my $has_role (@$roles) {
172 7 100       30 return 1 if $has_role eq $want_role;
173             }
174              
175 1         4 return 0;
176             }
177             register user_has_role => \&user_has_role;
178              
179              
180             sub user_roles {
181 10     10 1 16 my ($dsl, $username, $realm) = @_;
182 10         183 my $session = $dsl->app->session;
183              
184 10 100       10979 $username = http_username($dsl) unless defined $username;
185              
186 10 50       45 my $search_realm = ($realm ? $realm : '');
187              
188 10         18 my $roles = auth_provider($dsl, $search_realm)->get_user_roles($username);
189 10 50       152 return unless defined $roles;
190 10 100       40 return wantarray ? @$roles : $roles;
191             }
192             register user_roles => \&user_roles;
193              
194              
195              
196             sub http_authenticate_user {
197 19     19 0 25 my $dsl = shift;
198 19   33     45 my $realm = shift || http_default_realm($dsl);
199            
200 19         47 http_realm_exists($dsl, $realm);
201              
202 18 100       98 unless ($dsl->request->header('Authorization')) {
203 4 50       231 return wantarray ? (0, undef) : 0;
204             }
205             # my ($username, $password) = $dsl->request->headers->authorization_basic;
206            
207 14         881 my $auth
208             = HTTP::Headers::ActionPack::Authorization::Basic
209             ->new_from_string($dsl->request->header('Authorization'));
210 14         1307 my $username = $auth->username;
211 14         267 my $password = $auth->password;
212            
213             # TODO For now it only does Basic authentication
214             # Once we have Digest and others, it needs to choose itself
215            
216 14 50       76 my @realms_to_check = $realm ? ($realm) : (keys %{ plugin_setting->{realms} });
  0         0  
217              
218 14         31 for my $realm (@realms_to_check) { # XXX we only should have 1 ????
219 14         81 $dsl->app->log ( debug => "Attempting to authenticate $username against realm $realm");
220 14         7891 my $provider = auth_provider($dsl, $realm);
221 14 100       83 if ($provider->authenticate_user($username, $password)) {
222 13         399 $dsl->app->log ( debug => "$realm accepted user $username");
223 13         3643 $dsl->vars->{'http_username'} = $username;
224             # don't do `http_username($dsl, $username)`, SECURITY BREACH
225 13         130 $dsl->vars->{'http_realm' } = $realm;
226             # don't do `http_username($dsl, $username)`, SECURITY BREACH
227 13 50       137 return wantarray ? ($username, $realm) : $username;
228             }
229             }
230              
231             # If we get to here, we failed to authenticate against any realm using the
232             # details provided.
233             # TODO: allow providers to raise an exception if something failed, and catch
234             # that and do something appropriate, rather than just treating it as a
235             # failed login.
236 1 50       39 return wantarray ? (0, undef) : 0;
237             }
238              
239             register http_authenticate_user => \&http_authenticate_user;
240              
241             sub http_default_realm {
242 29     29 0 42 my $dsl = shift;
243            
244 29 100       43 if (1 == keys %{ plugin_setting->{realms} }) {
  29         85  
245 25         2607 return (keys %{ plugin_setting->{realms} })[0]; # only the first key in scalar context
  25         56  
246             }
247 4 50       453 if (exists plugin_setting->{default_realm} ) {
248 4         373 return plugin_setting->{default_realm};
249             }
250              
251             die
252 0         0 qq|Internal Server Error: |
253             . qq|"multiple realms without default"|;
254            
255 0         0 return;
256            
257             } # http_default_realm
258              
259             #register http_default_realm => \&http_default_realm;
260              
261             sub http_realm_exists {
262 61     61 0 61 my $dsl = shift;
263 61   33     117 my $realm = shift || http_default_realm($dsl);
264              
265 61 100       63 unless (grep {$realm eq $_} keys %{ plugin_setting->{realms} }) {
  68         8365  
  61         148  
266 1         11 die
267             qq|Internal Server Error: |
268             . qq|"required realm does not exist: '$realm'"|;
269             }
270            
271 60         132 return $realm;
272            
273             } # http_realm_exists
274              
275             #register http_realm_exists => \&http_realm_exists;
276              
277              
278             sub http_default_scheme {
279 5     5 0 6 my $dsl = shift;
280 5   33     30 my $realm = shift || http_default_realm($dsl);
281              
282 5         475 http_realm_exists($dsl, $realm);
283            
284 5         6 my $scheme;
285            
286 5 50       11 if (exists plugin_setting->{realms}->{$realm}->{scheme} ) {
287 5         531 $scheme = plugin_setting->{realms}->{$realm}->{scheme};
288             }
289             else {
290 0         0 $scheme = "Basic";
291             }
292            
293 5         634 return $scheme;
294            
295             } # http_default_schema
296              
297             #register http_default_scheme => \&http_default_scheme;
298              
299              
300             sub http_scheme_known {
301 0     0 0 0 my $dsl = shift;
302 0         0 my $scheme = shift;
303              
304 0 0       0 unless (grep $scheme eq $_, ('Basic', 'Digest')) {
305 0         0 warn
306             qq|unknown scheme '$scheme'!|;
307 0         0 return;
308             }
309            
310 0         0 return $scheme;
311            
312             } # http_scheme_known
313              
314             #register http_scheme_known => \&http_scheme_known;
315              
316              
317             sub http_username {
318 29     29 1 19466 my $dsl = shift;
319            
320 29 100       74 unless ( exists $dsl->vars->{http_username} ) {
321 2         21 $dsl->app->log( warning =>
322             qq|'http_username' should only be used in an authenticated route|
323             );
324             }
325            
326 29 100       759 if (@_ == 1) { # CAUTION: use with care
327 2         6 $dsl->vars->{http_username} = shift;
328 2         13 my $message
329             = qq|POTENTIONAL SECURITY BREACH: |
330             . qq|"impersonating different user: '|
331             . $dsl->vars->{http_username}
332             . qq|'"|;
333 2         80 warn $message;
334 2         15 $dsl->app->log ( warning => $message );
335             }
336            
337 29 100       594 return unless exists $dsl->vars->{http_username};
338 28         185 return $dsl->vars->{http_username};
339            
340             } # http_username
341              
342             register http_username => \&http_username;
343              
344              
345             sub http_realm {
346 32     32 1 21255 my $dsl = shift;
347            
348 32 100       69 unless ( exists $dsl->vars->{http_realm} ) {
349 2         26 $dsl->app->log( warning =>
350             qq|'http_realm' should only be used in an authenticated route|
351             );
352             }
353            
354 32 100       696 if (@_ == 1) { # CAUTION: use with care
355 2         6 $dsl->vars->{http_realm} = shift;
356 2         15 my $message
357             = qq|POTENTIONAL SECURITY BREACH: |
358             . qq|"switching to different realm: '|
359             . $dsl->vars->{http_realm}
360             . qq|'"|;
361 2         141 warn $message;
362 2         15 $dsl->app->log ( warning => $message );
363             }
364            
365 32 100       610 return unless exists $dsl->vars->{http_realm};
366 31         167 return $dsl->vars->{http_realm};
367            
368             } # http_realm
369              
370             register http_realm => \&http_realm;
371              
372              
373             {
374             # Given a realm, returns a configured and ready to use instance of the provider
375             # specified by that realm's config.
376             my %realm_provider;
377             sub auth_provider {
378 37     37 0 89 my $dsl = shift;
379 37   66     100 my $realm = shift || http_default_realm($dsl);
380            
381 37         1093 http_realm_exists($dsl, $realm); # can be in void, it dies when false
382              
383             # First, if we already have a provider for this realm, go ahead and use it:
384 37 100       153 return $realm_provider{$realm} if exists $realm_provider{$realm};
385              
386             # OK, we need to find out what provider this realm uses, and get an instance
387             # of that provider, configured with the settings from the realm.
388 3 50       11 my $realm_settings = plugin_setting->{realms}->{$realm}
389             or die "Invalid realm $realm";
390 3 50       354 my $provider_class = $realm_settings->{provider}
391             or die "No provider configured - consult documentation for "
392             . "Dancer2::Plugin::Auth::Extensible";
393              
394 3 50       13 if ($provider_class !~ /::/) {
395 3         12 $provider_class = "Dancer2::Plugin::Auth::Extensible" . "::Provider::$provider_class";
396             }
397 3         13 my ($ok, $error) = try_load_class($provider_class);
398              
399 3 50       12392 if (! $ok) {
400 0         0 die "Cannot load provider $provider_class: $error";
401             }
402              
403 3         38 return $realm_provider{$realm} = $provider_class->new($realm_settings);
404             }
405             }
406              
407             register_hook qw(http_authentication_required http_permission_denied);
408             register_plugin for_versions => [qw(1 2)];
409              
410              
411             # Given a class method name and a set of parameters, try calling that class
412             # method for each realm in turn, arranging for each to receive the configuration
413             # defined for that realm, until one returns a non-undef, then return the realm which
414             # succeeded and the response.
415             # Note: all provider class methods return a single value; if any need to return
416             # a list in future, this will need changing)
417             sub _try_realms {
418 0     0   0 my ($method, @args);
419 0         0 for my $realm (keys %{ plugin_setting->{realms} }) {
  0         0  
420 0         0 my $provider = auth_provider($realm);
421 0 0       0 if (!$provider->can($method)) {
422 0         0 die "Provider $provider does not provide a $method method!";
423             }
424 0 0       0 if (defined(my $result = $provider->$method(@args))) {
425 0         0 return $result;
426             }
427             }
428 0         0 return;
429             }
430              
431             on_plugin_import {
432             my $dsl = shift;
433             my $app = $dsl->app;
434              
435             };
436              
437              
438             # Replacement for much maligned and misunderstood smartmatch operator
439             sub _smart_match {
440 10     10   12 my ($got, $want) = @_;
441 10 100       24 if (!ref $want) {
    50          
    0          
442 8         31 return $got eq $want;
443             } elsif (ref $want eq 'Regexp') {
444 2         13 return $got =~ $want;
445             } elsif (ref $want eq 'ARRAY') {
446 0           return grep { $_ eq $got } @$want;
  0            
447             } else {
448 0           carp "Don't know how to match against a " . ref $want;
449             }
450             }
451              
452              
453              
454              
455              
456             1; # End of Dancer2::Plugin::HTTP::Auth::Extensible
457              
458             __END__