From 7787784b8023334abde10987392dac0b49abca3c Mon Sep 17 00:00:00 2001 From: Tomas Cohen Arazi Date: Mon, 23 Jul 2018 11:14:47 -0300 Subject: [PATCH] Bug 21116: Add API routes through plugins This patch adds plugins the capability of injecting new routes on the API. The plugins should provide the following methods to be considered valid API-generating plugins: - 'api_routes': returning the 'path' component of the OpenAPI specification corresponding to the routes served by the plugin - 'api_namespace': it should return a namespace to be used for grouping the endpoints provided by the plugin otherwise, they will be just skipped. All plugin-generated routes will be added the 'contrib' namespace, and will end up placed inside /contrib/, where is what the 'api_namespace' returns. A sample endpoint will be added to the Kitchen Sink plugin, and tests are being written. To test, wait a bit more for those samples :-D Signed-off-by: Benjamin Rokseth --- Koha/Exceptions/Plugin.pm | 84 +++++++++++++++++++++++ Koha/REST/Plugin/PluginRoutes.pm | 140 +++++++++++++++++++++++++++++++++++++++ Koha/REST/V1.pm | 37 +++++++++-- 3 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 Koha/Exceptions/Plugin.pm create mode 100644 Koha/REST/Plugin/PluginRoutes.pm diff --git a/Koha/Exceptions/Plugin.pm b/Koha/Exceptions/Plugin.pm new file mode 100644 index 0000000..5348744 --- /dev/null +++ b/Koha/Exceptions/Plugin.pm @@ -0,0 +1,84 @@ +package Koha::Exceptions::Plugin; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Koha::Exceptions::Exception; + +use Exception::Class ( + 'Koha::Exceptions::Plugin' => { + isa => 'Koha::Exceptions::Exception', + }, + 'Koha::Exceptions::Plugin::ForbiddenAction' => { + isa => 'Koha::Exceptions::Plugin', + description => 'The plugin is trying to do something it is not allowed to' + }, + 'Koha::Exceptions::Plugin::MissingMethod' => { + isa => 'Koha::Exceptions::Plugin', + description => 'Required method is missing', + fields => ['plugin_name','method'] + } +); + +sub full_message { + my $self = shift; + + my $msg = $self->message; + + unless ( $msg) { + if ( $self->isa('Koha::Exceptions::Plugin::MissingMethod') ) { + $msg = sprintf("Cannot use plugin (%s) because the it doesn't implement the '%s' method which is required.", $self->plugin_name, $self->method ); + } + } + + return $msg; +} + +=head1 NAME + +Koha::Exceptions::Plugin - Base class for Plugin exceptions + +=head1 Exceptions + +=head2 Koha::Exceptions::Plugin + +Generic Plugin exception + +=head2 Koha::Exceptions::Plugin::MissingMethod + +Exception to be used when a plugin is required to implement a specific +method and it doesn't. + +=head3 Parameters + +=over + +=item plugin_name: the plugin name for display purposes + +=item method: the missing method + +=back + +=head1 Class methods + +=head2 full_message + +Overloaded method for exception stringifying. + +=cut + +1; diff --git a/Koha/REST/Plugin/PluginRoutes.pm b/Koha/REST/Plugin/PluginRoutes.pm new file mode 100644 index 0000000..67b7b11 --- /dev/null +++ b/Koha/REST/Plugin/PluginRoutes.pm @@ -0,0 +1,140 @@ +package Koha::REST::Plugin::PluginRoutes; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Plugin'; + +use Koha::Exceptions::Plugin; +use Koha::Plugins; + +use Clone qw(clone); +use Try::Tiny; + +=head1 NAME + +Koha::REST::Plugin::PluginRoutes + +=head1 API + +=head2 Helper methods + +=head3 register + +=cut + +sub register { + my ( $self, $app, $config ) = @_; + + my $spec = $config->{spec}; + my $validator = $config->{validator}; + + my @plugins; + + if ( C4::Context->preference('UseKohaPlugins') + && C4::Context->config("enable_plugins") ) + { + @plugins = Koha::Plugins->new()->GetPlugins( + { + method => 'api_routes', + } + ); + # plugin needs to define a namespace + @plugins = grep { $_->api_namespace } @plugins; + } + + foreach my $plugin ( @plugins ) { + $spec = inject_routes( $spec, $plugin, $validator ); + } + + $spec; +} + +=head3 inject_routes + +=cut + +sub inject_routes { + my ( $spec, $plugin, $validator ) = @_; + + return try { + + my $backup_spec = merge_spec(clone($spec), $plugin ); + if ( spec_ok( $backup_spec, $validator ) ) { + $spec = merge_spec( $spec, $plugin ); + } + else { + Koha::Exceptions::Plugin->throw( + "The resulting spec is invalid. Skipping " . $plugin->get_metadata->{name} + ); + } + + return $spec; + } + catch { + warn "$_"; + return $spec; + }; +} + +=head3 merge_spec + +=cut + +sub merge_spec { + my ( $spec, $plugin ) = @_; + + my $plugin_spec = $plugin->api_routes; + + foreach my $route ( keys %{ $plugin_spec } ) { + + my $THE_route = '/contrib/' . $plugin->api_namespace . $route; + if ( exists $spec->{ $THE_route } ) { + # Route exists, overwriting is forbidden + Koha::Exceptions::Plugin::ForbiddenAction->throw( + "Attempted to overwrite $THE_route" + ); + } + + $spec->{'paths'}->{ $THE_route } = $plugin_spec->{ $route }; + } + + return $spec; +} + +=head3 spec_ok + +=cut + +sub spec_ok { + my ( $spec, $validator ) = @_; + + return try { + $validator->load_and_validate_schema( + $spec, + { + allow_invalid_ref => 1, + } + ); + return 1; + } + catch { + return 0; + } +} + +1; diff --git a/Koha/REST/V1.pm b/Koha/REST/V1.pm index 57ad113..2c94911 100644 --- a/Koha/REST/V1.pm +++ b/Koha/REST/V1.pm @@ -20,6 +20,7 @@ use Modern::Perl; use Mojo::Base 'Mojolicious'; use C4::Context; +use JSON::Validator::OpenAPI::Mojolicious; =head1 NAME @@ -51,13 +52,35 @@ sub startup { $self->secrets([$secret_passphrase]); } - $self->plugin(OpenAPI => { - url => $self->home->rel_file("api/v1/swagger/swagger.json"), - route => $self->routes->under('/api/v1')->to('Auth#under'), - allow_invalid_ref => 1, # required by our spec because $ref directly under - # Paths-, Parameters-, Definitions- & Info-object - # is not allowed by the OpenAPI specification. - }); + my $validator = JSON::Validator::OpenAPI::Mojolicious->new; + $validator->load_and_validate_schema( + $self->home->rel_file("api/v1/swagger/swagger.json"), + { + allow_invalid_ref => 1, + } + ); + + push @{$self->routes->namespaces}, 'Koha::Plugin'; + + my $spec = $validator->schema->data; + $self->plugin( + 'Koha::REST::Plugin::PluginRoutes' => { + spec => $spec, + validator => $validator + } + ); + + $self->plugin( + OpenAPI => { + spec => $spec, + route => $self->routes->under('/api/v1')->to('Auth#under'), + allow_invalid_ref => + 1, # required by our spec because $ref directly under + # Paths-, Parameters-, Definitions- & Info-object + # is not allowed by the OpenAPI specification. + } + ); + $self->plugin( 'Koha::REST::Plugin::Pagination' ); $self->plugin( 'Koha::REST::Plugin::Query' ); $self->plugin( 'Koha::REST::Plugin::Objects' ); -- 2.1.4