File Coverage

blib/lib/Tree/Authz.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Tree::Authz;
2 1     1   21946 use strict;
  1         2  
  1         30  
3 1     1   5 use warnings;
  1         2  
  1         38  
4 1     1   5 use Carp;
  1         12  
  1         101  
5              
6             # persistence doesn't work - propagating changes to other processes
7              
8             # TODO - plugin sets - specify a search path e.g. My::App::Roles
9             # any module My::App::Roles::rolename for a rolename defined in the authz
10             # is automatically loaded into that role
11              
12 1     1   364 use Lingua::EN::Inflect::Number ();
  0            
  0            
13             use Symbol;
14              
15             use Tree::Authz::Role;
16              
17             use base 'Class::Data::Inheritable';
18              
19             __PACKAGE__->mk_classdata( '_AllRoles' );
20             __PACKAGE__->mk_classdata( '_database' );
21             __PACKAGE__->mk_classdata( '__namespace' );
22              
23             __PACKAGE__->_AllRoles( {} );
24              
25             our $VERSION = '0.02_2';
26              
27             =head1 NAME
28              
29             Tree::Authz - inheritance-based authorization scheme
30              
31             =head1 VERSION
32              
33             0.02_1
34              
35             =head1 DEVELOPER RELEASE
36              
37             Re-organised to return objects (blessed into the new class C),
38             instead of strings, which are now referred to as C rather than C
39             in the documentation. Some method names changed to reflect the terminology.
40              
41             =head1 SYNOPSIS
42              
43             use Tree::Authz;
44              
45             my $roles = { superuser => [ qw( spymasters politicians ) ],
46             spymasters => [ qw( spies moles ) ],
47             spies => [ 'informants' ],
48             informants => [ 'base' ],
49             moles => [ 'base' ],
50             politicians => [ 'citizens' ],
51             citizens => [ 'base' ],
52             };
53              
54             my $authz = Tree::Authz->setup_hierarchy( $roles, 'SpyLand' );
55              
56             my $superuser = $authz->role( 'superuser' );
57             my $spies = $authz->role( 'spies' );
58             my $citizens = $authz->role( 'citizens' );
59             my $base = $authz->role( 'base' );
60              
61             $spies ->setup_permissions( [ qw( read_secrets wear_disguise ) ] );
62             $citizens->setup_permissions( 'vote' );
63             $base ->setup_permissions( 'breathe' );
64              
65             foreach my $role ( $superuser, $spies, $citizens, $base ) {
66             foreach my $ability ( qw( unspecified_ability
67             spy
68             spies
69             read_secrets
70             wear_disguise
71             vote
72             breathe
73             can ) ) {
74              
75             if ( $role->can( $ability ) ) {
76             print "$role can '$ability'\n";
77             }
78             else {
79             print "$role cannot '$ability'\n";
80             }
81             }
82             }
83              
84             # prints:
85              
86             superuser can 'unspecified_ability' # superpowers!
87             superuser can 'spy'
88             superuser can 'spies'
89             superuser can 'read_secrets'
90             superuser can 'wear_disguise'
91             superuser can 'vote'
92             superuser can 'breathe'
93             superuser can 'can'
94             spies cannot 'unspecified_ability'
95             spies can 'spy'
96             spies can 'spies'
97             spies can 'read_secrets'
98             spies can 'wear_disguise'
99             spies can 'vote'
100             spies can 'breathe'
101             spies can 'can'
102             citizens cannot 'unspecified_ability'
103             citizens cannot 'spy'
104             citizens cannot 'spies'
105             citizens cannot 'read_secrets'
106             citizens cannot 'wear_disguise'
107             citizens can 'vote'
108             citizens can 'breathe'
109             citizens can 'can'
110             base cannot 'unspecified_ability'
111             base cannot 'spy'
112             base cannot 'spies'
113             base cannot 'read_secrets'
114             base cannot 'wear_disguise'
115             base cannot 'vote'
116             base cannot 'breathe' # !
117             base cannot 'can' # !!
118              
119             # storing code on the nodes (roles) of the tree
120             $spies->setup_abilities( read_secret => $coderef );
121              
122             print $spies->read_secret( '/path/to/secret/file' );
123              
124             $spies->setup_plugins( 'My::Spies::Skills' );
125              
126             $spies->fly( $jet ); # My::Spies::Skills::fly
127              
128             =head1 DESCRIPTION
129              
130             Class for inheritable, role-based permissions system (Role Based Access
131             Control - RBAC).
132              
133             Custom methods can be placed on role objects. Authorization can be performed
134             either by checking whether the role name matches the required name, or by
135             testing (via C) whether the role can perform the method required.
136              
137             Two role are specified by default. At the top, Is can do anything
138             (C<< $superuser->can( $action ) >> always returns a coderef). At the bottom, the
139             I role can do nothing (C<< $base->can( $action ) >> always returns undef).
140              
141             All roles are automatically capable of authorizing actions named for the
142             singular and plural of the role name.
143              
144             =head2 ROADMAP
145              
146             I'm planning to implement some of the main features and terminology described
147             in this document, which describes a standard for Role Based Access Control:
148              
149             http://csrc.nist.gov/rbac/rbacSTD-ACM.pdf
150              
151             Thanks to Kingsley Kerce for providing the link.
152              
153             =head1 METHODS
154              
155             This class is a static class - all methods are class methods.
156              
157             Some methods return L subclass objects.
158              
159             =head2 Namespaces and class methods
160              
161             This class is designed to work in environments where multiple applications
162             run within the same process (i.e. websites under C). If the optional
163             namespace parameter is supplied to C, the roles are isolated
164             to the specified namespace. All methods should be called through the
165             class name returned from C.
166              
167             If your program is not operating in such an environment (e.g. CGI scripts),
168             then you can completely ignore this parameter, and call class methods either
169             through C, or through the string returned from C
170             (which, funnily enough, will be 'Tree::Authz').
171              
172             =over 4
173              
174             =item role( $role_name )
175              
176             Factory method, returns a L subclass
177             object.
178              
179             Sets up two permitted actions on the group - the singular and plural of
180             the group name. B
181             name in a near future release>. Opinions welcome.
182              
183             =item new( $role_name )
184              
185             DEPRECATED.
186              
187             Use C instead.
188              
189             =item get_group( $group_name )
190              
191             DEPRECATED.
192              
193             Use C instead.
194              
195             =cut
196              
197             sub role {
198             my ($proto, $role) = @_;
199              
200             croak 'No role name' unless $role;
201              
202             unless ( $proto->role_exists( $role ) ) {
203             carp( "Unknown role: $role - using 'base' instead" );
204             $role = 'base';
205             }
206              
207             my $authz_class = ref( $proto ) || $proto;
208              
209             my $class = "${authz_class}::Role::$role";
210              
211             return $class->new( $role, $authz_class );
212             }
213              
214             sub new {
215             carp "'new' is deprecated - use 'role' instead";
216             goto &role;
217             }
218              
219             sub get_group {
220             carp "'get_group' is deprecated - use 'role' instead";
221             goto &new;
222             }
223              
224              
225              
226              
227             =item role_exists( $role_name )
228              
229             Returns true if the specified group exists B within the hierarchy.
230              
231             =item group_exists( $group_name )
232              
233             DEPRECATED.
234              
235             Use C instead.
236              
237             =cut
238              
239             sub role_exists { exists $_[0]->_AllRoles->{ $_[1] } }
240              
241             sub group_exists {
242             carp "'group_exists' is deprecated - use 'role_exists' instead";
243             goto &role_exists;
244             }
245              
246             =item subrole_exists( $subrole_name, [ $role_name ] )
247              
248             B.
249              
250             Give me a nudge if this would be useful.
251              
252             Returns true if the specified role exists anywhere in the hierarchy
253             underneath the current or specified role.
254              
255             =cut
256              
257             sub subrole_exists { croak 'subrole_exists method not implemented yet - email me' }
258              
259             =item list_roles()
260              
261             Returns an array or arrayref of all the role names in the hierarchy, sorted by
262             name.
263              
264             =item list_groups()
265              
266             DEPRECATED.
267              
268             Use C instead.
269              
270             =cut
271              
272             sub list_roles {
273             my @roles = sort keys %{ $_[0]->_AllRoles };
274             wantarray ? @roles : [ @roles ];
275             }
276              
277             sub list_groups {
278             carp "'list_groups' is deprecated - use 'list_roles' instead";
279             goto &list_roles;
280             }
281              
282              
283             =item dump_hierarchy( [ $namespace ] )
284              
285             Get a simple printout of the structure of your hierarchy.
286              
287             This method Cs L.
288              
289             If you find yourself parsing the output and using it somehow in your code, let
290             me know, and I'll find a Better Way to provide the data. This method is just
291             intended for quick and dirty printouts and could B.
292              
293             =cut
294              
295             sub dump_hierarchy {
296             my ($proto) = @_;
297              
298             my $class = ref( $proto ) || $proto;
299              
300             require Devel::Symdump;
301              
302             my @classes = split( "\n", Devel::Symdump->isa_tree );
303              
304             my @wanted;
305             my $start = 0;
306             my $end = 0;
307             my $supers = "${class}::Role::superuser";
308              
309             foreach my $possible ( @classes ) {
310             $start = 1 if $possible =~ /^$supers/;
311             if ( $start && $possible !~ /^$supers/ ) {
312             $end = 1 if $possible =~ /^\w/;
313             }
314             push( @wanted, $possible ) if ( $start && ! $end && $possible =~ __PACKAGE__ );
315             }
316              
317             return join( "\n", @wanted );
318             }
319              
320             =item setup_hierarchy( $groups, [ $namespace ] )
321              
322             Class method.
323              
324             I<$groups> has:
325              
326             keys - group names
327             values - arrayrefs of subgroup name(s)
328              
329             Sets up a hierarchy of Perl classes representing the group structure.
330              
331             The hierarchy will be contained within the I<$namespace> top level if supplied.
332             This makes it easy to set up several independent hierarchies to use within the
333             same process, e.g. for different websites under C.
334              
335             Returns a class name through which group objects can be retrieved and other
336             class methods called. This will be 'Tree::Authz' if no namespace is specified.
337              
338             If called with a I<$namespace> argument, then all loaded packages within the
339             C<$namespace::Tree::Authz> symbol table hierarchy are removed (using
340             L from the symbol
341             table. This is experimental and may lead to bugs, the jury is still out. The
342             purpose of this is to allow re-initialisation of the setup within a long-running
343             process such as C. It could also allow dynamic updates to the
344             hierarchy.
345              
346             =cut
347              
348             sub setup_hierarchy {
349             my ($proto, $roles_data, $namespace) = @_;
350              
351             croak( 'No roles data' ) unless $roles_data;
352              
353             my $class = ref( $proto ) || $proto;
354             $class = "${namespace}::$class" if $namespace;
355              
356             # If we are reloading, remove any existing hierarchy from the symbol table.
357             # But not if there's no namespace, because then we would lose Tree::Authz
358             # itself
359             # Symbol::delete_package( $class ) if $namespace;
360              
361             my $roles_class = 'Tree::Authz::Role';
362             $roles_class = "${namespace}::$roles_class" if $namespace;
363              
364             my %roles;
365              
366             foreach my $role ( keys %$roles_data ) {
367             my @isa = map { "${roles_class}::$_" } @{ $roles_data->{ $role } };
368             my $role_class = "${roles_class}::${role}";
369             $roles{ $role } = $role_class;
370             no strict 'refs';
371             @{"${role_class}::ISA"} = @isa;
372             }
373              
374             my $supers_class = "${roles_class}::superuser";
375             my $base_class = "${roles_class}::base";
376              
377             {
378             no strict 'refs';
379              
380             # base for authz class
381             # push( @{"${class}::ISA"}, 'Tree::Authz' ) if $namespace;
382             # set, rather than push onto, because this has to be repeatably callable
383             # to allow updates after editing
384             @{"${class}::ISA"} = ( 'Tree::Authz' ) if $namespace;
385              
386             # add a base group
387             # push( @{"${base_class}::ISA"}, 'Tree::Authz::Role' ); # $roles_class );
388             @{"${base_class}::ISA"} = ( 'Tree::Authz::Role' );
389              
390             # superuser always returns a subref from 'can', even if the specified
391             # method doesn't exist.
392             *{"${supers_class}::can"} =
393             sub { UNIVERSAL::can( $_[0], $_[1] ) || sub {} };
394              
395             # base group cannot do anything
396             *{"${base_class}::can"} = sub {
397             my ($proto, @args) = @_;
398             my $class = ref( $proto ) || $proto;
399             return if $class =~ /::base$/;
400             return UNIVERSAL::can( $proto, @args );
401             };
402             }
403              
404             # classdata methods have to come down here, after @ISA is set up for $class
405             $class->_AllRoles( {} );
406             $class->_AllRoles->{ $_ } = $roles{ $_ } for keys %roles;
407             $class->_AllRoles->{ superuser } = $supers_class;
408             $class->_AllRoles->{ base } = $base_class;
409              
410             # __reload needs this
411             $class->__namespace( $namespace );
412              
413             foreach my $role ( keys %roles ) {
414             my @cando = ( Lingua::EN::Inflect::Number::to_PL( $role ),
415             Lingua::EN::Inflect::Number::to_S( $role ),
416             );
417             $class->setup_permissions_on_role( $role, \@cando )
418             }
419              
420             return $class;
421             }
422              
423             =back
424              
425             =head2 Persistence
426              
427             L can be used independently of a persistence mechanism
428             I C. However, if you want to manipulate the hierarchy at
429             runtime, a persistence mechanism is required. The implementation is left up to
430             you, but the API is defined. The persistence API should be
431             implemented by the object passed to C.
432              
433             =over
434              
435             =item setup_from_database( $database, [ $namespace ] )
436              
437             I<$database> should be an object that responds to the persistence API defined
438             below. The object is stored as class data and is available via the C<_database>
439             method.
440              
441             =back
442              
443             =head3 Pass-through methods
444              
445             The following methods are passed on to the database object, after checking
446             whether any changes would result in a recursive inheritance pattern, in which
447             case they return false. The database methods should return true on success.
448              
449             =over
450              
451             =item get_roles_data()
452              
453             Returns a hashref. Keys are role names, values are arrayrefs of subroles.
454              
455             C calls this method on the database object, then passes
456             the data on to C.
457              
458             =item add_role( $new_role, $parent, [ $children ] )
459              
460             Adds a new role to the scheme.
461              
462             I<$parent> is required, so no new top-level
463             roles can be inserted. It's up to you to decide whether to raise an error or
464             just return if I<$parent> is omitted.
465              
466             I<$children> can be a role name or an arrayref of role names. Defaults to
467             C<'base'> if omitted. It might be worth checking if these roles already exist.
468              
469             At the moment I am assuming no multiple inheritance, but things are shaping up
470             to look like there's no great difficulty about allowing it. If allowed, this
471             method should check if I<$new_role> already exists. If it does, ignore any
472             I<$children> (probably raise a warning), add <$new_role> to the sub-roles list
473             of I<$parent>, and return without trying to insert I<$new_role> into the
474             database (because it already exists).
475              
476             =item remove_role( $role )
477              
478             Removes the role from the database, including finding and removing any
479             occurrences of I<$role> in the sub-role lists of other roles.
480              
481             Returns the list of subroles for the role that was removed, in case you want
482             to put them somewhere else.
483              
484             =item move_role( $role, $to )
485              
486             Makes I<$role> a sub-role of I<$to>, and deletes it from the sub-roles list of
487             its current parent.
488              
489             =item add_subrole( $role, $subrole )
490              
491             Adds a subrole to a role. Must remove C<'base'> from the subroles list if
492             present.
493              
494             =item remove_subrole( $role, $subrole )
495              
496             Removes a subrole from a role. If the resulting list of subroles would be empty,
497             must insert C<'base'>.
498              
499             =cut
500              
501             sub setup_from_database {
502             my ($proto, $database, $namespace) = @_;
503              
504             croak( 'No database' ) unless $database;
505              
506             my $authz = $proto->setup_hierarchy( $database->get_roles_data, $namespace );
507              
508             # store away as class data
509             $authz->_database( $database );
510              
511             return $authz;
512             }
513              
514             # these methods all return true on success
515             sub get_roles_data { shift->_database->get_roles_data( @_ ) }
516              
517             sub remove_role {
518             my ($proto) = @_;
519              
520             $proto->_database->remove_role( @_ );
521              
522             $proto->__reload;
523             }
524              
525             sub remove_subrole {
526             my ($proto) = @_;
527              
528             $proto->_database->remove_subrole( @_ );
529              
530             $proto->__reload;
531             }
532              
533             # These methods look for potential recursion and return false if they find it.
534             # If the potential child/subrole can/isa parent/role, then they can not be
535             # put into the parent/child relationship specified, and the operations must
536             # abort.
537              
538             # If the operation is OK, it proceeds and returns a true value on success.
539              
540             sub move_role {
541             my ($proto, $role, $to) = @_;
542              
543             croak( 'No destination role in move_role' ) unless $to;
544              
545             my @parents;
546             foreach my $rl ( $proto->list_roles ) {
547             my %subrls = map { $_ => 1 } $proto->role( $rl )->list_roles;
548             push( @parents, $rl ) if $subrls{ $role };
549             }
550              
551             unless ( @parents ) {
552             croak( "Couldn't find parent(s) of $role" );
553             return;
554             }
555              
556             my $to_role = $proto->role( $to );
557              
558             foreach my $p ( @parents ) {
559             return if $to_role->can( $p );
560             }
561              
562             # OK, let's do it
563             $proto->_database->move_role( $role, $to );
564              
565             $proto->__reload;
566             }
567              
568             # $new_role wants to join $children as subrole of $parent
569             sub add_role {
570             my ($proto, $new_role, $parent, $children) = @_;
571              
572             $children ||= 'base';
573             my @children = ref( $children ) ? @$children : ( $children );
574              
575             # children must exist
576             my %all_roles = map { $_ => 1 } $proto->list_roles;
577             foreach my $child ( @children ) {
578             return unless $all_roles{ $child };
579             }
580              
581             # and none CAN parent
582             foreach my $child ( @children ) {
583             return if $proto->role( $child )->can( $parent );
584             }
585              
586             # OK, let's do it
587             $proto->_database->add_role( $new_role, $parent, [ @children ] );
588              
589             $proto->__reload;
590             }
591              
592             sub add_subrole {
593             my ($proto, $role, $subrole) = @_;
594              
595             return if $proto->role( $subrole )->can( $role );
596              
597             # OK, let's do it
598             $proto->_database->add_subrole ( $role, $subrole );
599              
600             $proto->__reload;
601             }
602              
603             # attempt to load any changes back into the symbol table
604             sub __reload {
605             my ($proto) = @_;
606              
607             # delete_package will delete these
608             my $namespace = $proto->__namespace;
609             my $database = $proto->_database;
610              
611             # Remove the current hierarchy from the symbol table. But not if there's
612             # no namespace, because then we would lose Tree::Authz itself
613             Symbol::delete_package( ref( $proto ) || $proto ) if $namespace;
614              
615             # $proto has namespace already in its name, but has been removed from
616             # the symbol table, so have to use __PACKAGE__, which breaks
617             # subclassability
618             __PACKAGE__->setup_from_database( $database, $namespace );
619             }
620              
621             =back
622              
623             =head2 Adding authorizations
624              
625             =over
626              
627             =item setup_permissions_on_role( $role_name, $cando )
628              
629             Class method version of C.
630              
631             =item setup_permissions_on_group( $group_name, $cando )
632              
633             DEPRECATED.
634              
635             Use C instead.
636              
637             =cut
638              
639             sub setup_permissions_on_role {
640             my ($class, $role, $cando) = @_;
641              
642             croak( 'Parameter(s) missing' ) unless $cando;
643             croak( 'Not an instance method' ) if ref( $class );
644              
645             my $role_class = "${class}::Role::$role";
646              
647             $role_class->_setup_perms( $cando );
648             }
649              
650             sub setup_permissions_on_group {
651             carp "'setup_permissions_on_group' is deprecated - use 'setup_permissions_on_role' instead";
652             goto &setup_permissions_on_role;
653             }
654              
655             =item setup_abilities_on_role( $role_name, %code )
656              
657             Class method version of C.
658              
659             =item setup_abilities_on_group( $group_name, %code )
660              
661             DEPRECATED.
662              
663             Use C instead.
664              
665             =cut
666              
667             sub setup_abilities_on_role {
668             my ($class, $role, %code) = @_;
669              
670             croak( 'Not an instance method' ) if ref( $class );
671             croak( 'Nothing to set up' ) unless %code;
672              
673             my $group_class = "${class}::Role::$role";
674              
675             $group_class->_setup_abil( %code );
676             }
677              
678             sub setup_abilities_on_group {
679             carp "'setup_abilities_on_group' is deprecated - use 'setup_abilities_on_role' instead";
680             goto &setup_abilities_on_role;
681             }
682              
683             =item setup_plugins_on_role( $role_name, $plugins )
684              
685             Class method version of C.
686              
687             =item setup_plugins_on_group( $group_name, $plugins )
688              
689             Deprecated version of C.
690              
691             =cut
692              
693             sub setup_plugins_on_role {
694             my ($class, $role, $plugins) = @_;
695              
696             croak( 'Parameter(s) missing' ) unless $plugins;
697             croak( 'Not an instance method' ) if ref( $class );
698              
699             my $group_class = "${class}::Role::$role";
700              
701             $group_class->_setup_plugins( $plugins );
702             }
703              
704             sub setup_plugins_on_group {
705             carp "'setup_plugins_on_group' is deprecated - use 'setup_plugins_on_role' instead";
706             goto &setup_plugins_on_role;
707             }
708              
709              
710             =back
711              
712             =cut
713              
714             1;
715              
716             =head1 CHANGES
717              
718             The deprecation policy is:
719              
720             1) DEPRECATED methods issue a warning (via C) and then call the new
721             method. They will be documented next to the replacement method.
722              
723             2) OBSOLETE methods will croak. These will be documented in a separate section.
724              
725             3) Removed methods will be documented in a separate section, in the first
726             version they no longer exist in.
727              
728             Main changes in 0.02
729              
730             - changed terminology to refer to I instead of I. Deprecated
731             all methods with I in their name. These methods now issue a
732             warning via C, and will be removed in a future release.
733             - added a new class to represent a role - L.
734             L is now a static class (all its methods are
735             class methods). The objects it returns from some methods are subclasses
736             of L.
737              
738             =head1 TODO
739              
740             Roles are now represented by their own class. This should make it easier to
741             add constraints and other RBAC features.
742              
743             More methods for returning meta information, e.g. immediate subroles of a
744             role, all subroles of a role, list available actions of a role and its
745             subroles.
746              
747             Might be nice to register users with roles.
748              
749             Make role objects be singletons - not necessary if the only data they carry is
750             their own name.
751              
752             Under C, all setup of hierarchies and permissions must be completed
753             during server startup, before the startup process forks off Apache children.
754             It would be nice to have some way of communicating updates to other processes.
755             Alternatively, you could run the full startup sequence every time you need to
756             access a Tree::Authz role, but that seems sub-optimal.
757              
758             =head1 DEPENDENCIES
759              
760             L,
761             L.
762              
763             Optional - L.
764              
765             L for the test suite.
766              
767             =head1 BUGS
768              
769             Please report all bugs via the CPAN Request Tracker at
770             L.
771              
772             =head1 COPYRIGHT AND LICENSE
773              
774             Copyright 2004 by David Baird.
775              
776             This library is free software; you can redistribute it and/or modify
777             it under the same terms as Perl itself.
778              
779             =head1 AUTHOR
780              
781             David Baird, C
782              
783             =head1 SEE ALSO
784              
785             L, L.
786              
787             =cut