aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@verizonmedia.com>2021-07-12 15:24:49 +0200
committerBjørn Christian Seime <bjorncs@verizonmedia.com>2021-07-14 15:15:43 +0200
commit8d2e351b21bc418b90d8ea56be3265e54da85137 (patch)
tree52a4892d105ac3e609e4b1eb74d9aa20c3aa65d1
parentd5a7b61b15c4a480a8ae049d907b149c872ac5cd (diff)
Support custom ACL action mapping for restapi methods through RequestHandlerSpec
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApi.java13
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java61
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java2
-rw-r--r--container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java35
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;