File Coverage

blib/lib/Mojolicious/Plugin/Authorization/AccessControl.pm
Criterion Covered Total %
statement 116 120 96.6
branch 28 32 87.5
condition 7 14 50.0
subroutine 20 21 95.2
pod 1 1 100.0
total 172 188 91.4


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Authorization::AccessControl 0.02;
2 5     5   5099497 use v5.26;
  5         57  
3 5     5   35 use warnings;
  5         13  
  5         351  
4              
5             # ABSTRACT: Integrate Authorization::AccessControl into Mojolicious
6              
7 5     5   903 use Mojo::Base 'Mojolicious::Plugin';
  5         14052  
  5         48  
8              
9 5     5   4942 use Authorization::AccessControl qw(acl);
  5         156172  
  5         481  
10 5     5   50 use Readonly;
  5         13  
  5         309  
11 5     5   3270 use Syntax::Keyword::Try;
  5         15722  
  5         35  
12              
13 5     5   633 use experimental qw(signatures);
  5         12  
  5         51  
14              
15             Readonly::Scalar my $DEFAULT_PREFIX => 'authz';
16              
17 8     8 1 4863 sub register($self, $app, $args) {
  8         23  
  8         19  
  8         18  
  8         104  
18 8   33     149 my $prefix = $args->{prefix} // $DEFAULT_PREFIX;
19 8         30 my $stash_ac = "_$prefix.request.accesscontrol";
20              
21 8     19   43 my $get_roles = sub($c) {$c->authn->current_user_roles};
  19         459  
  19         59  
  19         39  
  19         38  
22 3     3   12 $get_roles = $args->{get_roles} // sub($c) {[]}
  3         7  
  3         6  
  3         8  
23 8 100 66     52 if (exists($args->{get_roles}));
24 8 50 33     58 die("get_roles must be a CODEREF/anonymous subroutine") if (defined($get_roles) && ref($get_roles) ne 'CODE');
25              
26 8     33   38 my $log_f = sub($m) {$app->log->info($m)};
  33         176  
  33         2119  
  33         78  
  33         70  
27 8 100       36 if (exists($args->{log})) {
28 2 100       9 if (defined($args->{log})) {
29 1 50 33     25 if (ref($args->{log}) && $args->{log}->isa('Mojo::Log')) {
30 1     2   8 $log_f = sub($m) {$args->{log}->info($m)};
  2         9  
  2         152  
  2         7  
  2         5  
31             }
32             } else {
33       8     $log_f = sub { }
34 1         6 }
35             }
36              
37 8     24   40 acl->hook(on_permit => sub ($ctx) {$log_f->("[Authorization::AccessControl] Granted: $ctx")});
  24         119  
  24         13981  
  24         53  
  24         55  
38 8     19   993 acl->hook(on_deny => sub ($ctx) {$log_f->("[Authorization::AccessControl] Denied: $ctx")});
  19         99  
  19         11014  
  19         47  
  19         42  
39              
40 33     33   68 my $get_ac = sub($c) {
  33         68  
  33         58  
41 33         152 my $ac = acl;
42 33 100       342 if ($c->tx->connection) {
43 4 100       80 $c->stash($stash_ac => $ac->clone) unless (defined($c->stash($stash_ac)));
44 4         705 $ac = $c->stash($stash_ac);
45             }
46 33         629 return $ac;
47 8         368 };
48              
49 0         0 $app->helper(
50 0     0   0 "$prefix.acl" => sub ($c) {
  0         0  
51 0         0 return $get_ac->($c);
52             }
53 8         101 );
54              
55 8         24 $app->helper(
56 8     8   35903 "$prefix.role" => sub ($c, @params) {
  8         18  
  8         50  
57 8         30 return $get_ac->($c)->role(@params);
58             }
59 8         5226 );
60              
61 8         3829 my @get_attrs;
62 2         7 $app->helper(
63 2     2   1552 "$prefix.dynamic_attrs" => sub ($c, @params) {
  2         6  
  2         3  
64 2         5 my $get_attrs = {handler => pop(@params)};
65 2 50       8 $get_attrs->{resource} = shift(@params) if (@params);
66 2 100       12 $get_attrs->{action} = shift(@params) if (@params);
67 2         9 push(@get_attrs, $get_attrs);
68             }
69 8         89 );
70              
71 25     25   55 my $get_get_attrs = sub ($resource, $action) {
  25         52  
  25         56  
  25         48  
72 25         58 my @c = @get_attrs;
73 25 50       91 @c = grep {!defined($_->{resource}) || $_->{resource} eq $resource} @c if (defined($resource));
  6 100       31  
74 25 100       98 @c = grep {!defined($_->{action}) || $_->{action} eq $action} @c if (defined($action));
  6 100       16  
75 25         65 @c = sort {defined($b->{resource}) + defined($b->{action}) - (defined($a->{resource}) + defined($a->{action}))} @c;
  2         8  
76 25   100     190 return ($c[0] // {})->{handler};
77 8         4154 };
78              
79 25         66 $app->helper(
80 25     25   82621 "$prefix.request" => sub ($c, $resource = undef, $action = undef) {
  25         69  
  25         63  
  25         44  
81 25         53 my $roles = [];
82 25         135 try {$roles = $get_roles->($c)} catch ($e) {
83             }
84              
85 25         1640 my $req = $get_ac->($c)->request->with_roles($roles->@*);
86 25 100       13395 $req = $req->with_action($action) if (defined($action));
87 25 100       4411 $req = $req->with_resource($resource) if (defined($resource));
88 25 100       4145 if (my $f = $get_get_attrs->($resource, $action)) {
89 2         10 $req = $req->with_get_attrs(sub($ctx) {$f->($c, $ctx)});
  2         5  
90             }
91              
92 25         599 return $req;
93             }
94 8         75 );
95             }
96              
97             =head1 NAME
98              
99             Mojolicious::Plugin::Authorization::AccessControl - Integrate Authorization::AccessControl into Mojolicious
100              
101             =head1 SYNOPSIS
102              
103             # in startup
104             $app->plugin('Authorization::AccessControl' => {
105             get_roles => sub($c) { [$c->authn->current_user->roles] }
106             });
107             # static grants
108             $app->authz->role('admin')
109             ->grant(User => 'list')
110             ->grant(User => 'create')
111             ->grant(User => 'delete');
112            
113             $app->authz->dynamic_attrs(Book => list => undef);
114             $app->authz->dynamic_attrs(Book => sub($c, $ctx) {
115             return {
116             book_id => $ctx->id,
117             own => $ctx->owner_id == $c->authn->current_user->id,
118             deleted => defined($ctx->deleted_at)
119             }
120             });
121              
122             $app->hook(before_dispatch => sub($c) {
123             #dynamic grants specific to current request
124             $c->authz->grant(Book => 'delete', { book_id => $_->{book_id} })
125             foreach ($c->model("GrantDelete")->for_user($c->authn->current_user))
126             });
127              
128             # in controller
129             use Authorization::AccessControl qw(acl);
130              
131             # static grants
132             acl->role
133             ->grant(Book => 'list', { deleted => 0 })
134             ->grant(Book => "read")
135             ->grant(Book => "edit", { own => 1 })
136             ->role('admin')
137             ->grant(Book => "list")
138             ->grant(Book => "edit");
139              
140             sub list($self) {
141             my $deleted = !!$self->param('include_deleted');
142             $self->authz->request(Book => 'list')->with_attributes({ deleted => $deleted })->yield(sub() {
143             $deleted ? [$self->model("book")->all] : [$self->model("book")->all_except_deleted]
144             })
145             ->granted(sub ($books) {
146             $self->render(json => $books)
147             })
148             ->denied(sub() {
149             $self->render(status => 401, text => 'unauthorized')
150             })
151             ->null(sub() {
152             $self->render(status => 404, text => 'notfound')
153             })
154             }
155              
156             sub get($self) {
157             $self->authz->request(Book => 'read')->yield(sub() {
158             $self->model("book")->get($self->param('id'))
159             })
160             ->granted(sub ($book) {
161             $self->render(json => $book)
162             })
163             ->denied(sub () {
164             $self->render(status => 401, text => 'unauthorized')
165             })
166             ->null(sub () {
167             $self->render(status => 404, text => "book not found")
168             })
169              
170             sub edit($self) {
171             $self->authz->request(Book => 'edit')->yield(sub() {
172             $self->model("book")->get($self->param('id'))
173             })
174             ->granted(sub ($book) {
175             $book->update($self->req->body->json);
176             $self->render(json => $book)
177             })
178             ->denied(sub () {
179             $self->render(status => 401, text => 'unauthorized')
180             })
181             ->null(sub () {
182             $self->render(status => 404, text => "book not found")
183             })
184             }
185              
186             =head1 DESCRIPTION
187              
188             This plugin ties together the functionality of L
189             with the L framework. In essence, this means:
190              
191             =over
192              
193             =item * roles are computed and attached to requests automatically based on the L function
194              
195             =item * dynamic attributes callbacks can be registered and then they, too, will be used automatically at request time
196              
197             =item * privilege grants can be static/permanent, or dynamic (per-request, from database, etc). Determination is automatically made based on context.
198              
199             =item * all authorization checks are automatically logged to a Mojo::Log instance
200              
201             =back
202              
203             =head1 METHODS
204              
205             L inherits all methods from
206             L and implements the following new ones
207              
208             =head2 register
209              
210             Register plugin in L application. Configuration via named arguments:
211              
212             =head4 prefix
213              
214             Configures the prefix used for the module's Mojolicious helper functions and
215             stash values. This documentation assumes that it is left unchanged
216              
217             Default: C
218              
219             =head4 get_roles
220              
221             Configures a callback for obtaining the roles relevent to the authorization
222             requests (i.e., the roles of the current user). The function receives one
223             argument, the Mojolicious controller, and returns an ArrayRef of roles.
224              
225             Default: Ccurrent_user_roles }>
226              
227             =head4 log
228              
229             The L instance that will be used to log Authorization activity in.
230             Set to undef to disable logging entirely.
231              
232             Default: Clog>
233              
234             =head1 HELPERS
235              
236             =head2 authz.acl
237              
238             $app->authz->acl()
239             $c->authz->acl()
240              
241             Returns the L instance, depending on context
242             If called on a request controller, returns an ACL specific to that request;
243             otherwise, returns the global L instance.
244              
245             The request-specific ACLs are constructed by cloning the global instance,
246             so you may populate the global instance with static grants, and then augment
247             them with dynamic request-specific grants.
248              
249             =head2 authz.role
250              
251             $app->authz->role( $role = undef )
252             $c->authz->role( $role = undef )
253              
254             A shortcut for calling L on
255             L. Returns a dependent ACL instance contextualized on the given
256             C<$role> argument.
257              
258             =head2 authz.request
259              
260             $c->authz->request( $resource = undef, $action = undef )
261              
262             Creates and populates an L. If
263             C<$resource> and/or C<$action> are given, they are set on the request, but they
264             are also used for determining the
265             L function to assign.
266             The request's L are also configured,
267             using the L callback.
268              
269             =head2 authz.dynamic_attrs
270              
271             $c->authz->dynamic_attrs($coderef)
272             $c->authz->dynamic_attrs($resource, $coderef)
273             $c->authz->dynamic_attrs($resource, $action, $coderef)
274              
275             Registers a dynamic attributes callback function. C<$coderef> must be a CODEREF
276             or undefined. Dynamic attrs functions are searched from most to least specific,
277             so a general function may be overridden with a more-specific one, or blocked
278             entirely with an undefined entry, e.g.,
279              
280             $c->authz->dynamic_attrs(Book => sub($c, $ctx) { ... } );
281             $c->authz->dynamic_attrs(Book => list => undef);
282              
283             The first line declares a handler for attributes of Book resources. The second
284             clears the handler for the list action of Book resources, so the handler is used
285             for all Book resources, except for list actions.
286              
287             =head1 AUTHOR
288              
289             Mark Tyrrell C<< >>
290              
291             =head1 LICENSE
292              
293             Copyright (c) 2024 Mark Tyrrell
294              
295             Permission is hereby granted, free of charge, to any person obtaining a copy
296             of this software and associated documentation files (the "Software"), to deal
297             in the Software without restriction, including without limitation the rights
298             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
299             copies of the Software, and to permit persons to whom the Software is
300             furnished to do so, subject to the following conditions:
301              
302             The above copyright notice and this permission notice shall be included in all
303             copies or substantial portions of the Software.
304              
305             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
306             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
307             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
308             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
309             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
310             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
311             SOFTWARE.
312              
313             =cut
314              
315             1;
316              
317             __END__