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 );
26 use Koha::Account::Lines;
29 use Koha::Old::Checkouts;
33 use Koha::Exceptions::Authentication;
34 use Koha::Exceptions::Authorization;
36 use Scalar::Util qw( blessed );
41 This subroutine is called before every request to API.
46 my $c = shift->openapi->valid_input or return;;
51 $status = authenticate_api_request($c);
54 unless (blessed($_)) {
57 json => { error => 'Something went wrong, check the logs.' }
60 if ($_->isa('Koha::Exceptions::UnderMaintenance')) {
61 return $c->render(status => 503, json => { error => $_->error });
63 elsif ($_->isa('Koha::Exceptions::Authentication::SessionExpired')) {
64 return $c->render(status => 401, json => { error => $_->error });
66 elsif ($_->isa('Koha::Exceptions::Authentication::Required')) {
67 return $c->render(status => 401, json => { error => $_->error });
69 elsif ($_->isa('Koha::Exceptions::Authentication')) {
70 return $c->render(status => 500, json => { error => $_->error });
72 elsif ($_->isa('Koha::Exceptions::BadParameter')) {
73 return $c->render(status => 400, json => $_->error );
75 elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) {
76 return $c->render(status => 403, json => {
78 required_permissions => $_->required_permissions,
81 elsif ($_->isa('Koha::Exceptions')) {
82 return $c->render(status => 500, json => { error => $_->error });
87 json => { error => 'Something went wrong, check the logs.' }
95 =head3 authenticate_api_request
97 Validates authentication and allows access if authorization is not required or
98 if authorization is required and user has required permissions to access.
102 sub authenticate_api_request {
105 my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
106 my $authorization = $spec->{'x-koha-authorization'};
107 my $cookie = $c->cookie('CGISESSID');
108 my ($session, $user);
109 # Mojo doesn't use %ENV the way CGI apps do
110 # Manually pass the remote_address to check_auth_cookie
111 my $remote_addr = $c->tx->remote_address;
112 my ($status, $sessionID) = check_cookie_auth(
114 { remote_addr => $remote_addr });
115 if ($status eq "ok") {
116 $session = get_session($sessionID);
117 $user = Koha::Patrons->find($session->param('number'));
118 $c->stash('koha.user' => $user);
120 elsif ($status eq "maintenance") {
121 Koha::Exceptions::UnderMaintenance->throw(
122 error => 'System is under maintenance.'
125 elsif ($status eq "expired" and $authorization) {
126 Koha::Exceptions::Authentication::SessionExpired->throw(
127 error => 'Session has been expired.'
130 elsif ($status eq "failed" and $authorization) {
131 Koha::Exceptions::Authentication::Required->throw(
132 error => 'Authentication failure.'
135 elsif ($authorization) {
136 Koha::Exceptions::Authentication->throw(
137 error => 'Unexpected authentication status.'
141 # We do not need any authorization
142 unless ($authorization) {
143 # Check the parameters
144 validate_query_parameters( $c, $spec );
148 my $permissions = $authorization->{'permissions'};
149 # Check if the user is authorized
150 if ( haspermission($user->userid, $permissions)
151 or allow_owner($c, $authorization, $user)
152 or allow_guarantor($c, $authorization, $user) ) {
154 validate_query_parameters( $c, $spec );
160 Koha::Exceptions::Authorization::Unauthorized->throw(
161 error => "Authorization failure. Missing required permission(s).",
162 required_permissions => $permissions,
165 sub validate_query_parameters {
166 my ( $c, $action_spec ) = @_;
168 # Check for malformed query parameters
170 my %valid_parameters = map { ( $_->{in} eq 'query' ) ? ( $_->{name} => 1 ) : () } @{ $action_spec->{parameters} };
171 my $existing_params = $c->req->query_params->to_hash;
172 for my $param ( keys %{$existing_params} ) {
173 push @errors, { path => "/query/" . $param, message => 'Malformed query string' } unless exists $valid_parameters{$param};
176 Koha::Exceptions::BadParameter->throw(
184 Allows access to object for its owner.
186 There are endpoints that should allow access for the object owner even if they
187 do not have the required permission, e.g. access an own reserve. This can be
188 achieved by defining the operation as follows:
190 "/holds/{reserve_id}": {
193 "x-koha-authorization": {
205 my ($c, $authorization, $user) = @_;
207 return unless $authorization->{'allow-owner'};
209 return check_object_ownership($c, $user) if $user and $c;
212 =head3 allow_guarantor
214 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
219 sub allow_guarantor {
220 my ($c, $authorization, $user) = @_;
222 if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
226 my $guarantees = $user->guarantees->as_list;
227 foreach my $guarantee (@{$guarantees}) {
228 return 1 if check_object_ownership($c, $guarantee);
232 =head3 check_object_ownership
234 Determines ownership of an object from request parameters.
236 As introducing an endpoint that allows access for object's owner; if the
237 parameter that will be used to determine ownership is not already inside
238 $parameters, add a new subroutine that checks the ownership and extend
239 $parameters to contain a key with parameter_name and a value of a subref to
240 the subroutine that you created.
244 sub check_object_ownership {
247 return if not $c or not $user;
250 accountlines_id => \&_object_ownership_by_accountlines_id,
251 borrowernumber => \&_object_ownership_by_borrowernumber,
252 checkout_id => \&_object_ownership_by_checkout_id,
253 reserve_id => \&_object_ownership_by_reserve_id,
256 foreach my $param ( keys %{ $parameters } ) {
257 my $check_ownership = $parameters->{$param};
258 if ($c->stash($param)) {
259 return &$check_ownership($c, $user, $c->stash($param));
261 elsif ($c->param($param)) {
262 return &$check_ownership($c, $user, $c->param($param));
264 elsif ($c->match->stack->[-1]->{$param}) {
265 return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param});
267 elsif ($c->req->json && $c->req->json->{$param}) {
268 return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
273 =head3 _object_ownership_by_accountlines_id
275 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
280 sub _object_ownership_by_accountlines_id {
281 my ($c, $user, $accountlines_id) = @_;
283 my $accountline = Koha::Account::Lines->find($accountlines_id);
284 return $accountline && $user->borrowernumber == $accountline->borrowernumber;
287 =head3 _object_ownership_by_borrowernumber
289 Compares C<$borrowernumber> to currently logged in C<$user>.
293 sub _object_ownership_by_borrowernumber {
294 my ($c, $user, $borrowernumber) = @_;
296 return $user->borrowernumber == $borrowernumber;
299 =head3 _object_ownership_by_checkout_id
301 First, attempts to find a Koha::Checkout-object by C<$issue_id>. If we find one,
302 compare its borrowernumber to currently logged in C<$user>. However, if an issue
303 is not found, attempt to find a Koha::Old::Checkout-object instead and compare its
304 borrowernumber to currently logged in C<$user>.
308 sub _object_ownership_by_checkout_id {
309 my ($c, $user, $issue_id) = @_;
311 my $issue = Koha::Checkouts->find($issue_id);
312 $issue = Koha::Old::Checkouts->find($issue_id) unless $issue;
313 return $issue && $issue->borrowernumber
314 && $user->borrowernumber == $issue->borrowernumber;
317 =head3 _object_ownership_by_reserve_id
319 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
322 TODO: Also compare against old_reserves
326 sub _object_ownership_by_reserve_id {
327 my ($c, $user, $reserve_id) = @_;
329 my $reserve = Koha::Holds->find($reserve_id);
330 return $reserve && $user->borrowernumber == $reserve->borrowernumber;