Bug 14187 - DBRev 16.12.00.002
[koha.git] / Koha / REST / V1.pm
blobe307552ef2cd4ca5419d094d56d481bbcc3ad72c
1 package Koha::REST::V1;
3 # This file is part of Koha.
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.
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.
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.
18 use Modern::Perl;
19 use Mojo::Base 'Mojolicious';
21 use C4::Auth qw( check_cookie_auth get_session haspermission );
22 use C4::Context;
23 use Koha::Account::Lines;
24 use Koha::Checkouts;
25 use Koha::Holds;
26 use Koha::Old::Checkouts;
27 use Koha::Patrons;
29 =head1 NAME
31 Koha::REST::V1 - Main v.1 REST api class
33 =head1 API
35 =head2 Class Methods
37 =head3 startup
39 Overloaded Mojolicious->startup method. It is called at application startup.
41 =cut
43 sub startup {
44 my $self = shift;
46 # Force charset=utf8 in Content-Type header for JSON responses
47 $self->types->type(json => 'application/json; charset=utf8');
49 my $secret_passphrase = C4::Context->config('api_secret_passphrase');
50 if ($secret_passphrase) {
51 $self->secrets([$secret_passphrase]);
54 $self->plugin(Swagger2 => {
55 url => $self->home->rel_file("api/v1/swagger/swagger.json"),
56 validate => 1,
57 spec_path => '/spec'
58 });
61 =head3 authenticate_api_request
63 Validates authentication and allows access if authorization is not required or
64 if authorization is required and user has required permissions to access.
66 This subroutine is called before every request to API.
68 =cut
70 sub authenticate_api_request {
71 my ($next, $c, $action_spec) = @_;
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);
86 else {
87 return $c->render_swagger(
88 { error => "Authentication failure." },
89 {},
90 401
91 ) if $cookie and $action_spec->{'x-koha-authorization'};
95 # Then check the parameters
96 my @query_errors = validate_query_parameters( $c, $action_spec );
98 # We do not need any authorization
99 unless ( $action_spec->{'x-koha-authorization'} ) {
100 return @query_errors
101 ? $c->render_swagger({}, \@query_errors, 400)
102 : $next->($c);
105 unless ($user) {
106 return $c->render_swagger({ error => "Authentication required." },{},401);
109 my $authorization = $action_spec->{'x-koha-authorization'};
110 my $permissions = $authorization->{'permissions'};
112 # Check if the user is authorized
113 if ( C4::Auth::haspermission($user->userid, $permissions)
114 or allow_owner($c, $authorization, $user)
115 or allow_guarantor($c, $authorization, $user) ) {
117 # Return the query errors if exist
118 return $c->render_swagger({}, \@query_errors, 400) if @query_errors;
120 # Everything is ok
121 return $next->($c)
124 return $c->render_swagger(
125 { error => "Authorization failure. Missing required permission(s).",
126 required_permissions => $permissions },
132 sub validate_query_parameters {
133 my ( $c, $action_spec ) = @_;
135 # Check for malformed query parameters
136 my @errors;
137 my %valid_parameters = map { ( $_->{in} eq 'query' ) ? ( $_->{name} => 1 ) : () } @{ $action_spec->{parameters} };
138 my $existing_params = $c->req->query_params->to_hash;
139 for my $param ( keys %{$existing_params} ) {
140 push @errors, { path => "/query/" . $param, message => 'Malformed query string' } unless exists $valid_parameters{$param};
142 return @errors;
146 =head3 allow_owner
148 Allows access to object for its owner.
150 There are endpoints that should allow access for the object owner even if they
151 do not have the required permission, e.g. access an own reserve. This can be
152 achieved by defining the operation as follows:
154 "/holds/{reserve_id}": {
155 "get": {
156 ...,
157 "x-koha-authorization": {
158 "allow-owner": true,
159 "permissions": {
160 "borrowers": "1"
166 =cut
168 sub allow_owner {
169 my ($c, $authorization, $user) = @_;
171 return unless $authorization->{'allow-owner'};
173 return check_object_ownership($c, $user) if $user and $c;
176 =head3 allow_guarantor
178 Same as "allow_owner", but checks if the object is owned by one of C<$user>'s
179 guarantees.
181 =cut
183 sub allow_guarantor {
184 my ($c, $authorization, $user) = @_;
186 if (!$c || !$user || !$authorization || !$authorization->{'allow-guarantor'}){
187 return;
190 my $guarantees = $user->guarantees->as_list;
191 foreach my $guarantee (@{$guarantees}) {
192 return 1 if check_object_ownership($c, $guarantee);
196 =head3 check_object_ownership
198 Determines ownership of an object from request parameters.
200 As introducing an endpoint that allows access for object's owner; if the
201 parameter that will be used to determine ownership is not already inside
202 $parameters, add a new subroutine that checks the ownership and extend
203 $parameters to contain a key with parameter_name and a value of a subref to
204 the subroutine that you created.
206 =cut
208 sub check_object_ownership {
209 my ($c, $user) = @_;
211 return if not $c or not $user;
213 my $parameters = {
214 accountlines_id => \&_object_ownership_by_accountlines_id,
215 borrowernumber => \&_object_ownership_by_borrowernumber,
216 checkout_id => \&_object_ownership_by_checkout_id,
217 reserve_id => \&_object_ownership_by_reserve_id,
220 foreach my $param ( keys %{ $parameters } ) {
221 my $check_ownership = $parameters->{$param};
222 if ($c->stash($param)) {
223 return &$check_ownership($c, $user, $c->stash($param));
225 elsif ($c->param($param)) {
226 return &$check_ownership($c, $user, $c->param($param));
228 elsif ($c->req->json && $c->req->json->{$param}) {
229 return 1 if &$check_ownership($c, $user, $c->req->json->{$param});
234 =head3 _object_ownership_by_accountlines_id
236 Finds a Koha::Account::Line-object by C<$accountlines_id> and checks if it
237 belongs to C<$user>.
239 =cut
241 sub _object_ownership_by_accountlines_id {
242 my ($c, $user, $accountlines_id) = @_;
244 my $accountline = Koha::Account::Lines->find($accountlines_id);
245 return $accountline && $user->borrowernumber == $accountline->borrowernumber;
248 =head3 _object_ownership_by_borrowernumber
250 Compares C<$borrowernumber> to currently logged in C<$user>.
252 =cut
254 sub _object_ownership_by_borrowernumber {
255 my ($c, $user, $borrowernumber) = @_;
257 return $user->borrowernumber == $borrowernumber;
260 =head3 _object_ownership_by_checkout_id
262 First, attempts to find a Koha::Checkout-object by C<$issue_id>. If we find one,
263 compare its borrowernumber to currently logged in C<$user>. However, if an issue
264 is not found, attempt to find a Koha::Old::Checkout-object instead and compare its
265 borrowernumber to currently logged in C<$user>.
267 =cut
269 sub _object_ownership_by_checkout_id {
270 my ($c, $user, $issue_id) = @_;
272 my $issue = Koha::Checkouts->find($issue_id);
273 $issue = Koha::Old::Checkouts->find($issue_id) unless $issue;
274 return $issue && $issue->borrowernumber
275 && $user->borrowernumber == $issue->borrowernumber;
278 =head3 _object_ownership_by_reserve_id
280 Finds a Koha::Hold-object by C<$reserve_id> and checks if it
281 belongs to C<$user>.
283 TODO: Also compare against old_reserves
285 =cut
287 sub _object_ownership_by_reserve_id {
288 my ($c, $user, $reserve_id) = @_;
290 my $reserve = Koha::Holds->find($reserve_id);
291 return $reserve && $user->borrowernumber == $reserve->borrowernumber;