File Coverage

blib/lib/Authorization/AccessControl/Request.pm
Criterion Covered Total %
statement 113 125 90.4
branch 16 30 53.3
condition 9 18 50.0
subroutine 20 21 95.2
pod 8 10 80.0
total 166 204 81.3


line stmt bran cond sub pod time code
1             package Authorization::AccessControl::Request 0.04;
2 6     6   82 use v5.26;
  6         46  
3 6     6   38 use warnings;
  6         27  
  6         369  
4              
5             # ABSTRACT: Constructs an ACL request and checks if it is accepted
6              
7 6     6   3141 use Authorization::AccessControl::Dispatch;
  6         24  
  6         235  
8 6     6   40 use Readonly;
  6         11  
  6         366  
9 6     6   36 use Scalar::Util qw(looks_like_number);
  6         12  
  6         332  
10              
11 6     6   35 use constant true => !0;
  6         12  
  6         458  
12 6     6   32 use constant false => !1;
  6         11  
  6         321  
13              
14 6     6   33 use experimental qw(signatures);
  6         13  
  6         26  
15              
16             use overload
17 6     6   944 '""' => \&to_string;
  6         13  
  6         74  
18              
19 55     55 0 1282 sub new($class, %params) {
  55         79  
  55         175  
  55         71  
20 55         126 my $acl = delete($params{acl});
21 55         93 my $roles = delete($params{roles});
22 55         82 my $resource = delete($params{resource});
23 55         80 my $action = delete($params{action});
24 55   100     144 my $attributes = delete($params{attributes}) // {};
25 55   100     165 my $get_attrs = delete($params{get_attrs}) // undef;
26              
27 55 50       247 die("Unsupported params: ", join(', ', keys(%params))) if (keys(%params));
28 55 50 33     337 die("acl is a required property") unless (defined($acl) && ref($acl) && $acl->isa('Authorization::AccessControl::ACL'));
      33        
29              
30 55         342 Readonly::Scalar my $data => {
31             _acl => $acl,
32             _roles => $roles,
33             _resource => $resource,
34             _action => $action,
35             _attributes => $attributes,
36             _get_attrs => $get_attrs,
37             };
38 55         7189 bless($data, $class);
39             }
40              
41 2     2 0 40 sub to_string($self, @params) {
  2         3  
  2         4  
  2         2  
42 2 50 50     5 my $roles = ($self->{_roles} // [])->@* ? '[' . join(',', ($self->{_roles} // [])->@*) . ']' : '';
      0        
43 2         15 my $attributes = '';
44 2   50     4 my $resource = $self->{_resource} // '{NO_RESOURCE}';
45 2   50     11 my $action = $self->{_action} // '{NO_ACTION}';
46 2         9 foreach (keys($self->{_attributes}->%*)) {
47 0         0 my $v;
48 0 0       0 if ($self->{_attributes}->{$_}) {$v = $self->{_attributes}->{$_}}
  0 0       0  
49 0         0 elsif (looks_like_number($self->{_attributes}->{$_})) {$v = 0}
50 0         0 else {$v = 'false'}
51 0         0 $attributes .= "$_=$v,";
52             }
53 2         18 chop($attributes);
54 2         12 $roles . $resource . ' => ' . $action . '(' . $attributes . ')';
55             }
56              
57 40     40   70 sub __properties($self) {
  40         49  
  40         51  
58             (
59             acl => $self->{_acl},
60             roles => $self->{_roles},
61             resource => $self->{_resource},
62             action => $self->{_action},
63             attributes => $self->{_attributes},
64             get_attrs => $self->{_get_attrs},
65             )
66 40         232 }
67              
68 3     3 1 8 sub with_roles($self, @roles) {
  3         9  
  3         28  
  3         6  
69 3         13 return __PACKAGE__->new($self->__properties, roles => [@roles],);
70             }
71              
72 15     15 1 50 sub with_action($self, $action) {
  15         25  
  15         25  
  15         19  
73 15         36 return __PACKAGE__->new($self->__properties, action => $action,);
74             }
75              
76 15     15 1 26 sub with_resource($self, $resource) {
  15         29  
  15         24  
  15         17  
77 15         34 return __PACKAGE__->new($self->__properties, resource => $resource,);
78             }
79              
80 5     5 1 7 sub with_attributes($self, $attrs) {
  5         7  
  5         7  
  5         5  
81 5         10 return __PACKAGE__->new($self->__properties, attributes => {$self->{_attributes}->%*, $attrs->%*},);
82             }
83              
84 2     2 1 27 sub with_get_attrs($self, $sub) {
  2         14  
  2         3  
  2         3  
85 2         5 return __PACKAGE__->new($self->__properties, get_attrs => $sub,);
86             }
87              
88 15     15   22 sub _applicable_grants($self) {
  15         22  
  15         25  
89 15 50       48 return undef unless (defined($self->{_resource}));
90 15 50       124 return undef unless (defined($self->{_action}));
91              
92             my @grants =
93             grep {
94             $_->accepts(
95             roles => $self->{_roles},
96             resource => $self->{_resource},
97             action => $self->{_action},
98             attributes => $self->{_attributes},
99             )
100 15         102 } $self->{_acl}->get_grants;
  78         676  
101              
102 15         99 return \@grants;
103             }
104              
105 0     0 1 0 sub precheck($self) {
  0         0  
  0         0  
106 0         0 my $grants = $self->_applicable_grants;
107 0 0       0 return false unless (defined($grants));
108              
109 0         0 return $grants->@* > 0;
110             }
111              
112 15     15 1 26 sub permitted($self) {
  15         21  
  15         20  
113 15         43 my $grants = $self->_applicable_grants;
114 15 50       47 return false unless (defined($grants));
115              
116 15 100       36 if ($grants->@*) {
117 8         32 $self->{_acl}->_event(on_permit => $grants->[0]);
118 8         246 return true;
119             }
120 7         24 $self->{_acl}->_event(on_deny => $self);
121 7         152 return false;
122             }
123              
124 5     5 1 7 sub yield($self, $get_obj) {
  5         7  
  5         5  
  5         6  
125 5 100       11 unless (defined($self->{_get_attrs})) {
126 3 100       18 return Authorization::AccessControl::Dispatch->new(granted => false) unless ($self->permitted);
127 2         11 my $obj = $get_obj->();
128 2 50       11 return Authorization::AccessControl::Dispatch->new(granted => undef) unless (defined($obj));
129 2         8 return Authorization::AccessControl::Dispatch->new(granted => true, entity => $obj);
130             }
131 2         12 my $obj = $get_obj->();
132 2 50       10 return Authorization::AccessControl::Dispatch->new(granted => undef) unless (defined($obj));
133              
134 2         4 my $attrs = $self->{_get_attrs}->($obj);
135 2         38 $self = $self->with_attributes($attrs);
136 2 100       8 return Authorization::AccessControl::Dispatch->new(granted => true, entity => $obj) if ($self->permitted);
137 1         4 return Authorization::AccessControl::Dispatch->new(granted => false);
138             }
139              
140             =head1 NAME
141              
142             Authorization::AccessControl::Request - constructs an ACL request and checks if it is accepted
143              
144             =head1 SYNOPSIS
145              
146             return unless(acl->request
147             ->with_roles('admin')
148             ->with_resource('Media')
149             ->with_action('create')
150             ->permitted);
151              
152             acl->request...->yield(sub() { ... })
153             ->granted(sub ($x) { ... })
154             ->denied(sub() { ... })
155              
156             =head1 DESCRIPTION
157              
158             This class is used to construct a request and check if the ACL accepts it. The 4
159             C methods are used to configure it, with later calls to the same method
160             overwriting previous ones (with the exception of C which merges
161             instead).
162              
163             L can be called directly, or via L, but either way, it will
164             return false until L and L have been called to
165             configure it.
166              
167             Request instances are immutable: none of their properties may be altered after
168             object creation.
169              
170             =head1 METHODS
171              
172             =head2 with_roles
173              
174             $req->with_roles( @roles )
175              
176             Returns a new request instance with its C property configured to match
177             the parameter value.
178              
179             Chainable.
180              
181             =head2 with_action
182              
183             $req->with_action( $action )
184              
185             Returns a new request instance with its C property configured to match
186             the parameter value.
187              
188             Chainable.
189              
190             =head2 with_resource
191              
192             $req->with_resource( $resource )
193              
194             Returns a new request instance with its C property configured to match
195             the parameter value.
196              
197             Chainable.
198              
199             =head2 with_attributes
200              
201             $req->with_attributes( $attributes )
202              
203             Returns a new request instance with its C property merged with the
204             parameter value.
205              
206             Chainable.
207              
208             =head2 with_get_attrs
209              
210             $req->with_get_attrs( sub($value) { ... } )
211              
212             Returns a new request instance with its C property configured to match
213             the parameter value. This is a callback that receives a protected data value
214             (in L) and returns the corresponding dynamic attributes for it.
215              
216             Chainable.
217              
218             =head2 precheck
219              
220             $req->precheck()
221              
222             Identical to L, except that C and C handlers are not invoked.
223             Should only be used for preauthorizing an action (i.e., to determine what options to show the
224             user)
225              
226              
227             =head2 permitted
228              
229             $req->permitted()
230              
231             Returns a boolean value reflecting whether the request's configured properties
232             satisfy the requirements for any grant in the ACL.
233              
234             =head2 yield
235              
236             $req->yield(sub() { ... })
237              
238             Returns an L instance corresponding
239             to the data value returned by the callback and its permitted status.
240              
241             =head1 AUTHOR
242              
243             Mark Tyrrell C<< >>
244              
245             =head1 LICENSE
246              
247             Copyright (c) 2024 Mark Tyrrell
248              
249             Permission is hereby granted, free of charge, to any person obtaining a copy
250             of this software and associated documentation files (the "Software"), to deal
251             in the Software without restriction, including without limitation the rights
252             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
253             copies of the Software, and to permit persons to whom the Software is
254             furnished to do so, subject to the following conditions:
255              
256             The above copyright notice and this permission notice shall be included in all
257             copies or substantial portions of the Software.
258              
259             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
260             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
261             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
262             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
263             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
264             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
265             SOFTWARE.
266              
267             =cut
268              
269             1;
270              
271             __END__