"Fossies" - the Fresh Open Source Software Archive

Member "koha-19.11.15/Koha/REST/V1/Auth.pm" (23 Feb 2021, 16128 Bytes) of package /linux/misc/koha-19.11.15.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Perl source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "Auth.pm" see the Fossies "Dox" file reference documentation.

    1 package Koha::REST::V1::Auth;
    2 
    3 # Copyright Koha-Suomi Oy 2017
    4 #
    5 # This file is part of Koha.
    6 #
    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
   10 # version.
   11 #
   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.
   15 #
   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.
   19 
   20 use Modern::Perl;
   21 
   22 use Mojo::Base 'Mojolicious::Controller';
   23 
   24 use C4::Auth qw( check_cookie_auth checkpw_internal get_session haspermission );
   25 use C4::Context;
   26 
   27 use Koha::ApiKeys;
   28 use Koha::Account::Lines;
   29 use Koha::Checkouts;
   30 use Koha::Holds;
   31 use Koha::OAuth;
   32 use Koha::OAuthAccessTokens;
   33 use Koha::Old::Checkouts;
   34 use Koha::Patrons;
   35 
   36 use Koha::Exceptions;
   37 use Koha::Exceptions::Authentication;
   38 use Koha::Exceptions::Authorization;
   39 
   40 use MIME::Base64;
   41 use Module::Load::Conditional;
   42 use Scalar::Util qw( blessed );
   43 use Try::Tiny;
   44 
   45 =head1 NAME
   46 
   47 Koha::REST::V1::Auth
   48 
   49 =head2 Operations
   50 
   51 =head3 under
   52 
   53 This subroutine is called before every request to API.
   54 
   55 =cut
   56 
   57 sub under {
   58     my ( $c ) = @_;
   59 
   60     my $status = 0;
   61 
   62     try {
   63 
   64         # /api/v1/{namespace}
   65         my $namespace = $c->req->url->to_abs->path->[2] // '';
   66 
   67         my $is_public = 0; # By default routes are not public
   68         my $is_plugin = 0;
   69 
   70         if ( $namespace eq 'public' ) {
   71             $is_public = 1;
   72         } elsif ( $namespace eq 'contrib' ) {
   73             $is_plugin = 1;
   74         }
   75 
   76         if ( $is_public
   77             and !C4::Context->preference('RESTPublicAPI') )
   78         {
   79             Koha::Exceptions::Authorization->throw(
   80                 "Configuration prevents the usage of this endpoint by unprivileged users");
   81         }
   82 
   83         if ( $c->req->url->to_abs->path eq '/api/v1/oauth/token' ) {
   84             # Requesting a token shouldn't go through the API authenticaction chain
   85             $status = 1;
   86         }
   87         elsif ( $namespace eq '' or $namespace eq '.html' ) {
   88             $status = 1;
   89         }
   90         else {
   91             $status = authenticate_api_request($c, { is_public => $is_public, is_plugin => $is_plugin });
   92         }
   93 
   94     } catch {
   95         unless (blessed($_)) {
   96             return $c->render(
   97                 status => 500,
   98                 json => { error => 'Something went wrong, check the logs.' }
   99             );
  100         }
  101         if ($_->isa('Koha::Exceptions::UnderMaintenance')) {
  102             return $c->render(status => 503, json => { error => $_->error });
  103         }
  104         elsif ($_->isa('Koha::Exceptions::Authentication::SessionExpired')) {
  105             return $c->render(status => 401, json => { error => $_->error });
  106         }
  107         elsif ($_->isa('Koha::Exceptions::Authentication::Required')) {
  108             return $c->render(status => 401, json => { error => $_->error });
  109         }
  110         elsif ($_->isa('Koha::Exceptions::Authentication')) {
  111             return $c->render(status => 401, json => { error => $_->error });
  112         }
  113         elsif ($_->isa('Koha::Exceptions::BadParameter')) {
  114             return $c->render(status => 400, json => $_->error );
  115         }
  116         elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) {
  117             return $c->render(status => 403, json => {
  118                 error => $_->error,
  119                 required_permissions => $_->required_permissions,
  120             });
  121         }
  122         elsif ($_->isa('Koha::Exceptions::Authorization')) {
  123             return $c->render(status => 403, json => { error => $_->error });
  124         }
  125         elsif ($_->isa('Koha::Exceptions')) {
  126             return $c->render(status => 500, json => { error => $_->error });
  127         }
  128         else {
  129             return $c->render(
  130                 status => 500,
  131                 json => { error => 'Something went wrong, check the logs.' }
  132             );
  133         }
  134     };
  135 
  136     return $status;
  137 }
  138 
  139 =head3 authenticate_api_request
  140 
  141 Validates authentication and allows access if authorization is not required or
  142 if authorization is required and user has required permissions to access.
  143 
  144 =cut
  145 
  146 sub authenticate_api_request {
  147     my ( $c, $params ) = @_;
  148 
  149     my $user;
  150 
  151     # The following supports retrieval of spec with Mojolicious::Plugin::OpenAPI@1.17 and later (first one)
  152     # and older versions (second one).
  153     # TODO: remove the latter 'openapi.op_spec' if minimum version is bumped to at least 1.17.
  154     my $spec = $c->openapi->spec || $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
  155 
  156     my $cookie_auth = 0;
  157 
  158     my $authorization = $spec->{'x-koha-authorization'};
  159 
  160     my $authorization_header = $c->req->headers->authorization;
  161 
  162     if ($authorization_header and $authorization_header =~ /^Bearer /) {
  163         # attempt to use OAuth2 authentication
  164         if ( ! Module::Load::Conditional::can_load(
  165                     modules => {'Net::OAuth2::AuthorizationServer' => undef} )) {
  166             Koha::Exceptions::Authorization::Unauthorized->throw(
  167                 error => 'Authentication failure.'
  168             );
  169         }
  170         else {
  171             require Net::OAuth2::AuthorizationServer;
  172         }
  173 
  174         my $server = Net::OAuth2::AuthorizationServer->new;
  175         my $grant = $server->client_credentials_grant(Koha::OAuth::config);
  176         my ($type, $token) = split / /, $authorization_header;
  177         my ($valid_token, $error) = $grant->verify_access_token(
  178             access_token => $token,
  179         );
  180 
  181         if ($valid_token) {
  182             my $patron_id = Koha::ApiKeys->find( $valid_token->{client_id} )->patron_id;
  183             $user         = Koha::Patrons->find($patron_id);
  184         }
  185         else {
  186             # If we have "Authorization: Bearer" header and oauth authentication
  187             # failed, do not try other authentication means
  188             Koha::Exceptions::Authentication::Required->throw(
  189                 error => 'Authentication failure.'
  190             );
  191         }
  192     }
  193     elsif ( $authorization_header and $authorization_header =~ /^Basic / ) {
  194         unless ( C4::Context->preference('RESTBasicAuth') ) {
  195             Koha::Exceptions::Authentication::Required->throw(
  196                 error => 'Basic authentication disabled'
  197             );
  198         }
  199         $user = $c->_basic_auth( $authorization_header );
  200         unless ( $user ) {
  201             # If we have "Authorization: Basic" header and authentication
  202             # failed, do not try other authentication means
  203             Koha::Exceptions::Authentication::Required->throw(
  204                 error => 'Authentication failure.'
  205             );
  206         }
  207     }
  208     else {
  209 
  210         my $cookie = $c->cookie('CGISESSID');
  211 
  212         # Mojo doesn't use %ENV the way CGI apps do
  213         # Manually pass the remote_address to check_auth_cookie
  214         my $remote_addr = $c->tx->remote_address;
  215         my ($status, $sessionID) = check_cookie_auth(
  216                                                 $cookie, undef,
  217                                                 { remote_addr => $remote_addr });
  218         if ($status eq "ok") {
  219             my $session = get_session($sessionID);
  220             $user = Koha::Patrons->find( $session->param('number') )
  221               unless $session->param('sessiontype')
  222                  and $session->param('sessiontype') eq 'anon';
  223             $cookie_auth = 1;
  224         }
  225         elsif ($status eq "maintenance") {
  226             Koha::Exceptions::UnderMaintenance->throw(
  227                 error => 'System is under maintenance.'
  228             );
  229         }
  230         elsif ($status eq "expired" and $authorization) {
  231             Koha::Exceptions::Authentication::SessionExpired->throw(
  232                 error => 'Session has been expired.'
  233             );
  234         }
  235         elsif ($status eq "failed" and $authorization) {
  236             Koha::Exceptions::Authentication::Required->throw(
  237                 error => 'Authentication failure.'
  238             );
  239         }
  240         elsif ($authorization) {
  241             Koha::Exceptions::Authentication->throw(
  242                 error => 'Unexpected authentication status.'
  243             );
  244         }
  245     }
  246 
  247     $c->stash('koha.user' => $user);
  248     C4::Context->interface('api');
  249 
  250     if ( $user and !$cookie_auth ) { # cookie-auth sets this and more, don't mess with that
  251         $c->_set_userenv( $user );
  252     }
  253 
  254     if ( !$authorization and
  255          ( $params->{is_public} and
  256           ( C4::Context->preference('RESTPublicAnonymousRequests') or
  257             $user) or $params->{is_plugin} ) ) {
  258         # We do not need any authorization
  259         # Check the parameters
  260         validate_query_parameters( $c, $spec );
  261         return 1;
  262     }
  263     else {
  264         # We are required authorizarion, there needs
  265         # to be an identified user
  266         Koha::Exceptions::Authentication::Required->throw(
  267             error => 'Authentication failure.' )
  268           unless $user;
  269     }
  270 
  271 
  272     my $permissions = $authorization->{'permissions'};
  273     # Check if the user is authorized
  274     if ( ( defined($permissions) and haspermission($user->userid, $permissions) )
  275         or allow_owner($c, $authorization, $user)
  276         or allow_guarantor($c, $authorization, $user) ) {
  277 
  278         validate_query_parameters( $c, $spec );
  279 
  280         # Everything is ok
  281         return 1;
  282     }
  283 
  284     Koha::Exceptions::Authorization::Unauthorized->throw(
  285         error => "Authorization failure. Missing required permission(s).",
  286         required_permissions => $permissions,
  287     );
  288 }
  289 
  290 =head3 validate_query_parameters
  291 
  292 Validates the query parameters against the spec.
  293 
  294 =cut
  295 
  296 sub validate_query_parameters {
  297     my ( $c, $action_spec ) = @_;
  298 
  299     # Check for malformed query parameters
  300     my @errors;
  301     my %valid_parameters = map { ( $_->{in} eq 'query' ) ? ( $_->{name} => 1 ) : () } @{ $action_spec->{parameters} };
  302     my $existing_params = $c->req->query_params->to_hash;
  303     for my $param ( keys %{$existing_params} ) {
  304         push @errors, { path => "/query/" . $param, message => 'Malformed query string' } unless exists $valid_parameters{$param};
  305     }
  306 
  307     Koha::Exceptions::BadParameter->throw(
  308         error => \@errors
  309     ) if @errors;
  310 }
  311 
  312 
  313 =head3 allow_owner
  314 
  315 Allows access to object for its owner.
  316 
  317 There are endpoints that should allow access for the object owner even if they
  318 do not have the required permission, e.g. access an own reserve. This can be
  319 achieved by defining the operation as follows:
  320 
  321 "/holds/{reserve_id}": {
  322     "get": {
  323         ...,
  324         "x-koha-authorization": {
  325             "allow-owner": true,
  326             "permissions": {
  327                 "borrowers": "1"
  328             }
  329         }
  330     }
  331 }
  332 
  333 =cut
  334 
  335 sub allow_owner {
  336     my ($c, $authorization, $user) = @_;
  337 
  338     return unless $authorization->{'allow-owner'};
  339 
  340     return check_object_ownership($c, $user) if $user and $c;
  341 }
  342 
  343 =head3 allow_guarantor
  344 
  345 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
  346 guarantees.
  347 
  348 =cut
  349 
  350 sub allow_guarantor {
  351     my ($c, $authorization, $user) = @_;
  352 
  353     if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
  354         return;
  355     }
  356 
  357     my $guarantees = $user->guarantee_relationships->guarantees->as_list;
  358     foreach my $guarantee (@{$guarantees}) {
  359         return 1 if check_object_ownership($c, $guarantee);
  360     }
  361 }
  362 
  363 =head3 check_object_ownership
  364 
  365 Determines ownership of an object from request parameters.
  366 
  367 As introducing an endpoint that allows access for object's owner; if the
  368 parameter that will be used to determine ownership is not already inside
  369 $parameters, add a new subroutine that checks the ownership and extend
  370 $parameters to contain a key with parameter_name and a value of a subref to
  371 the subroutine that you created.
  372 
  373 =cut
  374 
  375 sub check_object_ownership {
  376     my ($c, $user) = @_;
  377 
  378     return if not $c or not $user;
  379 
  380     my $parameters = {
  381         accountlines_id => \&_object_ownership_by_accountlines_id,
  382         borrowernumber  => \&_object_ownership_by_patron_id,
  383         patron_id       => \&_object_ownership_by_patron_id,
  384         checkout_id     => \&_object_ownership_by_checkout_id,
  385         reserve_id      => \&_object_ownership_by_reserve_id,
  386     };
  387 
  388     foreach my $param ( keys %{ $parameters } ) {
  389         my $check_ownership = $parameters->{$param};
  390         if ($c->stash($param)) {
  391             return &$check_ownership($c, $user, $c->stash($param));
  392         }
  393         elsif ($c->param($param)) {
  394             return &$check_ownership($c, $user, $c->param($param));
  395         }
  396         elsif ($c->match->stack->[-1]->{$param}) {
  397             return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param});
  398         }
  399         elsif ($c->req->json && $c->req->json->{$param}) {
  400             return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
  401         }
  402     }
  403 }
  404 
  405 =head3 _object_ownership_by_accountlines_id
  406 
  407 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
  408 belongs to C<$user>.
  409 
  410 =cut
  411 
  412 sub _object_ownership_by_accountlines_id {
  413     my ($c, $user, $accountlines_id) = @_;
  414 
  415     my $accountline = Koha::Account::Lines->find($accountlines_id);
  416     return $accountline && $user->borrowernumber == $accountline->borrowernumber;
  417 }
  418 
  419 =head3 _object_ownership_by_borrowernumber
  420 
  421 Compares C<$borrowernumber> to currently logged in C<$user>.
  422 
  423 =cut
  424 
  425 sub _object_ownership_by_patron_id {
  426     my ($c, $user, $patron_id) = @_;
  427 
  428     return $user->borrowernumber == $patron_id;
  429 }
  430 
  431 =head3 _object_ownership_by_checkout_id
  432 
  433 First, attempts to find a Koha::Checkout-object by C<$issue_id>. If we find one,
  434 compare its borrowernumber to currently logged in C<$user>. However, if an issue
  435 is not found, attempt to find a Koha::Old::Checkout-object instead and compare its
  436 borrowernumber to currently logged in C<$user>.
  437 
  438 =cut
  439 
  440 sub _object_ownership_by_checkout_id {
  441     my ($c, $user, $issue_id) = @_;
  442 
  443     my $issue = Koha::Checkouts->find($issue_id);
  444     $issue = Koha::Old::Checkouts->find($issue_id) unless $issue;
  445     return $issue && $issue->borrowernumber
  446             && $user->borrowernumber == $issue->borrowernumber;
  447 }
  448 
  449 =head3 _object_ownership_by_reserve_id
  450 
  451 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
  452 belongs to C<$user>.
  453 
  454 TODO: Also compare against old_reserves
  455 
  456 =cut
  457 
  458 sub _object_ownership_by_reserve_id {
  459     my ($c, $user, $reserve_id) = @_;
  460 
  461     my $reserve = Koha::Holds->find($reserve_id);
  462     return $reserve && $user->borrowernumber == $reserve->borrowernumber;
  463 }
  464 
  465 =head3 _basic_auth
  466 
  467 Internal method that performs Basic authentication.
  468 
  469 =cut
  470 
  471 sub _basic_auth {
  472     my ( $c, $authorization_header ) = @_;
  473 
  474     my ( $type, $credentials ) = split / /, $authorization_header;
  475 
  476     unless ($credentials) {
  477         Koha::Exceptions::Authentication::Required->throw( error => 'Authentication failure.' );
  478     }
  479 
  480     my $decoded_credentials = decode_base64( $credentials );
  481     my ( $user_id, $password ) = split( /:/, $decoded_credentials, 2 );
  482 
  483     my $dbh = C4::Context->dbh;
  484     unless ( checkpw_internal($dbh, $user_id, $password ) ) {
  485         Koha::Exceptions::Authorization::Unauthorized->throw( error => 'Invalid password' );
  486     }
  487 
  488     return Koha::Patrons->find({ userid => $user_id });
  489 }
  490 
  491 =head3 _set_userenv
  492 
  493     $c->_set_userenv( $patron );
  494 
  495 Internal method that sets C4::Context->userenv
  496 
  497 =cut
  498 
  499 sub _set_userenv {
  500     my ( $c, $patron ) = @_;
  501 
  502     my $library = $patron->library;
  503 
  504     C4::Context->_new_userenv( $patron->borrowernumber );
  505     C4::Context->set_userenv(
  506         $patron->borrowernumber, # number,
  507         $patron->userid,         # userid,
  508         $patron->cardnumber,     # cardnumber
  509         $patron->firstname,      # firstname
  510         $patron->surname,        # surname
  511         $library->branchcode,    # branch
  512         $library->branchname,    # branchname
  513         $patron->flags,          # flags,
  514         $patron->email,          # emailaddress
  515     );
  516 
  517     return $c;
  518 }
  519 
  520 1;