Bug 17445: Add 'malformed query' error response
[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 =head1 NAME
30
31 Koha::REST::V1 - Main v.1 REST api class
32
33 =head1 API
34
35 =head2 Class Methods
36
37 =head3 startup
38
39 Overloaded Mojolicious->startup method. It is called at application startup.
40
41 =cut
42
43 sub startup {
44     my $self = shift;
45
46     # Force charset=utf8 in Content-Type header for JSON responses
47     $self->types->type(json => 'application/json; charset=utf8');
48
49     my $secret_passphrase = C4::Context->config('api_secret_passphrase');
50     if ($secret_passphrase) {
51         $self->secrets([$secret_passphrase]);
52     }
53
54     $self->plugin(Swagger2 => {
55         url => $self->home->rel_file("api/v1/swagger/swagger.json"),
56         validate => 1,
57         spec_path => '/spec'
58     });
59 }
60
61 =head3 authenticate_api_request
62
63 Validates authentication and allows access if authorization is not required or
64 if authorization is required and user has required permissions to access.
65
66 This subroutine is called before every request to API.
67
68 =cut
69
70 sub authenticate_api_request {
71     my ($next, $c, $action_spec) = @_;
72
73     my ($session, $user);
74     my $cookie = $c->cookie('CGISESSID');
75     # Mojo doesn't use %ENV the way CGI apps do
76     # Manually pass the remote_address to check_auth_cookie
77     my $remote_addr = $c->tx->remote_address;
78     my ($status, $sessionID) = check_cookie_auth(
79                                             $cookie, undef,
80                                             { remote_addr => $remote_addr });
81     if ($status eq "ok") {
82         $session = get_session($sessionID);
83         $user = Koha::Patrons->find($session->param('number'));
84         $c->stash('koha.user' => $user);
85     }
86     else {
87         return $c->render_swagger(
88             { error => "Authentication failure." },
89             {},
90             401
91         ) if $cookie and $action_spec->{'x-koha-authorization'};
92     }
93
94     # Check for malformed query parameters
95     my @errors;
96     my %valid_parameters = map { $_->{name} => 1 if $_->{in} eq 'query' } @{$action_spec->{parameters}};
97     my $existing_params = $c->req->query_params->to_hash;
98     for my $param ( keys %{$existing_params} ) {
99       push @errors, { path => "/query/".$param, message => 'Malformed query string' } unless exists $valid_parameters{$param};
100     }
101     return $c->render_swagger({},\@errors,400) if @errors;
102
103     return $next->($c) unless $action_spec->{'x-koha-authorization'};
104     unless ($user) {
105         return $c->render_swagger({ error => "Authentication required." },{},401);
106     }
107
108     my $authorization = $action_spec->{'x-koha-authorization'};
109     my $permissions = $authorization->{'permissions'};
110     return $next->($c) if C4::Auth::haspermission($user->userid, $permissions);
111     return $next->($c) if allow_owner($c, $authorization, $user);
112     return $next->($c) if allow_guarantor($c, $authorization, $user);
113     return $c->render_swagger(
114         { error => "Authorization failure. Missing required permission(s).",
115           required_permissions => $permissions },
116         {},
117         403
118     );
119 }
120
121 =head3 allow_owner
122
123 Allows access to object for its owner.
124
125 There are endpoints that should allow access for the object owner even if they
126 do not have the required permission, e.g. access an own reserve. This can be
127 achieved by defining the operation as follows:
128
129 "/holds/{reserve_id}": {
130     "get": {
131         ...,
132         "x-koha-authorization": {
133             "allow-owner": true,
134             "permissions": {
135                 "borrowers": "1"
136             }
137         }
138     }
139 }
140
141 =cut
142
143 sub allow_owner {
144     my ($c, $authorization, $user) = @_;
145
146     return unless $authorization->{'allow-owner'};
147
148     return check_object_ownership($c, $user) if $user and $c;
149 }
150
151 =head3 allow_guarantor
152
153 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
154 guarantees.
155
156 =cut
157
158 sub allow_guarantor {
159     my ($c, $authorization, $user) = @_;
160
161     if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
162         return;
163     }
164
165     my $guarantees = $user->guarantees->as_list;
166     foreach my $guarantee (@{$guarantees}) {
167         return 1 if check_object_ownership($c, $guarantee);
168     }
169 }
170
171 =head3 check_object_ownership
172
173 Determines ownership of an object from request parameters.
174
175 As introducing an endpoint that allows access for object's owner; if the
176 parameter that will be used to determine ownership is not already inside
177 $parameters, add a new subroutine that checks the ownership and extend
178 $parameters to contain a key with parameter_name and a value of a subref to
179 the subroutine that you created.
180
181 =cut
182
183 sub check_object_ownership {
184     my ($c, $user) = @_;
185
186     return if not $c or not $user;
187
188     my $parameters = {
189         accountlines_id => \&_object_ownership_by_accountlines_id,
190         borrowernumber  => \&_object_ownership_by_borrowernumber,
191         checkout_id     => \&_object_ownership_by_checkout_id,
192         reserve_id      => \&_object_ownership_by_reserve_id,
193     };
194
195     foreach my $param ( keys %{ $parameters } ) {
196         my $check_ownership = $parameters->{$param};
197         if ($c->stash($param)) {
198             return &$check_ownership($c, $user, $c->stash($param));
199         }
200         elsif ($c->param($param)) {
201             return &$check_ownership($c, $user, $c->param($param));
202         }
203         elsif ($c->req->json && $c->req->json->{$param}) {
204             return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
205         }
206     }
207 }
208
209 =head3 _object_ownership_by_accountlines_id
210
211 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
212 belongs to C<$user>.
213
214 =cut
215
216 sub _object_ownership_by_accountlines_id {
217     my ($c, $user, $accountlines_id) = @_;
218
219     my $accountline = Koha::Account::Lines->find($accountlines_id);
220     return $accountline && $user->borrowernumber == $accountline->borrowernumber;
221 }
222
223 =head3 _object_ownership_by_borrowernumber
224
225 Compares C<$borrowernumber> to currently logged in C<$user>.
226
227 =cut
228
229 sub _object_ownership_by_borrowernumber {
230     my ($c, $user, $borrowernumber) = @_;
231
232     return $user->borrowernumber == $borrowernumber;
233 }
234
235 =head3 _object_ownership_by_checkout_id
236
237 First, attempts to find a Koha::Issue-object by C<$issue_id>. If we find one,
238 compare its borrowernumber to currently logged in C<$user>. However, if an issue
239 is not found, attempt to find a Koha::OldIssue-object instead and compare its
240 borrowernumber to currently logged in C<$user>.
241
242 =cut
243
244 sub _object_ownership_by_checkout_id {
245     my ($c, $user, $issue_id) = @_;
246
247     my $issue = Koha::Issues->find($issue_id);
248     $issue = Koha::OldIssues->find($issue_id) unless $issue;
249     return $issue && $issue->borrowernumber
250             && $user->borrowernumber == $issue->borrowernumber;
251 }
252
253 =head3 _object_ownership_by_reserve_id
254
255 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
256 belongs to C<$user>.
257
258 TODO: Also compare against old_reserves
259
260 =cut
261
262 sub _object_ownership_by_reserve_id {
263     my ($c, $user, $reserve_id) = @_;
264
265     my $reserve = Koha::Holds->find($reserve_id);
266     return $reserve && $user->borrowernumber == $reserve->borrowernumber;
267 }
268
269 1;