View | Details | Raw Unified | Return to bug 13920
Collapse All | Expand All

(-)a/C4/Installer/PerlDependencies.pm (+10 lines)
Lines 49-54 our $PERL_DEPS = { Link Here
49
        'required' => '1',
49
        'required' => '1',
50
        'min_ver'  => '0.21'
50
        'min_ver'  => '0.21'
51
    },
51
    },
52
    'Data::Walk' => {
53
        'usage'    => 'Core',
54
        'required' => '1',
55
        'min_ver'  => '1.00'
56
    },
52
    'DBI' => {
57
    'DBI' => {
53
        'usage'    => 'Core',
58
        'usage'    => 'Core',
54
        'required' => '1',
59
        'required' => '1',
Lines 542-547 our $PERL_DEPS = { Link Here
542
        'required' => '1',
547
        'required' => '1',
543
        'min_ver'  => '0.09',
548
        'min_ver'  => '0.09',
544
    },
549
    },
550
    'DateTime::Format::HTTP' => {
551
        'usage'    => 'REST API',
552
        'required' => '1',
553
        'min_ver'  => '0.42',
554
    },
545
    'Template::Plugin::HtmlToText' => {
555
    'Template::Plugin::HtmlToText' => {
546
        'usage'    => 'Core',
556
        'usage'    => 'Core',
547
        'required' => '1',
557
        'required' => '1',
(-)a/Koha/REST/V1.pm (-15 / +13 lines)
Lines 3-8 package Koha::REST::V1; Link Here
3
use Modern::Perl;
3
use Modern::Perl;
4
use Mojo::Base 'Mojolicious';
4
use Mojo::Base 'Mojolicious';
5
use Mojo::Log;
5
use Mojo::Log;
6
use Mojolicious::Plugins; #Extend the plugin system
7
8
use Koha::Borrower;
9
use Koha::Borrowers;
10
use Koha::ApiKey;
11
use Koha::ApiKeys;
12
6
13
7
=head startup
14
=head startup
8
15
Lines 58-78 sub startup { Link Here
58
    $self->setKohaParamLogging();
65
    $self->setKohaParamLogging();
59
    $self->setKohaParamConfig();
66
    $self->setKohaParamConfig();
60
67
61
    my $route = $self->routes->under->to(
68
62
        cb => sub {
69
    ##Add the Koha namespace to the plugin engine to find plugins from.
63
            my $c = shift;
70
    my $plugin = $self->plugins();
64
            my $user = $c->param('user');
71
    push @{$plugin->namespaces}, 'Koha::REST::V1::Plugins';
65
            # Do the authentication stuff here...
72
66
            $c->stash('user', $user);
73
    $self->plugin(KohaliciousSwagtenticator => {
67
            return 1;
68
        }
69
    );
70
71
    # Force charset=utf8 in Content-Type header for JSON responses
72
    $self->types->type(json => 'application/json; charset=utf8');
73
74
    $self->plugin(Swagger2 => {
75
        route => $route,
76
        url => $self->home->rel_file("api/v1/swagger.json"),
74
        url => $self->home->rel_file("api/v1/swagger.json"),
77
    });
75
    });
78
}
76
}
(-)a/Koha/REST/V1/Plugins/KohaliciousSwagtenticator.pm (+236 lines)
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;
(-)a/api/v1/swagger.json (-3 / +39 lines)
Lines 17-24 Link Here
17
    "/borrowers": {
17
    "/borrowers": {
18
      "get": {
18
      "get": {
19
        "x-mojo-controller": "Koha::REST::V1::Borrowers",
19
        "x-mojo-controller": "Koha::REST::V1::Borrowers",
20
        "x-koha-permission": {
21
          "borrowers": "*"
22
        },
20
        "operationId": "listBorrowers",
23
        "operationId": "listBorrowers",
21
        "tags": ["borrowers"],
24
        "tags": ["borrowers"],
25
        "summary": "just a summary",
26
        "description": "long description",
22
        "produces": [
27
        "produces": [
23
          "application/json"
28
          "application/json"
24
        ],
29
        ],
Lines 32-43 Link Here
32
              }
37
              }
33
            }
38
            }
34
          }
39
          }
35
        }
40
        },
41
        "security": [
42
          { "multi_key_auth": [] }
43
        ]
36
      }
44
      }
37
    },
45
    },
38
    "/borrowers/{borrowernumber}": {
46
    "/borrowers/{borrowernumber}": {
39
      "get": {
47
      "get": {
40
        "x-mojo-controller": "Koha::REST::V1::Borrowers",
48
        "x-mojo-controller": "Koha::REST::V1::Borrowers",
49
        "x-koha-permission": {
50
          "borrowers": "*"
51
        },
41
        "operationId": "getBorrower",
52
        "operationId": "getBorrower",
42
        "tags": ["borrowers"],
53
        "tags": ["borrowers"],
43
        "parameters": [
54
        "parameters": [
Lines 61-67 Link Here
61
              "$ref": "#/definitions/error"
72
              "$ref": "#/definitions/error"
62
            }
73
            }
63
          }
74
          }
64
        }
75
        },
76
        "security": [
77
          { "multi_key_auth": [] }
78
        ]
65
      }
79
      }
66
    },
80
    },
67
    "/borrowers/{borrowernumber}/issues": {
81
    "/borrowers/{borrowernumber}/issues": {
Lines 466-470 Link Here
466
      "required": "true",
480
      "required": "true",
467
      "type": "integer"
481
      "type": "integer"
468
    }
482
    }
483
  },
484
  "securityDefinitions": {
485
    "multi_key_auth": {
486
      "type": "custom",
487
      "description": "Example: 'Authorization: Koha 1:0f049b5ba2f04da7e719b7166dd9e1b0efacf23747798f19efe51eb6e437f84c'\n\nConstructing the Authorization header\n\n-You brand the authorization header with 'Koha'\n-Then you give the userid/cardnumber of the user authenticating.\n-Then the hashed signature.\n\nThe signature is a HMAC-SHA256-HEX hash of several elements of the request,\nseparated by spaces:\n - HTTP method (uppercase)\n - userid/cardnumber\n - X-Koha-Date-header\nSigned with the Borrowers API key\n\n\nPseudocode example:\n\nSignature = HMAC-SHA256-HEX('HTTPS' + ' ' +\n                            '/api/v1/borrowers/12?howdoyoudo=voodoo' + ' ' +\n                            'admin69' + ' ' +\n                            '760818212' + ' ' +\n                            'frJIUN8DYpKDtOLCwo//yllqDzg='\n                           );\n",
488
      "keys": {
489
        "X-Koha_Date": {
490
          "type": "dateTime",
491
          "in": "header",
492
          "description": "The current time when the request is created. The standard HTTP Date header complying to RFC 1123"
493
        },
494
        "Authorization": {
495
          "type": "string",
496
          "in": "header",
497
          "description": "Starts with identifier 'Koha', then you give the userid/cardnumber of the user authenticating and finally the hashed signature."
498
        },
499
        "x-koha-permission": {
500
          "type": "object",
501
          "in": "not part of the request",
502
          "description": "The specific permission the user must have. Eg. 'circulation => force_checkout'. Only we can grant these permissions."
503
        }
504
      }
505
    }
469
  }
506
  }
470
}
507
}
471
- 

Return to bug 13920