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 {
118 my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
119 my $authorization = $spec->{'x-koha-authorization'};
121 my $authorization_header = $c->req->headers->authorization;
123 if ($authorization_header and $authorization_header =~ /^Bearer /) {
124 # attempt to use OAuth2 authentication
125 if ( ! Module::Load::Conditional::can_load(
126 modules => {'Net::OAuth2::AuthorizationServer' => undef} )) {
127 Koha::Exceptions::Authorization::Unauthorized->throw(
128 error => 'Authentication failure.'
132 require Net::OAuth2::AuthorizationServer;
135 my $server = Net::OAuth2::AuthorizationServer->new;
136 my $grant = $server->client_credentials_grant(Koha::OAuth::config);
137 my ($type, $token) = split / /, $authorization_header;
138 my ($valid_token, $error) = $grant->verify_access_token(
139 access_token => $token,
143 my $patron_id = Koha::ApiKeys->find( $valid_token->{client_id} )->patron_id;
144 $user = Koha::Patrons->find($patron_id);
147 # If we have "Authorization: Bearer" header and oauth authentication
148 # failed, do not try other authentication means
149 Koha::Exceptions::Authentication::Required->throw(
150 error => 'Authentication failure.'
156 my $cookie = $c->cookie('CGISESSID');
158 # Mojo doesn't use %ENV the way CGI apps do
159 # Manually pass the remote_address to check_auth_cookie
160 my $remote_addr = $c->tx->remote_address;
161 my ($status, $sessionID) = check_cookie_auth(
163 { remote_addr => $remote_addr });
164 if ($status eq "ok") {
165 my $session = get_session($sessionID);
166 $user = Koha::Patrons->find($session->param('number'));
167 # $c->stash('koha.user' => $user);
169 elsif ($status eq "maintenance") {
170 Koha::Exceptions::UnderMaintenance->throw(
171 error => 'System is under maintenance.'
174 elsif ($status eq "expired" and $authorization) {
175 Koha::Exceptions::Authentication::SessionExpired->throw(
176 error => 'Session has been expired.'
179 elsif ($status eq "failed" and $authorization) {
180 Koha::Exceptions::Authentication::Required->throw(
181 error => 'Authentication failure.'
184 elsif ($authorization) {
185 Koha::Exceptions::Authentication->throw(
186 error => 'Unexpected authentication status.'
191 $c->stash('koha.user' => $user);
193 # We do not need any authorization
194 unless ($authorization) {
195 # Check the parameters
196 validate_query_parameters( $c, $spec );
200 my $permissions = $authorization->{'permissions'};
201 # Check if the user is authorized
202 if ( haspermission($user->userid, $permissions)
203 or allow_owner($c, $authorization, $user)
204 or allow_guarantor($c, $authorization, $user) ) {
206 validate_query_parameters( $c, $spec );
212 Koha::Exceptions::Authorization::Unauthorized->throw(
213 error => "Authorization failure. Missing required permission(s).",
214 required_permissions => $permissions,
218 =head3 validate_query_parameters
220 Validates the query parameters against the spec.
224 sub validate_query_parameters {
225 my ( $c, $action_spec ) = @_;
227 # Check for malformed query parameters
229 my %valid_parameters = map { ( $_->{in} eq 'query' ) ? ( $_->{name} => 1 ) : () } @{ $action_spec->{parameters} };
230 my $existing_params = $c->req->query_params->to_hash;
231 for my $param ( keys %{$existing_params} ) {
232 push @errors, { path => "/query/" . $param, message => 'Malformed query string' } unless exists $valid_parameters{$param};
235 Koha::Exceptions::BadParameter->throw(
243 Allows access to object for its owner.
245 There are endpoints that should allow access for the object owner even if they
246 do not have the required permission, e.g. access an own reserve. This can be
247 achieved by defining the operation as follows:
249 "/holds/{reserve_id}": {
252 "x-koha-authorization": {
264 my ($c, $authorization, $user) = @_;
266 return unless $authorization->{'allow-owner'};
268 return check_object_ownership($c, $user) if $user and $c;
271 =head3 allow_guarantor
273 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
278 sub allow_guarantor {
279 my ($c, $authorization, $user) = @_;
281 if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
285 my $guarantees = $user->guarantees->as_list;
286 foreach my $guarantee (@{$guarantees}) {
287 return 1 if check_object_ownership($c, $guarantee);
291 =head3 check_object_ownership
293 Determines ownership of an object from request parameters.
295 As introducing an endpoint that allows access for object's owner; if the
296 parameter that will be used to determine ownership is not already inside
297 $parameters, add a new subroutine that checks the ownership and extend
298 $parameters to contain a key with parameter_name and a value of a subref to
299 the subroutine that you created.
303 sub check_object_ownership {
306 return if not $c or not $user;
309 accountlines_id => \&_object_ownership_by_accountlines_id,
310 borrowernumber => \&_object_ownership_by_patron_id,
311 patron_id => \&_object_ownership_by_patron_id,
312 checkout_id => \&_object_ownership_by_checkout_id,
313 reserve_id => \&_object_ownership_by_reserve_id,
316 foreach my $param ( keys %{ $parameters } ) {
317 my $check_ownership = $parameters->{$param};
318 if ($c->stash($param)) {
319 return &$check_ownership($c, $user, $c->stash($param));
321 elsif ($c->param($param)) {
322 return &$check_ownership($c, $user, $c->param($param));
324 elsif ($c->match->stack->[-1]->{$param}) {
325 return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param});
327 elsif ($c->req->json && $c->req->json->{$param}) {
328 return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
333 =head3 _object_ownership_by_accountlines_id
335 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
340 sub _object_ownership_by_accountlines_id {
341 my ($c, $user, $accountlines_id) = @_;
343 my $accountline = Koha::Account::Lines->find($accountlines_id);
344 return $accountline && $user->borrowernumber == $accountline->borrowernumber;
347 =head3 _object_ownership_by_borrowernumber
349 Compares C<$borrowernumber> to currently logged in C<$user>.
353 sub _object_ownership_by_patron_id {
354 my ($c, $user, $patron_id) = @_;
356 return $user->borrowernumber == $patron_id;
359 =head3 _object_ownership_by_checkout_id
361 First, attempts to find a Koha::Checkout-object by C<$issue_id>. If we find one,
362 compare its borrowernumber to currently logged in C<$user>. However, if an issue
363 is not found, attempt to find a Koha::Old::Checkout-object instead and compare its
364 borrowernumber to currently logged in C<$user>.
368 sub _object_ownership_by_checkout_id {
369 my ($c, $user, $issue_id) = @_;
371 my $issue = Koha::Checkouts->find($issue_id);
372 $issue = Koha::Old::Checkouts->find($issue_id) unless $issue;
373 return $issue && $issue->borrowernumber
374 && $user->borrowernumber == $issue->borrowernumber;
377 =head3 _object_ownership_by_reserve_id
379 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
382 TODO: Also compare against old_reserves
386 sub _object_ownership_by_reserve_id {
387 my ($c, $user, $reserve_id) = @_;
389 my $reserve = Koha::Holds->find($reserve_id);
390 return $reserve && $user->borrowernumber == $reserve->borrowernumber;