File Coverage

blib/lib/Catalyst/Plugin/RapidApp/AuthCore.pm
Criterion Covered Total %
statement 42 60 70.0
branch 6 16 37.5
condition 3 6 50.0
subroutine 14 14 100.0
pod 0 2 0.0
total 65 98 66.3


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::RapidApp::AuthCore;
2 1     1   692 use Moose::Role;
  1         3  
  1         9  
3 1     1   4615 use namespace::autoclean;
  1         2  
  1         10  
4              
5             with 'Catalyst::Plugin::RapidApp::CoreSchema';
6              
7 1     1   86 use RapidApp::Util qw(:all);
  1         2  
  1         502  
8             require Catalyst::Utils;
9 1     1   7 use CatalystX::InjectComponent;
  1         3  
  1         21  
10              
11 1     1   12 use Module::Runtime;
  1         3  
  1         7  
12 1     1   385 use RapidApp::CoreSchema;
  1         3  
  1         43  
13              
14 1     1   459 use Catalyst::Plugin::Session::Store::DBIC 0.14;
  1         32870  
  1         37  
15 1     1   405 use Catalyst::Plugin::Session::State::Cookie 0.17;
  1         12996  
  1         47  
16 1     1   410 use Catalyst::Plugin::Authorization::Roles 0.09;
  1         7325  
  1         35  
17 1     1   668 use Catalyst::Authentication::Store::DBIx::Class 0.1505;
  1         781  
  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   3 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         3 my $plugins = [ grep { ! $c->registered_plugins($_) } @req_plugins ];
  6         190  
37 1 50       40 $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 62 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       65 return 1 if ($c->module_root_namespace ne '');
171            
172             # Always require auth on requests to RapidApp Modules:
173 13 100 66     1151 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     8 );
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 50 my $c = shift;
190 21         113 $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   3 my $c = shift;
198            
199 1 50       5 my $model = $c->config->{'Plugin::RapidApp::AuthCore'}{linked_user_model} or return undef;
200 0 0         my $M = $c->model($model) or die "AuthCore: Failed to load linked_user_model '$model'";
201            
202 0           my $lSource = $M->result_source;
203 0           my $lClass = $lSource->result_class;
204            
205 0           my $cSource = $c->model('RapidApp::CoreSchema::User')->result_source;
206 0           my $cClass = $cSource->result_class;
207            
208 0           my $key_col = 'username';
209            
210 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         $_ ne 'id' && $cClass->has_column($_)
  0            
