blib/lib/Dancer/Plugin/Auth/Extensible.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 96 | 114 | 84.2 |
branch | 49 | 70 | 70.0 |
condition | 6 | 12 | 50.0 |
subroutine | 19 | 21 | 90.4 |
pod | 0 | 1 | 0.0 |
total | 170 | 218 | 77.9 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Dancer::Plugin::Auth::Extensible; | ||||||
2 | |||||||
3 | 2 | 2 | 602729 | use warnings; | |||
2 | 5 | ||||||
2 | 53 | ||||||
4 | 2 | 2 | 10 | use strict; | |||
2 | 4 | ||||||
2 | 55 | ||||||
5 | |||||||
6 | 2 | 2 | 11 | use Carp; | |||
2 | 6 | ||||||
2 | 116 | ||||||
7 | 2 | 2 | 1484 | use Dancer::Plugin; | |||
2 | 165778 | ||||||
2 | 185 | ||||||
8 | 2 | 2 | 2746 | use Dancer qw(:syntax); | |||
2 | 301046 | ||||||
2 | 13 | ||||||
9 | |||||||
10 | our $VERSION = '0.30'; | ||||||
11 | |||||||
12 | my $settings = plugin_setting; | ||||||
13 | |||||||
14 | my $loginpage = $settings->{login_page} || '/login'; | ||||||
15 | my $userhomepage = $settings->{user_home_page} || '/'; | ||||||
16 | my $logoutpage = $settings->{logout_page} || '/logout'; | ||||||
17 | my $deniedpage = $settings->{denied_page} || '/login/denied'; | ||||||
18 | my $exitpage = $settings->{exit_page}; | ||||||
19 | |||||||
20 | if(!$settings->{no_api_change_warning}) { | ||||||
21 | Dancer::Logger::warning(< | ||||||
22 | |||||||
23 | *************************************************************************** | ||||||
24 | * IMPORTANT NOTE: Dancer::Plugin::Auth::Extensible $VERSION * | ||||||
25 | * * | ||||||
26 | * This version of DPAE contains backwards-incompatible API changes, * | ||||||
27 | * replacing the subroutine-attributes based approach of previous versions * | ||||||
28 | * with new keywords. * | ||||||
29 | * * | ||||||
30 | * Please see http://advent.perldancer.org/2012/16 for details. * | ||||||
31 | *************************************************************************** | ||||||
32 | |||||||
33 | CHANGEWARNING | ||||||
34 | } | ||||||
35 | |||||||
36 | =head1 NAME | ||||||
37 | |||||||
38 | Dancer::Plugin::Auth::Extensible - extensible authentication framework for Dancer apps | ||||||
39 | |||||||
40 | =head1 DESCRIPTION | ||||||
41 | |||||||
42 | A user authentication and authorisation framework plugin for Dancer apps. | ||||||
43 | |||||||
44 | Makes it easy to require a user to be logged in to access certain routes, | ||||||
45 | provides role-based access control, and supports various authentication | ||||||
46 | methods/sources (config file, database, Unix system users, etc). | ||||||
47 | |||||||
48 | Designed to support multiple authentication realms and to be as extensible as | ||||||
49 | possible, and to make secure password handling easy (the base class for auth | ||||||
50 | providers makes handling C |
||||||
51 | have no excuse for storing plain-text passwords). | ||||||
52 | |||||||
53 | |||||||
54 | =head1 SYNOPSIS | ||||||
55 | |||||||
56 | Configure the plugin to use the authentication provider class you wish to use: | ||||||
57 | |||||||
58 | plugins: | ||||||
59 | Auth::Extensible: | ||||||
60 | realms: | ||||||
61 | users: | ||||||
62 | provider: Example | ||||||
63 | .... | ||||||
64 | |||||||
65 | The configuration you provide will depend on the authentication provider module | ||||||
66 | in use. For a simple example, see | ||||||
67 | L |
||||||
68 | |||||||
69 | Define that a user must be logged in and have the proper permissions to | ||||||
70 | access a route: | ||||||
71 | |||||||
72 | get '/secret' => require_role Confidant => sub { tell_secrets(); }; | ||||||
73 | |||||||
74 | Define that a user must be logged in to access a route - and find out who is | ||||||
75 | logged in with the C |
||||||
76 | |||||||
77 | get '/users' => require_login sub { | ||||||
78 | my $user = logged_in_user; | ||||||
79 | return "Hi there, $user->{username}"; | ||||||
80 | }; | ||||||
81 | |||||||
82 | =head1 AUTHENTICATION PROVIDERS | ||||||
83 | |||||||
84 | For flexibility, this authentication framework uses simple authentication | ||||||
85 | provider classes, which implement a simple interface and do whatever is required | ||||||
86 | to authenticate a user against the chosen source of authentication. | ||||||
87 | |||||||
88 | For an example of how simple provider classes are, so you can build your own if | ||||||
89 | required or just try out this authentication framework plugin easily, | ||||||
90 | see L |
||||||
91 | |||||||
92 | This framework supplies the following providers out-of-the-box: | ||||||
93 | |||||||
94 | =over 4 | ||||||
95 | |||||||
96 | =item L |
||||||
97 | |||||||
98 | Authenticates users using system accounts on Linux/Unix type boxes | ||||||
99 | |||||||
100 | =item L |
||||||
101 | |||||||
102 | Authenticates users stored in a database table | ||||||
103 | |||||||
104 | =item L |
||||||
105 | |||||||
106 | Authenticates users stored in the app's config | ||||||
107 | |||||||
108 | =back | ||||||
109 | |||||||
110 | Need to write your own? Just subclass | ||||||
111 | L |
||||||
112 | methods, and you're good to go! | ||||||
113 | |||||||
114 | =head1 CONTROLLING ACCESS TO ROUTES | ||||||
115 | |||||||
116 | Keywords are provided to check if a user is logged in / has appropriate roles. | ||||||
117 | |||||||
118 | =over | ||||||
119 | |||||||
120 | =item require_login - require the user to be logged in | ||||||
121 | |||||||
122 | get '/dashboard' => require_login sub { .... }; | ||||||
123 | |||||||
124 | If the user is not logged in, they will be redirected to the login page URL to | ||||||
125 | log in. The default URL is C - this may be changed with the | ||||||
126 | C |
||||||
127 | |||||||
128 | =item require_role - require the user to have a specified role | ||||||
129 | |||||||
130 | get '/beer' => require_role BeerDrinker => sub { ... }; | ||||||
131 | |||||||
132 | Requires that the user be logged in as a user who has the specified role. If | ||||||
133 | the user is not logged in, they will be redirected to the login page URL. If | ||||||
134 | they are logged in, but do not have the required role, they will be redirected | ||||||
135 | to the access denied URL. | ||||||
136 | |||||||
137 | =item require_any_roles - require the user to have one of a list of roles | ||||||
138 | |||||||
139 | get '/drink' => require_any_role [qw(BeerDrinker VodaDrinker)] => sub { | ||||||
140 | ... | ||||||
141 | }; | ||||||
142 | |||||||
143 | Requires that the user be logged in as a user who has any one (or more) of the | ||||||
144 | roles listed. If the user is not logged in, they will be redirected to the | ||||||
145 | login page URL. If they are logged in, but do not have any of the specified | ||||||
146 | roles, they will be redirected to the access denied URL. | ||||||
147 | |||||||
148 | =item require_all_roles - require the user to have all roles listed | ||||||
149 | |||||||
150 | get '/foo' => require_all_roles [qw(Foo Bar)] => sub { ... }; | ||||||
151 | |||||||
152 | Requires that the user be logged in as a user who has all of the roles listed. | ||||||
153 | If the user is not logged in, they will be redirected to the login page URL. If | ||||||
154 | they are logged in but do not have all of the specified roles, they will be | ||||||
155 | redirected to the access denied URL. | ||||||
156 | |||||||
157 | =back | ||||||
158 | |||||||
159 | =head2 Replacing the Default C< /login > and C< /login/denied > Routes | ||||||
160 | |||||||
161 | By default, the plugin adds a route to present a simple login form at that URL. | ||||||
162 | If you would rather add your own, set the C |
||||||
163 | value, and define your own route which responds to C with a login page. | ||||||
164 | |||||||
165 | If the user is logged in, but tries to access a route which requires a specific | ||||||
166 | role they don't have, they will be redirected to the "permission denied" page | ||||||
167 | URL, which defaults to C but may be changed using the | ||||||
168 | C |
||||||
169 | |||||||
170 | Again, by default a route is added to respond to that URL with a default page; | ||||||
171 | again, you can disable this by setting C |
||||||
172 | own. | ||||||
173 | |||||||
174 | This would still leave the routes C |
||||||
175 | routes in place. To disable them too, set the option C |
||||||
176 | to a true value. In this case, these routes should be defined by the user, | ||||||
177 | and should do at least the following: | ||||||
178 | |||||||
179 | post '/login' => sub { | ||||||
180 | my ($success, $realm) = authenticate_user( | ||||||
181 | params->{username}, params->{password} | ||||||
182 | ); | ||||||
183 | if ($success) { | ||||||
184 | session logged_in_user => params->{username}; | ||||||
185 | session logged_in_user_realm => $realm; | ||||||
186 | # other code here | ||||||
187 | } else { | ||||||
188 | # authentication failed | ||||||
189 | } | ||||||
190 | }; | ||||||
191 | |||||||
192 | any '/logout' => sub { | ||||||
193 | session->destroy; | ||||||
194 | }; | ||||||
195 | |||||||
196 | If you want to use the default C |
||||||
197 | you can configure them. See below. | ||||||
198 | |||||||
199 | =head2 Keywords | ||||||
200 | |||||||
201 | =over | ||||||
202 | |||||||
203 | =item require_login | ||||||
204 | |||||||
205 | Used to wrap a route which requires a user to be logged in order to access | ||||||
206 | it. | ||||||
207 | |||||||
208 | get '/secret' => require_login sub { .... }; | ||||||
209 | |||||||
210 | =cut | ||||||
211 | |||||||
212 | sub require_login { | ||||||
213 | 7 | 7 | 1679 | my $coderef = shift; | |||
214 | return sub { | ||||||
215 | 14 | 50 | 33 | 14 | 65908 | if (!$coderef || ref $coderef ne 'CODE') { | |
216 | 0 | 0 | croak "Invalid require_login usage, please see docs"; | ||||
217 | } | ||||||
218 | |||||||
219 | 14 | 49 | my $user = logged_in_user(); | ||||
220 | 14 | 100 | 57 | if (!$user) { | |||
221 | 3 | 16 | execute_hook('login_required', $coderef); | ||||
222 | # TODO: see if any code executed by that hook set up a response | ||||||
223 | 3 | 137 | return redirect uri_for($loginpage, { return_url => request->request_uri }); | ||||
224 | } | ||||||
225 | 11 | 43 | return $coderef->(); | ||||
226 | 7 | 37 | }; | ||||
227 | } | ||||||
228 | |||||||
229 | register require_login => \&require_login; | ||||||
230 | register requires_login => \&require_login; | ||||||
231 | |||||||
232 | =item require_role | ||||||
233 | |||||||
234 | Used to wrap a route which requires a user to be logged in as a user with the | ||||||
235 | specified role in order to access it. | ||||||
236 | |||||||
237 | get '/beer' => require_role BeerDrinker => sub { ... }; | ||||||
238 | |||||||
239 | You can also provide a regular expression, if you need to match the role using a | ||||||
240 | regex - for example: | ||||||
241 | |||||||
242 | get '/beer' => require_role qr/Drinker$/ => sub { ... }; | ||||||
243 | |||||||
244 | =cut | ||||||
245 | sub require_role { | ||||||
246 | 3 | 3 | 8 | return _build_wrapper(@_, 'single'); | |||
247 | } | ||||||
248 | |||||||
249 | register require_role => \&require_role; | ||||||
250 | register requires_role => \&require_role; | ||||||
251 | |||||||
252 | =item require_any_role | ||||||
253 | |||||||
254 | Used to wrap a route which requires a user to be logged in as a user with any | ||||||
255 | one (or more) of the specified roles in order to access it. | ||||||
256 | |||||||
257 | get '/foo' => require_any_role [qw(Foo Bar)] => sub { ... }; | ||||||
258 | |||||||
259 | =cut | ||||||
260 | |||||||
261 | sub require_any_role { | ||||||
262 | 1 | 1 | 4 | return _build_wrapper(@_, 'any'); | |||
263 | } | ||||||
264 | |||||||
265 | register require_any_role => \&require_any_role; | ||||||
266 | register requires_any_role => \&require_any_role; | ||||||
267 | |||||||
268 | =item require_all_roles | ||||||
269 | |||||||
270 | Used to wrap a route which requires a user to be logged in as a user with all | ||||||
271 | of the roles listed in order to access it. | ||||||
272 | |||||||
273 | get '/foo' => require_all_roles [qw(Foo Bar)] => sub { ... }; | ||||||
274 | |||||||
275 | =cut | ||||||
276 | |||||||
277 | sub require_all_roles { | ||||||
278 | 1 | 1 | 3 | return _build_wrapper(@_, 'all'); | |||
279 | } | ||||||
280 | |||||||
281 | register require_all_roles => \&require_all_roles; | ||||||
282 | register requires_all_roles => \&require_all_roles; | ||||||
283 | |||||||
284 | |||||||
285 | sub _build_wrapper { | ||||||
286 | 5 | 5 | 9 | my $require_role = shift; | |||
287 | 5 | 12 | my $coderef = shift; | ||||
288 | 5 | 6 | my $mode = shift; | ||||
289 | |||||||
290 | 5 | 100 | 20 | my @role_list = ref $require_role eq 'ARRAY' | |||
291 | ? @$require_role | ||||||
292 | : $require_role; | ||||||
293 | return sub { | ||||||
294 | 7 | 7 | 34885 | my $user = logged_in_user(); | |||
295 | 7 | 100 | 29 | if (!$user) { | |||
296 | 2 | 10 | execute_hook('login_required', $coderef); | ||||
297 | # TODO: see if any code executed by that hook set up a response | ||||||
298 | 2 | 95 | return redirect uri_for($loginpage, { return_url => request->request_uri }); | ||||
299 | } | ||||||
300 | |||||||
301 | 5 | 7 | my $role_match; | ||||
302 | 5 | 100 | 21 | if ($mode eq 'single') { | |||
100 | |||||||
50 | |||||||
303 | 3 | 10 | for (user_roles()) { | ||||
304 | 6 | 100 | 50 | 19 | $role_match++ and last if _smart_match($_, $require_role); | ||
305 | } | ||||||
306 | } elsif ($mode eq 'any') { | ||||||
307 | 1 | 2 | my %role_ok = map { $_ => 1 } @role_list; | ||||
2 | 8 | ||||||
308 | 1 | 5 | for (user_roles()) { | ||||
309 | 2 | 100 | 50 | 16 | $role_match++ and last if $role_ok{$_}; | ||
310 | } | ||||||
311 | } elsif ($mode eq 'all') { | ||||||
312 | 1 | 2 | $role_match++; | ||||
313 | 1 | 3 | for my $role (@role_list) { | ||||
314 | 2 | 50 | 6 | if (!user_has_role($role)) { | |||
315 | 0 | 0 | $role_match = 0; | ||||
316 | 0 | 0 | last; | ||||
317 | } | ||||||
318 | } | ||||||
319 | } | ||||||
320 | |||||||
321 | 5 | 100 | 16 | if ($role_match) { | |||
322 | 4 | 16 | return $coderef->(); | ||||
323 | } | ||||||
324 | |||||||
325 | 1 | 7 | execute_hook('permission_denied', $coderef); | ||||
326 | # TODO: see if any code executed by that hook set up a response | ||||||
327 | 1 | 101 | return redirect uri_for($deniedpage, { return_url => request->request_uri }); | ||||
328 | 5 | 37 | }; | ||||
329 | } | ||||||
330 | |||||||
331 | |||||||
332 | =item logged_in_user | ||||||
333 | |||||||
334 | Returns a hashref of details of the currently logged-in user, if there is one. | ||||||
335 | |||||||
336 | The details you get back will depend upon the authentication provider in use. | ||||||
337 | |||||||
338 | =cut | ||||||
339 | |||||||
340 | sub logged_in_user { | ||||||
341 | 22 | 100 | 22 | 101 | if (my $user = session 'logged_in_user') { | ||
342 | 17 | 3145 | my $realm = session 'logged_in_user_realm'; | ||||
343 | 17 | 2416 | my $provider = auth_provider($realm); | ||||
344 | 17 | 82 | return $provider->get_user_details($user, $realm); | ||||
345 | } else { | ||||||
346 | 5 | 1465 | return; | ||||
347 | } | ||||||
348 | } | ||||||
349 | register logged_in_user => \&logged_in_user; | ||||||
350 | |||||||
351 | =item user_has_role | ||||||
352 | |||||||
353 | Check if a user has the role named. | ||||||
354 | |||||||
355 | By default, the currently-logged-in user will be checked, so you need only name | ||||||
356 | the role you're looking for: | ||||||
357 | |||||||
358 | if (user_has_role('BeerDrinker')) { pour_beer(); } | ||||||
359 | |||||||
360 | You can also provide the username to check; | ||||||
361 | |||||||
362 | if (user_has_role($user, $role)) { .... } | ||||||
363 | |||||||
364 | =cut | ||||||
365 | |||||||
366 | sub user_has_role { | ||||||
367 | 2 | 2 | 3 | my ($username, $want_role); | |||
368 | 2 | 50 | 6 | if (@_ == 2) { | |||
369 | 0 | 0 | ($username, $want_role) = @_; | ||||
370 | } else { | ||||||
371 | 2 | 7 | $username = session 'logged_in_user'; | ||||
372 | 2 | 251 | $want_role = shift; | ||||
373 | } | ||||||
374 | |||||||
375 | 2 | 50 | 6 | return unless defined $username; | |||
376 | |||||||
377 | 2 | 3 | my $roles = user_roles($username); | ||||
378 | |||||||
379 | 2 | 4 | for my $has_role (@$roles) { | ||||
380 | 3 | 100 | 14 | return 1 if $has_role eq $want_role; | |||
381 | } | ||||||
382 | |||||||
383 | 0 | 0 | return 0; | ||||
384 | } | ||||||
385 | register user_has_role => \&user_has_role; | ||||||
386 | |||||||
387 | =item user_roles | ||||||
388 | |||||||
389 | Returns a list of the roles of a user. | ||||||
390 | |||||||
391 | By default, roles for the currently-logged-in user will be checked; | ||||||
392 | alternatively, you may supply a username to check. | ||||||
393 | |||||||
394 | Returns a list or arrayref depending on context. | ||||||
395 | |||||||
396 | =cut | ||||||
397 | |||||||
398 | sub user_roles { | ||||||
399 | 9 | 9 | 434 | my ($username, $realm) = @_; | |||
400 | 9 | 100 | 33 | $username = session 'logged_in_user' unless defined $username; | |||
401 | |||||||
402 | 9 | 100 | 705 | my $search_realm = ($realm ? $realm : ''); | |||
403 | |||||||
404 | 9 | 19 | my $roles = auth_provider($search_realm)->get_user_roles($username); | ||||
405 | 9 | 50 | 23 | return unless defined $roles; | |||
406 | 9 | 100 | 58 | return wantarray ? @$roles : $roles; | |||
407 | } | ||||||
408 | register user_roles => \&user_roles; | ||||||
409 | |||||||
410 | |||||||
411 | =item authenticate_user | ||||||
412 | |||||||
413 | Usually you'll want to let the built-in login handling code deal with | ||||||
414 | authenticating users, but in case you need to do it yourself, this keyword | ||||||
415 | accepts a username and password, and optionally a specific realm, and checks | ||||||
416 | whether the username and password are valid. | ||||||
417 | |||||||
418 | For example: | ||||||
419 | |||||||
420 | if (authenticate_user($username, $password)) { | ||||||
421 | ... | ||||||
422 | } | ||||||
423 | |||||||
424 | If you are using multiple authentication realms, by default each realm will be | ||||||
425 | consulted in turn. If you only wish to check one of them (for instance, you're | ||||||
426 | authenticating an admin user, and there's only one realm which applies to them), | ||||||
427 | you can supply the realm as an optional third parameter. | ||||||
428 | |||||||
429 | In boolean context, returns simply true or false; in list context, returns | ||||||
430 | C<($success, $realm)>. | ||||||
431 | |||||||
432 | =cut | ||||||
433 | |||||||
434 | sub authenticate_user { | ||||||
435 | 5 | 5 | 2065 | my ($username, $password, $realm) = @_; | |||
436 | |||||||
437 | 5 | 50 | 21 | my @realms_to_check = $realm? ($realm) : (keys %{ $settings->{realms} }); | |||
5 | 38 | ||||||
438 | |||||||
439 | 5 | 17 | for my $realm (@realms_to_check) { | ||||
440 | 8 | 59 | debug "Attempting to authenticate $username against realm $realm"; | ||||
441 | 8 | 500 | my $provider = auth_provider($realm); | ||||
442 | 8 | 100 | 37 | if ($provider->authenticate_user($username, $password)) { | |||
443 | 4 | 26344 | debug "$realm accepted user $username"; | ||||
444 | 4 | 50 | 236 | return wantarray ? (1, $realm) : 1; | |||
445 | } | ||||||
446 | } | ||||||
447 | |||||||
448 | # If we get to here, we failed to authenticate against any realm using the | ||||||
449 | # details provided. | ||||||
450 | # TODO: allow providers to raise an exception if something failed, and catch | ||||||
451 | # that and do something appropriate, rather than just treating it as a | ||||||
452 | # failed login. | ||||||
453 | 1 | 50 | 7 | return wantarray ? (0, undef) : 0; | |||
454 | } | ||||||
455 | |||||||
456 | register authenticate_user => \&authenticate_user; | ||||||
457 | |||||||
458 | |||||||
459 | =back | ||||||
460 | |||||||
461 | =head2 SAMPLE CONFIGURATION | ||||||
462 | |||||||
463 | In your application's configuation file: | ||||||
464 | |||||||
465 | session: simple | ||||||
466 | plugins: | ||||||
467 | Auth::Extensible: | ||||||
468 | # Set to 1 if you want to disable the use of roles (0 is default) | ||||||
469 | disable_roles: 0 | ||||||
470 | # After /login: If no return_url is given: land here ('/' is default) | ||||||
471 | user_home_page: '/user' | ||||||
472 | # After /logout: If no return_url is given: land here (no default) | ||||||
473 | exit_page: '/' | ||||||
474 | |||||||
475 | # List each authentication realm, with the provider to use and the | ||||||
476 | # provider-specific settings (see the documentation for the provider | ||||||
477 | # you wish to use) | ||||||
478 | realms: | ||||||
479 | realm_one: | ||||||
480 | provider: Database | ||||||
481 | db_connection_name: 'foo' | ||||||
482 | |||||||
483 | B |
||||||
484 | authentication framework requires sessions in order to track information about | ||||||
485 | the currently logged in user. | ||||||
486 | Please see L |
||||||
487 | management within your application. | ||||||
488 | |||||||
489 | =cut | ||||||
490 | |||||||
491 | # Given a realm, returns a configured and ready to use instance of the provider | ||||||
492 | # specified by that realm's config. | ||||||
493 | { | ||||||
494 | my %realm_provider; | ||||||
495 | sub auth_provider { | ||||||
496 | 34 | 34 | 0 | 59 | my $realm = shift; | ||
497 | |||||||
498 | # If no realm was provided, but we have a logged in user, use their realm: | ||||||
499 | 34 | 100 | 66 | 126 | if (!$realm && session->{logged_in_user}) { | ||
500 | 8 | 1057 | $realm = session->{logged_in_user_realm}; | ||||
501 | } | ||||||
502 | |||||||
503 | # First, if we already have a provider for this realm, go ahead and use it: | ||||||
504 | 34 | 100 | 1163 | return $realm_provider{$realm} if exists $realm_provider{$realm}; | |||
505 | |||||||
506 | # OK, we need to find out what provider this realm uses, and get an instance | ||||||
507 | # of that provider, configured with the settings from the realm. | ||||||
508 | 2 | 50 | 17 | my $realm_settings = $settings->{realms}{$realm} | |||
509 | or die "Invalid realm $realm"; | ||||||
510 | 2 | 50 | 9 | my $provider_class = $realm_settings->{provider} | |||
511 | or die "No provider configured - consult documentation for " | ||||||
512 | . __PACKAGE__; | ||||||
513 | |||||||
514 | 2 | 50 | 12 | if ($provider_class !~ /::/) { | |||
515 | 2 | 9 | $provider_class = __PACKAGE__ . "::Provider::$provider_class"; | ||||
516 | } | ||||||
517 | 2 | 50 | 21 | Dancer::ModuleLoader->load($provider_class) | |||
518 | or die "Cannot load provider $provider_class"; | ||||||
519 | |||||||
520 | 2 | 104 | return $realm_provider{$realm} = $provider_class->new($realm_settings); | ||||
521 | } | ||||||
522 | } | ||||||
523 | |||||||
524 | register_hook qw(login_required permission_denied); | ||||||
525 | register_plugin for_versions => [qw(1 2)]; | ||||||
526 | |||||||
527 | |||||||
528 | # Given a class method name and a set of parameters, try calling that class | ||||||
529 | # method for each realm in turn, arranging for each to receive the configuration | ||||||
530 | # defined for that realm, until one returns a non-undef, then return the realm which | ||||||
531 | # succeeded and the response. | ||||||
532 | # Note: all provider class methods return a single value; if any need to return | ||||||
533 | # a list in future, this will need changing) | ||||||
534 | sub _try_realms { | ||||||
535 | 0 | 0 | 0 | my ($method, @args); | |||
536 | 0 | 0 | for my $realm (keys %{ $settings->{realms} }) { | ||||
0 | 0 | ||||||
537 | 0 | 0 | my $provider = auth_provider($realm); | ||||
538 | 0 | 0 | 0 | if (!$provider->can($method)) { | |||
539 | 0 | 0 | die "Provider $provider does not provide a $method method!"; | ||||
540 | } | ||||||
541 | 0 | 0 | 0 | if (defined(my $result = $provider->$method(@args))) { | |||
542 | 0 | 0 | return $result; | ||||
543 | } | ||||||
544 | } | ||||||
545 | 0 | 0 | return; | ||||
546 | } | ||||||
547 | |||||||
548 | # Set up routes to serve default pages, if desired | ||||||
549 | if (!$settings->{no_default_pages}) { | ||||||
550 | get $loginpage => sub { | ||||||
551 | status 401; | ||||||
552 | return _default_login_page(); | ||||||
553 | }; | ||||||
554 | get $deniedpage => sub { | ||||||
555 | status 403; | ||||||
556 | return _default_permission_denied_page(); | ||||||
557 | }; | ||||||
558 | } | ||||||
559 | |||||||
560 | |||||||
561 | # If no_login_handler is set, let the user do the login/logout herself | ||||||
562 | if (!$settings->{no_login_handler}) { | ||||||
563 | |||||||
564 | # Handle logging in... | ||||||
565 | post $loginpage => sub { | ||||||
566 | my ($success, $realm) = authenticate_user( | ||||||
567 | params->{username}, params->{password} | ||||||
568 | ); | ||||||
569 | if ($success) { | ||||||
570 | session logged_in_user => params->{username}; | ||||||
571 | session logged_in_user_realm => $realm; | ||||||
572 | redirect params->{return_url} || $userhomepage; | ||||||
573 | } else { | ||||||
574 | vars->{login_failed}++; | ||||||
575 | forward $loginpage, { login_failed => 1 }, { method => 'GET' }; | ||||||
576 | } | ||||||
577 | }; | ||||||
578 | |||||||
579 | # ... and logging out. | ||||||
580 | any ['get','post'] => $logoutpage => sub { | ||||||
581 | session->destroy; | ||||||
582 | if (params->{return_url}) { | ||||||
583 | redirect params->{return_url}; | ||||||
584 | } elsif ($exitpage) { | ||||||
585 | redirect $exitpage; | ||||||
586 | } else { | ||||||
587 | # TODO: perhaps make this more configurable, perhaps by attempting to | ||||||
588 | # render a template first. | ||||||
589 | return "OK, logged out successfully."; | ||||||
590 | } | ||||||
591 | }; | ||||||
592 | |||||||
593 | } | ||||||
594 | |||||||
595 | |||||||
596 | sub _default_permission_denied_page { | ||||||
597 | return < | ||||||
598 | Permission Denied |
||||||
599 | |||||||
600 |
|
||||||
601 | Sorry, you're not allowed to access that page. | ||||||
602 | |||||||
603 | PAGE | ||||||
604 | 0 | 0 | 0 | } | |||
605 | |||||||
606 | sub _default_login_page { | ||||||
607 | 1 | 50 | 1 | 5 | my $login_fail_message = vars->{login_failed} | ||
608 | ? " LOGIN FAILED " |
||||||
609 | : ""; | ||||||
610 | 1 | 50 | 14 | my $return_url = params->{return_url} || ''; | |||
611 | 1 | 29 | return < | ||||
612 | Login Required |
||||||
613 | |||||||
614 |
|
||||||
615 | You need to log in to continue. | ||||||
616 | |||||||
617 | |||||||
618 | $login_fail_message | ||||||
619 | |||||||
620 | |||||||
621 | |||||||
622 | |||||||
623 | |
||||||
624 | |||||||
625 | |||||||
626 | |
||||||
627 | |||||||
628 | |||||||
629 | |||||||
630 | PAGE | ||||||
631 | } | ||||||
632 | |||||||
633 | # Replacement for much maligned and misunderstood smartmatch operator | ||||||
634 | sub _smart_match { | ||||||
635 | 6 | 6 | 11 | my ($got, $want) = @_; | |||
636 | 6 | 100 | 20 | if (!ref $want) { | |||
50 | |||||||
0 | |||||||
637 | 4 | 20 | return $got eq $want; | ||||
638 | } elsif (ref $want eq 'Regexp') { | ||||||
639 | 2 | 30 | return $got =~ $want; | ||||
640 | } elsif (ref $want eq 'ARRAY') { | ||||||
641 | 0 | return grep { $_ eq $got } @$want; | |||||
0 | |||||||
642 | } else { | ||||||
643 | 0 | carp "Don't know how to match against a " . ref $want; | |||||
644 | } | ||||||
645 | } | ||||||
646 | |||||||
647 | |||||||
648 | |||||||
649 | |||||||
650 | =head1 AUTHOR | ||||||
651 | |||||||
652 | David Precious, C<< |
||||||
653 | |||||||
654 | |||||||
655 | =head1 BUGS / FEATURE REQUESTS | ||||||
656 | |||||||
657 | This is an early version; there may still be bugs present or features missing. | ||||||
658 | |||||||
659 | This is developed on GitHub - please feel free to raise issues or pull requests | ||||||
660 | against the repo at: | ||||||
661 | L |
||||||
662 | |||||||
663 | |||||||
664 | |||||||
665 | =head1 ACKNOWLEDGEMENTS | ||||||
666 | |||||||
667 | Valuable feedback on the early design of this module came from many people, | ||||||
668 | including Matt S Trout (mst), David Golden (xdg), Damien Krotkine (dams), | ||||||
669 | Daniel Perrett, and others. | ||||||
670 | |||||||
671 | Configurable login/logout URLs added by Rene (hertell) | ||||||
672 | |||||||
673 | Regex support for require_role by chenryn | ||||||
674 | |||||||
675 | Support for user_roles looking in other realms by Colin Ewen (casao) | ||||||
676 | |||||||
677 | LDAP provider added by Mark Meyer (ofosos) | ||||||
678 | |||||||
679 | Config options for default login/logout handlers by Henk van Oers (hvoers) | ||||||
680 | |||||||
681 | =head1 LICENSE AND COPYRIGHT | ||||||
682 | |||||||
683 | |||||||
684 | Copyright 2012-13 David Precious. | ||||||
685 | |||||||
686 | This program is free software; you can redistribute it and/or modify it | ||||||
687 | under the terms of either: the GNU General Public License as published | ||||||
688 | by the Free Software Foundation; or the Artistic License. | ||||||
689 | |||||||
690 | See http://dev.perl.org/licenses/ for more information. | ||||||
691 | |||||||
692 | |||||||
693 | =cut | ||||||
694 | |||||||
695 | 1; # End of Dancer::Plugin::Auth::Extensible |