File Coverage

blib/lib/Catalyst/Plugin/RapidApp/AuthCore.pm
Criterion Covered Total %
statement 59 83 71.0
branch 11 26 42.3
condition 3 8 37.5
subroutine 15 15 100.0
pod 0 2 0.0
total 88 134 65.6


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::RapidApp::AuthCore;
2 1     1   754 use Moose::Role;
  1         3  
  1         6  
3 1     1   4381 use namespace::autoclean;
  1         2  
  1         7  
4              
5             with 'Catalyst::Plugin::RapidApp::CoreSchema';
6              
7 1     1   64 use RapidApp::Util qw(:all);
  1         2  
  1         463  
8             require Catalyst::Utils;
9 1     1   7 use CatalystX::InjectComponent;
  1         2  
  1         27  
10              
11 1     1   5 use Module::Runtime;
  1         2  
  1         6  
12 1     1   368 use RapidApp::CoreSchema;
  1         3  
  1         43  
13              
14 1     1   505 use Catalyst::Plugin::Session::Store::DBIC 0.14;
  1         33820  
  1         39  
15 1     1   409 use Catalyst::Plugin::Session::State::Cookie 0.17;
  1         12570  
  1         39  
16 1     1   399 use Catalyst::Plugin::Authorization::Roles 0.09;
  1         7690  
  1         30  
17 1     1   838 use Catalyst::Authentication::Store::DBIx::Class 0.1505;
  1         745  
  1         8  
