From ec78137dadacfac7726fb209df5d34e8309f7e64 Mon Sep 17 00:00:00 2001 From: Tomas Cohen Arazi Date: Mon, 23 Dec 2019 10:26:04 -0300 Subject: [PATCH] Bug 24302: Add a way to specify nested objects to embed in OpenAPI This patch introduces a helper for handling x-koha-embed headers on API requests. It reads the embed definitions and adds them to the stash for later use (either manually on the controllers, or in the objects.search helper. x-koha-embed needs to be defined as a list on the OpenAPI spec. It throws an exception when invalid combinations are found. To test: 1. Apply this patch 2. Run: $ kshell k$ prove t/Koha/REST/Plugin/Query.t => SUCCESS: Tests pass! 3.Sign off :-D TODO: Add tests for the authenticate_api_request change on the API itself. --- Koha/REST/Plugin/Query.pm | 86 ++++++++++++++++++++++++++++++++++++++ Koha/REST/V1/Auth.pm | 3 ++ t/Koha/REST/Plugin/Query.t | 82 +++++++++++++++++++++++++++++++++++- 3 files changed, 170 insertions(+), 1 deletion(-) diff --git a/Koha/REST/Plugin/Query.pm b/Koha/REST/Plugin/Query.pm index 14b3f5997e..e25bda1cbb 100644 --- a/Koha/REST/Plugin/Query.pm +++ b/Koha/REST/Plugin/Query.pm @@ -18,6 +18,7 @@ package Koha::REST::Plugin::Query; use Modern::Perl; use Mojo::Base 'Mojolicious::Plugin'; +use List::MoreUtils qw(any); use Scalar::Util qw(reftype); use Koha::Exceptions; @@ -143,6 +144,43 @@ is raised. return $params; } ); + +=head3 stash_embed + + $c->stash_embed( $c->match->endpoint->pattern->defaults->{'openapi.op_spec'} ); + +=cut + + $app->helper( + 'stash_embed' => sub { + + my ( $c, $args ) = @_; + + my $spec = $args->{spec} // {}; + + my $embed_spec = $spec->{'x-koha-embed'}; + my $embed_header = $c->req->headers->header('x-koha-embed'); + + Koha::Exceptions::WrongParameter->throw("Embedding objects is not allowed on this endpoint.") + if $embed_header and !defined $embed_spec; + + my $THE_embed = {}; + foreach my $embed_req ( split /\s*,\s*/, $embed_header ) { + my $matches = grep {lc $_ eq lc $embed_req} @{ $embed_spec }; + + Koha::Exceptions::WrongParameter->throw( + error => 'Embeding '.$embed_req. ' is not authorised. Check your x-koha-embed headers or remove it.' + ) unless $matches; + + _merge_embed( _parse_embed($embed_req), $THE_embed); + } + + $c->stash( 'koha.embed' => $THE_embed ) + if $THE_embed; + + return $c; + } + ); } =head2 Internal methods @@ -198,4 +236,52 @@ sub _build_order_atom { } } +=head3 _parse_embed + + my $embed = _parse_embed( $string ); + +Parses I<$string> and outputs data valid for passing to the Kohaa::Object(s)->to_api +method. + +=cut + +sub _parse_embed { + my $string = shift; + + my $result; + my ( $curr, $next ) = split /\s*\.\s*/, $string, 2; + + if ( $next ) { + $result->{$curr} = { children => _parse_embed( $next ) }; + } + else { + $result->{$curr} = {}; + } + + return $result; +} + +=head3 _merge_embed + + _merge_embed( $parsed_embed, $global_embed ); + +Merges the hash referenced by I<$parsed_embed> into I<$global_embed>. + +=cut + +sub _merge_embed { + my ( $structure, $embed ) = @_; + + my ($root) = keys %{ $structure }; + + if ( any { $root eq $_ } keys %{ $embed } ) { + # Recurse + _merge_embed( $structure->{$root}, $embed->{$root} ); + } + else { + # Embed + $embed->{$root} = $structure->{$root}; + } +} + 1; diff --git a/Koha/REST/V1/Auth.pm b/Koha/REST/V1/Auth.pm index 8bffc977da..94a2b81d93 100644 --- a/Koha/REST/V1/Auth.pm +++ b/Koha/REST/V1/Auth.pm @@ -137,6 +137,9 @@ sub authenticate_api_request { my $user; my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'}; + + $c->stash_embed( $spec ); + my $authorization = $spec->{'x-koha-authorization'}; my $authorization_header = $c->req->headers->authorization; diff --git a/t/Koha/REST/Plugin/Query.t b/t/Koha/REST/Plugin/Query.t index 2fc027c149..3aa451cbae 100644 --- a/t/Koha/REST/Plugin/Query.t +++ b/t/Koha/REST/Plugin/Query.t @@ -122,6 +122,54 @@ get '/build_query' => sub { }; }; +get '/stash_embed' => sub { + my $c = shift; + + try { + $c->stash_embed( + { + spec => { + 'x-koha-embed' => [ + 'checkouts', + 'checkouts.item', + 'library' + ] + } + } + ); + + $c->render( + status => 200, + json => $c->stash( 'koha.embed' ) + ); + } + catch { + $c->render( + status => 400, + json => { error => "$_" } + ); + }; +}; + +get '/stash_embed_no_spec' => sub { + my $c = shift; + + try { + $c->stash_embed({ spec => {} }); + + $c->render( + status => 200, + json => $c->stash( 'koha.embed' ) + ); + } + catch { + $c->render( + status => 400, + json => { error => "$_" } + ); + }; +}; + sub to_model { my ($args) = @_; $args->{three} = delete $args->{tres} @@ -131,7 +179,7 @@ sub to_model { # The tests -use Test::More tests => 3; +use Test::More tests => 4; use Test::Mojo; subtest 'extract_reserved_params() tests' => sub { @@ -232,3 +280,35 @@ subtest '_build_query_params_from_api' => sub { ->json_is( '/exception_type' => 'Koha::Exceptions::WrongParameter' ); }; + +subtest 'stash_embed() tests' => sub { + + plan tests => 12; + + my $t = Test::Mojo->new; + + $t->get_ok( '/stash_embed' => { 'x-koha-embed' => 'checkouts,checkouts.item' } ) + ->status_is(200) + ->json_is( { checkouts => { children => { item => {} } } } ); + + $t->get_ok( '/stash_embed' => { 'x-koha-embed' => 'checkouts,checkouts.item,library' } ) + ->status_is(200) + ->json_is( { checkouts => { children => { item => {} } }, library => {} } ); + + $t->get_ok( '/stash_embed' => { 'x-koha-embed' => 'checkouts,checkouts.item,patron' } ) + ->status_is(400) + ->json_is( + { + error => 'Embeding patron is not authorised. Check your x-koha-embed headers or remove it.' + } + ); + + $t->get_ok( '/stash_embed_no_spec' => { 'x-koha-embed' => 'checkouts,checkouts.item,patron' } ) + ->status_is(400) + ->json_is( + { + error => 'Embedding objects is not allowed on this endpoint.' + } + ); + +}; -- 2.24.1