Bugzilla – Attachment 41331 Details for
Bug 13799
Add base for building RESTful API
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 13799: 5. API authentication system - Swagtenticator authentication
Bug-13799-5-API-authentication-system---Swagtentic.patch (text/plain), 13.18 KB, created by
Olli-Antti Kivilahti
on 2015-08-04 07:57:43 UTC
(
hide
)
Description:
Bug 13799: 5. API authentication system - Swagtenticator authentication
Filename:
MIME Type:
Creator:
Olli-Antti Kivilahti
Created:
2015-08-04 07:57:43 UTC
Size:
13.18 KB
patch
obsolete
>From 4569cae6995be80b037309729fb34524b4fee998 Mon Sep 17 00:00:00 2001 >From: Olli-Antti Kivilahti <olli-antti.kivilahti@jns.fi> >Date: Wed, 22 Jul 2015 14:06:44 +0000 >Subject: [PATCH] Bug 13799: 5. API authentication system - Swagtenticator > authentication > >Reads the Swagger2 definitions and defines the API routes and controllers >for Mojolicious. > >Authentiates the API consumer using Koha::Auth::Challenge::RESTV1 >with all the necessary details inferred from Swagger2, like permissions. > >Validates all input to match the Swagger2 definition. > >Authentication is based on the permissions defined in the Swagger2 definition. >Add x-koha-permission to the Operation Object to define needed Koha permissions >to access the resource. > >Eg. > "/borrowers/{borrowernumber}": { > "get": { > "x-mojo-controller": "Koha::REST::V1::Borrowers", > "x-koha-permission": { > "borrowers": "*" > }, > "operationId": "getBorrower", > "tags": ["borrowers"], >--- > C4/Installer/PerlDependencies.pm | 10 ++ > Koha/REST/V1.pm | 28 ++-- > Koha/REST/V1/Plugins/KohaliciousSwagtenticator.pm | 185 ++++++++++++++++++++++ > api/v1/swagger.json | 41 ++++- > 4 files changed, 247 insertions(+), 17 deletions(-) > create mode 100644 Koha/REST/V1/Plugins/KohaliciousSwagtenticator.pm > >diff --git a/C4/Installer/PerlDependencies.pm b/C4/Installer/PerlDependencies.pm >index 9a9fa2d..844ae01 100644 >--- a/C4/Installer/PerlDependencies.pm >+++ b/C4/Installer/PerlDependencies.pm >@@ -49,6 +49,11 @@ our $PERL_DEPS = { > 'required' => '1', > 'min_ver' => '0.21' > }, >+ 'Data::Walk' => { >+ 'usage' => 'Core', >+ 'required' => '1', >+ 'min_ver' => '1.00' >+ }, > 'DBI' => { > 'usage' => 'Core', > 'required' => '1', >@@ -552,6 +557,11 @@ our $PERL_DEPS = { > 'required' => '1', > 'min_ver' => '0.09', > }, >+ 'DateTime::Format::HTTP' => { >+ 'usage' => 'REST API', >+ 'required' => '1', >+ 'min_ver' => '0.42', >+ }, > 'Template::Plugin::HtmlToText' => { > 'usage' => 'Core', > 'required' => '1', >diff --git a/Koha/REST/V1.pm b/Koha/REST/V1.pm >index 746910a..5558245 100644 >--- a/Koha/REST/V1.pm >+++ b/Koha/REST/V1.pm >@@ -3,6 +3,13 @@ package Koha::REST::V1; > use Modern::Perl; > use Mojo::Base 'Mojolicious'; > use Mojo::Log; >+use Mojolicious::Plugins; #Extend the plugin system >+ >+use Koha::Borrower; >+use Koha::Borrowers; >+use Koha::ApiKey; >+use Koha::ApiKeys; >+ > > =head startup > >@@ -58,21 +65,12 @@ sub startup { > $self->setKohaParamLogging(); > $self->setKohaParamConfig(); > >- my $route = $self->routes->under->to( >- cb => sub { >- my $c = shift; >- my $user = $c->param('user'); >- # Do the authentication stuff here... >- $c->stash('user', $user); >- return 1; >- } >- ); >- >- # Force charset=utf8 in Content-Type header for JSON responses >- $self->types->type(json => 'application/json; charset=utf8'); >- >- $self->plugin(Swagger2 => { >- route => $route, >+ >+ ##Add the Koha namespace to the plugin engine to find plugins from. >+ my $plugin = $self->plugins(); >+ push @{$plugin->namespaces}, 'Koha::REST::V1::Plugins'; >+ >+ $self->plugin(KohaliciousSwagtenticator => { > url => $self->home->rel_file("api/v1/swagger.json"), > }); > } >diff --git a/Koha/REST/V1/Plugins/KohaliciousSwagtenticator.pm b/Koha/REST/V1/Plugins/KohaliciousSwagtenticator.pm >new file mode 100644 >index 0000000..0304c50 >--- /dev/null >+++ b/Koha/REST/V1/Plugins/KohaliciousSwagtenticator.pm >@@ -0,0 +1,185 @@ >+package Koha::REST::V1::Plugins::KohaliciousSwagtenticator; >+ >+use Modern::Perl; >+ >+use base qw(Mojolicious::Plugin::Swagger2); >+ >+use Digest::SHA qw(hmac_sha256_hex); >+use Try::Tiny; >+use Scalar::Util qw(blessed); >+use Data::Walk; >+ >+use Koha::Auth; >+ >+use Koha::Exception::BadAuthenticationToken; >+use Koha::Exception::UnknownProgramState; >+use Koha::Exception::NoPermission; >+ >+use constant DEBUG => $ENV{SWAGGER2_DEBUG} || 0; >+ >+ >+ >+################################################################################ >+###################### STARTING OVERLOADING SUBROUTINES ###################### >+################################################################################ >+ >+ >+ >+=head _generate_request_handler >+@OVERLOADS Mojolicious::Plugin::Swagger2::_generate_request_handler() >+This is just a copy-paste of the parent function with a small incision to inject the Koha-authentication mechanism. >+Keep code changes minimal for upstream compatibility, so when problems arise, copy-pasting fixes them! >+ >+=cut >+ >+sub _generate_request_handler { >+ my ($self, $method, $config) = @_; >+ my $controller = $config->{'x-mojo-controller'} || $self->{controller}; # back compat >+ >+ return sub { >+ my $c = shift; >+ my $method_ref; >+ >+ unless (eval "require $controller;1") { >+ $c->app->log->error($@); >+ return $c->render_swagger($self->_not_implemented('Controller not implemented.'), {}, 501); >+ } >+ unless ($method_ref = $controller->can($method)) { >+ $method_ref = $controller->can(sprintf '%s_%s', $method, lc $c->req->method) >+ and warn "HTTP method name is not used in method name lookup anymore!"; >+ } >+ unless ($method_ref) { >+ $c->app->log->error( >+ qq(Can't locate object method "$method" via package "$controller. (Something is wrong in @{[$self->url]})")); >+ return $c->render_swagger($self->_not_implemented('Method not implemented.'), {}, 501); >+ } >+ ######################################### >+ ####### Koha-overload starts here ####### >+ ## Check for user api-key authentication and permissions. >+ my ($error, $data, $statusCode) = _koha_authenticate($c, $config); >+ return $c->render_swagger($error, $data, $statusCode) if $error; >+ ### END OF Koha-overload ### >+ ######################################### >+ >+ bless $c, $controller; # ugly hack? >+ >+ $c->delay( >+ sub { >+ my ($delay) = @_; >+ my ($v, $input) = $self->_validate_input($c, $config); >+ >+ return $c->render_swagger($v, {}, 400) unless $v->{valid}; >+ return $c->$method_ref($input, $delay->begin); >+ }, >+ sub { >+ my $delay = shift; >+ my $data = shift; >+ my $status = shift || 200; >+ my $format = $config->{responses}{$status} || $config->{responses}{default} || {}; >+ my @err = $self->_validator->validate($data, $format->{schema}); >+ >+ return $c->render_swagger({errors => \@err, valid => Mojo::JSON->false}, $data, 500) if @err; >+ return $c->render_swagger({}, $data, $status); >+ }, >+ ); >+ }; >+} >+ >+ >+ >+################################################################################ >+######### END OF OVERLOADED SUBROUTINES, STARTING EXTENDED FEATURES ########## >+################################################################################ >+ >+ >+ >+=head _koha_authenticate >+ >+ _koha_authenticate($c, $config); >+ >+Checks all authentications in Koha, and prepares the data for a >+Mojolicious::Plugin::Swagger2->render_swagger($errors, $data, $statusCode) -response >+if authentication failed for some reason. >+ >+@PARAM1 Mojolicious::Controller or a subclass >+@PARAM2 Reference to HASH, the "Operation Object" from Swagger2.0 specification, >+ matching the given "Path Item Object"'s HTTP Verb. >+@RETURNS List of: HASH Ref, errors encountered >+ HASH Ref, data to be sent >+ String, status code from the Koha::REST::V1::check_key_auth() >+=cut >+ >+sub _koha_authenticate { >+ my ($c, $opObj) = @_; >+ my ($error, $data, $statusCode); #define return values >+ >+ try { >+ >+ my $authParams = {}; >+ $authParams->{authnotrequired} = 1 unless $opObj->{"x-koha-permission"}; >+ Koha::Auth::authenticate($c, $opObj->{"x-koha-permission"}, $authParams); >+ >+ } catch { >+ my $e = $_; >+ if (blessed($e)) { >+ my $swagger2DocumentationUrl = findConfigurationParameterFromAnyConfigurationFile($c->app->config(), 'swagger2DocumentationUrl') || ''; >+ >+ if ($e->isa('Koha::Exception::NoPermission') || >+ $e->isa('Koha::Exception::LoginFailed') || >+ $e->isa('Koha::Exception::UnknownObject') >+ ) { >+ $error = {valid => Mojo::JSON->false, errors => [{message => $e->error, path => $c->req->url->path_query}, >+ {message => "See '$swagger2DocumentationUrl' for how to properly authenticate to Koha"},]}; >+ $data = {header => {"WWW-Authenticate" => "Koha $swagger2DocumentationUrl"}}; >+ $statusCode = 401; #Throw Unauthorized with instructions on how to properly authorize. >+ } >+ elsif ($e->isa('Koha::Exception::BadParameter')) { >+ $error = {valid => Mojo::JSON->false, errors => [{message => $e->error, path => $c->req->url->path_query}]}; >+ $data = {}; >+ $statusCode = 400; #Throw a Bad Request >+ } >+ elsif ($e->isa('Koha::Exception::VersionMismatch') || >+ $e->isa('Koha::Exception::BadSystemPreference') || >+ $e->isa('Koha::Exception::ServiceTemporarilyUnavailable') >+ ){ >+ $error = {valid => Mojo::JSON->false, errors => [{message => $e->error, path => $c->req->url->path_query}]}; >+ $data = {}; >+ $statusCode = 503; #Throw Service Unavailable, but will be available later. >+ } >+ else { >+ die $e; >+ } >+ } >+ else { >+ die $e; >+ } >+ }; >+ return ($error, $data, $statusCode); >+} >+ >+=head findConfigurationParameterFromAnyConfigurationFile >+ >+Because we can use this REST API with CGI, or Plack, or Hypnotoad, or Morbo, ... >+We cannot know which configuration file we are currently using. >+$conf = {hypnotoad => {#conf params}, >+ plack => {#conf params}, >+ ... >+ } >+So find the needed markers from any configuration file. >+=cut >+ >+sub findConfigurationParameterFromAnyConfigurationFile { >+ my ($conf, $paramLookingFor) = @_; >+ >+ my $found; >+ my $wanted = sub { >+ if ($_ eq $paramLookingFor) { >+ $found = $Data::Walk::container->{$_}; >+ return (); >+ } >+ }; >+ Data::Walk::walk( $wanted, $conf); >+ return $found; >+} >+ >+return 1; >\ No newline at end of file >diff --git a/api/v1/swagger.json b/api/v1/swagger.json >index 5e97d03..38e9541 100644 >--- a/api/v1/swagger.json >+++ b/api/v1/swagger.json >@@ -17,8 +17,13 @@ > "/borrowers": { > "get": { > "x-mojo-controller": "Koha::REST::V1::Borrowers", >+ "x-koha-permission": { >+ "borrowers": "*" >+ }, > "operationId": "listBorrowers", > "tags": ["borrowers"], >+ "summary": "just a summary", >+ "description": "long description", > "produces": [ > "application/json" > ], >@@ -32,12 +37,18 @@ > } > } > } >- } >+ }, >+ "security": [ >+ { "multi_key_auth": [] } >+ ] > } > }, > "/borrowers/{borrowernumber}": { > "get": { > "x-mojo-controller": "Koha::REST::V1::Borrowers", >+ "x-koha-permission": { >+ "borrowers": "*" >+ }, > "operationId": "getBorrower", > "tags": ["borrowers"], > "parameters": [ >@@ -61,7 +72,10 @@ > "$ref": "#/definitions/error" > } > } >- } >+ }, >+ "security": [ >+ { "multi_key_auth": [] } >+ ] > } > } > }, >@@ -104,5 +118,28 @@ > "required": "true", > "type": "integer" > } >+ }, >+ "securityDefinitions": { >+ "multi_key_auth": { >+ "type": "custom", >+ "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", >+ "keys": { >+ "X-Koha_Date": { >+ "type": "dateTime", >+ "in": "header", >+ "description": "The current time when the request is created. The standard HTTP Date header complying to RFC 1123" >+ }, >+ "Authorization": { >+ "type": "string", >+ "in": "header", >+ "description": "Starts with identifier 'Koha', then you give the userid/cardnumber of the user authenticating and finally the hashed signature." >+ }, >+ "x-koha-permission": { >+ "type": "object", >+ "in": "not part of the request", >+ "description": "The specific permission the user must have. Eg. 'circulation => force_checkout'. Only we can grant these permissions." >+ } >+ } >+ } > } > } >-- >1.9.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 13799
:
36700
|
36705
|
36938
|
36941
|
37119
|
37132
|
37172
|
38246
|
39156
|
39158
|
39159
|
39160
|
39161
|
39162
|
39163
|
39164
|
40088
|
40092
|
40093
|
40094
|
40098
|
40099
|
40101
|
40140
|
40141
|
40142
|
40463
|
40464
|
40465
|
40466
|
40467
|
40468
|
40469
|
40470
|
40471
|
40551
|
40552
|
40553
|
40554
|
40560
|
40561
|
40584
|
40585
|
40586
|
40587
|
40616
|
40617
|
40618
|
40619
|
41160
|
41161
|
41162
|
41268
|
41269
|
41270
|
41271
|
41272
|
41273
|
41274
|
41320
|
41321
|
41326
|
41327
|
41328
|
41329
|
41330
|
41331
|
41538
|
41539
|
41540
|
41541
|
41542
|
41543
|
41547
|
41554
|
42044
|
42085
|
42086
|
42123
|
42518
|
42766
|
42768
|
42801
|
42874
|
42875
|
42876
|
42877
|
42878
|
43186
|
43187
|
43189
|
43190
|
43191
|
43192
|
43204
|
43205
|
43206
|
43207
|
43208
|
43209
|
43210
|
43211
|
43212
|
43213
|
43214
|
43980
|
43981
|
43987
|
43989
|
43990
|
43991
|
43992
|
43993
|
43994
|
43995
|
43996
|
43997
|
43998
|
43999
|
44000
|
44001
|
44185
|
44285
|
44313
|
44375
|
44376
|
44377
|
44378
|
44379
|
44380
|
44381
|
44382
|
44383
|
44384
|
44385