File Coverage

blib/lib/Authorization/AccessControl/ACL.pm
Criterion Covered Total %
statement 84 94 89.3
branch 11 20 55.0
condition 2 3 66.6
subroutine 17 19 89.4
pod 7 7 100.0
total 121 143 84.6


line stmt bran cond sub pod time code
1             package Authorization::AccessControl::ACL 0.04;
2 6     6   103 use v5.26;
  6         31  
3 6     6   64 use warnings;
  6         14  
  6         330  
4              
5             # ABSTRACT: Access Control List of granted privileges
6              
7 6     6   3223 use Authorization::AccessControl::Grant;
  6         26  
  6         249  
8 6     6   3404 use Authorization::AccessControl::Request;
  6         21  
  6         301  
9 6     6   46 use List::Util qw(any);
  6         64  
  6         507  
10 6     6   71 use Readonly;
  6         22  
  6         315  
11              
12 6     6   35 use experimental qw(signatures);
  6         12  
  6         28  
13              
14 18     18 1 139 sub new($class, %params) {
  18         33  
  18         44  
  18         30  
15 18         39 my $base = delete($params{base});
16 18         34 my $role = delete($params{role});
17              
18 18 50       57 die("Unsupported params: ", join(', ', keys(%params))) if (keys(%params));
19              
20 18         84 Readonly::Hash1 my %hooks => (
21             on_permit => [],
22             on_deny => []
23             );
24              
25 18 100       868 Readonly::Hash1 my %data => (
    100          
26             _base => $base,
27             _role => $role,
28             _grants => ($base ? undef : []), # prevent privs from being saved in non-base instances
29             _hooks => ($base ? undef : \%hooks),
30             );
31 18         667 bless(\%data, $class);
32             }
33              
34 6     6 1 14 sub hook($self, $type, $sub) {
  6         21  
  6         12  
  6         12  
  6         11  
35 6         33 push($self->_base_instance->{_hooks}->{$type}->@*, $sub);
36             }
37              
38 1     1 1 2 sub clone($self) {
  1         2  
  1         1  
39 1         6 my $clone = __PACKAGE__->new();
40 1         5 push($clone->{_grants}->@*, $self->{_grants}->@*);
41 1         11 push($clone->{_hooks}->{$_}->@*, $self->{_hooks}->{$_}->@*) foreach (keys($self->{_hooks}->%*));
42 1         42 return $clone;
43             }
44              
45 113     113   167 sub _base_instance($self) {
  113         162  
  113         147  
46 113   66     378 $self->{_base} // $self;
47             }
48              
49 12     12 1 22 sub role($self, $role = undef) {
  12         18  
  12         22  
  12         19  
50 12         32 return __PACKAGE__->new(base => $self->_base_instance, role => $role);
51             }
52              
53 26     26 1 48 sub grant($self, $resource, $action, $restrictions = undef) {
  26         40  
  26         43  
  26         53  
  26         44  
  26         39  
54             my $p = Authorization::AccessControl::Grant->new(
55             role => $self->{_role},
56 26         185 resource => $resource,
57             action => $action,
58             restrictions => $restrictions,
59             );
60 26 100   69   148 if (any {$p->is_equal($_)} $self->_base_instance->{_grants}->@*) {
  69         678  
61 2         13 warn("skipping duplicate grant: $p\n");
62             } else {
63 24         378 push($self->_base_instance->{_grants}->@*, $p);
64             }
65 26         476 return $self;
66             }
67              
68 0     0   0 sub __contains($arr, $v) {
  0         0  
  0         0  
  0         0  
69 0 0       0 return 0 unless (defined($v));
70 0     0   0 any {$_ eq $v} $arr->@*;
  0         0  
71             }
72              
73 15     15 1 133 sub get_grants($self, %filters) {
  15         24  
  15         22  
  15         27  
74 15         32 my @grants = $self->_base_instance->{_grants}->@*;
75 15 50       204 @grants = grep {$_->resource eq $filters{resource}} @grants if (exists($filters{resource}));
  0         0  
76 15 50       35 @grants = grep {$_->action eq $filters{action}} @grants if (exists($filters{action}));
  0         0  
77 15 0       30 @grants = grep {__contains($filters{roles}, $_->role) || !defined($_->role)} @grants if (exists($filters{roles}));
  0 50       0  
78 15         48 return @grants;
79             }
80              
81 15     15 1 24 sub request($self) {
  15         24  
  15         20  
82 15 50       63 warn("Warning: Calling `roles` on the result of `role` or `grant` calls may not yield expected results\n") if ($self->{_base});
83 15         125 return Authorization::AccessControl::Request->new(acl => $self->_base_instance);
84             }
85              
86 15     15   86 sub _event($self, $type, $ctx) {
  15         26  
  15         26  
  15         33  
  15         21  
87 15         31 $_->($ctx) foreach ($self->_base_instance->{_hooks}->{$type}->@*);
88             }
89              
90             =head1 NAME
91              
92             Authorization::AccessControl::ACL - Access Control List of granted privileges
93              
94             =head1 SYNOPSIS
95              
96             use Authorization::AccessControl::ACL;
97              
98             my $acl = Authorization::AccessControl::ACL->new();
99             $acl->role("admin")
100             ->grant(User => "delete")
101             ->grant(User => "create");
102              
103             $acl->grant(Book => "search")
104             ->grant(Book => 'delete', {owned => true});
105              
106             my req = $acl->request;
107             ...
108              
109             =head1 DESCRIPTION
110              
111             The ACL class provides functionality for maintaining a set of granted privileges.
112             Each item in the list is an instance of L.
113             Every call to L creates a new grant instance and adds it to the ACL's
114             list. By default, these grants are role-less: they apply to all users. Calling
115             L with a role name argument allows you to chain subsquent calls to
116             L off of it: such grants are configured for that role only.
117              
118             The full grant list can be obtained via the L method, although this
119             is merely informational - the grants themselves are immutable and have little
120             relevent functionality outside of the ACL.
121              
122             The L method generates an L,
123             which is used to check if a specific action is permitted by the ACL.
124              
125             Most ACL instance properties are immutable: with the exception of the list
126             contents, none of their properties may be altered after object creation.
127              
128             =head1 METHODS
129              
130             =head2 new
131              
132             Authorizatrion::AccessControl::ACL->new()
133              
134             Constructor.
135              
136             Creates a new ACL instance. Each ACL instance created via this constructor is
137             entirely unrelated. For a global persistent ACL, see
138             L
139              
140             =head2 clone
141              
142             $acl->clone()
143              
144             Creates a new ACL instance pre-populated with the cloned object's grants and
145             hooks. Once cloned, the two instances are entirely unrelated and changes to one
146             will not be reflected in the other.
147              
148             N.B. contextual L is not taken into account when cloning:
149              
150             my $acl2 = $acl1->role('admin')->grant(User => "delete")->clone;
151             $acl2->grant(User => "update");
152              
153             The second grant is role-less, applying to all users, even though the admin role
154             context was active when the clone was performed. This may cause you to
155             inadvertently grant more privileges than you expect if not attended to.
156              
157             =head2 role
158              
159             $acl->role($role = undef)
160              
161             Returns a new I instance of C
162             facilitating chaining in order to create role-specific grants. Dependent
163             instances share a grant list with their "parent".
164              
165             The C<$role> argument is optional, if omitted or C, the returned instance
166             becomes role-less. If present, should be a string.
167              
168             Chainable.
169              
170             =head2 grant
171              
172             $acl->grant($resource => $action)
173              
174             Creates a privilege L and adds it to the
175             access control list.
176              
177             Chainable.
178              
179             =head2 get_grants
180              
181             $acl->get_grants()
182              
183             Returns an array of all grants contained in the access control list.
184              
185             =head2 request
186              
187             $acl->request()
188              
189             Returns an L instance linked to this ACL.
190             Subsequent changes to the ACL will be taken into account if made prior to the
191             request being evaluated. L first to avoid the implications of this
192             behavior, if required.
193              
194             =head2 hook
195              
196             $acl->hook(on_permit|on_deny => sub {})
197              
198             Register a callback to be executed when a permission request is granted or
199             denied, such as for comprehensive authorization logging. Multiple hooks may be
200             registered for each status, and will be called in order when the event occurs.
201              
202             C handlers receive a L that
203             accepted the request as their argument.
204              
205             C handlers receive a L that
206             failed to be accepted by any grant as their argument.
207              
208             =head1 AUTHOR
209              
210             Mark Tyrrell C<< >>
211              
212             =head1 LICENSE
213              
214             Copyright (c) 2024 Mark Tyrrell
215              
216             Permission is hereby granted, free of charge, to any person obtaining a copy
217             of this software and associated documentation files (the "Software"), to deal
218             in the Software without restriction, including without limitation the rights
219             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
220             copies of the Software, and to permit persons to whom the Software is
221             furnished to do so, subject to the following conditions:
222              
223             The above copyright notice and this permission notice shall be included in all
224             copies or substantial portions of the Software.
225              
226             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
227             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
228             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
229             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
230             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
231             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
232             SOFTWARE.
233              
234             =cut
235              
236             1;
237              
238             __END__