View | Details | Raw Unified | Return to bug 13799
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 (+185 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
################################################################################
91
#########  END OF OVERLOADED SUBROUTINES, STARTING EXTENDED FEATURES  ##########
92
################################################################################
93
94
95
96
=head _koha_authenticate
97
98
    _koha_authenticate($c, $config);
99
100
Checks all authentications in Koha, and prepares the data for a
101
Mojolicious::Plugin::Swagger2->render_swagger($errors, $data, $statusCode) -response
102
if authentication failed for some reason.
103
104
@PARAM1 Mojolicious::Controller or a subclass
105
@PARAM2 Reference to HASH, the "Operation Object" from Swagger2.0 specification,
106
                            matching the given "Path Item Object"'s HTTP Verb.
107
@RETURNS List of: HASH Ref, errors encountered
108
                  HASH Ref, data to be sent
109
                  String, status code from the Koha::REST::V1::check_key_auth()
110
=cut
111
112
sub _koha_authenticate {
113
    my ($c, $opObj) = @_;
114
    my ($error, $data, $statusCode); #define return values
115
116
    try {
117
118
        my $authParams = {};
119
        $authParams->{authnotrequired} = 1 unless $opObj->{"x-koha-permission"};
120
        Koha::Auth::authenticate($c, $opObj->{"x-koha-permission"}, $authParams);
121
122
    } catch {
123
      my $e = $_;
124
      if (blessed($e)) {
125
        my $swagger2DocumentationUrl = findConfigurationParameterFromAnyConfigurationFile($c->app->config(), 'swagger2DocumentationUrl') || '';
126
127
        if ($e->isa('Koha::Exception::NoPermission') ||
128
            $e->isa('Koha::Exception::LoginFailed') ||
129
            $e->isa('Koha::Exception::UnknownObject')
130
           ) {
131
          $error = {valid => Mojo::JSON->false, errors => [{message => $e->error, path => $c->req->url->path_query},
132
                                                           {message => "See '$swagger2DocumentationUrl' for how to properly authenticate to Koha"},]};
133
          $data = {header => {"WWW-Authenticate" => "Koha $swagger2DocumentationUrl"}};
134
          $statusCode = 401; #Throw Unauthorized with instructions on how to properly authorize.
135
        }
136
        elsif ($e->isa('Koha::Exception::BadParameter')) {
137
          $error = {valid => Mojo::JSON->false, errors => [{message => $e->error, path => $c->req->url->path_query}]};
138
          $data = {};
139
          $statusCode = 400; #Throw a Bad Request
140
        }
141
        elsif ($e->isa('Koha::Exception::VersionMismatch') ||
142
               $e->isa('Koha::Exception::BadSystemPreference') ||
143
               $e->isa('Koha::Exception::ServiceTemporarilyUnavailable')
144
              ){
145
          $error = {valid => Mojo::JSON->false, errors => [{message => $e->error, path => $c->req->url->path_query}]};
146
          $data = {};
147
          $statusCode = 503; #Throw Service Unavailable, but will be available later.
148
        }
149
        else {
150
          die $e;
151
        }
152
      }
153
      else {
154
        die $e;
155
      }
156
    };
157
    return ($error, $data, $statusCode);
158
}
159
160
=head findConfigurationParameterFromAnyConfigurationFile
161
162
Because we can use this REST API with CGI, or Plack, or Hypnotoad, or Morbo, ...
163
We cannot know which configuration file we are currently using.
164
$conf = {hypnotoad => {#conf params},
165
         plack     => {#conf params},
166
         ...
167
        }
168
So find the needed markers from any configuration file.
169
=cut
170
171
sub findConfigurationParameterFromAnyConfigurationFile {
172
  my ($conf, $paramLookingFor) = @_;
173
174
  my $found;
175
  my $wanted = sub {
176
    if ($_ eq $paramLookingFor) {
177
      $found = $Data::Walk::container->{$_};
178
      return ();
179
    }
180
  };
181
  Data::Walk::walk( $wanted, $conf);
182
  return $found;
183
}
184
185
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
  },
81
  },
Lines 104-108 Link Here
104
      "required": "true",
118
      "required": "true",
105
      "type": "integer"
119
      "type": "integer"
106
    }
120
    }
121
  },
122
  "securityDefinitions": {
123
    "multi_key_auth": {
124
      "type": "custom",
125
      "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",
126
      "keys": {
127
        "X-Koha_Date": {
128
          "type": "dateTime",
129
          "in": "header",
130
          "description": "The current time when the request is created. The standard HTTP Date header complying to RFC 1123"
131
        },
132
        "Authorization": {
133
          "type": "string",
134
          "in": "header",
135
          "description": "Starts with identifier 'Koha', then you give the userid/cardnumber of the user authenticating and finally the hashed signature."
136
        },
137
        "x-koha-permission": {
138
          "type": "object",
139
          "in": "not part of the request",
140
          "description": "The specific permission the user must have. Eg. 'circulation => force_checkout'. Only we can grant these permissions."
141
        }
142
      }
143
    }
107
  }
144
  }
108
}
145
}
109
- 

Return to bug 13799