Bug 19784: Adapt /v1/patrons to new naming guidelines
[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_patron_id,
258         patron_id       => \&_object_ownership_by_patron_id,
259         checkout_id     => \&_object_ownership_by_checkout_id,
260         reserve_id      => \&_object_ownership_by_reserve_id,
261     };
262
263     foreach my $param ( keys %{ $parameters } ) {
264         my $check_ownership = $parameters->{$param};
265         if ($c->stash($param)) {
266             return &$check_ownership($c, $user, $c->stash($param));
267         }
268         elsif ($c->param($param)) {
269             return &$check_ownership($c, $user, $c->param($param));
270         }
271         elsif ($c->match->stack->[-1]->{$param}) {
272             return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param});
273         }
274         elsif ($c->req->json && $c->req->json->{$param}) {
275             return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
276         }
277     }
278 }
279
280 =head3 _object_ownership_by_accountlines_id
281
282 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
283 belongs to C<$user>.
284
285 =cut
286
287 sub _object_ownership_by_accountlines_id {
288     my ($c, $user, $accountlines_id) = @_;
289
290     my $accountline = Koha::Account::Lines->find($accountlines_id);
291     return $accountline && $user->borrowernumber == $accountline->borrowernumber;
292 }
293
294 =head3 _object_ownership_by_borrowernumber
295
296 Compares C<$borrowernumber> to currently logged in C<$user>.
297
298 =cut
299
300 sub _object_ownership_by_patron_id {
301     my ($c, $user, $patron_id) = @_;
302
303     return $user->borrowernumber == $patron_id;
304 }
305
306 =head3 _object_ownership_by_checkout_id
307
308 First, attempts to find a Koha::Checkout-object by C<$issue_id>. If we find one,
309 compare its borrowernumber to currently logged in C<$user>. However, if an issue
310 is not found, attempt to find a Koha::Old::Checkout-object instead and compare its
311 borrowernumber to currently logged in C<$user>.
312
313 =cut
314
315 sub _object_ownership_by_checkout_id {
316     my ($c, $user, $issue_id) = @_;
317
318     my $issue = Koha::Checkouts->find($issue_id);
319     $issue = Koha::Old::Checkouts->find($issue_id) unless $issue;
320     return $issue && $issue->borrowernumber
321             && $user->borrowernumber == $issue->borrowernumber;
322 }
323
324 =head3 _object_ownership_by_reserve_id
325
326 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
327 belongs to C<$user>.
328
329 TODO: Also compare against old_reserves
330
331 =cut
332
333 sub _object_ownership_by_reserve_id {
334     my ($c, $user, $reserve_id) = @_;
335
336     my $reserve = Koha::Holds->find($reserve_id);
337     return $reserve && $user->borrowernumber == $reserve->borrowernumber;
338 }
339
340 1;