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
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.
22 use Mojo
::Base
'Mojolicious::Controller';
24 use C4
::Auth
qw( check_cookie_auth checkpw_internal get_session haspermission );
28 use Koha
::Account
::Lines
;
32 use Koha
::OAuthAccessTokens
;
33 use Koha
::Old
::Checkouts
;
37 use Koha
::Exceptions
::Authentication
;
38 use Koha
::Exceptions
::Authorization
;
41 use Module
::Load
::Conditional
;
42 use Scalar
::Util
qw( blessed );
53 This subroutine is called before every request to API.
65 my $namespace = $c->req->url->to_abs->path->[2] // '';
67 my $is_public = 0; # By default routes are not public
70 if ( $namespace eq 'public' ) {
72 } elsif ( $namespace eq 'contrib' ) {
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
87 elsif ( $namespace eq '' or $namespace eq '.html' ) {
91 $status = authenticate_api_request
($c, { is_public
=> $is_public, is_plugin
=> $is_plugin });
95 unless (blessed
($_)) {
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
=> {
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 });
131 json
=> { error
=> 'Something went wrong, check the logs.' }
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.
146 sub authenticate_api_request
{
147 my ( $c, $params ) = @_;
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'};
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.'
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,
182 my $patron_id = Koha
::ApiKeys
->find( $valid_token->{client_id
} )->patron_id;
183 $user = Koha
::Patrons
->find($patron_id);
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 );
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.'
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
(
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';
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 );
264 # We are required authorizarion, there needs
265 # to be an identified user
266 Koha
::Exceptions
::Authentication
::Required
->throw(
267 error
=> 'Authentication failure.' )
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 );
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.
296 sub validate_query_parameters
{
297 my ( $c, $action_spec ) = @_;
299 # Check for malformed query parameters
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(
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}": {
324 "x-koha-authorization": {
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
350 sub allow_guarantor
{
351 my ($c, $authorization, $user) = @_;
353 if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
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.
375 sub check_object_ownership
{
378 return if not $c or not $user;
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
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>.
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>.
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
454 TODO: Also compare against old_reserves
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;
467 Internal method that performs Basic authentication.
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 });
493 $c->_set_userenv( $patron );
495 Internal method that sets C4::Context->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