Bug 18137: (QA-follow-up) Fix pod fail
[koha.git] / Koha / REST / V1 / Auth.pm
1 package Koha::REST::V1::Auth;
2
3 # Copyright Koha-Suomi Oy 2017
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 3 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 use Modern::Perl;
21
22 use Mojo::Base 'Mojolicious::Controller';
23
24 use C4::Auth qw( check_cookie_auth get_session haspermission );
25
26 use Koha::Account::Lines;
27 use Koha::Checkouts;
28 use Koha::Holds;
29 use Koha::Old::Checkouts;
30 use Koha::Patrons;
31
32 use Koha::Exceptions;
33 use Koha::Exceptions::Authentication;
34 use Koha::Exceptions::Authorization;
35
36 use Scalar::Util qw( blessed );
37 use Try::Tiny;
38
39 =head1 NAME
40
41 Koha::REST::V1::Auth
42
43 =head2 Operations
44
45 =head3 under
46
47 This subroutine is called before every request to API.
48
49 =cut
50
51 sub under {
52     my $c = shift->openapi->valid_input or return;;
53
54     my $status = 0;
55     try {
56
57         $status = authenticate_api_request($c);
58
59     } catch {
60         unless (blessed($_)) {
61             return $c->render(
62                 status => 500,
63                 json => { error => 'Something went wrong, check the logs.' }
64             );
65         }
66         if ($_->isa('Koha::Exceptions::UnderMaintenance')) {
67             return $c->render(status => 503, json => { error => $_->error });
68         }
69         elsif ($_->isa('Koha::Exceptions::Authentication::SessionExpired')) {
70             return $c->render(status => 401, json => { error => $_->error });
71         }
72         elsif ($_->isa('Koha::Exceptions::Authentication::Required')) {
73             return $c->render(status => 401, json => { error => $_->error });
74         }
75         elsif ($_->isa('Koha::Exceptions::Authentication')) {
76             return $c->render(status => 500, json => { error => $_->error });
77         }
78         elsif ($_->isa('Koha::Exceptions::BadParameter')) {
79             return $c->render(status => 400, json => $_->error );
80         }
81         elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) {
82             return $c->render(status => 403, json => {
83                 error => $_->error,
84                 required_permissions => $_->required_permissions,
85             });
86         }
87         elsif ($_->isa('Koha::Exceptions')) {
88             return $c->render(status => 500, json => { error => $_->error });
89         }
90         else {
91             return $c->render(
92                 status => 500,
93                 json => { error => 'Something went wrong, check the logs.' }
94             );
95         }
96     };
97
98     return $status;
99 }
100
101 =head3 authenticate_api_request
102
103 Validates authentication and allows access if authorization is not required or
104 if authorization is required and user has required permissions to access.
105
106 =cut
107
108 sub authenticate_api_request {
109     my ( $c ) = @_;
110
111     my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
112     my $authorization = $spec->{'x-koha-authorization'};
113     my $cookie = $c->cookie('CGISESSID');
114     my ($session, $user);
115     # Mojo doesn't use %ENV the way CGI apps do
116     # Manually pass the remote_address to check_auth_cookie
117     my $remote_addr = $c->tx->remote_address;
118     my ($status, $sessionID) = check_cookie_auth(
119                                             $cookie, undef,
120                                             { remote_addr => $remote_addr });
121     if ($status eq "ok") {
122         $session = get_session($sessionID);
123         $user = Koha::Patrons->find($session->param('number'));
124         $c->stash('koha.user' => $user);
125     }
126     elsif ($status eq "maintenance") {
127         Koha::Exceptions::UnderMaintenance->throw(
128             error => 'System is under maintenance.'
129         );
130     }
131     elsif ($status eq "expired" and $authorization) {
132         Koha::Exceptions::Authentication::SessionExpired->throw(
133             error => 'Session has been expired.'
134         );
135     }
136     elsif ($status eq "failed" and $authorization) {
137         Koha::Exceptions::Authentication::Required->throw(
138             error => 'Authentication failure.'
139         );
140     }
141     elsif ($authorization) {
142         Koha::Exceptions::Authentication->throw(
143             error => 'Unexpected authentication status.'
144         );
145     }
146
147     # We do not need any authorization
148     unless ($authorization) {
149         # Check the parameters
150         validate_query_parameters( $c, $spec );
151         return 1;
152     }
153
154     my $permissions = $authorization->{'permissions'};
155     # Check if the user is authorized
156     if ( haspermission($user->userid, $permissions)
157         or allow_owner($c, $authorization, $user)
158         or allow_guarantor($c, $authorization, $user) ) {
159
160         validate_query_parameters( $c, $spec );
161
162         # Everything is ok
163         return 1;
164     }
165
166     Koha::Exceptions::Authorization::Unauthorized->throw(
167         error => "Authorization failure. Missing required permission(s).",
168         required_permissions => $permissions,
169     );
170 }
171 sub validate_query_parameters {
172     my ( $c, $action_spec ) = @_;
173
174     # Check for malformed query parameters
175     my @errors;
176     my %valid_parameters = map { ( $_->{in} eq 'query' ) ? ( $_->{name} => 1 ) : () } @{ $action_spec->{parameters} };
177     my $existing_params = $c->req->query_params->to_hash;
178     for my $param ( keys %{$existing_params} ) {
179         push @errors, { path => "/query/" . $param, message => 'Malformed query string' } unless exists $valid_parameters{$param};
180     }
181
182     Koha::Exceptions::BadParameter->throw(
183         error => \@errors
184     ) if @errors;
185 }
186
187
188 =head3 allow_owner
189
190 Allows access to object for its owner.
191
192 There are endpoints that should allow access for the object owner even if they
193 do not have the required permission, e.g. access an own reserve. This can be
194 achieved by defining the operation as follows:
195
196 "/holds/{reserve_id}": {
197     "get": {
198         ...,
199         "x-koha-authorization": {
200             "allow-owner": true,
201             "permissions": {
202                 "borrowers": "1"
203             }
204         }
205     }
206 }
207
208 =cut
209
210 sub allow_owner {
211     my ($c, $authorization, $user) = @_;
212
213     return unless $authorization->{'allow-owner'};
214
215     return check_object_ownership($c, $user) if $user and $c;
216 }
217
218 =head3 allow_guarantor
219
220 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
221 guarantees.
222
223 =cut
224
225 sub allow_guarantor {
226     my ($c, $authorization, $user) = @_;
227
228     if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
229         return;
230     }
231
232     my $guarantees = $user->guarantees->as_list;
233     foreach my $guarantee (@{$guarantees}) {
234         return 1 if check_object_ownership($c, $guarantee);
235     }
236 }
237
238 =head3 check_object_ownership
239
240 Determines ownership of an object from request parameters.
241
242 As introducing an endpoint that allows access for object's owner; if the
243 parameter that will be used to determine ownership is not already inside
244 $parameters, add a new subroutine that checks the ownership and extend
245 $parameters to contain a key with parameter_name and a value of a subref to
246 the subroutine that you created.
247
248 =cut
249
250 sub check_object_ownership {
251     my ($c, $user) = @_;
252
253     return if not $c or not $user;
254
255     my $parameters = {
256         accountlines_id => \&_object_ownership_by_accountlines_id,
257         borrowernumber  => \&_object_ownership_by_borrowernumber,
258         checkout_id     => \&_object_ownership_by_checkout_id,
259         reserve_id      => \&_object_ownership_by_reserve_id,
260     };
261
262     foreach my $param ( keys %{ $parameters } ) {
263         my $check_ownership = $parameters->{$param};
264         if ($c->stash($param)) {
265             return &$check_ownership($c, $user, $c->stash($param));
266         }
267         elsif ($c->param($param)) {
268             return &$check_ownership($c, $user, $c->param($param));
269         }
270         elsif ($c->match->stack->[-1]->{$param}) {
271             return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param});
272         }
273         elsif ($c->req->json && $c->req->json->{$param}) {
274             return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
275         }
276     }
277 }
278
279 =head3 _object_ownership_by_accountlines_id
280
281 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
282 belongs to C<$user>.
283
284 =cut
285
286 sub _object_ownership_by_accountlines_id {
287     my ($c, $user, $accountlines_id) = @_;
288
289     my $accountline = Koha::Account::Lines->find($accountlines_id);
290     return $accountline && $user->borrowernumber == $accountline->borrowernumber;
291 }
292
293 =head3 _object_ownership_by_borrowernumber
294
295 Compares C<$borrowernumber> to currently logged in C<$user>.
296
297 =cut
298
299 sub _object_ownership_by_borrowernumber {
300     my ($c, $user, $borrowernumber) = @_;
301
302     return $user->borrowernumber == $borrowernumber;
303 }
304
305 =head3 _object_ownership_by_checkout_id
306
307 First, attempts to find a Koha::Checkout-object by C<$issue_id>. If we find one,
308 compare its borrowernumber to currently logged in C<$user>. However, if an issue
309 is not found, attempt to find a Koha::Old::Checkout-object instead and compare its
310 borrowernumber to currently logged in C<$user>.
311
312 =cut
313
314 sub _object_ownership_by_checkout_id {
315     my ($c, $user, $issue_id) = @_;
316
317     my $issue = Koha::Checkouts->find($issue_id);
318     $issue = Koha::Old::Checkouts->find($issue_id) unless $issue;
319     return $issue && $issue->borrowernumber
320             && $user->borrowernumber == $issue->borrowernumber;
321 }
322
323 =head3 _object_ownership_by_reserve_id
324
325 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
326 belongs to C<$user>.
327
328 TODO: Also compare against old_reserves
329
330 =cut
331
332 sub _object_ownership_by_reserve_id {
333     my ($c, $user, $reserve_id) = @_;
334
335     my $reserve = Koha::Holds->find($reserve_id);
336     return $reserve && $user->borrowernumber == $reserve->borrowernumber;
337 }
338
339 1;