| Line 0
          
      
      
        Link Here | 
            
              |  |  | 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 get_session haspermission ); | 
            
              | 25 |  | 
            
              | 26 | use Koha::Account::Lines; | 
            
              | 27 | use Koha::Checkouts; | 
            
              | 28 | use Koha::Holds; | 
            
              | 29 | use Koha::Old::Checkouts; | 
            
              | 30 | use Koha::Patrons; | 
            
              | 31 |  | 
            
              | 32 | use Koha::Exceptions; | 
            
              | 33 | use Koha::Exceptions::Authentication; | 
            
              | 34 | use Koha::Exceptions::Authorization; | 
            
              | 35 |  | 
            
              | 36 | use Scalar::Util qw( blessed ); | 
            
              | 37 | use Try::Tiny; | 
            
              | 38 |  | 
            
              | 39 | =head3 under | 
            
              | 40 |  | 
            
              | 41 | This subroutine is called before every request to API. | 
            
              | 42 |  | 
            
              | 43 | =cut | 
            
              | 44 |  | 
            
              | 45 | sub under { | 
            
              | 46 |     my $c = shift->openapi->valid_input or return;; | 
            
              | 47 |  | 
            
              | 48 |     my $status = 0; | 
            
              | 49 |     try { | 
            
              | 50 |  | 
            
              | 51 |         $status = authenticate_api_request($c); | 
            
              | 52 |  | 
            
              | 53 |     } catch { | 
            
              | 54 |         unless (blessed($_)) { | 
            
              | 55 |             return $c->render( | 
            
              | 56 |                 status => 500, | 
            
              | 57 |                 json => { error => 'Something went wrong, check the logs.' } | 
            
              | 58 |             ); | 
            
              | 59 |         } | 
            
              | 60 |         if ($_->isa('Koha::Exceptions::UnderMaintenance')) { | 
            
              | 61 |             return $c->render(status => 503, json => { error => $_->error }); | 
            
              | 62 |         } | 
            
              | 63 |         elsif ($_->isa('Koha::Exceptions::Authentication::SessionExpired')) { | 
            
              | 64 |             return $c->render(status => 401, json => { error => $_->error }); | 
            
              | 65 |         } | 
            
              | 66 |         elsif ($_->isa('Koha::Exceptions::Authentication::Required')) { | 
            
              | 67 |             return $c->render(status => 401, json => { error => $_->error }); | 
            
              | 68 |         } | 
            
              | 69 |         elsif ($_->isa('Koha::Exceptions::Authentication')) { | 
            
              | 70 |             return $c->render(status => 500, json => { error => $_->error }); | 
            
              | 71 |         } | 
            
              | 72 |         elsif ($_->isa('Koha::Exceptions::BadParameter')) { | 
            
              | 73 |             return $c->render(status => 400, json => $_->error ); | 
            
              | 74 |         } | 
            
              | 75 |         elsif ($_->isa('Koha::Exceptions::Authorization::Unauthorized')) { | 
            
              | 76 |             return $c->render(status => 403, json => { | 
            
              | 77 |                 error => $_->error, | 
            
              | 78 |                 required_permissions => $_->required_permissions, | 
            
              | 79 |             }); | 
            
              | 80 |         } | 
            
              | 81 |         elsif ($_->isa('Koha::Exceptions')) { | 
            
              | 82 |             return $c->render(status => 500, json => { error => $_->error }); | 
            
              | 83 |         } | 
            
              | 84 |         else { | 
            
              | 85 |             return $c->render( | 
            
              | 86 |                 status => 500, | 
            
              | 87 |                 json => { error => 'Something went wrong, check the logs.' } | 
            
              | 88 |             ); | 
            
              | 89 |         } | 
            
              | 90 |     }; | 
            
              | 91 |  | 
            
              | 92 |     return $status; | 
            
              | 93 | } | 
            
              | 94 |  | 
            
              | 95 | =head3 authenticate_api_request | 
            
              | 96 |  | 
            
              | 97 | Validates authentication and allows access if authorization is not required or | 
            
              | 98 | if authorization is required and user has required permissions to access. | 
            
              | 99 |  | 
            
              | 100 | =cut | 
            
              | 101 |  | 
            
              | 102 | sub authenticate_api_request { | 
            
              | 103 |     my ( $c ) = @_; | 
            
              | 104 |  | 
            
              | 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( | 
            
              | 113 |                                             $cookie, undef, | 
            
              | 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); | 
            
              | 119 |     } | 
            
              | 120 |     elsif ($status eq "maintenance") { | 
            
              | 121 |         Koha::Exceptions::UnderMaintenance->throw( | 
            
              | 122 |             error => 'System is under maintenance.' | 
            
              | 123 |         ); | 
            
              | 124 |     } | 
            
              | 125 |     elsif ($status eq "expired" and $authorization) { | 
            
              | 126 |         Koha::Exceptions::Authentication::SessionExpired->throw( | 
            
              | 127 |             error => 'Session has been expired.' | 
            
              | 128 |         ); | 
            
              | 129 |     } | 
            
              | 130 |     elsif ($status eq "failed" and $authorization) { | 
            
              | 131 |         Koha::Exceptions::Authentication::Required->throw( | 
            
              | 132 |             error => 'Authentication failure.' | 
            
              | 133 |         ); | 
            
              | 134 |     } | 
            
              | 135 |     elsif ($authorization) { | 
            
              | 136 |         Koha::Exceptions::Authentication->throw( | 
            
              | 137 |             error => 'Unexpected authentication status.' | 
            
              | 138 |         ); | 
            
              | 139 |     } | 
            
              | 140 |  | 
            
              | 141 |     # We do not need any authorization | 
            
              | 142 |     unless ($authorization) { | 
            
              | 143 |         # Check the parameters | 
            
              | 144 |         validate_query_parameters( $c, $spec ); | 
            
              | 145 |         return 1; | 
            
              | 146 |     } | 
            
              | 147 |  | 
            
              | 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) ) { | 
            
              | 153 |  | 
            
              | 154 |         validate_query_parameters( $c, $spec ); | 
            
              | 155 |  | 
            
              | 156 |         # Everything is ok | 
            
              | 157 |         return 1; | 
            
              | 158 |     } | 
            
              | 159 |  | 
            
              | 160 |     Koha::Exceptions::Authorization::Unauthorized->throw( | 
            
              | 161 |         error => "Authorization failure. Missing required permission(s).", | 
            
              | 162 |         required_permissions => $permissions, | 
            
              | 163 |     ); | 
            
              | 164 | } | 
            
              | 165 | sub validate_query_parameters { | 
            
              | 166 |     my ( $c, $action_spec ) = @_; | 
            
              | 167 |  | 
            
              | 168 |     # Check for malformed query parameters | 
            
              | 169 |     my @errors; | 
            
              | 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}; | 
            
              | 174 |     } | 
            
              | 175 |  | 
            
              | 176 |     Koha::Exceptions::BadParameter->throw( | 
            
              | 177 |         error => \@errors | 
            
              | 178 |     ) if @errors; | 
            
              | 179 | } | 
            
              | 180 |  | 
            
              | 181 |  | 
            
              | 182 | =head3 allow_owner | 
            
              | 183 |  | 
            
              | 184 | Allows access to object for its owner. | 
            
              | 185 |  | 
            
              | 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: | 
            
              | 189 |  | 
            
              | 190 | "/holds/{reserve_id}": { | 
            
              | 191 |     "get": { | 
            
              | 192 |         ..., | 
            
              | 193 |         "x-koha-authorization": { | 
            
              | 194 |             "allow-owner": true, | 
            
              | 195 |             "permissions": { | 
            
              | 196 |                 "borrowers": "1" | 
            
              | 197 |             } | 
            
              | 198 |         } | 
            
              | 199 |     } | 
            
              | 200 | } | 
            
              | 201 |  | 
            
              | 202 | =cut | 
            
              | 203 |  | 
            
              | 204 | sub allow_owner { | 
            
              | 205 |     my ($c, $authorization, $user) = @_; | 
            
              | 206 |  | 
            
              | 207 |     return unless $authorization->{'allow-owner'}; | 
            
              | 208 |  | 
            
              | 209 |     return check_object_ownership($c, $user) if $user and $c; | 
            
              | 210 | } | 
            
              | 211 |  | 
            
              | 212 | =head3 allow_guarantor | 
            
              | 213 |  | 
            
              | 214 | Same as "allow_owner", but checks if the object is owned by one of C<$user>'s | 
            
              | 215 | guarantees. | 
            
              | 216 |  | 
            
              | 217 | =cut | 
            
              | 218 |  | 
            
              | 219 | sub allow_guarantor { | 
            
              | 220 |     my ($c, $authorization, $user) = @_; | 
            
              | 221 |  | 
            
              | 222 |     if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){ | 
            
              | 223 |         return; | 
            
              | 224 |     } | 
            
              | 225 |  | 
            
              | 226 |     my $guarantees = $user->guarantees->as_list; | 
            
              | 227 |     foreach my $guarantee (@{$guarantees}) { | 
            
              | 228 |         return 1 if check_object_ownership($c, $guarantee); | 
            
              | 229 |     } | 
            
              | 230 | } | 
            
              | 231 |  | 
            
              | 232 | =head3 check_object_ownership | 
            
              | 233 |  | 
            
              | 234 | Determines ownership of an object from request parameters. | 
            
              | 235 |  | 
            
              | 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. | 
            
              | 241 |  | 
            
              | 242 | =cut | 
            
              | 243 |  | 
            
              | 244 | sub check_object_ownership { | 
            
              | 245 |     my ($c, $user) = @_; | 
            
              | 246 |  | 
            
              | 247 |     return if not $c or not $user; | 
            
              | 248 |  | 
            
              | 249 |     my $parameters = { | 
            
              | 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, | 
            
              | 254 |     }; | 
            
              | 255 |  | 
            
              | 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)); | 
            
              | 260 |         } | 
            
              | 261 |         elsif ($c->param($param)) { | 
            
              | 262 |             return &$check_ownership($c, $user, $c->param($param)); | 
            
              | 263 |         } | 
            
              | 264 |         elsif ($c->match->stack->[-1]->{$param}) { | 
            
              | 265 |             return &$check_ownership($c, $user, $c->match->stack->[-1]->{$param}); | 
            
              | 266 |         } | 
            
              | 267 |         elsif ($c->req->json && $c->req->json->{$param}) { | 
            
              | 268 |             return 1 if &$check_ownership($c, $user, $c->req->json->{$param}); | 
            
              | 269 |         } | 
            
              | 270 |     } | 
            
              | 271 | } | 
            
              | 272 |  | 
            
              | 273 | =head3 _object_ownership_by_accountlines_id | 
            
              | 274 |  | 
            
              | 275 | Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it | 
            
              | 276 | belongs to C<$user>. | 
            
              | 277 |  | 
            
              | 278 | =cut | 
            
              | 279 |  | 
            
              | 280 | sub _object_ownership_by_accountlines_id { | 
            
              | 281 |     my ($c, $user, $accountlines_id) = @_; | 
            
              | 282 |  | 
            
              | 283 |     my $accountline = Koha::Account::Lines->find($accountlines_id); | 
            
              | 284 |     return $accountline && $user->borrowernumber == $accountline->borrowernumber; | 
            
              | 285 | } | 
            
              | 286 |  | 
            
              | 287 | =head3 _object_ownership_by_borrowernumber | 
            
              | 288 |  | 
            
              | 289 | Compares C<$borrowernumber> to currently logged in C<$user>. | 
            
              | 290 |  | 
            
              | 291 | =cut | 
            
              | 292 |  | 
            
              | 293 | sub _object_ownership_by_borrowernumber { | 
            
              | 294 |     my ($c, $user, $borrowernumber) = @_; | 
            
              | 295 |  | 
            
              | 296 |     return $user->borrowernumber == $borrowernumber; | 
            
              | 297 | } | 
            
              | 298 |  | 
            
              | 299 | =head3 _object_ownership_by_checkout_id | 
            
              | 300 |  | 
            
              | 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>. | 
            
              | 305 |  | 
            
              | 306 | =cut | 
            
              | 307 |  | 
            
              | 308 | sub _object_ownership_by_checkout_id { | 
            
              | 309 |     my ($c, $user, $issue_id) = @_; | 
            
              | 310 |  | 
            
              | 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; | 
            
              | 315 | } | 
            
              | 316 |  | 
            
              | 317 | =head3 _object_ownership_by_reserve_id | 
            
              | 318 |  | 
            
              | 319 | Finds a Koha::Hold-object by C<$reserve_id> and checks if it | 
            
              | 320 | belongs to C<$user>. | 
            
              | 321 |  | 
            
              | 322 | TODO: Also compare against old_reserves | 
            
              | 323 |  | 
            
              | 324 | =cut | 
            
              | 325 |  | 
            
              | 326 | sub _object_ownership_by_reserve_id { | 
            
              | 327 |     my ($c, $user, $reserve_id) = @_; | 
            
              | 328 |  | 
            
              | 329 |     my $reserve = Koha::Holds->find($reserve_id); | 
            
              | 330 |     return $reserve && $user->borrowernumber == $reserve->borrowernumber; | 
            
              | 331 | } | 
            
              | 332 |  | 
            
              | 333 | 1; |