aboutsummaryrefslogtreecommitdiffstats
path: root/container-core
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@yahooinc.com>2023-02-10 12:42:03 +0100
committerGitHub <noreply@github.com>2023-02-10 12:42:03 +0100
commitd82119dc70327c7ef052e2d6811b51c1116ca605 (patch)
treea2ec84887012adc776877fa40191f78a914c1339 /container-core
parent90d35e4e86e9469515d6e29aecd570d4b4664b29 (diff)
parentb5bb74bb4dff40e7a8df6489f0345f1083b8f0ae (diff)
Merge pull request #25963 from vespa-engine/bjorncs/capabilities
Bjorncs/capabilities
Diffstat (limited to 'container-core')
-rw-r--r--container-core/src/main/java/com/yahoo/container/jdisc/utils/CapabilityRequiringRequestHandler.java19
-rw-r--r--container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CapabilityEnforcingRequestHandler.java89
-rw-r--r--container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java3
-rw-r--r--container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java3
-rw-r--r--container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestUtils.java2
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApi.java18
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java63
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java7
-rw-r--r--container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java39
9 files changed, 236 insertions, 7 deletions
diff --git a/container-core/src/main/java/com/yahoo/container/jdisc/utils/CapabilityRequiringRequestHandler.java b/container-core/src/main/java/com/yahoo/container/jdisc/utils/CapabilityRequiringRequestHandler.java
new file mode 100644
index 00000000000..1dd866ae571
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/container/jdisc/utils/CapabilityRequiringRequestHandler.java
@@ -0,0 +1,19 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.container.jdisc.utils;
+
+import com.yahoo.container.jdisc.RequestView;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.security.tls.Capability;
+import com.yahoo.security.tls.CapabilitySet;
+
+/**
+ * @author bjorncs
+ */
+public interface CapabilityRequiringRequestHandler extends RequestHandler {
+
+ CapabilitySet DEFAULT_REQUIRED_CAPABILITIES = CapabilitySet.from(Capability.HTTP_UNCLASSIFIED);
+
+ default CapabilitySet requiredCapabilities(RequestView req) { return DEFAULT_REQUIRED_CAPABILITIES; }
+
+}
diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CapabilityEnforcingRequestHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CapabilityEnforcingRequestHandler.java
new file mode 100644
index 00000000000..d298f11860c
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CapabilityEnforcingRequestHandler.java
@@ -0,0 +1,89 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.jdisc.http.server.jetty;
+
+import com.yahoo.container.jdisc.RequestView;
+import com.yahoo.container.jdisc.utils.CapabilityRequiringRequestHandler;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.DelegatedRequestHandler;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.HttpResponse;
+import com.yahoo.security.tls.MissingCapabilitiesException;
+import com.yahoo.security.tls.TransportSecurityUtils;
+import com.yahoo.slime.Slime;
+import com.yahoo.slime.SlimeUtils;
+
+import javax.net.ssl.SSLSession;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Optional;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * Request handler wrapper that enforces required capabilities as specified by underlying {@link CapabilityRequiringRequestHandler}.
+ *
+ * @author bjorncs
+ */
+class CapabilityEnforcingRequestHandler implements DelegatedRequestHandler {
+
+ private final RequestHandler wrapped;
+
+ CapabilityEnforcingRequestHandler(RequestHandler wrapped) { this.wrapped = wrapped; }
+
+ @Override
+ public ContentChannel handleRequest(Request req, ResponseHandler responseHandler) {
+ var capabilityRequiringHandler =
+ DelegatedRequestHandler.resolve(CapabilityRequiringRequestHandler.class, wrapped).orElse(null);
+ var requiredCapabilities = capabilityRequiringHandler != null
+ ? capabilityRequiringHandler.requiredCapabilities(new View(req))
+ : CapabilityRequiringRequestHandler.DEFAULT_REQUIRED_CAPABILITIES;
+ var authCtx = Optional.ofNullable(req.context().get(RequestUtils.JDISC_REQUEST_SSLSESSION))
+ .flatMap(s -> TransportSecurityUtils.getConnectionAuthContext((SSLSession) s))
+ .orElse(null);
+
+ // Connection auth context will not be available if handler is bound to:
+ // 1) server with custom TLS configuration.
+ // 2) server without TLS (http)
+ if (authCtx != null) {
+ var peer = Optional.ofNullable(((HttpRequest)req).getRemoteAddress())
+ .map(Object::toString).orElse("<unknown>");
+ String method = ((HttpRequest) req).getMethod().name();
+ try {
+ authCtx.verifyCapabilities(requiredCapabilities, method, req.getUri().getPath(), peer);
+ } catch (MissingCapabilitiesException e) {
+ int code = HttpResponse.Status.FORBIDDEN;
+ var resp = new Response(code);
+ resp.headers().add("Content-Type", "application/json");
+ ContentChannel ch = responseHandler.handleResponse(resp);
+ var slime = new Slime();
+ var root = slime.setObject();
+ root.setString("error-code", Integer.toString(code));
+ root.setString("message", "Missing required capabilities");
+ ch.write(ByteBuffer.wrap(uncheck(() -> SlimeUtils.toJsonBytes(slime))), null);
+ ch.close(null);
+ return null;
+ }
+ }
+ return wrapped.handleRequest(req, responseHandler);
+ }
+
+ @Override public void release() { wrapped.release(); }
+ @Override public RequestHandler getDelegate() { return wrapped; }
+ @Override public void handleTimeout(Request request, ResponseHandler handler) { wrapped.handleRequest(request, handler); }
+ @Override public ResourceReference refer() { return wrapped.refer(); }
+ @Override public ResourceReference refer(Object context) { return wrapped.refer(context); }
+
+ private static class View implements RequestView {
+ private final HttpRequest req;
+ View(Request req) { this.req = (HttpRequest) req; }
+ @Override public HttpRequest.Method method() { return req.getMethod(); }
+ @Override public URI uri() { return req.getUri(); }
+ }
+
+}
diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java
index df606894b14..fe7b30ac7b3 100644
--- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java
+++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java
@@ -72,7 +72,8 @@ class FilteringRequestHandler extends AbstractRequestHandler {
getRequestHandlerSpec(resolvedRequestHandler)
.ifPresent(requestHandlerSpec -> request.context().put(RequestHandlerSpec.ATTRIBUTE_NAME, requestHandlerSpec));
- RequestHandler requestHandler = new ReferenceCountingRequestHandler(resolvedRequestHandler);
+ RequestHandler requestHandler = new ReferenceCountingRequestHandler(
+ new CapabilityEnforcingRequestHandler(resolvedRequestHandler));
ResponseHandler responseHandler;
if (responseFilter != null) {
diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java
index d45a8789e4c..1aa12808f71 100644
--- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java
+++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java
@@ -7,6 +7,7 @@ import jakarta.servlet.http.HttpServletRequest;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.Utf8Appendable;
+import javax.net.ssl.SSLSession;
import java.net.InetSocketAddress;
import java.net.URI;
import java.security.cert.X509Certificate;
@@ -34,6 +35,8 @@ class HttpRequestFactory {
getConnection((Request) servletRequest).getCreatedTimeStamp());
httpRequest.context().put(RequestUtils.JDISC_REQUEST_X509CERT, getCertChain(servletRequest));
httpRequest.context().put(RequestUtils.JDICS_REQUEST_PORT, servletRequest.getLocalPort());
+ SSLSession sslSession = (SSLSession) servletRequest.getAttribute(RequestUtils.JETTY_REQUEST_SSLSESSION);
+ httpRequest.context().put(RequestUtils.JDISC_REQUEST_SSLSESSION, sslSession);
servletRequest.setAttribute(HttpRequest.class.getName(), httpRequest);
return httpRequest;
} catch (Utf8Appendable.NotUtf8Exception e) {
diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestUtils.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestUtils.java
index da4de957739..2769f1f6729 100644
--- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestUtils.java
+++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestUtils.java
@@ -13,9 +13,11 @@ import org.eclipse.jetty.server.SecureRequestCustomizer;
*/
public class RequestUtils {
public static final String JDISC_REQUEST_X509CERT = "jdisc.request.X509Certificate";
+ public static final String JDISC_REQUEST_SSLSESSION = "jdisc.request.SSLSession";
public static final String JDISC_REQUEST_CHAIN = "jdisc.request.chain";
public static final String JDISC_RESPONSE_CHAIN = "jdisc.response.chain";
public static final String SERVLET_REQUEST_X509CERT = SecureRequestCustomizer.JAKARTA_SERVLET_REQUEST_X_509_CERTIFICATE;
+ public static final String JETTY_REQUEST_SSLSESSION = new SecureRequestCustomizer().getSslSessionAttribute();
// The local port as reported by servlet spec. This will be influenced by Host header and similar mechanisms.
// The request URI uses the local listen port as the URI is used for handler routing/bindings.
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 5dfb19029cc..555ebecec57 100644
--- a/container-core/src/main/java/com/yahoo/restapi/RestApi.java
+++ b/container-core/src/main/java/com/yahoo/restapi/RestApi.java
@@ -7,7 +7,13 @@ 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.security.tls.Capability;
+import com.yahoo.security.tls.CapabilitySet;
+import com.yahoo.security.tls.ConnectionAuthContext;
+import javax.net.ssl.SSLSession;
import java.io.InputStream;
import java.security.Principal;
import java.util.List;
@@ -32,6 +38,9 @@ public interface RestApi {
/** @see com.yahoo.container.jdisc.HttpRequestHandler#requestHandlerSpec() */
RequestHandlerSpec requestHandlerSpec();
+ /** @see com.yahoo.container.jdisc.utils.CapabilityRequiringRequestHandler */
+ CapabilitySet requiredCapabilities(RequestView req);
+
interface Builder {
Builder setObjectMapper(ObjectMapper mapper);
Builder setDefaultRoute(RouteBuilder route);
@@ -50,11 +59,15 @@ public interface RestApi {
/** Disables mappers listed in {@link RestApiMappers#DEFAULT_RESPONSE_MAPPERS} */
Builder disableDefaultResponseMappers();
Builder disableDefaultAclMapping();
+ Builder requiredCapabilities(Capability... capabilities);
+ Builder requiredCapabilities(CapabilitySet capabilities);
RestApi build();
}
interface RouteBuilder {
RouteBuilder name(String name);
+ RouteBuilder requiredCapabilities(Capability... capabilities);
+ RouteBuilder requiredCapabilities(CapabilitySet capabilities);
RouteBuilder addFilter(Filter filter);
// GET
@@ -115,6 +128,8 @@ public interface RestApi {
@FunctionalInterface interface Filter { HttpResponse filterRequest(FilterContext context); }
interface HandlerConfigBuilder {
+ HandlerConfigBuilder withRequiredCapabilities(Capability... capabilities);
+ HandlerConfigBuilder withRequiredCapabilities(CapabilitySet capabilities);
HandlerConfigBuilder withReadAclAction();
HandlerConfigBuilder withWriteAclAction();
HandlerConfigBuilder withCustomAclAction(AclMapping.Action action);
@@ -122,6 +137,7 @@ public interface RestApi {
interface RequestContext {
HttpRequest request();
+ Method method();
PathParameters pathParameters();
QueryParameters queryParameters();
Headers headers();
@@ -135,6 +151,8 @@ public interface RestApi {
AclMapping.Action aclAction();
Optional<Principal> userPrincipal();
Principal userPrincipalOrThrow();
+ Optional<SSLSession> sslSession();
+ Optional<ConnectionAuthContext> connectionAuthContext();
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 1bde8d635a5..59b78a1423d 100644
--- a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java
+++ b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java
@@ -10,10 +10,16 @@ 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.jdisc.http.server.jetty.RequestUtils;
import com.yahoo.restapi.RestApiMappers.ExceptionMapperHolder;
import com.yahoo.restapi.RestApiMappers.RequestMapperHolder;
import com.yahoo.restapi.RestApiMappers.ResponseMapperHolder;
+import com.yahoo.security.tls.Capability;
+import com.yahoo.security.tls.CapabilitySet;
+import com.yahoo.security.tls.ConnectionAuthContext;
+import com.yahoo.security.tls.TransportSecurityUtils;
+import javax.net.ssl.SSLSession;
import java.io.InputStream;
import java.net.URI;
import java.security.Principal;
@@ -41,6 +47,7 @@ class RestApiImpl implements RestApi {
private final List<Filter> filters;
private final ObjectMapper jacksonJsonMapper;
private final boolean disableDefaultAclMapping;
+ private final CapabilitySet requiredCapabilities;
private RestApiImpl(RestApi.Builder builder) {
BuilderImpl builderImpl = (BuilderImpl) builder;
@@ -55,6 +62,7 @@ class RestApiImpl implements RestApi {
this.filters = List.copyOf(builderImpl.filters);
this.jacksonJsonMapper = jacksonJsonMapper;
this.disableDefaultAclMapping = Boolean.TRUE.equals(builderImpl.disableDefaultAclMapping);
+ this.requiredCapabilities = builderImpl.requiredCapabilities;
}
@Override
@@ -83,6 +91,19 @@ class RestApiImpl implements RestApi {
.build();
}
+ private static final CapabilitySet DEFAULT_REQUIRED_CAPABILITIES = Capability.RESTAPI_UNCLASSIFIED.toCapabilitySet();
+
+ @Override
+ public CapabilitySet requiredCapabilities(RequestView req) {
+ Path pathMatcher = new Path(req.uri());
+ Route route = resolveRoute(pathMatcher);
+ HandlerHolder<?> handler = resolveHandler(req.method(), route);
+ return Optional.ofNullable(handler.config.requiredCapabilities)
+ .or(() -> Optional.ofNullable(route.requiredCapabilities))
+ .or(() -> Optional.ofNullable(requiredCapabilities))
+ .orElse(DEFAULT_REQUIRED_CAPABILITIES);
+ }
+
private AclMapping.Action getAclMapping(Method method, URI uri) {
Path pathMatcher = new Path(uri);
Route route = resolveRoute(pathMatcher);
@@ -216,6 +237,7 @@ class RestApiImpl implements RestApi {
private Boolean disableDefaultExceptionMappers;
private Boolean disableDefaultResponseMappers;
private Boolean disableDefaultAclMapping;
+ private CapabilitySet requiredCapabilities;
@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; }
@@ -246,6 +268,15 @@ class RestApiImpl implements RestApi {
@Override public Builder disableDefaultResponseMappers() { this.disableDefaultResponseMappers = true; return this; }
@Override public Builder disableDefaultAclMapping() { this.disableDefaultAclMapping = true; return this; }
+ @Override public Builder requiredCapabilities(Capability... capabilities) {
+ return requiredCapabilities(CapabilitySet.from(capabilities));
+ }
+ @Override public Builder requiredCapabilities(CapabilitySet capabilities) {
+ if (requiredCapabilities != null) throw new IllegalStateException("Capabilities already set");
+ requiredCapabilities = capabilities;
+ return this;
+ }
+
@Override public RestApi build() { return new RestApiImpl(this); }
}
@@ -255,10 +286,21 @@ class RestApiImpl implements RestApi {
private final Map<Method, HandlerHolder<?>> handlerPerMethod = new HashMap<>();
private final List<RestApi.Filter> filters = new ArrayList<>();
private HandlerHolder<?> defaultHandler;
+ private CapabilitySet requiredCapabilities;
RouteBuilderImpl(String pathPattern) { this.pathPattern = pathPattern; }
@Override public RestApi.RouteBuilder name(String name) { this.name = name; return this; }
+
+ @Override public RestApi.RouteBuilder requiredCapabilities(Capability... capabilities) {
+ return requiredCapabilities(CapabilitySet.from(capabilities));
+ }
+ @Override public RestApi.RouteBuilder requiredCapabilities(CapabilitySet capabilities) {
+ if (requiredCapabilities != null) throw new IllegalStateException("Capabilities already set");
+ requiredCapabilities = capabilities;
+ return this;
+ }
+
@Override public RestApi.RouteBuilder addFilter(RestApi.Filter filter) { filters.add(filter); return this; }
// GET
@@ -351,7 +393,16 @@ class RestApiImpl implements RestApi {
static class HandlerConfigBuilderImpl implements HandlerConfigBuilder {
private AclMapping.Action aclAction;
+ private CapabilitySet requiredCapabilities;
+ @Override public HandlerConfigBuilder withRequiredCapabilities(Capability... capabilities) {
+ return withRequiredCapabilities(CapabilitySet.from(capabilities));
+ }
+ @Override public HandlerConfigBuilder withRequiredCapabilities(CapabilitySet capabilities) {
+ if (requiredCapabilities != null) throw new IllegalStateException("Capabilities already set");
+ requiredCapabilities = capabilities;
+ return this;
+ }
@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) {
@@ -363,9 +414,11 @@ class RestApiImpl implements RestApi {
private static class HandlerConfig {
final AclMapping.Action aclAction;
+ final CapabilitySet requiredCapabilities;
HandlerConfig(HandlerConfigBuilderImpl builder) {
this.aclAction = builder.aclAction;
+ this.requiredCapabilities = builder.requiredCapabilities;
}
static HandlerConfig empty() { return new HandlerConfigBuilderImpl().build(); }
@@ -391,6 +444,7 @@ class RestApiImpl implements RestApi {
}
@Override public HttpRequest request() { return request; }
+ @Override public Method method() { return request.getMethod(); }
@Override public PathParameters pathParameters() { return pathParameters; }
@Override public QueryParameters queryParameters() { return queryParameters; }
@Override public Headers headers() { return headers; }
@@ -427,6 +481,13 @@ class RestApiImpl implements RestApi {
@Override public Principal userPrincipalOrThrow() {
return userPrincipal().orElseThrow(RestApiException.Unauthorized::new);
}
+ @Override public Optional<SSLSession> sslSession() {
+ return Optional.ofNullable((SSLSession) request.context().get(RequestUtils.JDISC_REQUEST_SSLSESSION));
+ }
+ @Override public Optional<ConnectionAuthContext> connectionAuthContext() {
+ return sslSession().flatMap(TransportSecurityUtils::getConnectionAuthContext);
+ }
+
private class PathParametersImpl implements RestApi.RequestContext.PathParameters {
@Override
@@ -543,6 +604,7 @@ class RestApiImpl implements RestApi {
private final Map<Method, HandlerHolder<?>> handlerPerMethod;
private final HandlerHolder<?> defaultHandler;
private final List<Filter> filters;
+ private final CapabilitySet requiredCapabilities;
private Route(RestApi.RouteBuilder builder) {
RouteBuilderImpl builderImpl = (RouteBuilderImpl)builder;
@@ -551,6 +613,7 @@ class RestApiImpl implements RestApi {
this.handlerPerMethod = Map.copyOf(builderImpl.handlerPerMethod);
this.defaultHandler = builderImpl.defaultHandler != null ? builderImpl.defaultHandler : createDefaultMethodHandler();
this.filters = List.copyOf(builderImpl.filters);
+ this.requiredCapabilities = builderImpl.requiredCapabilities;
}
private HandlerHolder<?> createDefaultMethodHandler() {
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 001d00c2b37..66fcc7c1442 100644
--- a/container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java
+++ b/container-core/src/main/java/com/yahoo/restapi/RestApiRequestHandler.java
@@ -3,16 +3,20 @@ package com.yahoo.restapi;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.RequestView;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
import com.yahoo.container.jdisc.RequestHandlerSpec;
+import com.yahoo.container.jdisc.utils.CapabilityRequiringRequestHandler;
import com.yahoo.jdisc.Metric;
+import com.yahoo.security.tls.CapabilitySet;
import java.util.concurrent.Executor;
/**
* @author bjorncs
*/
-public abstract class RestApiRequestHandler<T extends RestApiRequestHandler<T>> extends ThreadedHttpRequestHandler {
+public abstract class RestApiRequestHandler<T extends RestApiRequestHandler<T>> extends ThreadedHttpRequestHandler
+ implements CapabilityRequiringRequestHandler {
private final RestApi restApi;
@@ -50,6 +54,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(); }
+ @Override public CapabilitySet requiredCapabilities(RequestView req) { return restApi.requiredCapabilities(req); }
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 d4a53fb5d85..443fb18b448 100644
--- a/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java
+++ b/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java
@@ -7,6 +7,7 @@ import com.yahoo.container.jdisc.HttpRequestBuilder;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.RequestHandlerSpec;
import com.yahoo.container.jdisc.RequestView;
+import com.yahoo.security.tls.Capability;
import com.yahoo.test.json.JsonTestHelper;
import com.yahoo.yolean.Exceptions;
import org.junit.jupiter.api.Test;
@@ -19,6 +20,7 @@ import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import static com.yahoo.jdisc.http.HttpRequest.Method;
import static com.yahoo.restapi.RestApi.handlerConfig;
@@ -136,6 +138,26 @@ class RestApiImplTest {
assertRequestHandlerSpecAclMapping(spec, AclMapping.Action.WRITE, Method.POST, "/api2");
}
+ @Test
+ void resolves_correct_capabilities() {
+ var restApi = RestApi.builder()
+ .requiredCapabilities(Capability.CONTENT__METRICS_API)
+ .addRoute(route("/api1")
+ .requiredCapabilities(Capability.CONTENT__SEARCH_API)
+ .get(ctx -> new MessageResponse(ctx.aclAction().name()),
+ handlerConfig().withRequiredCapabilities(Capability.SLOBROK__API))
+ .post(ctx -> new MessageResponse(ctx.aclAction().name())))
+ .addRoute(route("/api2")
+ .get(ctx -> new MessageResponse(ctx.aclAction().name()))
+ .post(ctx -> new MessageResponse(ctx.aclAction().name()),
+ handlerConfig().withRequiredCapabilities(Capability.CONTENT__DOCUMENT_API)))
+ .build();
+ assertRequiredCapability(restApi, Method.GET, "/api1", Capability.SLOBROK__API);
+ assertRequiredCapability(restApi, Method.POST, "/api1", Capability.CONTENT__SEARCH_API);
+ assertRequiredCapability(restApi, Method.GET, "/api2", Capability.CONTENT__METRICS_API);
+ assertRequiredCapability(restApi, Method.POST, "/api2", Capability.CONTENT__DOCUMENT_API);
+ }
+
private static void verifyJsonResponse(
RestApi restApi, Method method, String path, String requestContent, int expectedStatusCode,
String expectedJson) {
@@ -164,15 +186,22 @@ 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));
+ assertEquals(expectedAction, spec.aclMapping().get(new View(method, uriPath)));
+ }
+
+ private static void assertRequiredCapability(RestApi api, Method method, String uriPath, Capability capability) {
+ assertEquals(Set.of(capability), api.requiredCapabilities(new View(method, uriPath)).asSet());
}
public static class TestEntity {
@JsonProperty("mystring") public String stringValue;
@JsonProperty("myinstant") public Instant instantValue;
}
+
+ private static class View implements RequestView {
+ final Method method; final String path;
+ View(Method m, String path) { this.method = m; this.path = path;}
+ @Override public Method method() { return method; }
+ @Override public URI uri() { return URI.create("http://localhost" + path); }
+ }
}