diff options
author | Bjørn Christian Seime <bjorncs@yahooinc.com> | 2023-02-10 12:42:03 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-10 12:42:03 +0100 |
commit | d82119dc70327c7ef052e2d6811b51c1116ca605 (patch) | |
tree | a2ec84887012adc776877fa40191f78a914c1339 /container-core | |
parent | 90d35e4e86e9469515d6e29aecd570d4b4664b29 (diff) | |
parent | b5bb74bb4dff40e7a8df6489f0345f1083b8f0ae (diff) |
Merge pull request #25963 from vespa-engine/bjorncs/capabilities
Bjorncs/capabilities
Diffstat (limited to 'container-core')
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); } + } } |