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 sub validate_query_parameters {
219 my ( $c, $action_spec ) = @_;
221 # Check for malformed query parameters
223 my %valid_parameters = map { ( $_->{in} eq 'query' ) ? ( $_->{name} => 1 ) : () } @{ $action_spec->{parameters} };
224 my $existing_params = $c->req->query_params->to_hash;
225 for my $param ( keys %{$existing_params} ) {
226 push @errors, { path => "/query/" . $param, message => 'Malformed query string' } unless exists $valid_parameters{$param};
229 Koha::Exceptions::BadParameter->throw(
237 Allows access to object for its owner.
239 There are endpoints that should allow access for the object owner even if they
240 do not have the required permission, e.g. access an own reserve. This can be
241 achieved by defining the operation as follows:
243 "/holds/{reserve_id}": {
246 "x-koha-authorization": {
258 my ($c, $authorization, $user) = @_;
260 return unless $authorization->{'allow-owner'};
262 return check_object_ownership($c, $user) if $user and $c;
265 =head3 allow_guarantor
267 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
272 sub allow_guarantor {
273 my ($c, $authorization, $user) = @_;
275 if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
279 my $guarantees = $user->guarantees->as_list;
280 foreach my $guarantee (@{$guarantees}) {
281 return 1 if check_object_ownership($c, $guarantee);
285 =head3 check_object_ownership
287 Determines ownership of an object from request parameters.
289 As introducing an endpoint that allows access for object's owner; if the
290 parameter that will be used to determine ownership is not already inside
291 $parameters, add a new subroutine that checks the ownership and extend
292 $parameters to contain a key with parameter_name and a value of a subref to
293 the subroutine that you created.
297 sub check_object_ownership {
300 return if not $c or not $user;
303 accountlines_id => \&_object_ownership_by_accountlines_id,
304 borrowernumber => \&_object_ownership_by_patron_id,
305 patron_id => \&_object_ownership_by_patron_id,
306 checkout_id => \&_object_ownership_by_checkout_id,
307 reserve_id => \&_object_ownership_by_reserve_id,
310 foreach my $param ( keys %{ $parameters } ) {
311 my $check_ownership = $parameters->{$param};
312 if ($c->stash($param)) {
313 return &$check_ownership($c, $user, $c->stash($param));
315 elsif ($c->param($param)) {
316 return &$check_ownership($c, $user, $c->param($param));
318 elsif ($c->match->stack->[-1]->{$param}) {
319 return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param});
321 elsif ($c->req->json && $c->req->json->{$param}) {
322 return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
327 =head3 _object_ownership_by_accountlines_id
329 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
334 sub _object_ownership_by_accountlines_id {
335 my ($c, $user, $accountlines_id) = @_;
337 my $accountline = Koha::Account::Lines->find($accountlines_id);
338 return $accountline && $user->borrowernumber == $accountline->borrowernumber;
341 =head3 _object_ownership_by_borrowernumber
343 Compares C<$borrowernumber> to currently logged in C<$user>.
347 sub _object_ownership_by_patron_id {
348 my ($c, $user, $patron_id) = @_;
350 return $user->borrowernumber == $patron_id;
353 =head3 _object_ownership_by_checkout_id
355 First, attempts to find a Koha::Checkout-object by C<$issue_id>. If we find one,
356 compare its borrowernumber to currently logged in C<$user>. However, if an issue
357 is not found, attempt to find a Koha::Old::Checkout-object instead and compare its
358 borrowernumber to currently logged in C<$user>.
362 sub _object_ownership_by_checkout_id {
363 my ($c, $user, $issue_id) = @_;
365 my $issue = Koha::Checkouts->find($issue_id);
366 $issue = Koha::Old::Checkouts->find($issue_id) unless $issue;
367 return $issue && $issue->borrowernumber
368 && $user->borrowernumber == $issue->borrowernumber;
371 =head3 _object_ownership_by_reserve_id
373 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
376 TODO: Also compare against old_reserves
380 sub _object_ownership_by_reserve_id {
381 my ($c, $user, $reserve_id) = @_;
383 my $reserve = Koha::Holds->find($reserve_id);
384 return $reserve && $user->borrowernumber == $reserve->borrowernumber;