diff options
Diffstat (limited to 'container-core')
5 files changed, 425 insertions, 184 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 6f5bf298de3..2ef14679553 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; @@ -20,38 +22,78 @@ public interface RestApi { static Builder builder() { return new RestApiImpl.BuilderImpl(); } static RouteBuilder route(String pathPattern) { return new RestApiImpl.RouteBuilderImpl(pathPattern); } + static HandlerConfigBuilder handlerConfig() { return new RestApiImpl.HandlerConfigBuilderImpl(); } 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); Builder addRoute(RouteBuilder route); Builder addFilter(Filter filter); + /** see {@link RestApiMappers#DEFAULT_EXCEPTION_MAPPERS} for default mappers */ <EXCEPTION extends RuntimeException> Builder addExceptionMapper(Class<EXCEPTION> type, ExceptionMapper<EXCEPTION> mapper); + /** see {@link RestApiMappers#DEFAULT_RESPONSE_MAPPERS} for default mappers */ <RESPONSE_ENTITY> Builder addResponseMapper(Class<RESPONSE_ENTITY> type, ResponseMapper<RESPONSE_ENTITY> mapper); + /** see {@link RestApiMappers#DEFAULT_REQUEST_MAPPERS} for default mappers */ <REQUEST_ENTITY> Builder addRequestMapper(Class<REQUEST_ENTITY> type, RequestMapper<REQUEST_ENTITY> mapper); <RESPONSE_ENTITY> Builder registerJacksonResponseEntity(Class<RESPONSE_ENTITY> type); <REQUEST_ENTITY> Builder registerJacksonRequestEntity(Class<REQUEST_ENTITY> type); + /** Disables mappers listed in {@link RestApiMappers#DEFAULT_EXCEPTION_MAPPERS} */ Builder disableDefaultExceptionMappers(); + /** Disables mappers listed in {@link RestApiMappers#DEFAULT_RESPONSE_MAPPERS} */ Builder disableDefaultResponseMappers(); + Builder disableDefaultAclMapping(); RestApi build(); } interface RouteBuilder { RouteBuilder name(String name); + RouteBuilder addFilter(Filter filter); + + // GET RouteBuilder get(Handler<?> handler); + RouteBuilder get(Handler<?> handler, HandlerConfigBuilder config); + + // POST RouteBuilder post(Handler<?> handler); - <REQUEST_ENTITY> RouteBuilder post(Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + <REQUEST_ENTITY> RouteBuilder post( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + RouteBuilder post(Handler<?> handler, HandlerConfigBuilder config); + <REQUEST_ENTITY> RouteBuilder post( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config); + + // PUT RouteBuilder put(Handler<?> handler); - <REQUEST_ENTITY> RouteBuilder put(Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + <REQUEST_ENTITY> RouteBuilder put( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + RouteBuilder put(Handler<?> handler, HandlerConfigBuilder config); + <REQUEST_ENTITY> RouteBuilder put( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config); + + // DELETE RouteBuilder delete(Handler<?> handler); + RouteBuilder delete(Handler<?> handler, HandlerConfigBuilder config); + + // PATCH RouteBuilder patch(Handler<?> handler); - <REQUEST_ENTITY> RouteBuilder patch(Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + <REQUEST_ENTITY> RouteBuilder patch( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + RouteBuilder patch(Handler<?> handler, HandlerConfigBuilder config); + <REQUEST_ENTITY> RouteBuilder patch( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config); + + // Default RouteBuilder defaultHandler(Handler<?> handler); - <REQUEST_ENTITY> RouteBuilder defaultHandler(Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); - RouteBuilder addFilter(Filter filter); + RouteBuilder defaultHandler(Handler<?> handler, HandlerConfigBuilder config); + <REQUEST_ENTITY> RouteBuilder defaultHandler( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler); + <REQUEST_ENTITY> RouteBuilder defaultHandler( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config); } @FunctionalInterface interface Handler<RESPONSE_ENTITY> { @@ -70,6 +112,12 @@ public interface RestApi { @FunctionalInterface interface Filter { HttpResponse filterRequest(FilterContext context); } + interface HandlerConfigBuilder { + HandlerConfigBuilder withReadAclAction(); + HandlerConfigBuilder withWriteAclAction(); + HandlerConfigBuilder withCustomAclAction(AclMapping.Action action); + } + interface RequestContext { HttpRequest request(); PathParameters pathParameters(); @@ -80,6 +128,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 d63add5ed1d..646177e60db 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java @@ -1,20 +1,20 @@ // Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. 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; -import com.yahoo.yolean.Exceptions; +import com.yahoo.restapi.RestApiMappers.ExceptionMapperHolder; +import com.yahoo.restapi.RestApiMappers.RequestMapperHolder; +import com.yahoo.restapi.RestApiMappers.ResponseMapperHolder; -import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.ArrayList; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.ListIterator; @@ -23,8 +23,6 @@ import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; -import static java.nio.charset.StandardCharsets.UTF_8; - /** * @author bjorncs */ @@ -39,6 +37,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; @@ -48,18 +47,19 @@ class RestApiImpl implements RestApi { this.exceptionMappers = combineWithDefaultExceptionMappers( builderImpl.exceptionMappers, Boolean.TRUE.equals(builderImpl.disableDefaultExceptionMappers)); this.responseMappers = combineWithDefaultResponseMappers( - builderImpl.responseMappers, jacksonJsonMapper, Boolean.TRUE.equals(builderImpl.disableDefaultResponseMappers)); - this.requestMappers = combineWithDefaultRequestMappers( - builderImpl.requestMappers, jacksonJsonMapper); + builderImpl.responseMappers, Boolean.TRUE.equals(builderImpl.disableDefaultResponseMappers)); + this.requestMappers = combineWithDefaultRequestMappers(builderImpl.requestMappers); 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, @@ -73,8 +73,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 { @@ -97,8 +122,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; } @@ -154,7 +179,7 @@ class RestApiImpl implements RestApi { List<ExceptionMapperHolder<?>> configuredExceptionMappers, boolean disableDefaultMappers) { List<ExceptionMapperHolder<?>> exceptionMappers = new ArrayList<>(configuredExceptionMappers); if (!disableDefaultMappers){ - exceptionMappers.add(new ExceptionMapperHolder<>(RestApiException.class, (context, exception) -> exception.response())); + exceptionMappers.addAll(RestApiMappers.DEFAULT_EXCEPTION_MAPPERS); } // Topologically sort children before superclasses, so most the specific match is found by iterating through mappers in order. exceptionMappers.sort((a, b) -> (a.type.isAssignableFrom(b.type) ? 1 : 0) + (b.type.isAssignableFrom(a.type) ? -1 : 0)); @@ -162,71 +187,21 @@ class RestApiImpl implements RestApi { } private static List<ResponseMapperHolder<?>> combineWithDefaultResponseMappers( - List<ResponseMapperHolder<?>> configuredResponseMappers, ObjectMapper jacksonJsonMapper, boolean disableDefaultMappers) { + List<ResponseMapperHolder<?>> configuredResponseMappers, boolean disableDefaultMappers) { List<ResponseMapperHolder<?>> responseMappers = new ArrayList<>(configuredResponseMappers); if (!disableDefaultMappers) { - responseMappers.add(new ResponseMapperHolder<>(HttpResponse.class, (context, entity) -> entity)); - responseMappers.add(new ResponseMapperHolder<>(String.class, (context, entity) -> new MessageResponse(entity))); - responseMappers.add(new ResponseMapperHolder<>(Slime.class, (context, entity) -> new SlimeJsonResponse(entity))); - responseMappers.add(new ResponseMapperHolder<>(JsonNode.class, (context, entity) -> new JacksonJsonResponse<>(200, entity, jacksonJsonMapper, true))); + responseMappers.addAll(RestApiMappers.DEFAULT_RESPONSE_MAPPERS); } return responseMappers; } private static List<RequestMapperHolder<?>> combineWithDefaultRequestMappers( - List<RequestMapperHolder<?>> configuredRequestMappers, ObjectMapper jacksonJsonMapper) { + List<RequestMapperHolder<?>> configuredRequestMappers) { List<RequestMapperHolder<?>> requestMappers = new ArrayList<>(configuredRequestMappers); - requestMappers.add(new RequestMapperHolder<>(Slime.class, RestApiImpl::toSlime)); - requestMappers.add(new RequestMapperHolder<>(JsonNode.class, ctx -> toJsonNode(ctx, jacksonJsonMapper))); - requestMappers.add(new RequestMapperHolder<>(String.class, RestApiImpl::toString)); - requestMappers.add(new RequestMapperHolder<>(byte[].class, RestApiImpl::toByteArray)); - requestMappers.add(new RequestMapperHolder<>(InputStream.class, RestApiImpl::toInputStream)); - requestMappers.add(new RequestMapperHolder<>(Void.class, ctx -> Optional.empty())); + requestMappers.addAll(RestApiMappers.DEFAULT_REQUEST_MAPPERS); return requestMappers; } - private static Optional<InputStream> toInputStream(RequestContext context) { - return context.requestContent().map(RequestContext.RequestContent::content); - } - - private static Optional<byte[]> toByteArray(RequestContext context) { - InputStream in = toInputStream(context).orElse(null); - if (in == null) return Optional.empty(); - return convertIoException(() -> Optional.of(in.readAllBytes())); - } - - private static Optional<String> toString(RequestContext context) { - try { - return toByteArray(context).map(bytes -> new String(bytes, UTF_8)); - } catch (RuntimeException e) { - throw new RestApiException.BadRequest("Failed parse request content as UTF-8: " + Exceptions.toMessageString(e), e); - } - } - - private static Optional<JsonNode> toJsonNode(RequestContext context, ObjectMapper jacksonJsonMapper) { - if (log.isLoggable(Level.FINE)) { - return toString(context).map(string -> { - log.fine(() -> "Request content: " + string); - return convertIoException("Failed to parse JSON", () -> jacksonJsonMapper.readTree(string)); - }); - } else { - return toInputStream(context) - .map(in -> convertIoException("Invalid JSON", () -> jacksonJsonMapper.readTree(in))); - } - } - - private static Optional<Slime> toSlime(RequestContext context) { - try { - return toString(context).map(string -> { - log.fine(() -> "Request content: " + string); - return SlimeUtils.jsonToSlimeOrThrow(string); - }); - } catch (com.yahoo.slime.JsonParseException e) { - log.log(Level.FINE, e.getMessage(), e); - throw new RestApiException.BadRequest("Invalid JSON: " + Exceptions.toMessageString(e), e); - } - } - static class BuilderImpl implements RestApi.Builder { private final List<Route> routes = new ArrayList<>(); private final List<ExceptionMapperHolder<?>> exceptionMappers = new ArrayList<>(); @@ -237,6 +212,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; } @@ -256,20 +232,21 @@ class RestApiImpl implements RestApi { } @Override public <ENTITY> Builder registerJacksonResponseEntity(Class<ENTITY> type) { - addResponseMapper(type, new JacksonResponseMapper<>()); return this; + addResponseMapper(type, new RestApiMappers.JacksonResponseMapper<>()); return this; } @Override public <ENTITY> Builder registerJacksonRequestEntity(Class<ENTITY> type) { - addRequestMapper(type, new JacksonRequestMapper<>(type)); return this; + addRequestMapper(type, new RestApiMappers.JacksonRequestMapper<>(type)); return this; } @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); } } - public static class RouteBuilderImpl implements RestApi.RouteBuilder { + static class RouteBuilderImpl implements RestApi.RouteBuilder { private final String pathPattern; private String name; private final Map<Method, HandlerHolder<?>> handlerPerMethod = new HashMap<>(); @@ -279,50 +256,118 @@ class RestApiImpl implements RestApi { RouteBuilderImpl(String pathPattern) { this.pathPattern = pathPattern; } @Override public RestApi.RouteBuilder name(String name) { this.name = name; return this; } - @Override public RestApi.RouteBuilder get(Handler<?> handler) { - return addHandler(Method.GET, handler); + @Override public RestApi.RouteBuilder addFilter(RestApi.Filter filter) { filters.add(filter); return this; } + + // GET + @Override public RouteBuilder get(Handler<?> handler) { return get(handler, null); } + @Override public RouteBuilder get(Handler<?> handler, HandlerConfigBuilder config) { + return addHandler(Method.GET, handler, config); } - @Override public RestApi.RouteBuilder post(Handler<?> handler) { - return addHandler(Method.POST, handler); + + // POST + @Override public RouteBuilder post(Handler<?> handler) { return post(handler, null); } + @Override public <REQUEST_ENTITY> RouteBuilder post( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler) { + return post(type, handler, null); } - @Override public <ENTITY> RouteBuilder post(Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler) { - return addHandler(Method.POST, type, handler); + @Override public RouteBuilder post(Handler<?> handler, HandlerConfigBuilder config) { + return addHandler(Method.POST, handler, config); } - @Override public RestApi.RouteBuilder put(Handler<?> handler) { - return addHandler(Method.PUT, handler); + @Override public <REQUEST_ENTITY> RouteBuilder post( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config) { + return addHandler(Method.POST, type, handler, config); } - @Override public <ENTITY> RouteBuilder put(Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler) { - return addHandler(Method.PUT, type, handler); + + // PUT + @Override public RouteBuilder put(Handler<?> handler) { return put(handler, null); } + @Override public <REQUEST_ENTITY> RouteBuilder put( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler) { + return put(type, handler, null); } - @Override public RestApi.RouteBuilder delete(Handler<?> handler) { - return addHandler(Method.DELETE, handler); + @Override public RouteBuilder put(Handler<?> handler, HandlerConfigBuilder config) { + return addHandler(Method.PUT, handler, null); } - @Override public RestApi.RouteBuilder patch(Handler<?> handler) { - return addHandler(Method.PATCH, handler); + @Override public <REQUEST_ENTITY> RouteBuilder put( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config) { + return addHandler(Method.PUT, type, handler, config); } - @Override public <ENTITY> RouteBuilder patch(Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler) { - return addHandler(Method.PATCH, type, handler); + + // DELETE + @Override public RouteBuilder delete(Handler<?> handler) { return delete(handler, null); } + @Override public RouteBuilder delete(Handler<?> handler, HandlerConfigBuilder config) { + return addHandler(Method.DELETE, handler, config); } - @Override public RestApi.RouteBuilder defaultHandler(Handler<?> handler) { - defaultHandler = HandlerHolder.of(handler); return this; + + // PATCH + @Override public RouteBuilder patch(Handler<?> handler) { return patch(handler, null); } + @Override public <REQUEST_ENTITY> RouteBuilder patch( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler) { + return patch(type, handler, null); } - @Override public <ENTITY> RouteBuilder defaultHandler(Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler) { - defaultHandler = HandlerHolder.of(type, handler); return this; + @Override public RouteBuilder patch(Handler<?> handler, HandlerConfigBuilder config) { + return addHandler(Method.PATCH, handler, config); + } + @Override public <REQUEST_ENTITY> RouteBuilder patch( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config) { + return addHandler(Method.PATCH, type, handler, config); } - @Override public RestApi.RouteBuilder addFilter(RestApi.Filter filter) { filters.add(filter); return this; } - private RestApi.RouteBuilder addHandler(Method method, Handler<?> handler) { - handlerPerMethod.put(method, HandlerHolder.of(handler)); return this; + // Default + @Override public RouteBuilder defaultHandler(Handler<?> handler) { + return defaultHandler(handler, null); + } + @Override public RouteBuilder defaultHandler(Handler<?> handler, HandlerConfigBuilder config) { + defaultHandler = HandlerHolder.of(handler, build(config)); return this; + } + @Override public <REQUEST_ENTITY> RouteBuilder defaultHandler( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler) { + return defaultHandler(type, handler, null); + } + @Override + public <REQUEST_ENTITY> RouteBuilder defaultHandler( + Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, HandlerConfigBuilder config) { + defaultHandler = HandlerHolder.of(type, handler, build(config)); return this; + } + + private RestApi.RouteBuilder addHandler(Method method, Handler<?> handler, HandlerConfigBuilder config) { + handlerPerMethod.put(method, HandlerHolder.of(handler, build(config))); return this; } private <ENTITY> RestApi.RouteBuilder addHandler( - Method method, Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler) { - handlerPerMethod.put(method, HandlerHolder.of(type, handler)); return this; + Method method, Class<ENTITY> type, HandlerWithRequestEntity<ENTITY, ?> handler, HandlerConfigBuilder config) { + handlerPerMethod.put(method, HandlerHolder.of(type, handler, build(config))); return this; + } + + private static HandlerConfig build(HandlerConfigBuilder builder) { + if (builder == null) return HandlerConfig.empty(); + return ((HandlerConfigBuilderImpl)builder).build(); } private Route build() { return new Route(this); } } + 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 { + final AclMapping.Action aclAction; + + HandlerConfig(HandlerConfigBuilderImpl builder) { + this.aclAction = builder.aclAction; + } + + static HandlerConfig empty() { return new HandlerConfigBuilderImpl().build(); } + } + private static class RequestContextImpl implements RestApi.RequestContext { final HttpRequest request; final Path pathMatcher; @@ -332,12 +377,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; } @@ -357,6 +404,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 @@ -433,63 +481,37 @@ class RestApiImpl implements RestApi { } } - private static class ExceptionMapperHolder<EXCEPTION extends RuntimeException> { - final Class<EXCEPTION> type; - final RestApi.ExceptionMapper<EXCEPTION> mapper; - - ExceptionMapperHolder(Class<EXCEPTION> type, RestApi.ExceptionMapper<EXCEPTION> mapper) { - this.type = type; - this.mapper = mapper; - } - - HttpResponse toResponse(RestApi.RequestContext context, RuntimeException e) { return mapper.toResponse(context, type.cast(e)); } - } - - private static class ResponseMapperHolder<ENTITY> { - final Class<ENTITY> type; - final RestApi.ResponseMapper<ENTITY> mapper; - - ResponseMapperHolder(Class<ENTITY> type, RestApi.ResponseMapper<ENTITY> mapper) { - this.type = type; - this.mapper = mapper; - } - - HttpResponse toHttpResponse(RestApi.RequestContext context, Object entity) { return mapper.toHttpResponse(context, type.cast(entity)); } - } - private static class HandlerHolder<REQUEST_ENTITY> { final Class<REQUEST_ENTITY> type; final HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler; + final HandlerConfig config; - HandlerHolder(Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler) { + private HandlerHolder( + Class<REQUEST_ENTITY> type, + HandlerWithRequestEntity<REQUEST_ENTITY, ?> handler, + HandlerConfig config) { this.type = type; this.handler = handler; + this.config = config; } static <RESPONSE_ENTITY, REQUEST_ENTITY> HandlerHolder<REQUEST_ENTITY> of( - Class<REQUEST_ENTITY> type, HandlerWithRequestEntity<REQUEST_ENTITY, RESPONSE_ENTITY> handler) { - return new HandlerHolder<>(type, handler); + Class<REQUEST_ENTITY> type, + HandlerWithRequestEntity<REQUEST_ENTITY, RESPONSE_ENTITY> handler, + HandlerConfig config) { + return new HandlerHolder<>(type, handler, config); } - static <RESPONSE_ENTITY> HandlerHolder<Void> of(Handler<RESPONSE_ENTITY> handler) { + static <RESPONSE_ENTITY> HandlerHolder<Void> of(Handler<RESPONSE_ENTITY> handler, HandlerConfig config) { return new HandlerHolder<>( Void.class, - (HandlerWithRequestEntity<Void, RESPONSE_ENTITY>) (context, nullEntity) -> handler.handleRequest(context)); + (HandlerWithRequestEntity<Void, RESPONSE_ENTITY>) (context, nullEntity) -> handler.handleRequest(context), + config); } Object executeHandler(RestApi.RequestContext context, Object entity) { return handler.handleRequest(context, type.cast(entity)); } } - private static class RequestMapperHolder<ENTITY> { - final Class<ENTITY> type; - final RestApi.RequestMapper<ENTITY> mapper; - - RequestMapperHolder(Class<ENTITY> type, RequestMapper<ENTITY> mapper) { - this.type = type; - this.mapper = mapper; - } - } - static class Route { private final String pathPattern; private final String name; @@ -507,47 +529,10 @@ class RestApiImpl implements RestApi { } private HandlerHolder<?> createDefaultMethodHandler() { - return HandlerHolder.of(context -> { throw new RestApiException.MethodNotAllowed(context.request()); }); + return HandlerHolder.of( + context -> { throw new RestApiException.MethodNotAllowed(context.request()); }, + HandlerConfig.empty()); } } - private static class JacksonRequestMapper<ENTITY> implements RequestMapper<ENTITY> { - private final Class<ENTITY> type; - - JacksonRequestMapper(Class<ENTITY> type) { this.type = type; } - - @Override - public Optional<ENTITY> toRequestEntity(RequestContext context) throws RestApiException { - if (log.isLoggable(Level.FINE)) { - return RestApiImpl.toString(context).map(string -> { - log.fine(() -> "Request content: " + string); - return convertIoException("Failed to parse JSON", () -> context.jacksonJsonMapper().readValue(string, type)); - }); - } else { - return RestApiImpl.toInputStream(context) - .map(in -> convertIoException("Invalid JSON", () -> context.jacksonJsonMapper().readValue(in, type))); - } - } - } - - private static class JacksonResponseMapper<ENTITY> implements ResponseMapper<ENTITY> { - @Override - public HttpResponse toHttpResponse(RequestContext context, ENTITY responseEntity) throws RestApiException { - return new JacksonJsonResponse<>(200, responseEntity, context.jacksonJsonMapper(), true); - } - } - - @FunctionalInterface private interface SupplierThrowingIoException<T> { T get() throws IOException; } - private static <T> T convertIoException(String messagePrefix, SupplierThrowingIoException<T> supplier) { - try { - return supplier.get(); - } catch (IOException e) { - log.log(Level.FINE, e.getMessage(), e); - throw new RestApiException.InternalServerError(messagePrefix + ": " + Exceptions.toMessageString(e), e); - } - } - - private static <T> T convertIoException(SupplierThrowingIoException<T> supplier) { - return convertIoException("Failed to read request content", supplier); - } } diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java b/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java new file mode 100644 index 00000000000..36d98421e6a --- /dev/null +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java @@ -0,0 +1,172 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.restapi; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.restapi.RestApi.ExceptionMapper; +import com.yahoo.restapi.RestApi.RequestMapper; +import com.yahoo.restapi.RestApi.ResponseMapper; +import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; +import com.yahoo.yolean.Exceptions; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Implementations of {@link ExceptionMapper}, {@link RequestMapper} and {@link ResponseMapper}. + * + * @author bjorncs + */ +public class RestApiMappers { + + private static final Logger log = Logger.getLogger(RestApiMappers.class.getName()); + + public static List<RequestMapperHolder<?>> DEFAULT_REQUEST_MAPPERS = List.of( + new RequestMapperHolder<>(Slime.class, RestApiMappers::toSlime), + new RequestMapperHolder<>(JsonNode.class, ctx -> toJsonNode(ctx, ctx.jacksonJsonMapper())), + new RequestMapperHolder<>(String.class, RestApiMappers::toString), + new RequestMapperHolder<>(byte[].class, RestApiMappers::toByteArray), + new RequestMapperHolder<>(InputStream .class, RestApiMappers::toInputStream), + new RequestMapperHolder<>(Void.class, ctx -> Optional.empty())); + + public static List<ResponseMapperHolder<?>> DEFAULT_RESPONSE_MAPPERS = List.of( + new ResponseMapperHolder<>(HttpResponse.class, (context, entity) -> entity), + new ResponseMapperHolder<>(String.class, (context, entity) -> new MessageResponse(entity)), + new ResponseMapperHolder<>(Slime.class, (context, entity) -> new SlimeJsonResponse(entity)), + new ResponseMapperHolder<>(JsonNode.class, + (context, entity) -> new JacksonJsonResponse<>(200, entity, context.jacksonJsonMapper(), true))); + + public static List<ExceptionMapperHolder<?>> DEFAULT_EXCEPTION_MAPPERS = List.of( + new ExceptionMapperHolder<>(RestApiException.class, (context, exception) -> exception.response())); + + private RestApiMappers() {} + + public static class JacksonRequestMapper<ENTITY> implements RequestMapper<ENTITY> { + private final Class<ENTITY> type; + + JacksonRequestMapper(Class<ENTITY> type) { this.type = type; } + + @Override + public Optional<ENTITY> toRequestEntity(RestApi.RequestContext context) throws RestApiException { + if (log.isLoggable(Level.FINE)) { + return RestApiMappers.toString(context).map(string -> { + log.fine(() -> "Request content: " + string); + return convertIoException("Failed to parse JSON", () -> context.jacksonJsonMapper().readValue(string, type)); + }); + } else { + return toInputStream(context) + .map(in -> convertIoException("Invalid JSON", () -> context.jacksonJsonMapper().readValue(in, type))); + } + } + } + + public static class JacksonResponseMapper<ENTITY> implements ResponseMapper<ENTITY> { + @Override + public HttpResponse toHttpResponse(RestApi.RequestContext context, ENTITY responseEntity) throws RestApiException { + return new JacksonJsonResponse<>(200, responseEntity, context.jacksonJsonMapper(), true); + } + } + + public static class RequestMapperHolder<ENTITY> { + final Class<ENTITY> type; + final RestApi.RequestMapper<ENTITY> mapper; + + RequestMapperHolder(Class<ENTITY> type, RequestMapper<ENTITY> mapper) { + this.type = type; + this.mapper = mapper; + } + } + + public static class ResponseMapperHolder<ENTITY> { + final Class<ENTITY> type; + final RestApi.ResponseMapper<ENTITY> mapper; + + ResponseMapperHolder(Class<ENTITY> type, RestApi.ResponseMapper<ENTITY> mapper) { + this.type = type; + this.mapper = mapper; + } + + HttpResponse toHttpResponse(RestApi.RequestContext ctx, Object entity) { + return mapper.toHttpResponse(ctx, type.cast(entity)); + } + } + + public static class ExceptionMapperHolder<EXCEPTION extends RuntimeException> { + final Class<EXCEPTION> type; + final RestApi.ExceptionMapper<EXCEPTION> mapper; + + ExceptionMapperHolder(Class<EXCEPTION> type, RestApi.ExceptionMapper<EXCEPTION> mapper) { + this.type = type; + this.mapper = mapper; + } + + HttpResponse toResponse(RestApi.RequestContext ctx, RuntimeException e) { + return mapper.toResponse(ctx, type.cast(e)); + } + } + + private static Optional<InputStream> toInputStream(RestApi.RequestContext context) { + return context.requestContent().map(RestApi.RequestContext.RequestContent::content); + } + + private static Optional<byte[]> toByteArray(RestApi.RequestContext context) { + InputStream in = toInputStream(context).orElse(null); + if (in == null) return Optional.empty(); + return convertIoException(() -> Optional.of(in.readAllBytes())); + } + + private static Optional<String> toString(RestApi.RequestContext context) { + try { + return toByteArray(context).map(bytes -> new String(bytes, UTF_8)); + } catch (RuntimeException e) { + throw new RestApiException.BadRequest("Failed parse request content as UTF-8: " + Exceptions.toMessageString(e), e); + } + } + + private static Optional<JsonNode> toJsonNode(RestApi.RequestContext context, ObjectMapper jacksonJsonMapper) { + if (log.isLoggable(Level.FINE)) { + return toString(context).map(string -> { + log.fine(() -> "Request content: " + string); + return convertIoException("Failed to parse JSON", () -> jacksonJsonMapper.readTree(string)); + }); + } else { + return toInputStream(context) + .map(in -> convertIoException("Invalid JSON", () -> jacksonJsonMapper.readTree(in))); + } + } + + @FunctionalInterface private interface SupplierThrowingIoException<T> { T get() throws IOException; } + private static <T> T convertIoException(String messagePrefix, SupplierThrowingIoException<T> supplier) { + try { + return supplier.get(); + } catch (IOException e) { + log.log(Level.FINE, e.getMessage(), e); + throw new RestApiException.InternalServerError(messagePrefix + ": " + Exceptions.toMessageString(e), e); + } + } + + private static <T> T convertIoException(SupplierThrowingIoException<T> supplier) { + return convertIoException("Failed to read request content", supplier); + } + + private static Optional<Slime> toSlime(RestApi.RequestContext context) { + try { + return toString(context).map(string -> { + log.fine(() -> "Request content: " + string); + return SlimeUtils.jsonToSlimeOrThrow(string); + }); + } catch (com.yahoo.slime.JsonParseException e) { + log.log(Level.FINE, e.getMessage(), e); + throw new RestApiException.BadRequest("Invalid JSON: " + Exceptions.toMessageString(e), e); + } + } + +} 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; |