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 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;
40 use Module::Load::Conditional;
41 use Scalar::Util qw( blessed );
52 This subroutine is called before every request to API.
57 my $c = shift->openapi->valid_input or return;;
62 $status = authenticate_api_request($c);
65 unless (blessed($_)) {
68 json => { error => 'Something went wrong, check the logs.' }
71 if ($_->isa('Koha::Exceptions::UnderMaintenance')) {
72 return $c->render(status => 503, json => { error => $_->error });
74 elsif ($_->isa('Koha::Exceptions::Authentication::SessionExpired')) {
75 return $c->render(status => 401, json => { error => $_->error });
77 elsif ($_->isa('Koha::Exceptions::Authentication::Required')) {
78 return $c->render(status => 401, json => { error => $_->error });
80 elsif ($_->isa('Koha::Exceptions::Authentication')) {
81 return $c->render(status => 500, json => { error => $_->error });
83 elsif ($_->isa('Koha::Exceptions::BadParameter')) {
84 return $c->render(status => 400, json => $_->error );
86 elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) {
87 return $c->render(status => 403, json => {
89 required_permissions => $_->required_permissions,
92 elsif ($_->isa('Koha::Exceptions')) {
93 return $c->render(status => 500, json => { error => $_->error });
98 json => { error => 'Something went wrong, check the logs.' }
106 =head3 authenticate_api_request
108 Validates authentication and allows access if authorization is not required or
109 if authorization is required and user has required permissions to access.
113 sub authenticate_api_request {
116 my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
117 my $authorization = $spec->{'x-koha-authorization'};
119 my $authorization_header = $c->req->headers->authorization;
121 if ($authorization_header and $authorization_header =~ /^Bearer /) {
122 # attempt to use OAuth2 authentication
123 if ( ! Module::Load::Conditional::can_load('Net::OAuth2::AuthorizationServer') ) {
124 Koha::Exceptions::Authorization::Unauthorized->throw(
125 error => 'Authentication failure.'
129 require Net::OAuth2::AuthorizationServer;
132 my $server = Net::OAuth2::AuthorizationServer->new;
133 my $grant = $server->client_credentials_grant(Koha::OAuth::config);
134 my ($type, $token) = split / /, $authorization_header;
135 my ($valid_token, $error) = $grant->verify_access_token(
136 access_token => $token,
140 my $patron_id = Koha::ApiKeys->find( $valid_token->{client_id} )->patron_id;
141 my $patron = Koha::Patrons->find($patron_id);
142 my $permissions = $authorization->{'permissions'};
143 # Check if the patron is authorized
144 if ( haspermission($patron->userid, $permissions)
145 or allow_owner($c, $authorization, $patron)
146 or allow_guarantor($c, $authorization, $patron) ) {
148 validate_query_parameters( $c, $spec );
154 Koha::Exceptions::Authorization::Unauthorized->throw(
155 error => "Authorization failure. Missing required permission(s).",
156 required_permissions => $permissions,
160 # If we have "Authorization: Bearer" header and oauth authentication
161 # failed, do not try other authentication means
162 Koha::Exceptions::Authentication::Required->throw(
163 error => 'Authentication failure.'
167 my $cookie = $c->cookie('CGISESSID');
168 my ($session, $user);
169 # Mojo doesn't use %ENV the way CGI apps do
170 # Manually pass the remote_address to check_auth_cookie
171 my $remote_addr = $c->tx->remote_address;
172 my ($status, $sessionID) = check_cookie_auth(
174 { remote_addr => $remote_addr });
175 if ($status eq "ok") {
176 $session = get_session($sessionID);
177 $user = Koha::Patrons->find($session->param('number'));
178 $c->stash('koha.user' => $user);
180 elsif ($status eq "maintenance") {
181 Koha::Exceptions::UnderMaintenance->throw(
182 error => 'System is under maintenance.'
185 elsif ($status eq "expired" and $authorization) {
186 Koha::Exceptions::Authentication::SessionExpired->throw(
187 error => 'Session has been expired.'
190 elsif ($status eq "failed" and $authorization) {
191 Koha::Exceptions::Authentication::Required->throw(
192 error => 'Authentication failure.'
195 elsif ($authorization) {
196 Koha::Exceptions::Authentication->throw(
197 error => 'Unexpected authentication status.'
201 # We do not need any authorization
202 unless ($authorization) {
203 # Check the parameters
204 validate_query_parameters( $c, $spec );
208 my $permissions = $authorization->{'permissions'};
209 # Check if the user is authorized
210 if ( haspermission($user->userid, $permissions)
211 or allow_owner($c, $authorization, $user)
212 or allow_guarantor($c, $authorization, $user) ) {
214 validate_query_parameters( $c, $spec );
220 Koha::Exceptions::Authorization::Unauthorized->throw(
221 error => "Authorization failure. Missing required permission(s).",
222 required_permissions => $permissions,
225 sub validate_query_parameters {
226 my ( $c, $action_spec ) = @_;
228 # Check for malformed query parameters
230 my %valid_parameters = map { ( $_->{in} eq 'query' ) ? ( $_->{name} => 1 ) : () } @{ $action_spec->{parameters} };
231 my $existing_params = $c->req->query_params->to_hash;
232 for my $param ( keys %{$existing_params} ) {
233 push @errors, { path => "/query/" . $param, message => 'Malformed query string' } unless exists $valid_parameters{$param};
236 Koha::Exceptions::BadParameter->throw(
244 Allows access to object for its owner.
246 There are endpoints that should allow access for the object owner even if they
247 do not have the required permission, e.g. access an own reserve. This can be
248 achieved by defining the operation as follows:
250 "/holds/{reserve_id}": {
253 "x-koha-authorization": {
265 my ($c, $authorization, $user) = @_;
267 return unless $authorization->{'allow-owner'};
269 return check_object_ownership($c, $user) if $user and $c;
272 =head3 allow_guarantor
274 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
279 sub allow_guarantor {
280 my ($c, $authorization, $user) = @_;
282 if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
286 my $guarantees = $user->guarantees->as_list;
287 foreach my $guarantee (@{$guarantees}) {
288 return 1 if check_object_ownership($c, $guarantee);
292 =head3 check_object_ownership
294 Determines ownership of an object from request parameters.
296 As introducing an endpoint that allows access for object's owner; if the
297 parameter that will be used to determine ownership is not already inside
298 $parameters, add a new subroutine that checks the ownership and extend
299 $parameters to contain a key with parameter_name and a value of a subref to
300 the subroutine that you created.
304 sub check_object_ownership {
307 return if not $c or not $user;
310 accountlines_id => \&_object_ownership_by_accountlines_id,
311 borrowernumber => \&_object_ownership_by_patron_id,
312 patron_id => \&_object_ownership_by_patron_id,
313 checkout_id => \&_object_ownership_by_checkout_id,
314 reserve_id => \&_object_ownership_by_reserve_id,
317 foreach my $param ( keys %{ $parameters } ) {
318 my $check_ownership = $parameters->{$param};
319 if ($c->stash($param)) {
320 return &$check_ownership($c, $user, $c->stash($param));
322 elsif ($c->param($param)) {
323 return &$check_ownership($c, $user, $c->param($param));
325 elsif ($c->match->stack->[-1]->{$param}) {
326 return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param});
328 elsif ($c->req->json && $c->req->json->{$param}) {
329 return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
334 =head3 _object_ownership_by_accountlines_id
336 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
341 sub _object_ownership_by_accountlines_id {
342 my ($c, $user, $accountlines_id) = @_;
344 my $accountline = Koha::Account::Lines->find($accountlines_id);
345 return $accountline && $user->borrowernumber == $accountline->borrowernumber;
348 =head3 _object_ownership_by_borrowernumber
350 Compares C<$borrowernumber> to currently logged in C<$user>.
354 sub _object_ownership_by_patron_id {
355 my ($c, $user, $patron_id) = @_;
357 return $user->borrowernumber == $patron_id;
360 =head3 _object_ownership_by_checkout_id
362 First, attempts to find a Koha::Checkout-object by C<$issue_id>. If we find one,
363 compare its borrowernumber to currently logged in C<$user>. However, if an issue
364 is not found, attempt to find a Koha::Old::Checkout-object instead and compare its
365 borrowernumber to currently logged in C<$user>.
369 sub _object_ownership_by_checkout_id {
370 my ($c, $user, $issue_id) = @_;
372 my $issue = Koha::Checkouts->find($issue_id);
373 $issue = Koha::Old::Checkouts->find($issue_id) unless $issue;
374 return $issue && $issue->borrowernumber
375 && $user->borrowernumber == $issue->borrowernumber;
378 =head3 _object_ownership_by_reserve_id
380 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
383 TODO: Also compare against old_reserves
387 sub _object_ownership_by_reserve_id {
388 my ($c, $user, $reserve_id) = @_;
390 my $reserve = Koha::Holds->find($reserve_id);
391 return $reserve && $user->borrowernumber == $reserve->borrowernumber;