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