Bug 17600: Standardize our EXPORT_OK
[srvgit] / Koha / REST / V1 / Auth.pm
index 75f2a9e..2361aae 100644 (file)
@@ -4,28 +4,33 @@ package Koha::REST::V1::Auth;
 #
 # This file is part of Koha.
 #
-# Koha is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 3 of the License, or (at your option) any later
-# version.
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
 #
-# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public License along
-# with Koha; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
 
 use Modern::Perl;
 
 use Mojo::Base 'Mojolicious::Controller';
 
-use C4::Auth qw( check_cookie_auth get_session haspermission );
+use C4::Auth qw( check_cookie_auth checkpw_internal get_session haspermission );
+use C4::Context;
 
+use Koha::ApiKeys;
 use Koha::Account::Lines;
 use Koha::Checkouts;
 use Koha::Holds;
+use Koha::Libraries;
+use Koha::OAuth;
+use Koha::OAuthAccessTokens;
 use Koha::Old::Checkouts;
 use Koha::Patrons;
 
@@ -33,8 +38,16 @@ use Koha::Exceptions;
 use Koha::Exceptions::Authentication;
 use Koha::Exceptions::Authorization;
 
+use MIME::Base64 qw( decode_base64 );
+use Module::Load::Conditional;
 use Scalar::Util qw( blessed );
-use Try::Tiny;
+use Try::Tiny qw( catch try );
+
+=head1 NAME
+
+Koha::REST::V1::Auth
+
+=head2 Operations
 
 =head3 under
 