215             } $lClass->columns;
216            
217 0           $lClass->load_components('+RapidApp::DBIC::Component::LinkedResult');
218 0           $lSource->{_linked_source} = $cSource;
219 0           $lSource->{_linked_key_column} = $key_col;
220 0           $lSource->{_linked_shared_columns} = [@shared_cols];
221            
222 0           $cClass->load_components('+RapidApp::DBIC::Component::LinkedResult');
223 0           $cSource->{_linked_source} = $lSource;
224 0           $cSource->{_linked_key_column} = $key_col;
225 0           $cSource->{_linked_shared_columns} = [@shared_cols];
226            
227             }
228              
229              
230             1;
231              
232             __END__
233              
234             =head1 NAME
235              
236             Catalyst::Plugin::RapidApp::AuthCore - instant authentication, authorization and sessions
237              
238             =head1 SYNOPSIS
239              
240             package MyApp;
241            
242             use Catalyst qw/ RapidApp::AuthCore /;
243              
244             =head1 DESCRIPTION
245              
246             This plugin provides a full suite of standard users and sessions with authentication and
247             authorization for Catalyst/RapidApp applications.
248              
249             It loads and auto-configures a bundle of standard Catalyst plugins:
250              
251             Authentication
252             Authorization::Roles
253             Session
254             Session::State::Cookie
255             Session::Store::DBIC
256              
257             As well as the L<RapidApp::CoreSchema|Catalyst::Plugin::RapidApp::CoreSchema> plugin,
258             which sets up the backend model/store.
259              
260             The common DBIC-based L<Catalyst::Model::RapidApp::CoreSchema> database is used for
261             the store and persistence of all data, which is automatically deployed as an SQLite
262             database on first load.
263              
264             New databases are automatically setup with one user:
265              
266             username: admin
267             password: pass
268              
269             An C<administrator> role is also automatically setup, which the admin user belongs to.
270              
271             For managing users and roles, seeing active sessions, etc, see the
272             L<CoreSchemaAdmin|Catalyst::Plugin::RapidApp::CoreSchemaAdmin> plugin.
273              
274             =head1 AUTH CONTROLLER
275              
276             The AuthCore plugin automatically injects an L<Auth|Catalyst::Plugin::RapidApp::AuthCore::Controller::Auth>
277             controller at C</auth> in the Catalyst application. This controller implements a login (C</auth/login>)
278             and logout (C</auth/logout>) action.
279              
280             The C</auth/login> action path handles both the rendering a login form (when accessed via GET) and
281             the actual login/authentication (when accessed via POST). The login POST should send these params:
282              
283             =over
284              
285             =item *
286              
287             username
288              
289             =item *
290              
291             password
292              
293             =item *
294              
295             redirect (optional)
296              
297             =back
298              
299             A C<redirect> URL can be supplied for the client to redirect to after successful login. The C<redirect>
300             param can also be supplied to a GET/POST to C</auth/logout> to redirect after logout.
301              
302             The login form is also internally rendered from other URL paths which are password-protected (which
303             by default is all Module paths when AuthCore is loaded). The built-in login form template automatically
304             detects this case and sends the path in C<redirect> with the login POST. This allows RESTful paths
305             to be accessed and automatically authenticate, if needed, and then continue on to the desired location
306             thereafter.
307              
308             =head1 CONFIG
309              
310             Custom options can be set within the C<'Plugin::RapidApp::AuthCore'> config key in the main
311             Catalyst application config. All configuration params are optional.
312              
313             =head2 init_admin_password
314              
315             Default password to assign to the C<admin> user when initializing a fresh
316             L<CoreSchema|Catalyst::Model::RapidApp::CoreSchema> database for the first time. Defaults to
317              
318             pass
319              
320             =head2 passphrase_class
321              
322             L<Authen::Passphrase> class to use for password hashing. Defaults to C<'BlowfishCrypt'>.
323             The Authen::Passphrase interface is implemented using the L<DBIx::Class::PassphraseColumn>
324             component class in the L<CoreSchema|Catalyst::Model::RapidApp::CoreSchema> database.
325              
326             =head2 passphrase_params
327              
328             Params supplied to the C<passphrase_class> above. Defaults to:
329              
330             {
331             cost => 9,
332             salt_random => 1,
333             }
334              
335             =head2 expires
336              
337             Set the timeout for Session expiration. Defaults to C<3600> (1 hour)
338              
339             =head2 role_checker
340              
341             Optional CodeRef used to check user roles. By default, this is just a pass-through to the standard
342             C<check_user_roles()> function. When AuthCore is active, Modules which are configured with the
343             C<require_role> param will call the role_checker to verify the current user is allowed before rendering.
344             This provides a very simple API for permissions and authorization. More complex authorization rules
345             simply need to be implemented in code.
346              
347             The C<require_role> API is utilized by the
348             L<CoreSchemaAdmin|Catalyst::Plugin::RapidApp::CoreSchemaAdmin> plugin to restrict access to the
349             L<CoreSchema|Catalyst::Model::RapidApp::CoreSchema> database to users with the C<administrator>
350             role (which will both hide the menu point and block module paths to non-administrator users).
351              
352             Note that the C<role_checker> is only called by AuthCore-aware code (Modules or custom user-code),
353             and doesn't modify the behavior of the standard role methods setup by
354             L<Catalyst::Plugin::Authorization::Roles> which is automatically loaded by AuthCore. You can
355             still call C<check_user_roles()> in your custom controller code which will function in the
356             normal manner (which performs lookups against the Role tables in the CoreSchema)
357             regardless of the custom role_checker.
358              
359              
360             =head1 OVERRIDE CONFIGS
361              
362             If any of the following configs are supplied, they will completely bypass and override the config
363             settings above.
364              
365             =head2 no_validate_passwords
366              
367             Special temp/admin bool option which when set to a true value will make any supplied password
368             successfully authenticate for any user. This is useful if you forget the admin password, so
369             you don't have to either manually edit the C<rapidapp_coreschema.db> database, or delete it
370             to have it recreated. The setting can also be used if the passphrase settings are changed
371             (which will break all pre-existing passwords already in the database) to be able to get back
372             into the app, if needed.
373              
374             Obviously, this setting would never be used in production.
375              
376             =head2 credential
377              
378             To override the C<credential> param supplied to C<Plugin::Authentication>
379              
380             =head2 store
381              
382             To override the C<store> param supplied to C<Plugin::Authentication>
383              
384             =head2 Plugin::Authentication
385              
386             To completely override the C<Plugin::Authentication> config.
387              
388             =head2 Plugin::Session
389              
390             To completely override the C<Plugin::Session> config.
391              
392             =head1 SEE ALSO
393              
394             =over
395              
396             =item *
397              
398             L<RapidApp::Manual::Plugins>
399              
400             =item *
401              
402             L<Catalyst::Plugin::RapidApp>
403              
404             =item *
405              
406             L<Catalyst::Plugin::RapidApp::CoreSchema>
407              
408             =item *
409              
410             L<Catalyst::Plugin::RapidApp::CoreSchemaAdmin>
411              
412             =item *
413              
414             L<Catalyst>
415              
416             =back
417              
418             =head1 AUTHOR
419              
420             Henry Van Styn <vanstyn@cpan.org>
421              
422             =head1 COPYRIGHT AND LICENSE
423              
424             This software is copyright (c) 2013 by IntelliTree Solutions llc.
425              
426             This is free software; you can redistribute it and/or modify it under
427             the same terms as the Perl 5 programming language system itself.
428              
429             =cut