File Coverage

blib/lib/Authorization/RBAC.pm
Criterion Covered Total %
statement 73 74 98.6
branch 23 28 82.1
condition 2 2 100.0
subroutine 9 9 100.0
pod 2 2 100.0
total 109 115 94.7


line stmt bran cond sub pod time code
1             package Authorization::RBAC;
2             $Authorization::RBAC::VERSION = '0.10';
3 3     3   34881 use utf8;
  3         28  
  3         14  
4 3     3   1438 use Moose;
  3         904662  
  3         14  
5             with 'MooseX::Object::Pluggable';
6              
7 3     3   13190 use Moose::Util::TypeConstraints;
  3         10  
  3         23  
8 3     3   5433 use Config::JFDI;
  3         442119  
  3         108  
9 3     3   18 use Carp qw/croak/;
  3         3  
  3         2516  
10              
11             subtype MyConf => as 'HashRef';
12             coerce 'MyConf'
13             => from 'Str' => via {
14             my $conf = shift;
15             my ($jfdi_h, $jfdi) = Config::JFDI->open($conf)
16             or croak "Error (conf: $conf) : $!\n";
17             return $jfdi->get;
18             };
19              
20             has conf => ( is => 'rw',
21             isa => 'MyConf',
22             coerce => 1,
23             trigger => sub {
24             my $self = shift;
25             my $args = shift;
26              
27             croak "Error: Can not find " . $self->ns . " in your conf !"
28             if ( ! $args->{$self->ns});
29              
30             $self->config($args->{$self->ns});
31              
32             $self->debug($self->config->{'debug'}) if ( defined $self->config->{'debug'} );
33              
34             $self->_load_backend if ! $self->can('backend');
35             }
36             );
37              
38             # optional (used by DBIX backend )
39             has 'schema' => (
40             is => 'rw',
41             predicate => 'has_schema',
42             );
43              
44             has 'debug' => (
45             is => 'rw',
46             );
47              
48             has _plugin_ns => (
49             is => 'rw',
50             required => 1,
51             isa => 'Str',
52             default => sub{ 'Backend' },
53             );
54              
55             # namespace
56             has 'ns' => (
57             is => 'rw',
58             default => sub { __PACKAGE__ },
59             );
60              
61             has config => (
62             isa => "HashRef",
63             is => "rw",
64             );
65              
66             has cache => (
67             is => "rw",
68             required => 0,
69             );
70              
71              
72             sub _load_backend {
73 1     1   2 my $self = shift;
74 1         22 my $backend = $self->config->{'backend'}->{name};
75              
76 1         5 $self->_log("Loading $backend backend ...");
77 1         4 $self->load_plugin( $backend );
78             }
79              
80              
81             # can_access check if user or roles have permissions on all operations
82             # of a object or more.
83             sub can_access {
84 28     28 1 206559 my ( $self, $roles, $objects, $additional_operations ) = @_;
85              
86             # Check perm on all objects
87 28         71 foreach my $obj (@$objects) {
88              
89             # Not necessary to check if it's not an object
90 34 100       133 next if ! ref($obj);
91              
92             # copy obj to not disturb objets
93 28         36 my $obj2 = $obj;
94              
95             # build parent objects
96 28         32 my @allobjs;
97 28         70 while ( $obj2 ) {
98 70         88608 push(@allobjs, $obj2);
99 70         110 my $typeobj = ref($obj2);
100 70         249 $typeobj =~ s/.*:://;
101 70   100     1747 my $parent_field = $self->config->{typeobj}->{$typeobj}->{parent_field} || 'parent';
102 70 100       298 if ( $obj2->can( $parent_field)) {
103 68         949 $obj2 = $obj2->$parent_field;
104             }
105 2         5 else { $obj2 = 0 }
106              
107             }
108              
109             # check permission on each object
110 28         51420 my $ops;
111 28         62 foreach my $obj2 ( reverse @allobjs ) {
112              
113 64         344 my $typeobj2 = ref($obj2);
114 64         223 $typeobj2 =~ s/.*:://;
115 64         1108 $self->_log("> Search if we can access to ${typeobj2}_" . $obj2->id);
116              
117 64 100       189 if ( $obj2 eq $obj ) {
118 24         30 $ops = $additional_operations;
119             }
120             else {
121 40         67 $ops = [];
122             }
123 64 100       126 if ( ! $self->check_permission( $roles, $obj2, $ops )){
124 12         33 $self->_log("return 0");
125 12         86 return 0;
126             }
127             }
128             }
129              
130 16         155 $self->_log("return 1");
131 16         111 return 1;
132             }
133              
134              
135             # Is roles can an operation on object
136             sub check_permission {
137 64     64 1 81 my ( $self, $roles, $obj, $additional_operations ) = @_;
138              
139 64         75 my $typeobj = ref($obj);
140 64         175 $typeobj =~ s/.*:://;
141              
142 64         188 my @ops_to_access = $obj->ops_to_access;
143 64 100       328694 if ( $additional_operations ) {
144 43         163 push(@ops_to_access, $self->get_operations($additional_operations));
145             }
146              
147 64 100       140 $self->_log(" [OK] Object is not protected") if ( ! $ops_to_access[0] );
148              
149             # Looking operations protecting the object
150 64         64 my %already_checked;
151 64         83 foreach my $op ( @ops_to_access ) {
152              
153 50 50       86 next if ! $op;
154              
155 50 50       1086 next if $already_checked{$op->name};
156 50         1208 $already_checked{$op->name} = 1;
157              
158 50         338 my $ret = 0;
159 50         87 ROLES: foreach my $r ( @$roles ) {
160              
161 51 50       109 next if ! $r;
162 51         752 $self->_log("- Search if role " . $r->name ." can '" . $op->name . "' '${typeobj}_" . $obj->id."'");
163              
164             # get permission from backend
165 51         170 my ($result, $inheritable) = $self->get_permission($r, $op, $obj);
166              
167 51 100       4926 if ( ! defined $result ) {
    100          
    50          
168 4         10 next;
169             }
170             elsif ( ! $result ) {
171 9         149 $self->_log( " [KO] ".$r->name." cannot '" . $op->name . "' '${typeobj}_" . $obj->id."'" );
172 9         23 $ret = 0;
173             }
174             elsif ( $result ) {
175 38         640 $self->_log( " [OK] ".$r->name." can '" . $op->name . "' '${typeobj}_" . $obj->id."'" );
176 38         66 $ret = 1;
177 38         83 last ROLES;
178             }
179             }
180 50 100       146 return 0 if ! $ret;
181             }
182              
183 52         162 return 1;
184             }
185              
186              
187              
188             sub _log{
189 222     222   5331 my ($self, $msg ) = @_;
190              
191 222 50       5145 return if ! $self->debug;
192              
193 0           say STDERR "[debug] $msg";
194             }
195              
196             =encoding utf8
197              
198             =head1 NAME
199              
200             Authorization::RBAC - Role-Based Access Control system
201              
202              
203              
204             =head1 VERSION
205              
206             version 0.10
207              
208             =head1 SYNOPSIS
209              
210             use Authorization::RBAC;
211              
212             my $conf = 't/conf/permsfromdbix.yml';
213             my $rbac = Authorization::RBAC->new( conf => $conf ); # add schema => $schema if DBIx backend
214              
215             my $page = $rbac->schema->resultset('Page')->search( name => 'add' , parent => 7 );
216             my $roles = ... function that returns the roles ...
217              
218             if ( $rbac->can_access($roles, $page, [ 'create_Page' ]) ){
219             # Role 'member' can access to Page /page/wiki/add
220             }
221              
222             =head1 DESCRIPTION
223              
224             Role-based access control (RBAC) is an approach to restricting system access to authorized users.
225              
226             User -> Role(s)
227              
228             Role -> Permission -> Object (Typeobj, unique)
229             -> Operation
230              
231              
232             So you can apply a permission to an instance of a Object and not only on all the class of the Object.
233              
234             La suite en Français ...
235              
236             Pourquoi ce module: J'étais à la recherche d'un module pouvant assurer la protection des accès à des objets. J'ai bien trouvé des modules sur le CPAN qui semblait répondre au besoin mais il y avait toujours un détail, une approche qui ne me convenait pas. La plupart de ces modules répondent à la question 'Est-ce que ce rôle peut faire cette opération ?'. Par exemple 'Est-ce que le rôle 'anonyme' peut créer un commentaire ?' mais jamais à la question 'Est-ce que ce rôle peut faire cette opération sur cet objet ?', exemple : 'Est que le role anonyme peut créer un commentaire sur cette page ?'. De plus je souhaitais que ce système de permissions soit récursif si l'objet à protéger comportait un champ 'parent'.
237              
238              
239             Comment ça marche :
240              
241             Actuellement Authorization::RBAC comporte un seul backend : DBIx
242              
243             Définition des acteurs du système :
244              
245             - Un Type d'objet : Ce peut être une 'Page', un 'Commentaire', 'Une pièce jointe' ...
246              
247             - Opération : Il s'agit d'un action sur un Type d'objet'. ( 'Add_Page', 'Del_Comment')
248              
249             - Un Objet : c'est une instance du Type d'objet. 'Page login', 'Comment n°33', ...
250              
251             - Une Permission : c'est une opération sur un Objet. Elle peut être récursive.
252              
253             - Un Role : Il hérite de Permission(s)
254              
255             Pour accéder à un Objet, le(s) role(s) doit posséder une Permission répondant à une Opération par défaut. Pour cela l'Objet doit avoir une méthode 'ops_to_access' qui retourne le ou les Operations qui le protège (en fait une référence à un tableau d'Opération). Par exemple la méthode ops_to_access de l'Objet 'Page /' retourne ['view_Page'], ce qui signifie "Pour accéder à la Page / le role doit avoir la Permission view_Page sur Page /".
256              
257             La méthode 'can_access' permet d'interroger le système:
258              
259             my $access = $rbac->can_access($roles, $objet, $additional_operations );
260              
261              
262             Un Objet peut avoir une méthode 'parent'. Si c'est le cas alors 2 mécanismes s'ajoute au système de permission :
263              
264             - L'accès à un objet est obtenu si l'accès est permit à tous ses parents. Par exemple, pour accéder à la Page '/admin/user/add', le(s) role(s) devra successivement accéder à '/', 'admin', 'user' et enfin 'add'.
265              
266             - En second lieu une Permission peut être rechercher récursivement sur les parents de l'Objet.
267              
268             Par exemple si nous avons les relations suivantes :
269              
270             Page:
271             /:
272             ops_to_access: view_Page
273             admin:
274             ops_to_access: view_Page
275             add:
276             ops_to_access: create_Page
277              
278             Pour accéder à l'Objet 'Page /admin/user/add', le(s) role(s) devra posséder des Permissions répondant à 'view_Page sur Page /', 'view_Page sur Page admin' et 'create_Page sur Page add'.
279             Imaginons que le(s) role(s) ne peut accèder à l'Objet. Par exemple si le(s) role(s) ne possède pas la Permission 'create_Page sur Page add' alors la recherche de la Permission se fera aussi sur 'admin' puis sur '/'. Pour que cette règle s'applique il faut que la Permission ait une méthode 'inheritable' qui si elle retourne 1 rendra la Permission héritable par ses enfants. Si elle retourne 0 cela a l'effet inverse, ça signifie que le role n'a pas ou plus cette Permission.
280              
281             Imaginons maintenant que les roles aient les Permissions suivantes :
282              
283             Roles:
284             administrateur:
285             view_Page:
286             Page_/: 1
287             inheritable: 1
288             Page_admin: 1
289             inheritable: 1
290             create_Page:
291             Page_/: 1
292             inheritable: 1
293             anonymous:
294             view_Page:
295             Page_/; 1
296             inheritable: 1
297             Page_admin: 0
298             inheritable: 1
299              
300              
301             Ainsi lorsque l'on recherche si le role 'administrateur' peut accéder à / admin / user / add, on ne trouvera pas de Permission 'create_Page sur Page add' mais on la retrouvera sur le parent racine et cette Permission est héritable.
302              
303             Attention car nous avons donné la Permission 'view_Page sur Page / héritable' à 'anonymous' donc si rien n'est fait il a aussi accès à /admin. C'est pourquoi Nous lui avons aussi donné la Permission 'Ne peut view_Page sur Page_Admin héritable'. Ainsi l'accès à /admin est bloqué.
304              
305              
306              
307             =head1 CONFIGURATION
308              
309             See t/conf/permfromdb.yml
310              
311             And also L<Authorization::RBAC::Backend::DBIx>
312              
313             =head1 PROVIDED METHODS
314              
315             =head2 can_access($roles, $objects, $additional_operations )
316              
317             =head2 check_permission($roles, $objects, $additional_operations )
318              
319             =head1 AUTHOR
320              
321             Daniel Brosseau, C<< <dab at catapulse.org> >>
322              
323             =head1 BUGS
324              
325             Please report any bugs or feature requests to C<bug-authorization-rbac at rt.cpan.org>, or through
326             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Authorization-RBAC>. I will be notified, and then you'll
327             automatically be notified of progress on your bug as I make changes.
328              
329              
330              
331              
332             =head1 SUPPORT
333              
334             You can find documentation for this module with the perldoc command.
335              
336             perldoc Authorization::RBAC
337              
338              
339             You can also look for information at:
340              
341             =over 4
342              
343             =item * RT: CPAN's request tracker (report bugs here)
344              
345             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Authorization-RBAC>
346              
347             =item * AnnoCPAN: Annotated CPAN documentation
348              
349             L<http://annocpan.org/dist/Authorization-RBAC>
350              
351             =item * CPAN Ratings
352              
353             L<http://cpanratings.perl.org/d/Authorization-RBAC>
354              
355             =item * Search CPAN
356              
357             L<http://search.cpan.org/dist/Authorization-RBAC/>
358              
359             =back
360              
361              
362             =head1 ACKNOWLEDGEMENTS
363              
364              
365             =head1 LICENSE AND COPYRIGHT
366              
367             Copyright 2015 Daniel Brosseau.
368              
369             This program is free software; you can redistribute it and/or modify it
370             under the terms of either: the GNU General Public License as published
371             by the Free Software Foundation; or the Artistic License.
372              
373             See http://dev.perl.org/licenses/ for more information.
374              
375              
376             =cut
377              
378             1; # End of Authorization::RBAC