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