File Coverage

blib/lib/Catalyst/Plugin/Authorization/CDBI/GroupToken.pm
Criterion Covered Total %
statement 6 74 8.1
branch 0 28 0.0
condition 0 26 0.0
subroutine 2 6 33.3
pod 2 3 66.6
total 10 137 7.3


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::Authorization::CDBI::GroupToken;
2              
3 1     1   1131 use strict;
  1         2  
  1         41  
4 1     1   2781 use NEXT;
  1         2483  
  1         985  
5              
6             our $VERSION = '0.01';
7              
8             =head1 NAME
9              
10             Catalyst::Plugin::Authorization::CDBI::GroupToken - CDBI Authorization for Catalyst
11              
12             =head1 SYNOPSIS
13              
14             use Catalyst qw/Authorization::CDBI::GroupToken/;
15              
16             __PACKAGE__->config->{authorization} = {
17             user_class => 'MyApp::Model::CDBI::User'
18             ,token_class => 'MyApp::Model::CDBI::Token'
19             ,token_field => 'name'
20             ,user_token_class => 'MyApp::Model::CDBI::UserToken'
21             ,user_token_user_field => 'user'
22             ,user_token_token_field => 'token'
23             ,group_class => 'MyApp::Model::CDBI::Group'
24             ,group_field => 'name'
25             ,group_description_field => 'description'
26             ,user_group_class => 'MyApp::Model::CDBI::UserGroup'
27             ,user_group_user_field => 'user'
28             ,user_group_group_field => 'group'
29             ,token_group_class => 'MyApp::Model::CDBI::TokenGroup'
30             ,token_group_token_field => 'token'
31             ,token_group_group_field => 'group'
32             ,group_group_class => 'MyApp::Model::CDBI::GroupGroup'
33             ,group_group_parent_field => 'parent'
34             ,group_group_child_field => 'child'
35             };
36              
37             $c->token(qw/myapp.access/);
38              
39              
40             # the basic setup
41             CREATE TABLE user (
42             id INTEGER PRIMARY KEY,
43             email TEXT,
44             password TEXT
45             );
46              
47             CREATE TABLE token (
48             id INTEGER PRIMARY KEY,
49             name TEXT
50             );
51              
52             CREATE TABLE user_token (
53             id INTEGER PRIMARY KEY,
54             user INTEGER REFERENCES customer,
55             token INTEGER REFERENCES token
56             );
57              
58             # user-groups and token-groups
59             CREATE TABLE group (
60             id INTEGER PRIMARY KEY,
61             group TEXT
62             );
63              
64             CREATE TABLE token_group (
65             id INTEGER PRIMARY KEY,
66             token INTEGER REFERENCES token,
67             group INTEGER REFERENCES group
68             );
69              
70             CREATE TABLE user_group (
71             id INTEGER PRIMARY KEY,
72             customer INTEGER REFERENCES user,
73             group INTEGER REFERENCES group
74             );
75              
76             # group-groups
77             CREATE TABLE group_group (
78             id INTEGER PRIMARY KEY,
79             parent INTEGER REFERENCES group
80             child INTEGER REFERENCES group
81             );
82              
83             =head1 DESCRIPTION
84              
85             This is a simplified version of the group-role-permission-token paradigm.
86             Working from the theory that at the end of the day all the developer really
87             cares about is whether someone has permission to access something or not.
88             Traditional roles and groups are just storage and assignment mechanisms.
89             This model changes the notion of a permission to a "token". Roles and groups are
90             simplified to "group". And a user is still a user. Tokens (permissions) are
91             assigned to a user and or a group. A user is assigned to groups. Groups can
92             also be assigned to groups (think of roles assigned to groups without all
93             the headaches of realizing that a role has suddenly morphed into a group or
94             into a permission). The flexibility is that exceptions are easily handled.
95             If Rob is in Group A, but also needs also needs a permission for something
96             from group B we just give him the permission directly. These alleviates the
97             need to build another role or group just to handle the special case for Rob.
98             Why all this you ask? Again it gets back to the concept of "all I really
99             care about is can this user do this". So outside of an administrative
100             interface the only thing to query is the tokens (permissions). This is
101             similar to testing for a particular capability in javascript versus doing a
102             browser detect and branching off from there.
103              
104             For example given the following setup:
105            
106             User Rob
107             Group WholeDamnCompany
108             Group Foo
109             widgets_inc.sales.leads
110            
111             Group Accounting
112             widgets_inc.acct.access
113             widgets_inc.acct.edit
114            
115             Group HR
116             widgets_inc.hr.admin.access
117             widgets_inc.hr.admin.add_user
118            
119             Group WholeDamnCompany
120             Group Accounting
121             Group HR
122             widgets_inc.widget_view
123            
124             Group Foo
125             widgets_inc.bar
126            
127             Group IT
128             widgets_inc.it.root
129            
130             Token
131             widgets_inc.bldg1.access
132            
133             We test with $c->tokens('[token name]'), each of these will return true for Rob:
134              
135             widgets_inc.wizbang.feature
136             widgets_inc.acct.access
137             widgets_inc.acct.edit
138             widgets_inc.hr.admin.access
139             widgets_inc.hr.admin.add_user
140             widgets_inc.sales.leads
141             widgets_inc.bar
142              
143             Each of these will return false for Rob as he is not in IT nor has the widgets_inc.bldg1.access directly assigned:
144            
145             widgets_inc.it.root
146             widgets_inc.bldg1.access
147              
148             So why the hierarchy in the token naming? Really this is a matter of
149             preference. You can name your tokens whatever works best for your needs, but
150             the idea here is to make the permission self describing. I also have some
151             interesting future features in mind, such as tying user specific data to a
152             given token via key/value and predefining settings for these keys(See TODO).
153             Why "tokens"? No real reason, its what the group I work with has been
154             calling them for years, so just what I am used to. Also it is to clearly
155             delineate this school of thought from "roles". Oh and I could not come up
156             with a catchy acronym for Tokens Aint Roles like YAML.
157              
158             Note that this plugin is designed to work with
159             C<Catalyst::Plugin::Authentication::CDBI> and works much the same way as the
160             roles method in this plugin. It will pick up the user_class and user_field
161             settings from Authentication::CDBI if omitted. In theory it should work with
162             any Authentication plugin that sets $c->request->{user_id}.
163              
164             =head1 CONFIGURATION
165              
166             Most of configuration is optional. The _class suffixed configuration options
167             essentially enable a given feature. There are three different setups that
168             build upon one another:
169              
170             =head2 Basic Configuration
171              
172             Start with the user and a simple token assignment. This is identical to
173             roles in L<Catalyst::Plugin::Authentication::CDBI> v0.09
174              
175             =over 4
176              
177             =item user_class
178              
179             The User Model Class. i.e., 'MyApp::Model::CDBI::User'
180             Optional. Defaults to $c->config->{authentication}->{user_class}
181              
182             =item token_class
183              
184             The Token Model Class. i.e., 'MyApp::Model::CDBI::Token'
185             Required.
186              
187             =item token_field
188              
189             The Token Field from the Token Model Class. i.e., 'name'
190             Optional. Defaults to 'name'
191              
192             =item user_token_class
193              
194             The User-Token Model Class. i.e., 'MyApp::Model::CDBI::UserToken'
195             Required.
196              
197             =item user_token_user_field
198              
199             The User Field from the User-Token Model Class. i.e., 'user'
200             Optional. Defaults to 'user'
201              
202             =item user_token_token_field
203              
204             The Token Field from the User-Token Model Class. i.e., 'token'
205             Optional. Defaults to 'token'
206              
207             =back
208              
209             =head2 Group Configuration
210              
211             This builds upon all the settings above. It adds User-Group and
212             Token-Group to the setup.
213              
214             =over 4
215              
216             =item group_class
217              
218             The Group Model Class. i.e., 'MyApp::Model::CDBI::Group'
219             Optional. Future plans include an out of the box admin scripts.
220              
221             =item group_field
222              
223             The Group Field from the Group Model Class. i.e., 'name'
224             Optional. Defaults to 'name'
225              
226             =item group_description_field
227              
228             The Description Field from the Group Model Class. i.e., 'description'
229             Optional. Defaults to 'description'
230              
231             =item user_group_class
232              
233             The User-Group Model Class. i.e., 'MyApp::Model::CDBI::UserGroup'
234             Optional. If omitted then just User-Token will be used.
235             Enables Group Configuration along with token_group_class
236              
237             =item user_group_user_field
238              
239             The User Field from the User-Group Model Class. i.e., 'user'
240             Optional. Defaults to 'user'
241              
242             =item user_group_group_field
243              
244             The Group Field from the User-Group Model Class. i.e., 'group'
245             Optional. Defaults to 'group'
246              
247             =item token_group_class
248              
249             The Token-Group Model Class. i.e., 'MyApp::Model::CDBI::TokenGroup'
250             Optional. If omitted then just User-Token will be used.
251             Enables Group Configuration along with user_group_class
252              
253             =item token_group_token_field
254              
255             The Token Field from the Token-Group Model Class. i.e., 'token'
256             Optional. Defaults to 'token'
257              
258             =item token_group_group_field
259              
260             The Group Field from the Token-Group Model Class. i.e., 'group'
261             Optional. Defaults to 'group'
262              
263             =back
264              
265             =head2 Group Group Configuration
266              
267             This builds upon all the settings above. It adds Group-Group to the setup.
268              
269             =over 4
270              
271             =item group_group_class
272              
273             The Group_Group Model Class. i.e., 'MyApp::Model::CDBI::GroupGroup'
274             Enables use of Group Group Configuration
275              
276             =item group_group_parent_field
277              
278             The Parent Group Field from the Group-Group Model Class. i.e., 'parent'
279             Optional. Defaults to 'parent'
280              
281             =item group_group_child_field
282              
283             The Child Group Field from the Group-Group Model Class. i.e., 'child'
284             Optional. Defaults to 'child'
285              
286             =back
287              
288             =head2 A Minimal Configuration Example
289              
290             __PACKAGE__->config->{authorization} = {
291             user_class => 'MyApp::Model::CDBI::User'
292             ,token_class => 'MyApp::Model::CDBI::Token'
293             ,user_token_class => 'MyApp::Model::CDBI::UserToken'
294             };
295              
296             =head1 METHODS
297              
298             =over 4
299              
300             =item token
301              
302             Check permissions return true or false.
303              
304             $c->tokens(qw/widgets_inc.foo widgets_inc.bar/);
305              
306             Returns an arrayref containing the verified tokens. This is the same as
307             C<Catalyst::Plugin::Authentic ation::CDBI>->roles
308              
309             my @tokens = @{ $c->tokens };
310              
311             =cut
312              
313             sub tokens {
314 0     0 0   my $c = shift;
315 0   0       $c->{tokens} ||= [];
316 0 0         my $tokens = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
317 0 0         if ( $_[0] ) {
318 0           my @tokens;
319 0           foreach my $token (@$tokens) {
320 0 0         push @tokens, $token unless grep $_ eq $token, @{ $c->{tokens} };
  0            
321             }
322 0 0         return 1 unless @tokens;
323 0 0         if ( $c->process_tokens( \@tokens ) ) {
324 0           $c->{tokens} = [ @{ $c->{tokens} }, @tokens ];
  0            
325 0           return 1;
326             }
327 0           else { return 0 }
328             }
329 0           return $c->{tokens};
330             }
331              
332              
333             =back
334              
335             =head2 EXTENDED METHODS
336              
337             =over 4
338              
339             =item setup
340              
341             sets up $c->config->{authorization}.
342              
343             =cut
344              
345             sub setup {
346 0     0 1   my $c = shift;
347 0           my $conf = $c->config->{authorization};
348 0 0         $conf = ref $conf eq 'ARRAY' ? {@$conf} : $conf;
349 0           $c->config->{authorization} = $conf;
350 0           return $c->NEXT::setup(@_);
351             }
352              
353             =back
354              
355             =head2 OVERLOADED METHODS
356              
357             =over 4
358              
359             =item process_tokens
360              
361             Takes an arrayref of tokens and checks if user has the supplied tokens.
362             Returns 1/0.
363              
364             =cut
365              
366             sub process_tokens {
367 0     0 1   my ( $c, $tokens ) = @_;
368              
369             # Basic Configuration
370 0   0       my $user_class =
371             $c->config->{authorization}->{user_class}
372             || $c->config->{authentication}->{user_class};
373              
374 0           my $token_class =
375             $c->config->{authorization}->{token_class}; # || die '\$c->config->{authorization}->{token_class} required for Catalyst::Plugin::Authorization:: CDBI::GroupToken'
376              
377 0   0       my $token_field =
378             $c->config->{authorization}->{token_field}
379             || 'name';
380              
381 0           my $user_token_class =
382             $c->config->{authorization}->{user_token_class}; # || die '\$c->config->{authorization}->{user_token_class} required for Catalyst::Plugin::Authorization:: CDBI::GroupToken'
383              
384 0   0       my $user_token_user_field =
385             $c->config->{authorization}->{user_token_user_field}
386             || 'user';
387              
388 0   0       my $user_token_token_field =
389             $c->config->{authorization}->{user_token_token_field} || 'token';
390              
391             # User-Group Token-Group Configuration
392 0           my $group_class =
393             $c->config->{authorization}->{group_class};
394              
395 0           my $group_field =
396             $c->config->{authorization}->{group_field};
397              
398 0           my $user_group_class =
399             $c->config->{authorization}->{user_group_class};
400              
401 0   0       my $user_group_user_field =
402             $c->config->{authorization}->{user_group_user_field}
403             || 'user';
404              
405 0   0       my $user_group_group_field =
406             $c->config->{authorization}->{user_group_group_field}
407             || 'group';
408              
409 0           my $ token_group_class =
410             $c->config->{authorization}->{token_group_class};
411              
412 0   0       my $token_group_token_field =
413             $c->config->{authorization}->{token_group_token_field}
414             || 'token';
415              
416 0   0       my $token_group_group_field =
417             $c->config->{authorization}->{token_group_group_field}
418             || 'group';
419              
420             # Group-Group Configuration
421 0           my $ group_group_class =
422             $c->config->{authorization}->{group_group_class};
423              
424 0   0       my $group_group_parent_field =
425             $c->config->{authorization}->{group_group_parent_field}
426             || 'parent';
427              
428 0   0       my $group_group_child_field =
429             $c->config->{authorization}->{group_group_child_field}
430             || 'child';
431            
432 0 0         if ( my $user = $user_class->retrieve( $c->request->{user_id} ) ) {
433 0           for my $token_name (@$tokens) {
434 0 0         $c->log->debug("Checking tokens for '$token_name'") if $c->debug;
435 0 0         if ( my $token =
436             $token_class->search( { $token_field => $token_name } )->first )
437             { # check if user has token directly assigned
438 0 0         return 1
439             if $user_token_class->search(
440             {
441             $user_token_user_field => $user->id,
442             $user_token_token_field => $token->id
443             }
444             );
445 0 0 0       if ( $token_group_class && $user_group_class ) { # feature enabled?
446             # get a list of all groups the token is assigned to
447 0           my @token_groups = $token_group_class->search(
448             { $token_group_token_field => $token->id }
449             );
450              
451             # merge @token_groups and all of its pa rent (if a user has
452             # a parent group then they have the children as well)
453 0           my %groups_to_check; # hash to store unique group ids
454 0           foreach my $token_group (@token_groups) {
455 0           $groups_to_check{$token_group->$token_group_group_field} = 1;
456 0 0         if ( $group_group_class ) { # feature enabled?
457             # flatten out the ancestors
458 0           my @parents = _get_all_group_parents(
459             $group_group_class
460             ,$group_group_parent_field
461             ,$group_group_child_field
462             ,$token_group->$token_group_group_field
463             );
464 0           foreach my $parent_group (@parents) {
465 0           $groups_to_check{$parent_group} = 1;
466             }
467             }
468             }
469              
470             # check to see if user is in one of these groups
471 0           foreach my $group (keys %groups_to_check) {
472 0 0         if( $user_group_class->search(
473             {
474             $user_group_user_field => $user->id,
475             $user_group_group_field => $group
476             }
477             )
478             ) {
479 0 0         $c->log->debug("'$token_name' passed token-group-user check") if $c->debug;
480 0           return 1;
481 0           last;
482             }
483             }
484             }
485 0           return 0;
486             }
487 0           else { return 0 }
488             }
489             }
490 0           else { return 0 }
491 0           return 1;
492             }
493              
494             # this could be down easily with co nnect by in oracle,
495             # but other dbs don't readily support heirarchical queries, so we hack away...
496             sub _get_all_group_parents {
497 0     0     my ($class, $parent_field, $child_field, $child) = @_;
498 0           my @parents = $class->search( $child_field => $child );
499 0           my @results;
500 0           foreach my $parent( @parents ) {
501 0           push @results, $parent->$parent_field;
502 0           push @results, _get_all_group_parents( $class
503             ,$parent_field
504             ,$child_field
505             ,$parent->$parent_field
506             );
507             }
508 0           return @results;
509             }
510              
511             =back
512              
513             =head1 TODO
514              
515             =over 4
516              
517             =item -structure to restrict parent group assignment to child exceptions
518            
519             =item -OTB admin interface
520              
521             =item -implement token attributes
522              
523             if ( my $token = $c->tokens('widgets_inc.sales') ) {
524             my $region = $token->attribute('region'); # specific region for current user
525             }
526              
527             =back
528              
529             =head1 SEE ALSO
530              
531             L<Catalyst> L<Catalyst::Plugin::Authentication::CDBI>.
532              
533             =head1 AUTHOR
534              
535             Scott Connelly, C<ssc@cpan.org>
536              
537             =head1 THANKS
538              
539             Andy Grundman, C<andy@hyrbidized.org>
540              
541             The authors of L<Catalyst::Plugin::Authentication::CDBI>
542              
543             Sebastian Riedel, C<sri@cpan.org>
544             Marcus Ramberg, C<mramberg@cpan.org>
545              
546             =head1 COPYRIGHT
547              
548             This program is free software, you can redistribute it and/or modify it
549             under the same terms as Perl itself.
550              
551             =cut
552              
553             1;