File Coverage

blib/lib/Mojolicious/Plugin/SimpleAuthorization.pm
Criterion Covered Total %
statement 30 31 96.7
branch 20 26 76.9
condition 5 8 62.5
subroutine 4 4 100.0
pod 1 1 100.0
total 60 70 85.7


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::SimpleAuthorization;
2 1     1   954 use Mojo::Base 'Mojolicious::Plugin';
  1         2  
  1         7  
3            
4             our $VERSION = '0.02';
5            
6             sub register {
7 1     1 1 41 my ($self, $app, $conf) = @_;
8            
9             # Default mojo stash values for the user and their roles
10 1   50     8 $conf->{roles} //= 'roles';
11 1   50     6 $conf->{user} //= 'user';
12            
13             # Hook if assert_user_roles fails
14             delete $conf->{on_assert_failure}
15 1 50       5 unless ref $conf->{on_assert_failure} eq 'CODE';
16            
17             # Add "assert_user_roles" helper
18             $app->helper(
19             assert_user_roles => sub {
20 13     13   119009 my $self = shift;
21            
22 13         75 my $result = $self->check_user_roles(@_);
23 13 100       104 if ($result == 0) {
24             $conf->{on_assert_failure}->($self, @_)
25 5 50       29 if defined $conf->{on_assert_failure};
26             }
27            
28 13         2264 return $result;
29             }
30 1         11 );
31            
32             # Add "check_user_roles" helper
33             $app->helper(
34             check_user_roles => sub {
35 16     16   30917 my $self = shift;
36 16         28 my $tests = shift;
37 16 100       49 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
38            
39 16   100     45 my $roles = $self->stash->{$conf->{roles}} // {};
40 16   50     215 my $user = $self->stash->{$conf->{user}} // {};
41 16 100       181 $tests = [$tests] unless ref $tests eq 'ARRAY';
42            
43 16         22 my $cbresult;
44 16 100       36 if ($cb) {
45 6 100       16 return 1 if $cbresult = $cb->($user, $roles);
46             }
47            
48             # An undefired cbresult indicates the authorization chain should continue
49 14 100       53 if (not defined $cbresult) {
50             # Superuser value for the user hash, or code ref to check if this user
51             # has full permission for every role.
52 12 50       32 if (defined $conf->{superuser}) {
53 12 50       34 if (ref $conf->{superuser} eq 'CODE') {
54 12 100       40 return 1 if $conf->{superuser}->($user, $roles);
55             }
56             else {
57 0 0       0 return 1 if $user->{$conf->{superuser}};
58             }
59             }
60 8 100       46 map { return 1 if $roles->{$_} } @$tests;
  8         37  
61             }
62            
63 6         22 return 0;
64             }
65 1         42 );
66             }
67            
68             1;
69            
70             =encoding utf8
71            
72             =head1 NAME
73            
74             Mojolicious::Plugin::SimpleAuthorization - Simple role-based authorization
75            
76             =head1 SYNOPSIS
77            
78             # Mojolicious example
79             package SimpleApp;
80             use Mojo::Base 'Mojolicious';
81            
82             sub startup {
83             my $self = shift;
84            
85             $self->plugin(
86             'SimpleAuthorization' => {
87             'on_assert_failure' => sub { # assert failure hook
88             my ($self, $tests) = @_;
89            
90             $self->render(text => 'Permission denied.');
91             },
92             }
93             );
94            
95             # Add route not requiring authentication/authorization
96             my $r = $self->routes;
97             $r->get('/')->to(cb => sub { shift->render(text => "I am public. Hi.") });
98            
99             # Add authentication under (which populates stash with the user/roles)
100             #
101             # In your under, set the user and user's roles C every request.
102             # The user can contain any arbitrary data. Roles should contain key/value
103             # pairs, where allocated roles evaluate to true.
104             my $auth = $r->under->to(
105             cb => sub {
106             my $self = shift;
107            
108             #if ($user_is_authenticated) {
109             $self->stash(roles => {'user.delete' => 0, 'user.search' => 1});
110             $self->stash(user => {username => 'paul', administrator => 0});
111             #}
112             }
113             );
114            
115             # Search user controller - success!
116             $auth->get('/user/search')->to(
117             cb => sub {
118             my $self = shift;
119             return unless $self->assert_user_roles([qw/user.search/]);
120            
121             $self->render(text => "Success! Let's do some searching!");
122             }
123             );
124            
125             # Delete user controller - oh noes! (Will execute C.)
126             $auth->get('/user/delete')->to(
127             cb => sub {
128             my $self = shift;
129             return unless $self->assert_user_roles([qw/user.delete/]);
130            
131             $self->render(text => "Damn! Not authorized so won't see this!");
132             }
133             );
134             }
135            
136             1;
137            
138             =head1 DESCRIPTION
139            
140             L is a simple role-based authorization
141             plugin for L.
142            
143             It attempts to keep a sane control flow by not croaking or dying if the user
144             does not have the relevant roles/permissions. As such, C or
145             C should be called at the beginning of your controllers.
146            
147             L does offer the hook
148             C if you want to render a permission denied response or
149             similar for every request that isn't authorized. (Or if you would prefer to
150             croak/die.)
151            
152             =head1 OPTIONS
153            
154             L supports the following options.
155            
156             =head2 on_assert_failure
157            
158             # Mojolicious::Lite
159             plugin SimpleAuthorization => {
160             on_assert_failure => sub {
161             my ($self, $tests) = @_;
162            
163             $self->render(
164             text => 'You don't have permission to access this resource.');
165             }
166             };
167            
168             If assert_user_roles fails to authorize, this code ref is called.
169            
170             =head2 roles
171            
172             # Mojolicious::Lite
173             plugin SimpleAuthorization => {roles => 'auth_roles'};
174            
175             # In your under or controller
176             $self->stash(auth_roles => {'user.delete' => 1, 'user.search' => 1});
177            
178             Name of stash value which holds all the roles for the current user. Must be a
179             C. Defaults to C.
180            
181             =head2 superuser
182            
183             # Mojolicious::Lite
184             plugin SimpleAuthorization => {superuser => 'administrator'};
185             plugin SimpleAuthorization => {
186             'superuser' => sub {
187             my ($user, $roles) = @_;
188             return 1 if $user->{administrator};
189             }
190             };
191            
192             $self->check_user_roles([qw/some_random_role/]); # returns 1
193             $self->check_user_roles([qw/crazy_role/]); # returns 1
194            
195             Adds the possibility of a superuser - a user that can assume every role.
196            
197             The above two examples are the same. If the C key exists in the
198             C hash and it evaluates to true, the user will pass every role check. The
199             C CODE example performs an equivalent evaluation.
200            
201             =head2 user
202            
203             # Mojolicious::Lite
204             plugin SimpleAuthorization => {user => 'auth_user'};
205            
206             # In your under or controller
207             $self->stash(auth_user => {username => 'paul.williams', administrator => 0});
208            
209             Name of stash value which holds the user's information. Must be a C.
210             Defaults to C.
211            
212             =head1 METHODS
213            
214             L inherits all methods from
215             L and implements the following new ones.
216            
217             =head2 assert_user_roles
218            
219             Same as C, except it calls the hook C if
220             the user isn't authorized. Returns a boolean value.
221            
222             =head2 check_user_roles
223            
224             my $assert = $self->check_user_roles('user.create');
225             my $assert = $self->check_user_roles([qw/user.editor user.create/]);
226            
227             Checks the user and returns a boolean value.
228            
229             my $user_to_delete = 'admin';
230             my $assert = $self->check_user_roles(
231             ['user.delete'],
232             sub {
233             my ($user, $roles) = @_;
234             return 0 if $user_to_delete eq 'admin';
235             }
236             );
237            
238             Optionally pass a callback to apply your own one-off role check. Useful as in
239             the example above, where the user 'admin' cannot be deleted.
240            
241             If the callback returns a positive value, the user is authorized. 0 and the user
242             is not authorized, undef and the authorization chain continues.
243            
244             my $message = get_message_to_delete();
245             my $assert = $self->check_user_roles(
246             ['message.delete'],
247             sub {
248             my ($user, $roles) = @_;
249             $roles->{'message.delete'}++
250             if $message->{username} eq $user->{username};
251             return undef;
252             }
253             );
254            
255             This technique can also be used to give the user a role based on certain
256             criteria. In the example above, a user who cannot delete all messages, can
257             delete their own message.
258            
259             =head2 register
260            
261             $plugin->register(Mojolicious->new);
262            
263             Register plugin in L application.
264            
265             =head1 HOW TO CONTRIBUTE
266            
267             Contributions welcome, though this plugin is pretty basic. I currently only
268             accept GitHub pull requests.
269            
270             =over
271            
272             =item * GitHub: L
273            
274             =back
275            
276             =head1 COPYRIGHT AND LICENSE
277            
278             Copyright (C) 2014-2015, Paul Williams.
279            
280             This program is free software, you can redistribute it and/or modify it under
281             the terms of the Artistic License version 2.0.
282            
283             =head1 AUTHOR
284            
285             Paul Williams
286            
287             =head1 SEE ALSO
288            
289             L, L.
290            
291             =cut