File Coverage

blib/lib/Abilities.pm
Criterion Covered Total %
statement 45 49 91.8
branch 23 28 82.1
condition 7 9 77.7
subroutine 8 8 100.0
pod 4 4 100.0
total 87 98 88.7


line stmt bran cond sub pod time code
1             package Abilities;
2              
3             # ABSTRACT: Simple, hierarchical user authorization for web applications, with optional support for plan-based (paid) services.
4              
5 2     2   106543 use Carp;
  2         4  
  2         139  
6 2     2   1695 use Hash::Merge qw/merge/;
  2         4979  
  2         117  
7 2     2   963 use Moo::Role;
  2         88090  
  2         19  
8 2     2   1587 use namespace::autoclean;
  2         17240  
  2         16  
9              
10             our $VERSION = "0.5";
11             $VERSION = eval $VERSION;
12              
13             =head1 NAME
14              
15             Abilities - Simple, hierarchical user authorization for web applications, with optional support for plan-based (paid) services.
16              
17             =head1 VERSION
18              
19             version 0.5
20              
21             =head1 SYNOPSIS
22              
23             package User;
24            
25             use Moose; # or Moo
26             with 'Abilities';
27            
28             # ... define required methods ...
29            
30             # somewhere else in your code:
31              
32             # get a user object that consumed the Abilities role
33             my $user = MyApp->get_user('username'); # $user is a User object
34              
35             # check if the user is able to do something
36             if ($user->can_perform('something')) {
37             do_something();
38             } else {
39             die "Hey you can't do that, you can only do " . join(', ', keys %{$user->abilities});
40             }
41              
42             =head1 DESCRIPTION
43              
44             Abilities is a simple yet powerful mechanism for authorizing users of web
45             applications (or any applications) to perform certain actions in the application. This is an
46             extension of the familiar role-based access control that is common in
47             various systems and frameworks like L (See L
48             for the role-based implementation and L
49             for the ability-based implementation that inspired this module).
50              
51             As opposed to role-based access control - where users are allowed access
52             to a certain feature (here called 'action') only through their association
53             to a certain role that is hard-coded into the program - in ability-based
54             acccess control, a list of actions is assigned to every user, and they are
55             only allowed to perform these actions. Actions are not assigned by the
56             developer during development, but rather by the end-user during deployment.
57             This allows for much more flexibility, and also speeds up development,
58             as you (the developer) do not need to think about who should be allowed
59             to perform a certain action, and can easily grant access later-on after
60             deployment (assuming you're also the end-user).
61              
62             Abilities to perform certain actions can be given to a user specifically, or
63             via roles the user can assume (as in role-based access control). For example,
64             if user 'user01' is a member of role 'admin', and this user wishes to perform
65             some action, for example 'delete_foo', then they will only be able to do
66             so if the 'delete_foo' ability was given to either the user itself or the
67             'admin' role itself. Furthermore, roles can recursively inherit other roles;
68             for example, the role 'mega_mods' can inherit the roles 'mods' and 'editors'.
69             Users of the 'mega_mods' role will assume all actions owned by the 'mods'
70             and 'editors' roles.
71              
72             A commonly known use-case for this type of access control is message boards,
73             where the administrator might wish to create roles with certain actions
74             and associate users with the roles (more commonly called 'user groups');
75             for example, the admin can create an 'editor' role, giving users of this
76             role the ability to edit and delete posts, but not any other administrative
77             action. So in essence, this type of access control relieves the developer
78             of deciding who gets to do what and passes these decisions to the
79             end-user, which might actually be necessary in certain situations.
80              
81             The C module is implemented as a L (which makes
82             it compatible with L code). In order to be able to use this mechanism,
83             applications must implement a user management system that will consume this role.
84             More specifically, a user class and a role class must be implemented, consuming this role. L is a reference implementation that can be used by applications, or
85             just taken as an example of an ability-based authorization system. L
86             and L are the user and role classes that consume the Abilities
87             role in the Entities distribution.
88              
89             =head2 CONSTRAINTS
90              
91             Generally, an ability is a yes/no option. Either the user can or can't perform
92             a specific action. At times, this might not be flexible enough, and the user's
93             ability to perform a certain action should be constrained. For example, a user
94             might be granted the ability to edit posts in a blog, but this ability should
95             be constrained to the user's posts only. The user is not to be allowed to edit
96             posts created by other users. C supports constraints by allowing to
97             set a name-based constraint when granting a user/role a certain ability. Then,
98             checking the user's ability to perform an action can include the constraint,
99             for example:
100              
101             if ($post->{user_id} eq $user->id && $user->can_perform('edit_posts', 'only_his')) {
102             # allow
103             } else {
104             # do not allow
105             }
106              
107             Here, the C module allows you to check if the user's ability is constrained,
108             but the responsibility for making sure the constraint is actually relevant
109             to the case is left to you. In the above example, it is the application that
110             checks if the post the user is trying to edit was created by them, not the C
111             module.
112              
113             =head2 (PAID) SUBSCRIPTION-BASED WEB SERVICES
114              
115             Apart from the scenario described above, this module also provides optional
116             support for subscription-based web services, such as those where customers
117             subscribe to a certain paid (or free, doesn't matter) plan from a list
118             of available plans (GitHub is an example of such a service). This functionality
119             is also implemented as a Moo(se) role, in the L module provided
120             with this distribution. Read its documentation for detailed information.
121              
122             =head1 REQUIRED METHODS
123              
124             Classes that consume this role are required to implement the following
125             methods:
126              
127             =head2 roles()
128              
129             Returns a list of all role names that a user object belongs to, or a role object
130             inherits from.
131              
132             Example return structure:
133              
134             ( 'moderator', 'supporter' )
135              
136             NOTE: In previous versions, this method was required to return
137             an array of role objects, not a list of role names. This has been changed
138             in version 0.3.
139              
140             =cut
141              
142             requires 'roles';
143              
144             =head2 actions()
145              
146             Returns a list of all action names that a user object has been explicitely granted,
147             or that a role object has been granted. If a certain action is constrained, then
148             it should be added to the list as an array reference with two items, the first being
149             the name of the action, the second being the name of the constraint.
150              
151             Example return structure:
152              
153             ( 'create_posts', ['edit_posts', 'only_his'], 'comment_on_posts' )
154              
155             NOTE: In previous versions, this method was required to return
156             an array of action objects, not a list of action names. This has been changed
157             in version 0.3.
158              
159             =cut
160              
161             requires 'actions';
162              
163             =head2 is_super()
164              
165             This is a boolean attribute that both user and role objects should have.
166             If a user/role object has a true value for this attribute, then they
167             will be able to perform any action, even if it wasn't granted to them.
168              
169             =cut
170              
171             requires 'is_super';
172              
173             =head2 get_role( $name )
174              
175             This is a method that returns the object of the role named C<$name>.
176              
177             =cut
178              
179             requires 'get_role';
180              
181             =head1 PROVIDED METHODS
182              
183             Classes that consume this role will have the following methods available
184             to them:
185              
186             =head2 can_perform( $action, [ $constraint ] )
187              
188             Receives the name of an action, and possibly a constraint, and returns a true
189             value if the user/role can perform the provided action.
190              
191             =cut
192              
193             sub can_perform {
194 16     16 1 40 my ($self, $action, $constraint) = @_;
195              
196             # a super-user/super-role can do whatever they want
197 16 100       69 return 1 if $self->is_super;
198              
199             # return false if user/role doesn't have that ability
200 12 100       27 return unless $self->abilities->{$action};
201              
202             # user/role has ability, but is there a constraint?
203 11 100 100     64 if ($constraint && $constraint ne '_all_') {
204             # return true if user/role's ability is not constrained
205 4 100       10 return 1 if !ref $self->abilities->{$action};
206            
207             # it is constrained (or at least it should be, let's make
208             # sure we have an array-ref of constraints)
209 2 50       33 if (ref $self->abilities->{$action} eq 'ARRAY') {
210 2 100       13 return 1 if $constraint eq '_any_'; # caller wants to know if
211             # user/role has any constraint,
212             # which we now know is true
213 1         3 foreach (@{$self->abilities->{$action}}) {
  1         4  
214 1 50       8 return 1 if $_ eq $constraint;
215             }
216 0         0 return; # constraint not met
217             } else {
218 0         0 carp "Expected an array-ref of constraints for action $action, received ".ref($self->abilities->{$action}).", returning false.";
219 0         0 return;
220             }
221             } else {
222             # no constraint, make sure user/role's ability is indeed
223             # not constrained
224 7 100       16 return if ref $self->abilities->{$action}; # implied: ref == 'ARRAY', thus constrained
225 6         35 return 1; # not constrained
226             }
227             }
228              
229             =head2 assigned_role( $role_name )
230              
231             This method receives a role name and returns a true value if the user/role
232             is a direct member of the provided role. Only direct membership is checked,
233             so the user/role must be specifically assigned to the provided role, and
234             not to a role that inherits from that role (see L
235             instead).
236              
237             =cut
238              
239             sub assigned_role {
240 5     5 1 6016 my ($self, $role) = @_;
241              
242 5 50       18 return unless $role;
243              
244 5         144 foreach ($self->roles) {
245 5 100       138 return 1 if $_ eq $role;
246             }
247              
248 3         41 return;
249             }
250              
251             =head2 does_role( $role_name )
252              
253             Receives the name of a role, and returns a true value if the user/role
254             inherits the abilities of the provided role. This method takes inheritance
255             into account, so if a user was directly assigned to the 'admins' role,
256             and the 'admins' role inherits from the 'devs' role, then C
257             will return true for that user (while C returns false).
258              
259             =cut
260              
261             sub does_role {
262 5     5 1 10 my ($self, $role) = @_;
263              
264 5 50       14 return unless $role;
265              
266 5         145 foreach (map([$_, $self->get_role($_)], $self->roles)) {
267 4 100 100     148 return 1 if $_->[0] eq $role || $_->[1]->does_role($role);
268             }
269              
270 3         80 return;
271             }
272              
273             =head2 abilities()
274              
275             Returns a hash reference of all the abilities a user/role object can
276             perform, after consolidating abilities inherited from roles (including
277             recursively) and directly granted. Keys in the hash-ref will be names
278             of actions, values will be 1 (for yes/no actions) or a single-item array-ref
279             with the name of a constraint (for constrained actions).
280              
281             =cut
282              
283             sub abilities {
284 70     70 1 255 my $self = shift;
285              
286 70         89 my $abilities = {};
287              
288             # load direct actions granted to this user/role
289 70         1729 foreach ($self->actions) {
290             # is this action constrained/scoped?
291 131 100 33     8090 unless (ref $_) {
    50          
292 88         229 $abilities->{$_} = 1;
293             } elsif (ref $_ eq 'ARRAY' && scalar @$_ == 2) {
294 43         166 $abilities->{$_->[0]} = [$_->[1]];
295             } else {
296 0         0 carp "Can't handle action of reference ".ref($_);
297             }
298             }
299              
300             # load actions from roles this user/role consumes
301 70         1879 my @hashes = map { $self->get_role($_)->abilities } $self->roles;
  44         1093  
302              
303             # merge all abilities
304 70         1108 while (scalar @hashes) {
305 44         1043 $abilities = merge($abilities, shift @hashes);
306             }
307              
308 70         10082 return $abilities;
309             }
310              
311             =head1 UPGRADING FROM v0.2
312              
313             Up to version 0.2, C required the C and C
314             attributes to return objects. While this made it easier to calculate
315             abilities, it made this system a bit less flexible.
316              
317             In version 0.3, C changed the requirement such that both these
318             attributes need to return strings (the names of the roles/actions). If your implementation
319             has granted roles and actions stored in a database by names, this made life a bit easier
320             for you. On other implementations, however, this has the potential of
321             requiring you to write a bit more code. If that is the case, I apologize,
322             but keep in mind that you can still store granted roles and actions
323             any way you want in a database (either by names or by references), just
324             as long as you correctly provide C and C.
325              
326             Unfortunately, in both versions 0.3 and 0.4, I made a bit of a mess
327             that rendered both versions unusable. While I documented the C
328             attribute as requiring role names instead of role objects, the actual
329             implementation still required role objects. This has now been fixed,
330             but it also meant I had to add a new requirement: consuming classes
331             now have to provide a method called C that takes the name
332             of a role and returns its object. This will probably means loading the
333             role from a database and blessing it into your role class that also consumes
334             this module.
335              
336             I apologize for any inconvenience this might have caused.
337              
338             =head1 AUTHOR
339              
340             Ido Perlmuter, C<< >>
341              
342             =head1 BUGS
343              
344             Please report any bugs or feature requests to C, or through
345             the web interface at L. I will be notified, and then you'll
346             automatically be notified of progress on your bug as I make changes.
347              
348             =head1 SUPPORT
349              
350             You can find documentation for this module with the perldoc command.
351              
352             perldoc Abilities
353              
354             You can also look for information at:
355              
356             =over 4
357              
358             =item * RT: CPAN's request tracker
359              
360             L
361              
362             =item * AnnoCPAN: Annotated CPAN documentation
363              
364             L
365              
366             =item * CPAN Ratings
367              
368             L
369              
370             =item * Search CPAN
371              
372             L
373              
374             =back
375              
376             =head1 LICENSE AND COPYRIGHT
377              
378             Copyright 2010-2013 Ido Perlmuter.
379              
380             This program is free software; you can redistribute it and/or modify it
381             under the terms of either: the GNU General Public License as published
382             by the Free Software Foundation; or the Artistic License.
383              
384             See http://dev.perl.org/licenses/ for more information.
385              
386             =cut
387              
388             1;