Bug 20402: Don't look at cookies if OAuth2 is attempted and has failed
[koha-ffzg.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 use C4::Context;
26
27 use Koha::Account::Lines;
28 use Koha::Checkouts;
29 use Koha::Holds;
30 use Koha::OAuth;
31 use Koha::Old::Checkouts;
32 use Koha::Patrons;
33
34 use Koha::Exceptions;
35 use Koha::Exceptions::Authentication;
36 use Koha::Exceptions::Authorization;
37
38 use Scalar::Util qw( blessed );
39 use Try::Tiny;
40
41 =head1 NAME
42
43 Koha::REST::V1::Auth
44
45 =head2 Operations
46
47 =head3 under
48
49 This subroutine is called before every request to API.
50
51 =cut
52
53 sub under {
54     my $c = shift->openapi->valid_input or return;;
55
56     my $status = 0;
57     try {
58
59         $status = authenticate_api_request($c);
60
61     } catch {
62         unless (blessed($_)) {
63             return $c->render(
64                 status => 500,
65                 json => { error => 'Something went wrong, check the logs.' }
66             );
67         }
68         if ($_->isa('Koha::Exceptions::UnderMaintenance')) {
69             return $c->render(status => 503, json => { error => $_->error });
70         }
71         elsif ($_->isa('Koha::Exceptions::Authentication::SessionExpired')) {
72             return $c->render(status => 401, json => { error => $_->error });
73         }
74         elsif ($_->isa('Koha::Exceptions::Authentication::Required')) {
75             return $c->render(status => 401, json => { error => $_->error });
76         }
77         elsif ($_->isa('Koha::Exceptions::Authentication')) {
78             return $c->render(status => 500, json => { error => $_->error });
79         }
80         elsif ($_->isa('Koha::Exceptions::BadParameter')) {
81             return $c->render(status => 400, json => $_->error );
82         }
83         elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) {
84             return $c->render(status => 403, json => {
85                 error => $_->error,
86                 required_permissions => $_->required_permissions,
87             });
88         }
89         elsif ($_->isa('Koha::Exceptions')) {
90             return $c->render(status => 500, json => { error => $_->error });
91         }
92         else {
93             return $c->render(
94                 status => 500,
95                 json => { error => 'Something went wrong, check the logs.' }
96             );
97         }
98     };
99
100     return $status;
101 }
102
103 =head3 authenticate_api_request
104
105 Validates authentication and allows access if authorization is not required or
106 if authorization is required and user has required permissions to access.
107
108 =cut
109
110 sub authenticate_api_request {
111     my ( $c ) = @_;
112
113     my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
114     my $authorization = $spec->{'x-koha-authorization'};
115
116     my $authorization_header = $c->req->headers->authorization;
117     if ($authorization_header and $authorization_header =~ /^Bearer /) {
118         if (my $oauth = $c->oauth) {
119             my $clients = C4::Context->config('api_client');
120             $clients = [ $clients ] unless ref $clients eq 'ARRAY';
121             my ($client) = grep { $_->{client_id} eq $oauth->{client_id} } @$clients;
122
123             my $patron = Koha::Patrons->find($client->{patron_id});
124             my $permissions = $authorization->{'permissions'};
125             # Check if the patron is authorized
126             if ( haspermission($patron->userid, $permissions)
127                 or allow_owner($c, $authorization, $patron)
128                 or allow_guarantor($c, $authorization, $patron) ) {
129
130                 validate_query_parameters( $c, $spec );
131
132                 # Everything is ok
133                 return 1;
134             }
135
136             Koha::Exceptions::Authorization::Unauthorized->throw(
137                 error => "Authorization failure. Missing required permission(s).",
138                 required_permissions => $permissions,
139             );
140         }
141
142         # If we have "Authorization: Bearer" header and oauth authentication
143         # failed, do not try other authentication means
144         Koha::Exceptions::Authentication::Required->throw(
145             error => 'Authentication failure.'
146         );
147     }
148
149     my $cookie = $c->cookie('CGISESSID');
150     my ($session, $user);
151     # Mojo doesn't use %ENV the way CGI apps do
152     # Manually pass the remote_address to check_auth_cookie
153     my $remote_addr = $c->tx->remote_address;
154     my ($status, $sessionID) = check_cookie_auth(
155                                             $cookie, undef,
156                                             { remote_addr => $remote_addr });
157     if ($status eq "ok") {
158         $session = get_session($sessionID);
159         $user = Koha::Patrons->find($session->param('number'));
160         $c->stash('koha.user' => $user);
161     }
162     elsif ($status eq "maintenance") {
163         Koha::Exceptions::UnderMaintenance->throw(
164             error => 'System is under maintenance.'
165         );
166     }
167     elsif ($status eq "expired" and $authorization) {
168         Koha::Exceptions::Authentication::SessionExpired->throw(
169             error => 'Session has been expired.'
170         );
171     }
172     elsif ($status eq "failed" and $authorization) {
173         Koha::Exceptions::Authentication::Required->throw(
174             error => 'Authentication failure.'
175         );
176     }
177     elsif ($authorization) {
178         Koha::Exceptions::Authentication->throw(
179             error => 'Unexpected authentication status.'
180         );
181     }
182
183     # We do not need any authorization
184     unless ($authorization) {
185         # Check the parameters
186         validate_query_parameters( $c, $spec );
187         return 1;
188     }
189
190     my $permissions = $authorization->{'permissions'};
191     # Check if the user is authorized
192     if ( haspermission($user->userid, $permissions)
193         or allow_owner($c, $authorization, $user)
194         or allow_guarantor($c, $authorization, $user) ) {
195
196         validate_query_parameters( $c, $spec );
197
198         # Everything is ok
199         return 1;
200     }
201
202     Koha::Exceptions::Authorization::Unauthorized->throw(
203         error => "Authorization failure. Missing required permission(s).",
204         required_permissions => $permissions,
205     );
206 }
207 sub validate_query_parameters {
208     my ( $c, $action_spec ) = @_;
209
210     # Check for malformed query parameters
211     my @errors;
212     my %valid_parameters = map { ( $_->{in} eq 'query' ) ? ( $_->{name} => 1 ) : () } @{ $action_spec->{parameters} };
213     my $existing_params = $c->req->query_params->to_hash;
214     for my $param ( keys %{$existing_params} ) {
215         push @errors, { path => "/query/" . $param, message => 'Malformed query string' } unless exists $valid_parameters{$param};
216     }
217
218     Koha::Exceptions::BadParameter->throw(
219         error => \@errors
220     ) if @errors;
221 }
222
223
224 =head3 allow_owner
225
226 Allows access to object for its owner.
227
228 There are endpoints that should allow access for the object owner even if they
229 do not have the required permission, e.g. access an own reserve. This can be
230 achieved by defining the operation as follows:
231
232 "/holds/{reserve_id}": {
233     "get": {
234         ...,
235         "x-koha-authorization": {
236             "allow-owner": true,
237             "permissions": {
238                 "borrowers": "1"
239             }
240         }
241     }
242 }
243
244 =cut
245
246 sub allow_owner {
247     my ($c, $authorization, $user) = @_;
248
249     return unless $authorization->{'allow-owner'};
250
251     return check_object_ownership($c, $user) if $user and $c;
252 }
253
254 =head3 allow_guarantor
255
256 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
257 guarantees.
258
259 =cut
260
261 sub allow_guarantor {
262     my ($c, $authorization, $user) = @_;
263
264     if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
265         return;
266     }
267
268     my $guarantees = $user->guarantees->as_list;
269     foreach my $guarantee (@{$guarantees}) {
270         return 1 if check_object_ownership($c, $guarantee);
271     }
272 }
273
274 =head3 check_object_ownership
275
276 Determines ownership of an object from request parameters.
277
278 As introducing an endpoint that allows access for object's owner; if the
279 parameter that will be used to determine ownership is not already inside
280 $parameters, add a new subroutine that checks the ownership and extend
281 $parameters to contain a key with parameter_name and a value of a subref to
282 the subroutine that you created.
283
284 =cut
285
286 sub check_object_ownership {
287     my ($c, $user) = @_;
288
289     return if not $c or not $user;
290
291     my $parameters = {
292         accountlines_id => \&_object_ownership_by_accountlines_id,
293         borrowernumber  => \&_object_ownership_by_patron_id,
294         patron_id       => \&_object_ownership_by_patron_id,
295         checkout_id     => \&_object_ownership_by_checkout_id,
296         reserve_id      => \&_object_ownership_by_reserve_id,
297     };
298
299     foreach my $param ( keys %{ $parameters } ) {
300         my $check_ownership = $parameters->{$param};
301         if ($c->stash($param)) {
302             return &$check_ownership($c, $user, $c->stash($param));
303         }
304         elsif ($c->param($param)) {
305             return &$check_ownership($c, $user, $c->param($param));
306         }
307         elsif ($c->match->stack->[-1]->{$param}) {
308             return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param});
309         }
310         elsif ($c->req->json && $c->req->json->{$param}) {
311             return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
312         }
313     }
314 }
315
316 =head3 _object_ownership_by_accountlines_id
317
318 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
319 belongs to C<$user>.
320
321 =cut
322
323 sub _object_ownership_by_accountlines_id {
324     my ($c, $user, $accountlines_id) = @_;
325
326     my $accountline = Koha::Account::Lines->find($accountlines_id);
327     return $accountline && $user->borrowernumber == $accountline->borrowernumber;
328 }
329
330 =head3 _object_ownership_by_borrowernumber
331
332 Compares C<$borrowernumber> to currently logged in C<$user>.
333
334 =cut
335
336 sub _object_ownership_by_patron_id {
337     my ($c, $user, $patron_id) = @_;
338
339     return $user->borrowernumber == $patron_id;
340 }
341
342 =head3 _object_ownership_by_checkout_id
343
344 First, attempts to find a Koha::Checkout-object by C<$issue_id>. If we find one,
345 compare its borrowernumber to currently logged in C<$user>. However, if an issue
346 is not found, attempt to find a Koha::Old::Checkout-object instead and compare its
347 borrowernumber to currently logged in C<$user>.
348
349 =cut
350
351 sub _object_ownership_by_checkout_id {
352     my ($c, $user, $issue_id) = @_;
353
354     my $issue = Koha::Checkouts->find($issue_id);
355     $issue = Koha::Old::Checkouts->find($issue_id) unless $issue;
356     return $issue && $issue->borrowernumber
357             && $user->borrowernumber == $issue->borrowernumber;
358 }
359
360 =head3 _object_ownership_by_reserve_id
361
362 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
363 belongs to C<$user>.
364
365 TODO: Also compare against old_reserves
366
367 =cut
368
369 sub _object_ownership_by_reserve_id {
370     my ($c, $user, $reserve_id) = @_;
371
372     my $reserve = Koha::Holds->find($reserve_id);
373     return $reserve && $user->borrowernumber == $reserve->borrowernumber;
374 }
375
376 1;