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.
58 my $c = shift->openapi->valid_input or return;;
65 my $namespace = $c->req->url->to_abs->path->[2];
67 if ( $namespace eq 'public'
68 and !C4::Context->preference('RESTPublicAPI') )
70 Koha::Exceptions::Authorization->throw(
71 "Configuration prevents the usage of this endpoint by unprivileged users");
74 $status = authenticate_api_request($c);
77 unless (blessed($_)) {
80 json => { error => 'Something went wrong, check the logs.' }
83 if ($_->isa('Koha::Exceptions::UnderMaintenance')) {
84 return $c->render(status => 503, json => { error => $_->error });
86 elsif ($_->isa('Koha::Exceptions::Authentication::SessionExpired')) {
87 return $c->render(status => 401, json => { error => $_->error });
89 elsif ($_->isa('Koha::Exceptions::Authentication::Required')) {
90 return $c->render(status => 401, json => { error => $_->error });
92 elsif ($_->isa('Koha::Exceptions::Authentication')) {
93 return $c->render(status => 401, json => { error => $_->error });
95 elsif ($_->isa('Koha::Exceptions::BadParameter')) {
96 return $c->render(status => 400, json => $_->error );
98 elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) {
99 return $c->render(status => 403, json => {
101 required_permissions => $_->required_permissions,
104 elsif ($_->isa('Koha::Exceptions::Authorization')) {
105 return $c->render(status => 403, json => { error => $_->error });
107 elsif ($_->isa('Koha::Exceptions')) {
108 return $c->render(status => 500, json => { error => $_->error });
113 json => { error => 'Something went wrong, check the logs.' }
121 =head3 authenticate_api_request
123 Validates authentication and allows access if authorization is not required or
124 if authorization is required and user has required permissions to access.
128 sub authenticate_api_request {
133 my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
134 my $authorization = $spec->{'x-koha-authorization'};
136 my $authorization_header = $c->req->headers->authorization;
138 if ($authorization_header and $authorization_header =~ /^Bearer /) {
139 # attempt to use OAuth2 authentication
140 if ( ! Module::Load::Conditional::can_load(
141 modules => {'Net::OAuth2::AuthorizationServer' => undef} )) {
142 Koha::Exceptions::Authorization::Unauthorized->throw(
143 error => 'Authentication failure.'
147 require Net::OAuth2::AuthorizationServer;
150 my $server = Net::OAuth2::AuthorizationServer->new;
151 my $grant = $server->client_credentials_grant(Koha::OAuth::config);
152 my ($type, $token) = split / /, $authorization_header;
153 my ($valid_token, $error) = $grant->verify_access_token(
154 access_token => $token,
158 my $patron_id = Koha::ApiKeys->find( $valid_token->{client_id} )->patron_id;
159 $user = Koha::Patrons->find($patron_id);
160 C4::Context->interface('api');
163 # If we have "Authorization: Bearer" header and oauth authentication
164 # failed, do not try other authentication means
165 Koha::Exceptions::Authentication::Required->throw(
166 error => 'Authentication failure.'
170 elsif ( $authorization_header and $authorization_header =~ /^Basic / ) {
171 unless ( C4::Context->preference('RESTBasicAuth') ) {
172 Koha::Exceptions::Authentication::Required->throw(
173 error => 'Basic authentication disabled'
176 $user = $c->_basic_auth( $authorization_header );
177 C4::Context->interface('api');
179 # If we have "Authorization: Basic" header and authentication
180 # failed, do not try other authentication means
181 Koha::Exceptions::Authentication::Required->throw(
182 error => 'Authentication failure.'
188 my $cookie = $c->cookie('CGISESSID');
190 # Mojo doesn't use %ENV the way CGI apps do
191 # Manually pass the remote_address to check_auth_cookie
192 my $remote_addr = $c->tx->remote_address;
193 my ($status, $sessionID) = check_cookie_auth(
195 { remote_addr => $remote_addr });
196 if ($status eq "ok") {
197 my $session = get_session($sessionID);
198 $user = Koha::Patrons->find($session->param('number'));
199 # $c->stash('koha.user' => $user);
201 elsif ($status eq "maintenance") {
202 Koha::Exceptions::UnderMaintenance->throw(
203 error => 'System is under maintenance.'
206 elsif ($status eq "expired" and $authorization) {
207 Koha::Exceptions::Authentication::SessionExpired->throw(
208 error => 'Session has been expired.'
211 elsif ($status eq "failed" and $authorization) {
212 Koha::Exceptions::Authentication::Required->throw(
213 error => 'Authentication failure.'
216 elsif ($authorization) {
217 Koha::Exceptions::Authentication->throw(
218 error => 'Unexpected authentication status.'
223 $c->stash('koha.user' => $user);
225 # We do not need any authorization
226 unless ($authorization) {
227 # Check the parameters
228 validate_query_parameters( $c, $spec );
232 my $permissions = $authorization->{'permissions'};
233 # Check if the user is authorized
234 if ( ( defined($permissions) and haspermission($user->userid, $permissions) )
235 or allow_owner($c, $authorization, $user)
236 or allow_guarantor($c, $authorization, $user) ) {
238 validate_query_parameters( $c, $spec );
244 Koha::Exceptions::Authorization::Unauthorized->throw(
245 error => "Authorization failure. Missing required permission(s).",
246 required_permissions => $permissions,
250 =head3 validate_query_parameters
252 Validates the query parameters against the spec.
256 sub validate_query_parameters {
257 my ( $c, $action_spec ) = @_;
259 # Check for malformed query parameters
261 my %valid_parameters = map { ( $_->{in} eq 'query' ) ? ( $_->{name} => 1 ) : () } @{ $action_spec->{parameters} };
262 my $existing_params = $c->req->query_params->to_hash;
263 for my $param ( keys %{$existing_params} ) {
264 push @errors, { path => "/query/" . $param, message => 'Malformed query string' } unless exists $valid_parameters{$param};
267 Koha::Exceptions::BadParameter->throw(
275 Allows access to object for its owner.
277 There are endpoints that should allow access for the object owner even if they
278 do not have the required permission, e.g. access an own reserve. This can be
279 achieved by defining the operation as follows:
281 "/holds/{reserve_id}": {
284 "x-koha-authorization": {
296 my ($c, $authorization, $user) = @_;
298 return unless $authorization->{'allow-owner'};
300 return check_object_ownership($c, $user) if $user and $c;
303 =head3 allow_guarantor
305 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
310 sub allow_guarantor {
311 my ($c, $authorization, $user) = @_;
313 if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
317 my $guarantees = $user->guarantees->as_list;
318 foreach my $guarantee (@{$guarantees}) {
319 return 1 if check_object_ownership($c, $guarantee);
323 =head3 check_object_ownership
325 Determines ownership of an object from request parameters.
327 As introducing an endpoint that allows access for object's owner; if the
328 parameter that will be used to determine ownership is not already inside
329 $parameters, add a new subroutine that checks the ownership and extend
330 $parameters to contain a key with parameter_name and a value of a subref to
331 the subroutine that you created.
335 sub check_object_ownership {
338 return if not $c or not $user;
341 accountlines_id => \&_object_ownership_by_accountlines_id,
342 borrowernumber => \&_object_ownership_by_patron_id,
343 patron_id => \&_object_ownership_by_patron_id,
344 checkout_id => \&_object_ownership_by_checkout_id,
345 reserve_id => \&_object_ownership_by_reserve_id,
348 foreach my $param ( keys %{ $parameters } ) {
349 my $check_ownership = $parameters->{$param};
350 if ($c->stash($param)) {
351 return &$check_ownership($c, $user, $c->stash($param));
353 elsif ($c->param($param)) {
354 return &$check_ownership($c, $user, $c->param($param));
356 elsif ($c->match->stack->[-1]->{$param}) {
357 return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param});
359 elsif ($c->req->json && $c->req->json->{$param}) {
360 return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
365 =head3 _object_ownership_by_accountlines_id
367 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
372 sub _object_ownership_by_accountlines_id {
373 my ($c, $user, $accountlines_id) = @_;
375 my $accountline = Koha::Account::Lines->find($accountlines_id);
376 return $accountline && $user->borrowernumber == $accountline->borrowernumber;
379 =head3 _object_ownership_by_borrowernumber
381 Compares C<$borrowernumber> to currently logged in C<$user>.
385 sub _object_ownership_by_patron_id {
386 my ($c, $user, $patron_id) = @_;
388 return $user->borrowernumber == $patron_id;
391 =head3 _object_ownership_by_checkout_id
393 First, attempts to find a Koha::Checkout-object by C<$issue_id>. If we find one,
394 compare its borrowernumber to currently logged in C<$user>. However, if an issue
395 is not found, attempt to find a Koha::Old::Checkout-object instead and compare its
396 borrowernumber to currently logged in C<$user>.
400 sub _object_ownership_by_checkout_id {
401 my ($c, $user, $issue_id) = @_;
403 my $issue = Koha::Checkouts->find($issue_id);
404 $issue = Koha::Old::Checkouts->find($issue_id) unless $issue;
405 return $issue && $issue->borrowernumber
406 && $user->borrowernumber == $issue->borrowernumber;
409 =head3 _object_ownership_by_reserve_id
411 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
414 TODO: Also compare against old_reserves
418 sub _object_ownership_by_reserve_id {
419 my ($c, $user, $reserve_id) = @_;
421 my $reserve = Koha::Holds->find($reserve_id);
422 return $reserve && $user->borrowernumber == $reserve->borrowernumber;
427 Internal method that performs Basic authentication.
432 my ( $c, $authorization_header ) = @_;
434 my ( $type, $credentials ) = split / /, $authorization_header;
436 unless ($credentials) {
437 Koha::Exceptions::Authentication::Required->throw( error => 'Authentication failure.' );
440 my $decoded_credentials = decode_base64( $credentials );
441 my ( $user_id, $password ) = split( /:/, $decoded_credentials, 2 );
443 my $dbh = C4::Context->dbh;
444 unless ( checkpw_internal($dbh, $user_id, $password ) ) {
445 Koha::Exceptions::Authorization::Unauthorized->throw( error => 'Invalid password' );
448 return Koha::Patrons->find({ userid => $user_id });