Bug 14868: Give users possibility to request their own object
[koha.git] / Koha / REST / V1.pm
1 package Koha::REST::V1;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with Koha; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 use Modern::Perl;
19 use Mojo::Base 'Mojolicious';
20
21 use C4::Auth qw( check_cookie_auth get_session haspermission );
22 use C4::Context;
23 use Koha::Account::Lines;
24 use Koha::Issues;
25 use Koha::Holds;
26 use Koha::OldIssues;
27 use Koha::Patrons;
28
29 sub startup {
30     my $self = shift;
31
32     # Force charset=utf8 in Content-Type header for JSON responses
33     $self->types->type(json => 'application/json; charset=utf8');
34
35     my $secret_passphrase = C4::Context->config('api_secret_passphrase');
36     if ($secret_passphrase) {
37         $self->secrets([$secret_passphrase]);
38     }
39
40     $self->plugin(Swagger2 => {
41         url => $self->home->rel_file("api/v1/swagger/swagger.min.json"),
42     });
43 }
44
45 =head3 authenticate_api_request
46
47 Validates authentication and allows access if authorization is not required or
48 if authorization is required and user has required permissions to access.
49
50 This subroutine is called before every request to API.
51
52 =cut
53
54 sub authenticate_api_request {
55     my ($next, $c, $action_spec) = @_;
56
57     my ($session, $user);
58     my $cookie = $c->cookie('CGISESSID');
59     # Mojo doesn't use %ENV the way CGI apps do
60     # Manually pass the remote_address to check_auth_cookie
61     my $remote_addr = $c->tx->remote_address;
62     my ($status, $sessionID) = check_cookie_auth(
63                                             $cookie, undef,
64                                             { remote_addr => $remote_addr });
65     if ($status eq "ok") {
66         $session = get_session($sessionID);
67         $user = Koha::Patrons->find($session->param('number'));
68         $c->stash('koha.user' => $user);
69     }
70     else {
71         return $c->render_swagger(
72             { error => "Authentication failure." },
73             {},
74             401
75         ) if $cookie and $action_spec->{'x-koha-authorization'};
76     }
77
78     return $next->($c) unless $action_spec->{'x-koha-authorization'};
79     unless ($user) {
80         return $c->render_swagger({ error => "Authentication required." },{},401);
81     }
82
83     my $authorization = $action_spec->{'x-koha-authorization'};
84     return $next->($c) if allow_owner($c, $authorization, $user);
85     return $next->($c) if allow_guarantor($c, $authorization, $user);
86
87     my $permissions = $authorization->{'permissions'};
88     return $next->($c) if C4::Auth::haspermission($user->userid, $permissions);
89     return $c->render_swagger(
90         { error => "Authorization failure. Missing required permission(s)." },
91         {},
92         403
93     );
94 }
95
96 =head3 allow_owner
97
98 Allows access to object for its owner.
99
100 There are endpoints that should allow access for the object owner even if they
101 do not have the required permission, e.g. access an own reserve. This can be
102 achieved by defining the operation as follows:
103
104 "/holds/{reserve_id}": {
105     "get": {
106         ...,
107         "x-koha-authorization": {
108             "allow-owner": true,
109             "permissions": {
110                 "borrowers": "1"
111             }
112         }
113     }
114 }
115
116 =cut
117
118 sub allow_owner {
119     my ($c, $authorization, $user) = @_;
120
121     return unless $authorization->{'allow-owner'};
122
123     return check_object_ownership($c, $user) if $user and $c;
124 }
125
126 =head3 allow_guarantor
127
128 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
129 guarantees.
130
131 =cut
132
133 sub allow_guarantor {
134     my ($c, $authorization, $user) = @_;
135
136     if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
137         return;
138     }
139
140     my $guarantees = $user->guarantees->as_list;
141     foreach my $guarantee (@{$guarantees}) {
142         return 1 if check_object_ownership($c, $guarantee);
143     }
144 }
145
146 =head3 check_object_ownership
147
148 Determines ownership of an object from request parameters.
149
150 As introducing an endpoint that allows access for object's owner; if the
151 parameter that will be used to determine ownership is not already inside
152 $parameters, add a new subroutine that checks the ownership and extend
153 $parameters to contain a key with parameter_name and a value of a subref to
154 the subroutine that you created.
155
156 =cut
157
158 sub check_object_ownership {
159     my ($c, $user) = @_;
160
161     return if not $c or not $user;
162
163     my $parameters = {
164         accountlines_id => \&_object_ownership_by_accountlines_id,
165         borrowernumber  => \&_object_ownership_by_borrowernumber,
166         checkout_id     => \&_object_ownership_by_checkout_id,
167         reserve_id      => \&_object_ownership_by_reserve_id,
168     };
169
170     foreach my $param (keys $parameters) {
171         my $check_ownership = $parameters->{$param};
172         if ($c->stash($param)) {
173             return &$check_ownership($c, $user, $c->stash($param));
174         }
175         elsif ($c->param($param)) {
176             return &$check_ownership($c, $user, $c->param($param));
177         }
178         elsif ($c->req->json && $c->req->json->{$param}) {
179             return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
180         }
181     }
182 }
183
184 =head3 _object_ownership_by_accountlines_id
185
186 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
187 belongs to C<$user>.
188
189 =cut
190
191 sub _object_ownership_by_accountlines_id {
192     my ($c, $user, $accountlines_id) = @_;
193
194     my $accountline = Koha::Account::Lines->find($accountlines_id);
195     return $accountline && $user->borrowernumber == $accountline->borrowernumber;
196 }
197
198 =head3 _object_ownership_by_borrowernumber
199
200 Compares C<$borrowernumber> to currently logged in C<$user>.
201
202 =cut
203
204 sub _object_ownership_by_borrowernumber {
205     my ($c, $user, $borrowernumber) = @_;
206
207     return $user->borrowernumber == $borrowernumber;
208 }
209
210 =head3 _object_ownership_by_checkout_id
211
212 First, attempts to find a Koha::Issue-object by C<$issue_id>. If we find one,
213 compare its borrowernumber to currently logged in C<$user>. However, if an issue
214 is not found, attempt to find a Koha::OldIssue-object instead and compare its
215 borrowernumber to currently logged in C<$user>.
216
217 =cut
218
219 sub _object_ownership_by_checkout_id {
220     my ($c, $user, $issue_id) = @_;
221
222     my $issue = Koha::Issues->find($issue_id);
223     $issue = Koha::OldIssues->find($issue_id) unless $issue;
224     return $issue && $issue->borrowernumber
225             && $user->borrowernumber == $issue->borrowernumber;
226 }
227
228 =head3 _object_ownership_by_reserve_id
229
230 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
231 belongs to C<$user>.
232
233 TODO: Also compare against old_reserves
234
235 =cut
236
237 sub _object_ownership_by_reserve_id {
238     my ($c, $user, $reserve_id) = @_;
239
240     my $reserve = Koha::Holds->find($reserve_id);
241     return $reserve && $user->borrowernumber == $reserve->borrowernumber;
242 }
243
244 1;