18              
19             my @req_plugins = qw/
20             Authentication
21             Authorization::Roles
22             Session
23             Session::State::Cookie
24             Session::Store::DBIC
25             RapidApp::AuthCore::PlugHook
26             /;
27              
28             sub _authcore_load_plugins {
29 1     1   2 my $c = shift;
30            
31             # Note: these plugins have to be loaded like this because they
32             # aren't Moose Roles. But this causes load order issues that
33             # we overcome by loading our own extra plugin, 'RapidApp::AuthCore::PlugHook'
34             # which contains extra method modifiers that need to be applied
35             # after the other plugins are loaded.
36 1         4 my $plugins = [ grep { ! $c->registered_plugins($_) } @req_plugins ];
  6         196  
37 1 50       41 $c->setup_plugins($plugins) if (scalar(@$plugins) > 0);
38             }
39              
40             before 'setup_dispatcher' => sub {
41             my $c = shift;
42            
43             # FIXME: see comments in Catalyst::Plugin::RapidApp::AuthCore::PlugHook
44             $c->_authcore_load_plugins;
45            
46             $c->config->{'Plugin::RapidApp::AuthCore'} ||= {};
47             my $config = $c->config->{'Plugin::RapidApp::AuthCore'};
48            
49             # Default CodeRef used to check if the current user/request has a given role
50             # - automatically includes administrator - Used by other modules, such as
51             # TabGui to optionally filter navtrees
52             $config->{role_checker} ||= sub {
53             my $ctx = shift; #<-- expects CONTEXT object
54             return $ctx->check_any_user_role('administrator',@_);
55             };
56            
57             # Passthrough config:
58             if($config->{init_admin_password}) {
59             $c->config->{'Model::RapidApp::CoreSchema'} ||= {};
60             my $cs_cnf = $c->config->{'Model::RapidApp::CoreSchema'};
61             die "Conflicting 'init_admin_password' cnf (AuthCore/CoreSchema)"
62             if ($cs_cnf->{init_admin_password});
63             $cs_cnf->{init_admin_password} = $config->{init_admin_password};
64             }
65            
66             # Default session expire 2 hour
67             $config->{expires} ||= 2*60*60;
68            
69             $config->{credential} ||= {
70             class => 'Password',
71             password_field => 'password',
72             password_type => 'self_check'
73             };
74            
75             $config->{store} ||= {
76             class => 'DBIx::Class',
77             user_model => 'RapidApp::CoreSchema::User',
78             role_relation => 'roles',
79             role_field => 'role',
80             use_userdata_from_session => '1',
81             };
82            
83             if($config->{passphrase_class}) {
84             Module::Runtime::require_module($config->{passphrase_class});
85             my $rclass = 'RapidApp::CoreSchema::Result::User';
86             $rclass->authen_passphrase_class($config->{passphrase_class});
87             if(exists $config->{passphrase_params}) {
88             die "passphrase_params must be a HashRef"
89             unless (ref $config->{passphrase_params} eq 'HASH');
90             $rclass->authen_passphrase_params($config->{passphrase_params});
91             }
92             }
93            
94             # Admin/backdoor option. This is useful if the passphrase config is changed
95             # after the user database is already setup to be able to login and
96             # set the password to be hashed by the new function (or if you forget the pw).
97             if($config->{no_validate_passwords} && !$c->config->{'Plugin::Authentication'}) {
98             $c->log->warn(join("\n",'',
99             ' AuthCore: WARNING: "no_validate_passwords" enabled. Any password',
100             ' typed will be accepted for any valid username. This is meant for',
101             ' temp admin access, don\'t forget to turn this back off!',''
102             ));
103             $config->{credential}{password_type} = 'none';
104             }
105            
106             $c->log->warn(
107             ' AuthCore: WARNING: using custom "Plugin::Authentication" config'
108             ) if ($c->config->{'Plugin::Authentication'});
109            
110             # Allow the user to totally override the auto config:
111             $c->config->{'Plugin::Authentication'} ||= {
112             default_realm => 'dbic',
113             realms => {
114             dbic => {
115             credential => $config->{credential},
116             store => $config->{store}
117             }
118             }
119             };
120            
121             $c->log->warn(
122             ' AuthCore: WARNING: using custom "Plugin::Session" config'
123             ) if ($c->config->{'Plugin::Session'});
124            
125             $c->config->{'Plugin::Session'} ||= {
126             dbic_class => 'RapidApp::CoreSchema::Session',
127             expires => $config->{expires}
128             };
129             };
130              
131              
132             after 'setup_components' => sub {
133             my $class = shift;
134            
135             CatalystX::InjectComponent->inject(
136             into => $class,
137             component => 'Catalyst::Plugin::RapidApp::AuthCore::Controller::Auth',
138             as => 'Controller::Auth'
139             );
140             };
141              
142             after 'setup_finalize' => sub {
143             my $class = shift;
144            
145             $class->rapidApp->rootModule->_around_Controller(sub {
146             my $orig = shift;
147             my $self = shift;
148             my $c = $self->c;
149             my $args = $c->req->arguments;
150            
151             # Do auth_verify for auth required paths. If it fails it will detach:
152             $c->controller('Auth')->auth_verify($c) if $class->is_auth_required_path($self,@$args);
153            
154             return $self->$orig(@_);
155             });
156            
157             $class->_initialize_linked_user_model;
158             };
159              
160              
161             sub is_auth_required_path {
162 13     13 0 93 my ($c, $rootModule, @path) = @_;
163            
164             # TODO: add config opt for 'public_module_paths' and check here
165             # OR - user can wrap this method to override a given path/request
166             # to not require auth according to whatever rules they wish
167              
168             # All RapidApp Module requests require auth, including the root
169             # module when not deployed at /
170 13 50       75 return 1 if ($c->module_root_namespace ne '');
171            
172             # Always require auth on requests to RapidApp Modules:
173 13 100 66     1504 return 1 if ($path[0] && $rootModule->has_subarg($path[0]));
174            
175             # Special handling for '/' - require auth unless a root template has
176             # been defined
177             return 1 if (
178             @path == 0 &&
179             ! $c->config->{'Model::RapidApp'}->{root_template}
180 1 50 33     9 );
181            
182             # Temp - no auth on other paths (template alias paths)
183 0         0 return 0;
184             }
185              
186             # Updated version of original method from Catalyst::Plugin::Session::Store::DBIC -
187             # also deletes sessions with undef expires
188             sub delete_expired_sessions {
189 21     21 0 67 my $c = shift;
190 21         164 $c->session_store_model->search([
191             $c->session_store_dbic_expires_field => { '<', time() },
192             $c->session_store_dbic_expires_field => undef,
193             ])->delete;
194             }
195              
196             sub _initialize_linked_user_model {
197 1     1   4 my $c = shift;
198            
199 1 50       5 my $model = $c->config->{'Plugin::RapidApp::AuthCore'}{linked_user_model} or return undef;
200 0 0       0 my $M = $c->model($model) or die "AuthCore: Failed to load linked_user_model '$model'";
201            
202 0         0 my $lSource = $M->result_source;
203 0         0 my $lClass = $lSource->result_class;
204            
205 0         0 my $cSource = $c->model('RapidApp::CoreSchema::User')->result_source;
206 0         0 my $cClass = $cSource->result_class;
207            
208 0         0 my $key_col = 'username';
209            
210 0 0       0 die "linked_user_model '$model' does not have '$key_col' column"
211             unless ($lSource->has_column($key_col));
212              
213             my @shared_cols = grep {
214 0 0       0 $_ ne 'id' && $cClass->has_column($_)
  0         0  
215             } $lClass->columns;
216            
217 0         0 $lClass->load_components('+RapidApp::DBIC::Component::LinkedResult');
218 0         0 $lSource->{_linked_source} = $cSource;
219 0         0 $lSource->{_linked_key_column} = $key_col;
220 0         0 $lSource->{_linked_shared_columns} = [@shared_cols];
221            
222 0         0 $cClass->load_components('+RapidApp::DBIC::Component::LinkedResult');
223 0         0 $cSource->{_linked_source} = $lSource;
224 0         0 $cSource->{_linked_key_column} = $key_col;
225 0         0 $cSource->{_linked_shared_columns} = [@shared_cols];
226            
227             }
228              
229             # Can be called as user/pass, or just user as a user object
230             sub _authcore_apply_login {
231 5     5   23 my ($c, $user, $pass) = @_;
232              
233 5         10 my $error = undef;
234 5         11 my $username = undef;
235            
236 5         28 $c->delete_expired_sessions;
237            
238 5 50       16141 if($c->user) {
239 0         0 $c->delete_session('logout');
240 0         0 $c->logout;
241             }
242            
243 5 50       6093 if(blessed $user) {
244 0         0 $c->set_authenticated( $user )
245             }
246             else {
247 5 50       58 if($c->authenticate({ username => $user, password => $pass })) {
248 5         1552 $c->log->info("Password authentication success for user '$user'")
249             }
250             else {
251 0         0 $error = "Password authentication failure for user '$user'";
252             }
253             }
254            
255 5         1154 $username = $c->user->username;
256            
257 5 50 0     116 $error ||= 'unknown login failure' unless ($username);
258            
259 5 50       20 if ($error) {
260 0         0 $c->log->info("AuthCore: $error");
261 0         0 return 0;
262             }
263            
264 5         25 $c->session->{RapidApp_username} = $username;
265            
266             # New: set the X-RapidApp-Authenticated header now so the response
267             # itself will reflect the successful login (since in either case, the
268             # immediate response is a simple redirect). This is for client info/debug only
269 5         731 $c->res->header('X-RapidApp-Authenticated' => $username);
270              
271 5         2286 my $dt = DateTime->now( time_zone => 'local' );
272 5         7469 $c->user->update({ last_login_ts => join(' ',$dt->ymd('-'),$dt->hms(':')) });
273            
274             # Something is broken!
275 5         52 $c->_save_session_expires;
276            
277 5         5514 1
278             }
279              
280              
281             1;
282              
283             __END__
284              
285             =head1 NAME
286              
287             Catalyst::Plugin::RapidApp::AuthCore - instant authentication, authorization and sessions
288              
289             =head1 SYNOPSIS
290              
291             package MyApp;
292            
293             use Catalyst qw/ RapidApp::AuthCore /;
294              
295             =head1 DESCRIPTION
296              
297             This plugin provides a full suite of standard users and sessions with authentication and
298             authorization for Catalyst/RapidApp applications.
299              
300             It loads and auto-configures a bundle of standard Catalyst plugins:
301              
302             Authentication
303             Authorization::Roles
304             Session
305             Session::State::Cookie
306             Session::Store::DBIC
307              
308             As well as the L<RapidApp::CoreSchema|Catalyst::Plugin::RapidApp::CoreSchema> plugin,
309             which sets up the backend model/store.
310              
311             The common DBIC-based L<Catalyst::Model::RapidApp::CoreSchema> database is used for
312             the store and persistence of all data, which is automatically deployed as an SQLite
313             database on first load.
314              
315             New databases are automatically setup with one user:
316              
317             username: admin
318             password: pass
319              
320             An C<administrator> role is also automatically setup, which the admin user belongs to.
321              
322             For managing users and roles, seeing active sessions, etc, see the
323             L<CoreSchemaAdmin|Catalyst::Plugin::RapidApp::CoreSchemaAdmin> plugin.
324              
325             =head1 AUTH CONTROLLER
326              
327             The AuthCore plugin automatically injects an L<Auth|Catalyst::Plugin::RapidApp::AuthCore::Controller::Auth>
328             controller at C</auth> in the Catalyst application. This controller implements a login (C</auth/login>)
329             and logout (C</auth/logout>) action.
330              
331             The C</auth/login> action path handles both the rendering a login form (when accessed via GET) and
332             the actual login/authentication (when accessed via POST). The login POST should send these params:
333              
334             =over
335              
336             =item *
337              
338             username
339              
340             =item *
341              
342             password
343              
344             =item *
345              
346             redirect (optional)
347              
348             =back
349              
350             A C<redirect> URL can be supplied for the client to redirect to after successful login. The C<redirect>
351             param can also be supplied to a GET/POST to C</auth/logout> to redirect after logout.
352              
353             The login form is also internally rendered from other URL paths which are password-protected (which
354             by default is all Module paths when AuthCore is loaded). The built-in login form template automatically
355             detects this case and sends the path in C<redirect> with the login POST. This allows RESTful paths
356             to be accessed and automatically authenticate, if needed, and then continue on to the desired location
357             thereafter.
358              
359             =head1 CONFIG
360              
361             Custom options can be set within the C<'Plugin::RapidApp::AuthCore'> config key in the main
362             Catalyst application config. All configuration params are optional.
363              
364             =head2 init_admin_password
365              
366             Default password to assign to the C<admin> user when initializing a fresh
367             L<CoreSchema|Catalyst::Model::RapidApp::CoreSchema> database for the first time. Defaults to
368              
369             pass
370              
371             =head2 passphrase_class
372              
373             L<Authen::Passphrase> class to use for password hashing. Defaults to C<'BlowfishCrypt'>.
374             The Authen::Passphrase interface is implemented using the L<DBIx::Class::PassphraseColumn>
375             component class in the L<CoreSchema|Catalyst::Model::RapidApp::CoreSchema> database.
376              
377             =head2 passphrase_params
378              
379             Params supplied to the C<passphrase_class> above. Defaults to:
380              
381             {
382             cost => 9,
383             salt_random => 1,
384             }
385              
386             =head2 expires
387              
388             Set the timeout for Session expiration. Defaults to C<3600> (1 hour)
389              
390             =head2 role_checker
391              
392             Optional CodeRef used to check user roles. By default, this is just a pass-through to the standard
393             C<check_user_roles()> function. When AuthCore is active, Modules which are configured with the
394             C<require_role> param will call the role_checker to verify the current user is allowed before rendering.
395             This provides a very simple API for permissions and authorization. More complex authorization rules
396             simply need to be implemented in code.
397              
398             The C<require_role> API is utilized by the
399             L<CoreSchemaAdmin|Catalyst::Plugin::RapidApp::CoreSchemaAdmin> plugin to restrict access to the
400             L<CoreSchema|Catalyst::Model::RapidApp::CoreSchema> database to users with the C<administrator>
401             role (which will both hide the menu point and block module paths to non-administrator users).
402              
403             Note that the C<role_checker> is only called by AuthCore-aware code (Modules or custom user-code),
404             and doesn't modify the behavior of the standard role methods setup by
405             L<Catalyst::Plugin::Authorization::Roles> which is automatically loaded by AuthCore. You can
406             still call C<check_user_roles()> in your custom controller code which will function in the
407             normal manner (which performs lookups against the Role tables in the CoreSchema)
408             regardless of the custom role_checker.
409              
410              
411             =head1 OVERRIDE CONFIGS
412              
413             If any of the following configs are supplied, they will completely bypass and override the config
414             settings above.
415              
416             =head2 no_validate_passwords
417              
418             Special temp/admin bool option which when set to a true value will make any supplied password
419             successfully authenticate for any user. This is useful if you forget the admin password, so
420             you don't have to either manually edit the C<rapidapp_coreschema.db> database, or delete it
421             to have it recreated. The setting can also be used if the passphrase settings are changed
422             (which will break all pre-existing passwords already in the database) to be able to get back
423             into the app, if needed.
424              
425             Obviously, this setting would never be used in production.
426              
427             =head2 credential
428              
429             To override the C<credential> param supplied to C<Plugin::Authentication>
430              
431             =head2 store
432              
433             To override the C<store> param supplied to C<Plugin::Authentication>
434              
435             =head2 Plugin::Authentication
436              
437             To completely override the C<Plugin::Authentication> config.
438              
439             =head2 Plugin::Session
440              
441             To completely override the C<Plugin::Session> config.
442              
443             =head1 SEE ALSO
444              
445             =over
446              
447             =item *
448              
449             L<RapidApp::Manual::Plugins>
450              
451             =item *
452              
453             L<Catalyst::Plugin::RapidApp>
454              
455             =item *
456              
457             L<Catalyst::Plugin::RapidApp::CoreSchema>
458              
459             =item *
460              
461             L<Catalyst::Plugin::RapidApp::CoreSchemaAdmin>
462              
463             =item *
464              
465             L<Catalyst>
466              
467             =back
468              
469             =head1 AUTHOR
470              
471             Henry Van Styn <vanstyn@cpan.org>
472              
473             =head1 COPYRIGHT AND LICENSE
474              
475             This software is copyright (c) 2013 by IntelliTree Solutions llc.
476              
477             This is free software; you can redistribute it and/or modify it under
478             the same terms as the Perl 5 programming language system itself.
479              
480             =cut