"Fossies" - the Fresh Open Source Software Archive 
Member "koha-19.11.15/Koha/REST/V1/Auth.pm" (23 Feb 2021, 16128 Bytes) of package /linux/misc/koha-19.11.15.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Perl source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file.
For more information about "Auth.pm" see the
Fossies "Dox" file reference documentation.
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 checkpw_internal 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 MIME::Base64;
41 use Module::Load::Conditional;
42 use Scalar::Util qw( blessed );
43 use Try::Tiny;
44
45 =head1 NAME
46
47 Koha::REST::V1::Auth
48
49 =head2 Operations
50
51 =head3 under
52
53 This subroutine is called before every request to API.
54
55 =cut
56
57 sub under {
58 my ( $c ) = @_;
59
60 my $status = 0;
61
62 try {
63
64 # /api/v1/{namespace}
65 my $namespace = $c->req->url->to_abs->path->[2] // '';
66
67 my $is_public = 0; # By default routes are not public
68 my $is_plugin = 0;
69
70 if ( $namespace eq 'public' ) {
71 $is_public = 1;
72 } elsif ( $namespace eq 'contrib' ) {
73 $is_plugin = 1;
74 }
75
76 if ( $is_public
77 and !C4::Context->preference('RESTPublicAPI') )
78 {
79 Koha::Exceptions::Authorization->throw(
80 "Configuration prevents the usage of this endpoint by unprivileged users");
81 }
82
83 if ( $c->req->url->to_abs->path eq '/api/v1/oauth/token' ) {
84 # Requesting a token shouldn't go through the API authenticaction chain
85 $status = 1;
86 }
87 elsif ( $namespace eq '' or $namespace eq '.html' ) {
88 $status = 1;
89 }
90 else {
91 $status = authenticate_api_request($c, { is_public => $is_public, is_plugin => $is_plugin });
92 }
93
94 } catch {
95 unless (blessed($_)) {
96 return $c->render(
97 status => 500,
98 json => { error => 'Something went wrong, check the logs.' }
99 );
100 }
101 if ($_->isa('Koha::Exceptions::UnderMaintenance')) {
102 return $c->render(status => 503, json => { error => $_->error });
103 }
104 elsif ($_->isa('Koha::Exceptions::Authentication::SessionExpired')) {
105 return $c->render(status => 401, json => { error => $_->error });
106 }
107 elsif ($_->isa('Koha::Exceptions::Authentication::Required')) {
108 return $c->render(status => 401, json => { error => $_->error });
109 }
110 elsif ($_->isa('Koha::Exceptions::Authentication')) {
111 return $c->render(status => 401, json => { error => $_->error });
112 }
113 elsif ($_->isa('Koha::Exceptions::BadParameter')) {
114 return $c->render(status => 400, json => $_->error );
115 }
116 elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) {
117 return $c->render(status => 403, json => {
118 error => $_->error,
119 required_permissions => $_->required_permissions,
120 });
121 }
122 elsif ($_->isa('Koha::Exceptions::Authorization')) {
123 return $c->render(status => 403, json => { error => $_->error });
124 }
125 elsif ($_->isa('Koha::Exceptions')) {
126 return $c->render(status => 500, json => { error => $_->error });
127 }
128 else {
129 return $c->render(
130 status => 500,
131 json => { error => 'Something went wrong, check the logs.' }
132 );
133 }
134 };
135
136 return $status;
137 }
138
139 =head3 authenticate_api_request
140
141 Validates authentication and allows access if authorization is not required or
142 if authorization is required and user has required permissions to access.
143
144 =cut
145
146 sub authenticate_api_request {
147 my ( $c, $params ) = @_;
148
149 my $user;
150
151 # The following supports retrieval of spec with Mojolicious::Plugin::OpenAPI@1.17 and later (first one)
152 # and older versions (second one).
153 # TODO: remove the latter 'openapi.op_spec' if minimum version is bumped to at least 1.17.
154 my $spec = $c->openapi->spec || $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
155
156 my $cookie_auth = 0;
157
158 my $authorization = $spec->{'x-koha-authorization'};
159
160 my $authorization_header = $c->req->headers->authorization;
161
162 if ($authorization_header and $authorization_header =~ /^Bearer /) {
163 # attempt to use OAuth2 authentication
164 if ( ! Module::Load::Conditional::can_load(
165 modules => {'Net::OAuth2::AuthorizationServer' => undef} )) {
166 Koha::Exceptions::Authorization::Unauthorized->throw(
167 error => 'Authentication failure.'
168 );
169 }
170 else {
171 require Net::OAuth2::AuthorizationServer;
172 }
173
174 my $server = Net::OAuth2::AuthorizationServer->new;
175 my $grant = $server->client_credentials_grant(Koha::OAuth::config);
176 my ($type, $token) = split / /, $authorization_header;
177 my ($valid_token, $error) = $grant->verify_access_token(
178 access_token => $token,
179 );
180
181 if ($valid_token) {
182 my $patron_id = Koha::ApiKeys->find( $valid_token->{client_id} )->patron_id;
183 $user = Koha::Patrons->find($patron_id);
184 }
185 else {
186 # If we have "Authorization: Bearer" header and oauth authentication
187 # failed, do not try other authentication means
188 Koha::Exceptions::Authentication::Required->throw(
189 error => 'Authentication failure.'
190 );
191 }
192 }
193 elsif ( $authorization_header and $authorization_header =~ /^Basic / ) {
194 unless ( C4::Context->preference('RESTBasicAuth') ) {
195 Koha::Exceptions::Authentication::Required->throw(
196 error => 'Basic authentication disabled'
197 );
198 }
199 $user = $c->_basic_auth( $authorization_header );
200 unless ( $user ) {
201 # If we have "Authorization: Basic" header and authentication
202 # failed, do not try other authentication means
203 Koha::Exceptions::Authentication::Required->throw(
204 error => 'Authentication failure.'
205 );
206 }
207 }
208 else {
209
210 my $cookie = $c->cookie('CGISESSID');
211
212 # Mojo doesn't use %ENV the way CGI apps do
213 # Manually pass the remote_address to check_auth_cookie
214 my $remote_addr = $c->tx->remote_address;
215 my ($status, $sessionID) = check_cookie_auth(
216 $cookie, undef,
217 { remote_addr => $remote_addr });
218 if ($status eq "ok") {
219 my $session = get_session($sessionID);
220 $user = Koha::Patrons->find( $session->param('number') )
221 unless $session->param('sessiontype')
222 and $session->param('sessiontype') eq 'anon';
223 $cookie_auth = 1;
224 }
225 elsif ($status eq "maintenance") {
226 Koha::Exceptions::UnderMaintenance->throw(
227 error => 'System is under maintenance.'
228 );
229 }
230 elsif ($status eq "expired" and $authorization) {
231 Koha::Exceptions::Authentication::SessionExpired->throw(
232 error => 'Session has been expired.'
233 );
234 }
235 elsif ($status eq "failed" and $authorization) {
236 Koha::Exceptions::Authentication::Required->throw(
237 error => 'Authentication failure.'
238 );
239 }
240 elsif ($authorization) {
241 Koha::Exceptions::Authentication->throw(
242 error => 'Unexpected authentication status.'
243 );
244 }
245 }
246
247 $c->stash('koha.user' => $user);
248 C4::Context->interface('api');
249
250 if ( $user and !$cookie_auth ) { # cookie-auth sets this and more, don't mess with that
251 $c->_set_userenv( $user );
252 }
253
254 if ( !$authorization and
255 ( $params->{is_public} and
256 ( C4::Context->preference('RESTPublicAnonymousRequests') or
257 $user) or $params->{is_plugin} ) ) {
258 # We do not need any authorization
259 # Check the parameters
260 validate_query_parameters( $c, $spec );
261 return 1;
262 }
263 else {
264 # We are required authorizarion, there needs
265 # to be an identified user
266 Koha::Exceptions::Authentication::Required->throw(
267 error => 'Authentication failure.' )
268 unless $user;
269 }
270
271
272 my $permissions = $authorization->{'permissions'};
273 # Check if the user is authorized
274 if ( ( defined($permissions) and haspermission($user->userid, $permissions) )
275 or allow_owner($c, $authorization, $user)
276 or allow_guarantor($c, $authorization, $user) ) {
277
278 validate_query_parameters( $c, $spec );
279
280 # Everything is ok
281 return 1;
282 }
283
284 Koha::Exceptions::Authorization::Unauthorized->throw(
285 error => "Authorization failure. Missing required permission(s).",
286 required_permissions => $permissions,
287 );
288 }
289
290 =head3 validate_query_parameters
291
292 Validates the query parameters against the spec.
293
294 =cut
295
296 sub validate_query_parameters {
297 my ( $c, $action_spec ) = @_;
298
299 # Check for malformed query parameters
300 my @errors;
301 my %valid_parameters = map { ( $_->{in} eq 'query' ) ? ( $_->{name} => 1 ) : () } @{ $action_spec->{parameters} };
302 my $existing_params = $c->req->query_params->to_hash;
303 for my $param ( keys %{$existing_params} ) {
304 push @errors, { path => "/query/" . $param, message => 'Malformed query string' } unless exists $valid_parameters{$param};
305 }
306
307 Koha::Exceptions::BadParameter->throw(
308 error => \@errors
309 ) if @errors;
310 }
311
312
313 =head3 allow_owner
314
315 Allows access to object for its owner.
316
317 There are endpoints that should allow access for the object owner even if they
318 do not have the required permission, e.g. access an own reserve. This can be
319 achieved by defining the operation as follows:
320
321 "/holds/{reserve_id}": {
322 "get": {
323 ...,
324 "x-koha-authorization": {
325 "allow-owner": true,
326 "permissions": {
327 "borrowers": "1"
328 }
329 }
330 }
331 }
332
333 =cut
334
335 sub allow_owner {
336 my ($c, $authorization, $user) = @_;
337
338 return unless $authorization->{'allow-owner'};
339
340 return check_object_ownership($c, $user) if $user and $c;
341 }
342
343 =head3 allow_guarantor
344
345 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
346 guarantees.
347
348 =cut
349
350 sub allow_guarantor {
351 my ($c, $authorization, $user) = @_;
352
353 if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
354 return;
355 }
356
357 my $guarantees = $user->guarantee_relationships->guarantees->as_list;
358 foreach my $guarantee (@{$guarantees}) {
359 return 1 if check_object_ownership($c, $guarantee);
360 }
361 }
362
363 =head3 check_object_ownership
364
365 Determines ownership of an object from request parameters.
366
367 As introducing an endpoint that allows access for object's owner; if the
368 parameter that will be used to determine ownership is not already inside
369 $parameters, add a new subroutine that checks the ownership and extend
370 $parameters to contain a key with parameter_name and a value of a subref to
371 the subroutine that you created.
372
373 =cut
374
375 sub check_object_ownership {
376 my ($c, $user) = @_;
377
378 return if not $c or not $user;
379
380 my $parameters = {
381 accountlines_id => \&_object_ownership_by_accountlines_id,
382 borrowernumber => \&_object_ownership_by_patron_id,
383 patron_id => \&_object_ownership_by_patron_id,
384 checkout_id => \&_object_ownership_by_checkout_id,
385 reserve_id => \&_object_ownership_by_reserve_id,
386 };
387
388 foreach my $param ( keys %{ $parameters } ) {
389 my $check_ownership = $parameters->{$param};
390 if ($c->stash($param)) {
391 return &$check_ownership($c, $user, $c->stash($param));
392 }
393 elsif ($c->param($param)) {
394 return &$check_ownership($c, $user, $c->param($param));
395 }
396 elsif ($c->match->stack->[-1]->{$param}) {
397 return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param});
398 }
399 elsif ($c->req->json && $c->req->json->{$param}) {
400 return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
401 }
402 }
403 }
404
405 =head3 _object_ownership_by_accountlines_id
406
407 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
408 belongs to C<$user>.
409
410 =cut
411
412 sub _object_ownership_by_accountlines_id {
413 my ($c, $user, $accountlines_id) = @_;
414
415 my $accountline = Koha::Account::Lines->find($accountlines_id);
416 return $accountline && $user->borrowernumber == $accountline->borrowernumber;
417 }
418
419 =head3 _object_ownership_by_borrowernumber
420
421 Compares C<$borrowernumber> to currently logged in C<$user>.
422
423 =cut
424
425 sub _object_ownership_by_patron_id {
426 my ($c, $user, $patron_id) = @_;
427
428 return $user->borrowernumber == $patron_id;
429 }
430
431 =head3 _object_ownership_by_checkout_id
432
433 First, attempts to find a Koha::Checkout-object by C<$issue_id>. If we find one,
434 compare its borrowernumber to currently logged in C<$user>. However, if an issue
435 is not found, attempt to find a Koha::Old::Checkout-object instead and compare its
436 borrowernumber to currently logged in C<$user>.
437
438 =cut
439
440 sub _object_ownership_by_checkout_id {
441 my ($c, $user, $issue_id) = @_;
442
443 my $issue = Koha::Checkouts->find($issue_id);
444 $issue = Koha::Old::Checkouts->find($issue_id) unless $issue;
445 return $issue && $issue->borrowernumber
446 && $user->borrowernumber == $issue->borrowernumber;
447 }
448
449 =head3 _object_ownership_by_reserve_id
450
451 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
452 belongs to C<$user>.
453
454 TODO: Also compare against old_reserves
455
456 =cut
457
458 sub _object_ownership_by_reserve_id {
459 my ($c, $user, $reserve_id) = @_;
460
461 my $reserve = Koha::Holds->find($reserve_id);
462 return $reserve && $user->borrowernumber == $reserve->borrowernumber;
463 }
464
465 =head3 _basic_auth
466
467 Internal method that performs Basic authentication.
468
469 =cut
470
471 sub _basic_auth {
472 my ( $c, $authorization_header ) = @_;
473
474 my ( $type, $credentials ) = split / /, $authorization_header;
475
476 unless ($credentials) {
477 Koha::Exceptions::Authentication::Required->throw( error => 'Authentication failure.' );
478 }
479
480 my $decoded_credentials = decode_base64( $credentials );
481 my ( $user_id, $password ) = split( /:/, $decoded_credentials, 2 );
482
483 my $dbh = C4::Context->dbh;
484 unless ( checkpw_internal($dbh, $user_id, $password ) ) {
485 Koha::Exceptions::Authorization::Unauthorized->throw( error => 'Invalid password' );
486 }
487
488 return Koha::Patrons->find({ userid => $user_id });
489 }
490
491 =head3 _set_userenv
492
493 $c->_set_userenv( $patron );
494
495 Internal method that sets C4::Context->userenv
496
497 =cut
498
499 sub _set_userenv {
500 my ( $c, $patron ) = @_;
501
502 my $library = $patron->library;
503
504 C4::Context->_new_userenv( $patron->borrowernumber );
505 C4::Context->set_userenv(
506 $patron->borrowernumber, # number,
507 $patron->userid, # userid,
508 $patron->cardnumber, # cardnumber
509 $patron->firstname, # firstname
510 $patron->surname, # surname
511 $library->branchcode, # branch
512 $library->branchname, # branchname
513 $patron->flags, # flags,
514 $patron->email, # emailaddress
515 );
516
517 return $c;
518 }
519
520 1;