Line 0
Link Here
|
|
|
1 |
package Koha::REST::V1::Plugins::KohaliciousSwagtenticator; |
2 |
|
3 |
use Modern::Perl; |
4 |
|
5 |
use base qw(Mojolicious::Plugin::Swagger2); |
6 |
|
7 |
use Digest::SHA qw(hmac_sha256_hex); |
8 |
use Try::Tiny; |
9 |
use Scalar::Util qw(blessed); |
10 |
use Data::Walk; |
11 |
|
12 |
use Koha::Auth; |
13 |
|
14 |
use Koha::Exception::BadAuthenticationToken; |
15 |
use Koha::Exception::UnknownProgramState; |
16 |
use Koha::Exception::NoPermission; |
17 |
|
18 |
use constant DEBUG => $ENV{SWAGGER2_DEBUG} || 0; |
19 |
|
20 |
|
21 |
|
22 |
################################################################################ |
23 |
###################### STARTING OVERLOADING SUBROUTINES ###################### |
24 |
################################################################################ |
25 |
|
26 |
|
27 |
|
28 |
=head _generate_request_handler |
29 |
@OVERLOADS Mojolicious::Plugin::Swagger2::_generate_request_handler() |
30 |
This is just a copy-paste of the parent function with a small incision to inject the Koha-authentication mechanism. |
31 |
Keep code changes minimal for upstream compatibility, so when problems arise, copy-pasting fixes them! |
32 |
|
33 |
=cut |
34 |
|
35 |
sub _generate_request_handler { |
36 |
my ($self, $method, $config) = @_; |
37 |
my $controller = $config->{'x-mojo-controller'} || $self->{controller}; # back compat |
38 |
|
39 |
return sub { |
40 |
my $c = shift; |
41 |
my $method_ref; |
42 |
|
43 |
unless (eval "require $controller;1") { |
44 |
$c->app->log->error($@); |
45 |
return $c->render_swagger($self->_not_implemented('Controller not implemented.'), {}, 501); |
46 |
} |
47 |
unless ($method_ref = $controller->can($method)) { |
48 |
$method_ref = $controller->can(sprintf '%s_%s', $method, lc $c->req->method) |
49 |
and warn "HTTP method name is not used in method name lookup anymore!"; |
50 |
} |
51 |
unless ($method_ref) { |
52 |
$c->app->log->error( |
53 |
qq(Can't locate object method "$method" via package "$controller. (Something is wrong in @{[$self->url]})")); |
54 |
return $c->render_swagger($self->_not_implemented('Method not implemented.'), {}, 501); |
55 |
} |
56 |
######################################### |
57 |
####### Koha-overload starts here ####### |
58 |
## Check for user api-key authentication and permissions. |
59 |
my ($error, $data, $statusCode) = _koha_authenticate($c, $config); |
60 |
return $c->render_swagger($error, $data, $statusCode) if $error; |
61 |
### END OF Koha-overload ### |
62 |
######################################### |
63 |
|
64 |
bless $c, $controller; # ugly hack? |
65 |
|
66 |
$c->delay( |
67 |
sub { |
68 |
my ($delay) = @_; |
69 |
my ($v, $input) = $self->_validate_input($c, $config); |
70 |
|
71 |
return $c->render_swagger($v, {}, 400) unless $v->{valid}; |
72 |
return $c->$method_ref($input, $delay->begin); |
73 |
}, |
74 |
sub { |
75 |
my $delay = shift; |
76 |
my $data = shift; |
77 |
my $status = shift || 200; |
78 |
my $format = $config->{responses}{$status} || $config->{responses}{default} || {}; |
79 |
my @err = $self->_validator->validate($data, $format->{schema}); |
80 |
|
81 |
return $c->render_swagger({errors => \@err, valid => Mojo::JSON->false}, $data, 500) if @err; |
82 |
return $c->render_swagger({}, $data, $status); |
83 |
}, |
84 |
); |
85 |
}; |
86 |
} |
87 |
|
88 |
|
89 |
|
90 |
=head _validate_input |
91 |
@OVERLOADS Mojolicious::Plugin::Swagger2::_validate_input() |
92 |
|
93 |
Validates the parameters from the "Operation Object" from Swagger2 specification. |
94 |
Overloading to allow OPTIONAL parameters. |
95 |
=cut |
96 |
|
97 |
sub _validate_input { |
98 |
my ($self, $c, $config) = @_; |
99 |
my $headers = $c->req->headers; |
100 |
my $query = $c->req->url->query; |
101 |
my (%input, %v); |
102 |
|
103 |
for my $p (@{$config->{parameters} || []}) { |
104 |
my ($in, $name) = @$p{qw( in name )}; |
105 |
my ($value, @e); |
106 |
|
107 |
$value |
108 |
= $in eq 'query' ? $query->param($name) |
109 |
: $in eq 'path' ? $c->stash($name) |
110 |
: $in eq 'header' ? $headers->header($name) |
111 |
: $in eq 'body' ? $c->req->json |
112 |
: $in eq 'formData' ? $c->req->body_params->to_hash |
113 |
: "Invalid 'in' for parameter $name in schema definition"; |
114 |
|
115 |
if (defined $value or $p->{required}) { |
116 |
my $type = $p->{type} || 'object'; |
117 |
$value += 0 if $type =~ /^(?:integer|number)/ and $value =~ /^\d/; |
118 |
$value = ($value eq 'false' or !$value) ? Mojo::JSON->false : Mojo::JSON->true if $type eq 'boolean'; |
119 |
|
120 |
if ($in eq 'body' or $in eq 'formData') { |
121 |
warn "[Swagger2] Validate $in @{[$c->req->body]}\n"; |
122 |
push @e, map { $_->{path} = "/$name$_->{path}"; $_; } $self->_validator->validate($value, $p->{schema}); |
123 |
} |
124 |
else { |
125 |
warn "[Swagger2] Validate $in $name=$value\n"; |
126 |
push @e, $self->_validator->validate({$name => $value}, {properties => {$name => $p}}); |
127 |
} |
128 |
} |
129 |
|
130 |
$input{$name} = $value unless @e; |
131 |
push @{$v{errors}}, @e; |
132 |
} |
133 |
|
134 |
$v{valid} = @{$v{errors} || []} ? Mojo::JSON->false : Mojo::JSON->true; |
135 |
return \%v, \%input; |
136 |
} |
137 |
|
138 |
|
139 |
|
140 |
################################################################################ |
141 |
######### END OF OVERLOADED SUBROUTINES, STARTING EXTENDED FEATURES ########## |
142 |
################################################################################ |
143 |
|
144 |
|
145 |
|
146 |
=head _koha_authenticate |
147 |
|
148 |
_koha_authenticate($c, $config); |
149 |
|
150 |
Checks all authentications in Koha, and prepares the data for a |
151 |
Mojolicious::Plugin::Swagger2->render_swagger($errors, $data, $statusCode) -response |
152 |
if authentication failed for some reason. |
153 |
|
154 |
@PARAM1 Mojolicious::Controller or a subclass |
155 |
@PARAM2 Reference to HASH, the "Operation Object" from Swagger2.0 specification, |
156 |
matching the given "Path Item Object"'s HTTP Verb. |
157 |
@RETURNS List of: HASH Ref, errors encountered |
158 |
HASH Ref, data to be sent |
159 |
String, status code from the Koha::REST::V1::check_key_auth() |
160 |
=cut |
161 |
|
162 |
sub _koha_authenticate { |
163 |
my ($c, $opObj) = @_; |
164 |
my ($error, $data, $statusCode); #define return values |
165 |
|
166 |
try { |
167 |
|
168 |
my $authParams = {}; |
169 |
$authParams->{authnotrequired} = 1 unless $opObj->{"x-koha-permission"}; |
170 |
Koha::Auth::authenticate($c, $opObj->{"x-koha-permission"}, $authParams); |
171 |
|
172 |
} catch { |
173 |
my $e = $_; |
174 |
if (blessed($e)) { |
175 |
my $swagger2DocumentationUrl = findConfigurationParameterFromAnyConfigurationFile($c->app->config(), 'swagger2DocumentationUrl'); |
176 |
|
177 |
#my $apiSpecificationUrl = $c-> |
178 |
if ($e->isa('Koha::Exception::NoPermission') || |
179 |
$e->isa('Koha::Exception::LoginFailed') || |
180 |
$e->isa('Koha::Exception::UnknownObject') |
181 |
) { |
182 |
$error = {valid => Mojo::JSON->false, errors => [{message => $e->error, path => $c->req->url->path_query}, |
183 |
{message => "See '$swagger2DocumentationUrl' for how to properly authenticate to Koha"},]}; |
184 |
$data = {header => {"WWW-Authenticate" => "Koha $swagger2DocumentationUrl"}}; |
185 |
$statusCode = 401; #Throw Unauthorized with instructions on how to properly authorize. |
186 |
} |
187 |
if ($e->isa('Koha::Exception::BadParameter')) { |
188 |
$error = {valid => Mojo::JSON->false, errors => [{message => $e->error, path => $c->req->url->path_query}]}; |
189 |
$data = {}; |
190 |
$statusCode = 400; #Throw a Bad Request |
191 |
} |
192 |
elsif ($e->isa('Koha::Exception::VersionMismatch') || |
193 |
$e->isa('Koha::Exception::BadSystemPreference') || |
194 |
$e->isa('Koha::Exception::ServiceTemporarilyUnavailable') |
195 |
){ |
196 |
$error = {valid => Mojo::JSON->false, errors => [{message => $e->error, path => $c->req->url->path_query}]}; |
197 |
$data = {}; |
198 |
$statusCode = 503; #Throw Service Unavailable, but will be available later. |
199 |
} |
200 |
else { |
201 |
die $e; |
202 |
} |
203 |
} |
204 |
else { |
205 |
die $e; |
206 |
} |
207 |
}; |
208 |
return ($error, $data, $statusCode); |
209 |
} |
210 |
|
211 |
=head findConfigurationParameterFromAnyConfigurationFile |
212 |
|
213 |
Because we can use this REST API with CGI, or Plack, or Hypnotoad, or Morbo, ... |
214 |
We cannot know which configuration file we are currently using. |
215 |
$conf = {hypnotoad => {#conf params}, |
216 |
plack => {#conf params}, |
217 |
... |
218 |
} |
219 |
So find the needed markers from any configuration file. |
220 |
=cut |
221 |
|
222 |
sub findConfigurationParameterFromAnyConfigurationFile { |
223 |
my ($conf, $paramLookingFor) = @_; |
224 |
|
225 |
my $found; |
226 |
my $wanted = sub { |
227 |
if ($_ eq $paramLookingFor) { |
228 |
$found = $Data::Walk::container->{$_}; |
229 |
return (); |
230 |
} |
231 |
}; |
232 |
Data::Walk::walk( $wanted, $conf); |
233 |
return $found; |
234 |
} |
235 |
|
236 |
return 1; |