diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2021-07-12 15:24:49 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2021-07-14 15:15:43 +0200 |
commit | 8d2e351b21bc418b90d8ea56be3265e54da85137 (patch) | |
tree | 52a4892d105ac3e609e4b1eb74d9aa20c3aa65d1 /container-core | |
parent | d5a7b61b15c4a480a8ae049d907b149c872ac5cd (diff) |
Support custom ACL action mapping for restapi methods through RequestHandlerSpec
Diffstat (limited to 'container-core')
4 files changed, 103 insertions, 8 deletions
diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApi.java b/container-core/src/main/java/com/yahoo/restapi/RestApi.java index 6ca7135c628..f438dd66cf1 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApi.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApi.java @@ -2,8 +2,10 @@ package com.yahoo.restapi; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.container.jdisc.AclMapping; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.RequestHandlerSpec; import java.io.InputStream; import java.util.List; @@ -25,6 +27,9 @@ public interface RestApi { HttpResponse handleRequest(HttpRequest request); ObjectMapper jacksonJsonMapper(); + /** @see com.yahoo.container.jdisc.HttpRequestHandler#requestHandlerSpec() */ + RequestHandlerSpec requestHandlerSpec(); + interface Builder { Builder setObjectMapper(ObjectMapper mapper); Builder setDefaultRoute(RouteBuilder route); @@ -37,6 +42,7 @@ public interface RestApi { <REQUEST_ENTITY> Builder registerJacksonRequestEntity(Class<REQUEST_ENTITY> type); Builder disableDefaultExceptionMappers(); Builder disableDefaultResponseMappers(); + Builder disableDefaultAclMapping(); RestApi build(); } @@ -101,7 +107,11 @@ public interface RestApi { @FunctionalInterface interface Filter { HttpResponse filterRequest(FilterContext context); } - interface HandlerConfigBuilder {} + interface HandlerConfigBuilder { + HandlerConfigBuilder withReadAclAction(); + HandlerConfigBuilder withWriteAclAction(); + HandlerConfigBuilder withCustomAclAction(AclMapping.Action action); + } interface RequestContext { HttpRequest request(); @@ -113,6 +123,7 @@ public interface RestApi { RequestContent requestContentOrThrow(); ObjectMapper jacksonJsonMapper(); UriBuilder uriBuilder(); + AclMapping.Action aclAction(); interface Parameters { Optional<String> getString(String name); diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java index 5b18ac428cd..115780c9833 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java @@ -3,8 +3,11 @@ package com.yahoo.restapi; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.container.jdisc.AclMapping; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.RequestHandlerSpec; +import com.yahoo.container.jdisc.RequestView; import com.yahoo.jdisc.http.HttpRequest.Method; import com.yahoo.slime.Slime; import com.yahoo.slime.SlimeUtils; @@ -38,6 +41,7 @@ class RestApiImpl implements RestApi { private final List<RequestMapperHolder<?>> requestMappers; private final List<Filter> filters; private final ObjectMapper jacksonJsonMapper; + private final boolean disableDefaultAclMapping; private RestApiImpl(RestApi.Builder builder) { BuilderImpl builderImpl = (BuilderImpl) builder; @@ -52,13 +56,15 @@ class RestApiImpl implements RestApi { builderImpl.requestMappers, jacksonJsonMapper); this.filters = List.copyOf(builderImpl.filters); this.jacksonJsonMapper = jacksonJsonMapper; + this.disableDefaultAclMapping = Boolean.TRUE.equals(builderImpl.disableDefaultAclMapping); } @Override public HttpResponse handleRequest(HttpRequest request) { Path pathMatcher = new Path(request.getUri()); Route resolvedRoute = resolveRoute(pathMatcher); - RequestContextImpl requestContext = new RequestContextImpl(request, pathMatcher, jacksonJsonMapper); + AclMapping.Action aclAction = getAclMapping(request.getMethod(), request.getUri()); + RequestContextImpl requestContext = new RequestContextImpl(request, pathMatcher, aclAction, jacksonJsonMapper); FilterContextImpl filterContext = createFilterContextRecursive( resolvedRoute, requestContext, filters, @@ -72,8 +78,33 @@ class RestApiImpl implements RestApi { @Override public ObjectMapper jacksonJsonMapper() { return jacksonJsonMapper; } + @Override + public RequestHandlerSpec requestHandlerSpec() { + return RequestHandlerSpec.builder() + .withAclMapping(requestView -> getAclMapping(requestView.method(), requestView.uri())) + .build(); + } + + private AclMapping.Action getAclMapping(Method method, URI uri) { + Path pathMatcher = new Path(uri); + Route route = resolveRoute(pathMatcher); + HandlerHolder<?> handler = resolveHandler(method, route); + AclMapping.Action aclAction = handler.config.aclAction; + if (aclAction != null) return aclAction; + if (!disableDefaultAclMapping) { + // Fallback to default request handler spec which is used by the default implementation of + // HttpRequestHandler.requestHandlerSpec(). + return RequestHandlerSpec.DEFAULT_INSTANCE.aclMapping().get( + new RequestView() { + @Override public Method method() { return method; } + @Override public URI uri() { return uri; } + }); + } + throw new IllegalStateException(String.format("No ACL mapping configured for '%s' to '%s'", method, route.name)); + } + private HttpResponse dispatchToRoute(Route route, RequestContextImpl context) { - HandlerHolder<?> resolvedHandler = resolveHandler(context, route); + HandlerHolder<?> resolvedHandler = resolveHandler(context.request.getMethod(), route); RequestMapperHolder<?> resolvedRequestMapper = resolveRequestMapper(resolvedHandler); Object requestEntity; try { @@ -96,8 +127,8 @@ class RestApiImpl implements RestApi { } } - private HandlerHolder<?> resolveHandler(RequestContextImpl context, Route route) { - HandlerHolder<?> resolvedHandler = route.handlerPerMethod.get(context.request().getMethod()); + private HandlerHolder<?> resolveHandler(Method method, Route route) { + HandlerHolder<?> resolvedHandler = route.handlerPerMethod.get(method); return resolvedHandler == null ? route.defaultHandler : resolvedHandler; } @@ -236,6 +267,7 @@ class RestApiImpl implements RestApi { private ObjectMapper jacksonJsonMapper; private Boolean disableDefaultExceptionMappers; private Boolean disableDefaultResponseMappers; + private Boolean disableDefaultAclMapping; @Override public RestApi.Builder setObjectMapper(ObjectMapper mapper) { this.jacksonJsonMapper = mapper; return this; } @Override public RestApi.Builder setDefaultRoute(RestApi.RouteBuilder route) { this.defaultRoute = ((RouteBuilderImpl)route).build(); return this; } @@ -264,6 +296,7 @@ class RestApiImpl implements RestApi { @Override public Builder disableDefaultExceptionMappers() { this.disableDefaultExceptionMappers = true; return this; } @Override public Builder disableDefaultResponseMappers() { this.disableDefaultResponseMappers = true; return this; } + @Override public Builder disableDefaultAclMapping() { this.disableDefaultAclMapping = true; return this; } @Override public RestApi build() { return new RestApiImpl(this); } } @@ -369,11 +402,23 @@ class RestApiImpl implements RestApi { } static class HandlerConfigBuilderImpl implements HandlerConfigBuilder { + private AclMapping.Action aclAction; + + @Override public HandlerConfigBuilder withReadAclAction() { return withCustomAclAction(AclMapping.Action.READ); } + @Override public HandlerConfigBuilder withWriteAclAction() { return withCustomAclAction(AclMapping.Action.WRITE); } + @Override public HandlerConfigBuilder withCustomAclAction(AclMapping.Action action) { + this.aclAction = action; return this; + } + HandlerConfig build() { return new HandlerConfig(this); } } private static class HandlerConfig { - HandlerConfig(HandlerConfigBuilderImpl builder) {} + final AclMapping.Action aclAction; + + HandlerConfig(HandlerConfigBuilderImpl builder) { + this.aclAction = builder.aclAction; + } static HandlerConfig empty() { return new HandlerConfigBuilderImpl().build(); } } @@ -387,12 +432,14 @@ class RestApiImpl implements RestApi { final Headers headers = new HeadersImpl(); final Attributes attributes = new AttributesImpl(); final RequestContent requestContent; + final AclMapping.Action aclAction; - RequestContextImpl(HttpRequest request, Path pathMatcher, ObjectMapper jacksonJsonMapper) { + RequestContextImpl(HttpRequest request, Path pathMatcher, AclMapping.Action aclAction, ObjectMapper jacksonJsonMapper) { this.request = request; this.pathMatcher = pathMatcher; this.jacksonJsonMapper = jacksonJsonMapper; this.requestContent = request.getData() != null ? new RequestContentImpl() : null; + this.aclAction = aclAction; } @Override public HttpRequest request() { return request; } @@ -412,6 +459,7 @@ class RestApiImpl implements RestApi { ? new UriBuilder(uri.getScheme() + "://" + uri.getHost() + ':' + uriPort) : new UriBuilder(uri.getScheme() + "://" + uri.getHost()); } + @Override public AclMapping.Action aclAction() { return aclAction; } private class PathParametersImpl implements RestApi.RequestContext.PathParameters { @Override @@ -486,6 +534,7 @@ class RestApiImpl implements RestApi { return dispatchToRoute(route, requestContext); } } + } private static class ExceptionMapperHolder<EXCEPTION extends RuntimeException> { diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java b/container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java index c501ad8c804..4f8adfe9bef 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java @@ -4,6 +4,7 @@ package com.yahoo.restapi; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; +import com.yahoo.container.jdisc.RequestHandlerSpec; import com.yahoo.jdisc.Metric; import java.util.concurrent.Executor; @@ -48,6 +49,7 @@ public abstract class RestApiRequestHandler<T extends RestApiRequestHandler<T>> } @Override public final HttpResponse handle(HttpRequest request) { return restApi.handleRequest(request); } + @Override public RequestHandlerSpec requestHandlerSpec() { return restApi.requestHandlerSpec(); } public RestApi restApi() { return restApi; } } diff --git a/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java b/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java index 06fc6d80741..44dd61836d6 100644 --- a/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java +++ b/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java @@ -1,14 +1,18 @@ package com.yahoo.restapi;// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. import com.fasterxml.jackson.annotation.JsonProperty; +import com.yahoo.container.jdisc.AclMapping; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.RequestHandlerSpec; +import com.yahoo.container.jdisc.RequestView; import com.yahoo.test.json.JsonTestHelper; import com.yahoo.yolean.Exceptions; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; @@ -16,6 +20,7 @@ import java.util.List; import java.util.Map; import static com.yahoo.jdisc.http.HttpRequest.Method; +import static com.yahoo.restapi.RestApi.handlerConfig; import static com.yahoo.restapi.RestApi.route; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -103,13 +108,32 @@ class RestApiImplTest { } @Test - public void uri_builder_creates_valid_uri_prefix() { + void uri_builder_creates_valid_uri_prefix() { RestApi restApi = RestApi.builder() .addRoute(route("/test").get(ctx -> new MessageResponse(ctx.uriBuilder().toString()))) .build(); verifyJsonResponse(restApi, Method.GET, "/test", null, 200, "{\"message\":\"http://localhost\"}"); } + @Test + void resolves_correct_acl_action() { + AclMapping.Action customAclAction = AclMapping.Action.custom("custom-action"); + RestApi restApi = RestApi.builder() + .addRoute(route("/api1") + .get(ctx -> new MessageResponse(ctx.aclAction().name()), + handlerConfig().withCustomAclAction(customAclAction))) + .addRoute(route("/api2") + .post(ctx -> new MessageResponse(ctx.aclAction().name()))) + .build(); + + verifyJsonResponse(restApi, Method.GET, "/api1", null, 200, "{\"message\":\"custom-action\"}"); + verifyJsonResponse(restApi, Method.POST, "/api2", "ignored", 200, "{\"message\":\"write\"}"); + + RequestHandlerSpec spec = restApi.requestHandlerSpec(); + assertRequestHandlerSpecAclMapping(spec, customAclAction, Method.GET, "/api1"); + assertRequestHandlerSpecAclMapping(spec, AclMapping.Action.WRITE, Method.POST, "/api2"); + } + private static void verifyJsonResponse(RestApi restApi, Method method, String path, String requestContent, int expectedStatusCode, String expectedJson) { HttpRequest testRequest; String uri = "http://localhost" + path; @@ -132,6 +156,15 @@ class RestApiImplTest { } } + private static void assertRequestHandlerSpecAclMapping( + RequestHandlerSpec spec, AclMapping.Action expectedAction, Method method, String uriPath) { + RequestView requestView = new RequestView() { + @Override public Method method() { return method; } + @Override public URI uri() { return URI.create("http://localhost" + uriPath); } + }; + assertEquals(expectedAction, spec.aclMapping().get(requestView)); + } + public static class TestEntity { @JsonProperty("mystring") public String stringValue; @JsonProperty("myinstant") public Instant instantValue; |