Bug 25327: Do not call authenticate_api_request to render the spec
[koha.git] / Koha / REST / V1 / Auth.pm
blobca26530f619a6b93dd22a5c2f8388a132252cc81
1 package Koha::REST::V1::Auth;
3 # Copyright Koha-Suomi Oy 2017
5 # This file is part of Koha.
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.
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.
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.
20 use Modern::Perl;
22 use Mojo::Base 'Mojolicious::Controller';
24 use C4::Auth qw( check_cookie_auth checkpw_internal get_session haspermission );
25 use C4::Context;
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;
36 use Koha::Exceptions;
37 use Koha::Exceptions::Authentication;
38 use Koha::Exceptions::Authorization;
40 use MIME::Base64;
41 use Module::Load::Conditional;
42 use Scalar::Util qw( blessed );
43 use Try::Tiny;
45 =head1 NAME
47 Koha::REST::V1::Auth
49 =head2 Operations
51 =head3 under
53 This subroutine is called before every request to API.
55 =cut
57 sub under {
58 my ( $c ) = @_;
60 my $status = 0;
62 try {
64 # /api/v1/{namespace}
65 my $namespace = $c->req->url->to_abs->path->[2] // '';
67 my $is_public = 0; # By default routes are not public
68 my $is_plugin = 0;
70 if ( $namespace eq 'public' ) {
71 $is_public = 1;
72 } elsif ( $namespace eq 'contrib' ) {
73 $is_plugin = 1;
76 if ( $is_public
77 and !C4::Context->preference('RESTPublicAPI') )
79 Koha::Exceptions::Authorization->throw(
80 "Configuration prevents the usage of this endpoint by unprivileged users");
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;
87 elsif ( $namespace eq '' or $namespace eq '.html' ) {
88 $status = 1;
90 else {
91 $status = authenticate_api_request($c, { is_public => $is_public, is_plugin => $is_plugin });
94 } catch {
95 unless (blessed($_)) {
96 return $c->render(
97 status => 500,
98 json => { error => 'Something went wrong, check the logs.' }
101 if ($_->isa('Koha::Exceptions::UnderMaintenance')) {
102 return $c->render(status => 503, json => { error => $_->error });
104 elsif ($_->isa('Koha::Exceptions::Authentication::SessionExpired')) {
105 return $c->render(status => 401, json => { error => $_->error });
107 elsif ($_->isa('Koha::Exceptions::Authentication::Required')) {
108 return $c->render(status => 401, json => { error => $_->error });
110 elsif ($_->isa('Koha::Exceptions::Authentication')) {
111 return $c->render(status => 401, json => { error => $_->error });
113 elsif ($_->isa('Koha::Exceptions::BadParameter')) {
114 return $c->render(status => 400, json => $_->error );
116 elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) {
117 return $c->render(status => 403, json => {
118 error => $_->error,
119 required_permissions => $_->required_permissions,
122 elsif ($_->isa('Koha::Exceptions::Authorization')) {
123 return $c->render(status => 403, json => { error => $_->error });
125 elsif ($_->isa('Koha::Exceptions')) {
126 return $c->render(status => 500, json => { error => $_->error });
128 else {
129 return $c->render(
130 status => 500,
131 json => { error => 'Something went wrong, check the logs.' }
136 return $status;
139 =head3 authenticate_api_request
141 Validates authentication and allows access if authorization is not required or
142 if authorization is required and user has required permissions to access.
144 =cut
146 sub authenticate_api_request {
147 my ( $c, $params ) = @_;
149 my $user;
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'};
156 my $cookie_auth = 0;
158 my $authorization = $spec->{'x-koha-authorization'};
160 my $authorization_header = $c->req->headers->authorization;
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.'
170 else {
171 require Net::OAuth2::AuthorizationServer;
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,
181 if ($valid_token) {
182 my $patron_id = Koha::ApiKeys->find( $valid_token->{client_id} )->patron_id;
183 $user = Koha::Patrons->find($patron_id);
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.'
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'
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.'
208 else {
210 my $cookie = $c->cookie('CGISESSID');
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;
225 elsif ($status eq "maintenance") {
226 Koha::Exceptions::UnderMaintenance->throw(
227 error => 'System is under maintenance.'
230 elsif ($status eq "expired" and $authorization) {
231 Koha::Exceptions::Authentication::SessionExpired->throw(
232 error => 'Session has been expired.'
235 elsif ($status eq "failed" and $authorization) {
236 Koha::Exceptions::Authentication::Required->throw(
237 error => 'Authentication failure.'
240 elsif ($authorization) {
241 Koha::Exceptions::Authentication->throw(
242 error => 'Unexpected authentication status.'
247 $c->stash('koha.user' => $user);
248 C4::Context->interface('api');
250 if ( $user and !$cookie_auth ) { # cookie-auth sets this and more, don't mess with that
251 $c->_set_userenv( $user );
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;
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;
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) ) {
278 validate_query_parameters( $c, $spec );
280 # Everything is ok
281 return 1;
284 Koha::Exceptions::Authorization::Unauthorized->throw(
285 error => "Authorization failure. Missing required permission(s).",
286 required_permissions => $permissions,
290 =head3 validate_query_parameters
292 Validates the query parameters against the spec.
294 =cut
296 sub validate_query_parameters {
297 my ( $c, $action_spec ) = @_;
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};
307 Koha::Exceptions::BadParameter->throw(
308 error => \@errors
309 ) if @errors;
313 =head3 allow_owner
315 Allows access to object for its owner.
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:
321 "/holds/{reserve_id}": {
322 "get": {
323 ...,
324 "x-koha-authorization": {
325 "allow-owner": true,
326 "permissions": {
327 "borrowers": "1"
333 =cut
335 sub allow_owner {
336 my ($c, $authorization, $user) = @_;
338 return unless $authorization->{'allow-owner'};
340 return check_object_ownership($c, $user) if $user and $c;
343 =head3 allow_guarantor
345 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
346 guarantees.
348 =cut
350 sub allow_guarantor {
351 my ($c, $authorization, $user) = @_;
353 if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
354 return;
357 my $guarantees = $user->guarantee_relationships->guarantees->as_list;
358 foreach my $guarantee (@{$guarantees}) {
359 return 1 if check_object_ownership($c, $guarantee);
363 =head3 check_object_ownership
365 Determines ownership of an object from request parameters.
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.
373 =cut
375 sub check_object_ownership {
376 my ($c, $user) = @_;
378 return if not $c or not $user;
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,
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));
393 elsif ($c->param($param)) {
394 return &$check_ownership($c, $user, $c->param($param));
396 elsif ($c->match->stack->[-1]->{$param}) {
397 return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param});
399 elsif ($c->req->json && $c->req->json->{$param}) {
400 return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
405 =head3 _object_ownership_by_accountlines_id
407 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
408 belongs to C<$user>.
410 =cut
412 sub _object_ownership_by_accountlines_id {
413 my ($c, $user, $accountlines_id) = @_;
415 my $accountline = Koha::Account::Lines->find($accountlines_id);
416 return $accountline && $user->borrowernumber == $accountline->borrowernumber;
419 =head3 _object_ownership_by_borrowernumber
421 Compares C<$borrowernumber> to currently logged in C<$user>.
423 =cut
425 sub _object_ownership_by_patron_id {
426 my ($c, $user, $patron_id) = @_;
428 return $user->borrowernumber == $patron_id;
431 =head3 _object_ownership_by_checkout_id
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>.
438 =cut
440 sub _object_ownership_by_checkout_id {
441 my ($c, $user, $issue_id) = @_;
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;
449 =head3 _object_ownership_by_reserve_id
451 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
452 belongs to C<$user>.
454 TODO: Also compare against old_reserves
456 =cut
458 sub _object_ownership_by_reserve_id {
459 my ($c, $user, $reserve_id) = @_;
461 my $reserve = Koha::Holds->find($reserve_id);
462 return $reserve && $user->borrowernumber == $reserve->borrowernumber;
465 =head3 _basic_auth
467 Internal method that performs Basic authentication.
469 =cut
471 sub _basic_auth {
472 my ( $c, $authorization_header ) = @_;
474 my ( $type, $credentials ) = split / /, $authorization_header;
476 unless ($credentials) {
477 Koha::Exceptions::Authentication::Required->throw( error => 'Authentication failure.' );
480 my $decoded_credentials = decode_base64( $credentials );
481 my ( $user_id, $password ) = split( /:/, $decoded_credentials, 2 );
483 my $dbh = C4::Context->dbh;
484 unless ( checkpw_internal($dbh, $user_id, $password ) ) {
485 Koha::Exceptions::Authorization::Unauthorized->throw( error => 'Invalid password' );
488 return Koha::Patrons->find({ userid => $user_id });
491 =head3 _set_userenv
493 $c->_set_userenv( $patron );
495 Internal method that sets C4::Context->userenv
497 =cut
499 sub _set_userenv {
500 my ( $c, $patron ) = @_;
502 my $library = $patron->library;
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
517 return $c;