From 540c1fe6421f04dbd6943faac8f968a337de576e Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Fri, 17 Jun 2016 11:43:52 +0300 Subject: [PATCH] Bug 14868: Give users possibility to request their own object Allow access to user's own objects even if they do not have required permissions. This will be very useful in many cases where an user wants to request their own object, for example renewing their checkouts or placing a hold for themselves. First, this patch renames "x-koha-permission" to "x-koha-authorization" in order to describe the new functionality better. Second, we can now add two extra parameters under "x-koha-authorization": - "allow-owner"; Allows the owner of object to access it (without permission) - "allow-guarantor"; Allows guarantor of the owner of object to access it (without permission) Third, since permission checking is outside of actual controller, we need a way to find out ownership from different types of parameters, e.g. checkout_id from /checkouts/{checkout_id}, borrowernumber from /patrons/{borrowernumber} etc. A solution is to match the parameter with a subroutine that is designed to verify the ownership for that object. See the new subroutines in Koha::REST::V1. To use this functionality you will simply define it in Swagger: "/patrons/{borrowernumber}": { "get": { ..., "x-koha-authorization": { "allow-owner": true, "permissions": { "borrowers": "1" } } } } If a parameter that is not yet defined in Koha::REST::V1::check_object_ownership, you also need to define it and implement a subroutine that determines ownership. Tests are provided in a following patch that adds this functionality for current API operations. --- Koha/REST/V1.pm | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 165 insertions(+), 18 deletions(-) diff --git a/Koha/REST/V1.pm b/Koha/REST/V1.pm index 7c7cea9..fc76639 100644 --- a/Koha/REST/V1.pm +++ b/Koha/REST/V1.pm @@ -20,6 +20,10 @@ use Mojo::Base 'Mojolicious'; use C4::Auth qw( check_cookie_auth get_session haspermission ); use C4::Context; +use Koha::Account::Lines; +use Koha::Issues; +use Koha::Holds; +use Koha::OldIssues; use Koha::Patrons; sub startup { @@ -63,30 +67,173 @@ sub authenticate_api_request { { error => "Authentication failure." }, {}, 401 - ) if $cookie and $action_spec->{'x-koha-permission'}; + ) if $cookie and $action_spec->{'x-koha-authorization'}; } - if ($action_spec->{'x-koha-permission'}) { - return $c->render_swagger( - { error => "Authentication required." }, - {}, - 401 - ) unless $user; + return $next->($c) unless $action_spec->{'x-koha-authorization'}; + unless ($user) { + return $c->render_swagger({ error => "Authentication required." },{},401); + } - if (C4::Auth::haspermission($user->userid, $action_spec->{'x-koha-permission'})) { - return $next->($c); - } - else { - return $c->render_swagger( - { error => "Authorization failure. Missing required permission(s)." }, - {}, - 403 - ); + my $authorization = $action_spec->{'x-koha-authorization'}; + return $next->($c) if allow_owner($c, $authorization, $user); + return $next->($c) if allow_guarantor($c, $authorization, $user); + + my $permissions = $authorization->{'permissions'}; + return $next->($c) if C4::Auth::haspermission($user->userid, $permissions); + return $c->render_swagger( + { error => "Authorization failure. Missing required permission(s)." }, + {}, + 403 + ); +} + +=head3 allow_owner + +Allows access to object for its owner. + +There are endpoints that should allow access for the object owner even if they +do not have the required permission, e.g. access an own reserve. This can be +achieved by defining the operation as follows: + +"/holds/{reserve_id}": { + "get": { + ..., + "x-koha-authorization": { + "allow-owner": true, + "permissions": { + "borrowers": "1" + } } } - else { - return $next->($c); +} + +=cut + +sub allow_owner { + my ($c, $authorization, $user) = @_; + + return unless $authorization->{'allow-owner'}; + + return check_object_ownership($c, $user) if $user and $c; +} + +=head3 allow_guarantor + +Same as "allow_owner", but checks if the object is owned by one of C<$user>'s +guarantees. + +=cut + +sub allow_guarantor { + my ($c, $authorization, $user) = @_; + + if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){ + return; + } + + my $guarantees = $user->guarantees->as_list; + foreach my $guarantee (@{$guarantees}) { + return 1 if check_object_ownership($c, $guarantee); } } +=head3 check_object_ownership + +Determines ownership of an object from request parameters. + +As introducing an endpoint that allows access for object's owner; if the +parameter that will be used to determine ownership is not already inside +$parameters, add a new subroutine that checks the ownership and extend +$parameters to contain a key with parameter_name and a value of a subref to +the subroutine that you created. + +=cut + +sub check_object_ownership { + my ($c, $user) = @_; + + return if not $c or not $user; + + my $parameters = { + accountlines_id => \&_object_ownership_by_accountlines_id, + borrowernumber => \&_object_ownership_by_borrowernumber, + checkout_id => \&_object_ownership_by_checkout_id, + reserve_id => \&_object_ownership_by_reserve_id, + }; + + foreach my $param (keys $parameters) { + my $check_ownership = $parameters->{$param}; + if ($c->stash($param)) { + return &$check_ownership($c, $user, $c->stash($param)); + } + elsif ($c->param($param)) { + return &$check_ownership($c, $user, $c->param($param)); + } + elsif ($c->req->json && $c->req->json->{$param}) { + return 1 if &$check_ownership($c, $user, $c->req->json->{$param}); + } + } +} + +=head3 _object_ownership_by_accountlines_id + +Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it +belongs to C<$user>. + +=cut + +sub _object_ownership_by_accountlines_id { + my ($c, $user, $accountlines_id) = @_; + + my $accountline = Koha::Account::Lines->find($accountlines_id); + return $accountline && $user->borrowernumber == $accountline->borrowernumber; +} + +=head3 _object_ownership_by_borrowernumber + +Compares C<$borrowernumber> to currently logged in C<$user>. + +=cut + +sub _object_ownership_by_borrowernumber { + my ($c, $user, $borrowernumber) = @_; + + return $user->borrowernumber == $borrowernumber; +} + +=head3 _object_ownership_by_checkout_id + +First, attempts to find a Koha::Issue-object by C<$issue_id>. If we find one, +compare its borrowernumber to currently logged in C<$user>. However, if an issue +is not found, attempt to find a Koha::OldIssue-object instead and compare its +borrowernumber to currently logged in C<$user>. + +=cut + +sub _object_ownership_by_checkout_id { + my ($c, $user, $issue_id) = @_; + + my $issue = Koha::Issues->find($issue_id); + $issue = Koha::OldIssues->find($issue_id) unless $issue; + return $issue && $issue->borrowernumber + && $user->borrowernumber == $issue->borrowernumber; +} + +=head3 _object_ownership_by_reserve_id + +Finds a Koha::Hold-object by C<$reserve_id> and checks if it +belongs to C<$user>. + +TODO: Also compare against old_reserves + +=cut + +sub _object_ownership_by_reserve_id { + my ($c, $user, $reserve_id) = @_; + + my $reserve = Koha::Holds->find($reserve_id); + return $reserve && $user->borrowernumber == $reserve->borrowernumber; +} + 1; -- 1.9.1