@@ -43,12 +56,41 @@ This subroutine is called before every request to API.
 =cut
 
 sub under {
-    my $c = shift->openapi->valid_input or return;;
+    my ( $c ) = @_;
 
     my $status = 0;
+
     try {
 
-        $status = authenticate_api_request($c);
+        # /api/v1/{namespace}
+        my $namespace = $c->req->url->to_abs->path->[2] // '';
+
+        my $is_public = 0; # By default routes are not public
+        my $is_plugin = 0;
+
+        if ( $namespace eq 'public' ) {
+            $is_public = 1;
+        } elsif ( $namespace eq 'contrib' ) {
+            $is_plugin = 1;
+        }
+
+        if ( $is_public
+            and !C4::Context->preference('RESTPublicAPI') )
+        {
+            Koha::Exceptions::Authorization->throw(
+                "Configuration prevents the usage of this endpoint by unprivileged users");
+        }
+
+        if ( $c->req->url->to_abs->path eq '/api/v1/oauth/token' ) {
+            # Requesting a token shouldn't go through the API authenticaction chain
+            $status = 1;
+        }
+        elsif ( $namespace eq '' or $namespace eq '.html' ) {
+            $status = 1;
+        }
+        else {
+            $status = authenticate_api_request($c, { is_public => $is_public, is_plugin => $is_plugin });
+        }
 
     } catch {
         unless (blessed($_)) {
@@ -67,7 +109,7 @@ sub under {
             return $c->render(status => 401, json => { error => $_->error });
         }
         elsif ($_->isa('Koha::Exceptions::Authentication')) {
-            return $c->render(status => 500, json => { error => $_->error });
+            return $c->render(status => 401, json => { error => $_->error });
         }
         elsif ($_->isa('Koha::Exceptions::BadParameter')) {
             return $c->render(status => 400, json => $_->error );
@@ -78,6 +120,9 @@ sub under {
                 required_permissions => $_->required_permissions,
             });
         }
+        elsif ($_->isa('Koha::Exceptions::Authorization')) {
+            return $c->render(status => 403, json => { error => $_->error });
+        }
         elsif ($_->isa('Koha::Exceptions')) {
             return $c->render(status => 500, json => { error => $_->error });
         }
@@ -100,54 +145,137 @@ if authorization is required and user has required permissions to access.
 =cut
 
 sub authenticate_api_request {
-    my ( $c ) = @_;
+    my ( $c, $params ) = @_;
+
+    my $user;
+
+    # The following supports retrieval of spec with Mojolicious::Plugin::OpenAPI@1.17 and later (first one)
+    # and older versions (second one).
+    # TODO: remove the latter 'openapi.op_spec' if minimum version is bumped to at least 1.17.
+    my $spec = $c->openapi->spec || $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
+
+    $c->stash_embed({ spec => $spec });
+    $c->stash_overrides();
+
+    my $cookie_auth = 0;
 
-    my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
     my $authorization = $spec->{'x-koha-authorization'};
-    my $cookie = $c->cookie('CGISESSID');
-    my ($session, $user);
-    # Mojo doesn't use %ENV the way CGI apps do
-    # Manually pass the remote_address to check_auth_cookie
-    my $remote_addr = $c->tx->remote_address;
-    my ($status, $sessionID) = check_cookie_auth(
-                                            $cookie, undef,
-                                            { remote_addr => $remote_addr });
-    if ($status eq "ok") {
-        $session = get_session($sessionID);
-        $user = Koha::Patrons->find($session->param('number'));
-        $c->stash('koha.user' => $user);
-    }
-    elsif ($status eq "maintenance") {
-        Koha::Exceptions::UnderMaintenance->throw(
-            error => 'System is under maintenance.'
+
+    my $authorization_header = $c->req->headers->authorization;
+
+    if ($authorization_header and $authorization_header =~ /^Bearer /) {
+        # attempt to use OAuth2 authentication
+        if ( ! Module::Load::Conditional::can_load(
+                    modules => {'Net::OAuth2::AuthorizationServer' => undef} )) {
+            Koha::Exceptions::Authorization::Unauthorized->throw(
+                error => 'Authentication failure.'
+            );
+        }
+        else {
+            require Net::OAuth2::AuthorizationServer;
+        }
+
+        my $server = Net::OAuth2::AuthorizationServer->new;
+        my $grant = $server->client_credentials_grant(Koha::OAuth::config);
+        my ($type, $token) = split / /, $authorization_header;
+        my ($valid_token, $error) = $grant->verify_access_token(
+            access_token => $token,
         );
+
+        if ($valid_token) {
+            my $patron_id = Koha::ApiKeys->find( $valid_token->{client_id} )->patron_id;
+            $user         = Koha::Patrons->find($patron_id);
+        }
+        else {
+            # If we have "Authorization: Bearer" header and oauth authentication
+            # failed, do not try other authentication means
+            Koha::Exceptions::Authentication::Required->throw(
+                error => 'Authentication failure.'
+            );
+        }
     }
-    elsif ($status eq "expired" and $authorization) {
-        Koha::Exceptions::Authentication::SessionExpired->throw(
-            error => 'Session has been expired.'
-        );
+    elsif ( $authorization_header and $authorization_header =~ /^Basic / ) {
+        unless ( C4::Context->preference('RESTBasicAuth') ) {
+            Koha::Exceptions::Authentication::Required->throw(
+                error => 'Basic authentication disabled'
+            );
+        }
+        $user = $c->_basic_auth( $authorization_header );
+        unless ( $user ) {
+            # If we have "Authorization: Basic" header and authentication
+            # failed, do not try other authentication means
+            Koha::Exceptions::Authentication::Required->throw(
+                error => 'Authentication failure.'
+            );
+        }
     }
-    elsif ($status eq "failed" and $authorization) {
-        Koha::Exceptions::Authentication::Required->throw(
-            error => 'Authentication failure.'
-        );
+    else {
+
+        my $cookie = $c->cookie('CGISESSID');
+
+        # Mojo doesn't use %ENV the way CGI apps do
+        # Manually pass the remote_address to check_auth_cookie
+        my $remote_addr = $c->tx->remote_address;
+        my ($status, $sessionID) = check_cookie_auth(
+                                                $cookie, undef,
+                                                { remote_addr => $remote_addr });
+        if ($status eq "ok") {
+            my $session = get_session($sessionID);
+            $user = Koha::Patrons->find( $session->param('number') )
+              unless $session->param('sessiontype')
+                 and $session->param('sessiontype') eq 'anon';
+            $cookie_auth = 1;
+        }
+        elsif ($status eq "maintenance") {
+            Koha::Exceptions::UnderMaintenance->throw(
+                error => 'System is under maintenance.'
+            );
+        }
+        elsif ($status eq "expired" and $authorization) {
+            Koha::Exceptions::Authentication::SessionExpired->throw(
+                error => 'Session has been expired.'
+            );
+        }
+        elsif ($status eq "failed" and $authorization) {
+            Koha::Exceptions::Authentication::Required->throw(
+                error => 'Authentication failure.'
+            );
+        }
+        elsif ($authorization) {
+            Koha::Exceptions::Authentication->throw(
+                error => 'Unexpected authentication status.'
+            );
+        }
     }
-    elsif ($authorization) {
-        Koha::Exceptions::Authentication->throw(
-            error => 'Unexpected authentication status.'
-        );
+
+    $c->stash('koha.user' => $user);
+    C4::Context->interface('api');
+
+    if ( $user and !$cookie_auth ) { # cookie-auth sets this and more, don't mess with that
+        $c->_set_userenv( $user );
     }
 
-    # We do not need any authorization
-    unless ($authorization) {
+    if ( !$authorization and
+         ( $params->{is_public} and
+          ( C4::Context->preference('RESTPublicAnonymousRequests') or
+            $user) or $params->{is_plugin} ) ) {
+        # We do not need any authorization
         # Check the parameters
         validate_query_parameters( $c, $spec );
         return 1;
     }
+    else {
+        # We are required authorizarion, there needs
+        # to be an identified user
+        Koha::Exceptions::Authentication::Required->throw(
+            error => 'Authentication failure.' )
+          unless $user;
+    }
+
 
     my $permissions = $authorization->{'permissions'};
     # Check if the user is authorized
-    if ( haspermission($user->userid, $permissions)
+    if ( ( defined($permissions) and haspermission($user->userid, $permissions) )
         or allow_owner($c, $authorization, $user)
         or allow_guarantor($c, $authorization, $user) ) {
 
@@ -162,6 +290,13 @@ sub authenticate_api_request {
         required_permissions => $permissions,
     );
 }
+
+=head3 validate_query_parameters
+
+Validates the query parameters against the spec.
+
+=cut
+
 sub validate_query_parameters {
     my ( $c, $action_spec ) = @_;
 
@@ -178,7 +313,6 @@ sub validate_query_parameters {
     ) if @errors;
 }
 
-
 =head3 allow_owner
 
 Allows access to object for its owner.
@@ -223,7 +357,7 @@ sub allow_guarantor {
         return;
     }
 
-    my $guarantees = $user->guarantees->as_list;
+    my $guarantees = $user->guarantee_relationships->guarantees->as_list;
     foreach my $guarantee (@{$guarantees}) {
         return 1 if check_object_ownership($c, $guarantee);
     }
@@ -248,7 +382,8 @@ sub check_object_ownership {
 
     my $parameters = {
         accountlines_id => \&_object_ownership_by_accountlines_id,
-        borrowernumber  => \&_object_ownership_by_borrowernumber,
+        borrowernumber  => \&_object_ownership_by_patron_id,
+        patron_id       => \&_object_ownership_by_patron_id,
         checkout_id     => \&_object_ownership_by_checkout_id,
         reserve_id      => \&_object_ownership_by_reserve_id,
     };
@@ -290,10 +425,10 @@ Compares C<$borrowernumber> to currently logged in C<$user>.
 
 =cut
 
-sub _object_ownership_by_borrowernumber {
-    my ($c, $user, $borrowernumber) = @_;
+sub _object_ownership_by_patron_id {
+    my ($c, $user, $patron_id) = @_;
 
-    return $user->borrowernumber == $borrowernumber;
+    return $user->borrowernumber == $patron_id;
 }
 
 =head3 _object_ownership_by_checkout_id
@@ -330,4 +465,70 @@ sub _object_ownership_by_reserve_id {
     return $reserve && $user->borrowernumber == $reserve->borrowernumber;
 }
 
+=head3 _basic_auth
+
+Internal method that performs Basic authentication.
+
+=cut
+
+sub _basic_auth {
+    my ( $c, $authorization_header ) = @_;
+
+    my ( $type, $credentials ) = split / /, $authorization_header;
+
+    unless ($credentials) {
+        Koha::Exceptions::Authentication::Required->throw( error => 'Authentication failure.' );
+    }
+
+    my $decoded_credentials = decode_base64( $credentials );
+    my ( $user_id, $password ) = split( /:/, $decoded_credentials, 2 );
+
+    my $dbh = C4::Context->dbh;
+    unless ( checkpw_internal($dbh, $user_id, $password ) ) {
+        Koha::Exceptions::Authorization::Unauthorized->throw( error => 'Invalid password' );
+    }
+
+    return Koha::Patrons->find({ userid => $user_id });
+}
+
+=head3 _set_userenv
+
+    $c->_set_userenv( $patron );
+
+Internal method that sets C4::Context->userenv
+
+=cut
+
+sub _set_userenv {
+    my ( $c, $patron ) = @_;
+
+    my $passed_library_id = $c->req->headers->header('x-koha-library');
+    my $THE_library;
+
+    if ( $passed_library_id ) {
+        $THE_library = Koha::Libraries->find( $passed_library_id );
+        Koha::Exceptions::Authorization::Unauthorized->throw(
+            "Unauthorized attempt to set library to $passed_library_id"
+        ) unless $THE_library and $patron->can_log_into($THE_library);
+    }
+    else {
+        $THE_library = $patron->library;
+    }
+
+    C4::Context->_new_userenv( $patron->borrowernumber );
+    C4::Context->set_userenv(
+        $patron->borrowernumber,  # number,
+        $patron->userid,          # userid,
+        $patron->cardnumber,      # cardnumber
+        $patron->firstname,       # firstname
+        $patron->surname,         # surname
+        $THE_library->branchcode, # branch
+        $THE_library->branchname, # branchname
+        $patron->flags,           # flags,
+        $patron->email,           # emailaddress
+    );
+
+    return $c;
+}
+
 1;