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