diff options
author | Øyvind Grønnesby <oyving@verizonmedia.com> | 2021-07-16 10:23:12 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-16 10:23:12 +0200 |
commit | 236c15593258ec5e0d495332edc64468df269fc4 (patch) | |
tree | 97920ab02f74f18cda048c8d6914cc1137295f99 | |
parent | 73d916d2164c9654366cff7e96f5a5687bd23f59 (diff) | |
parent | f449a4f2d1e477afcc30050f6f72f2cb996f26ae (diff) |
Merge pull request #18620 from vespa-engine/bjorncs/restapi
Bjorncs/restapi
5 files changed, 54 insertions, 66 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 2ef14679553..d15a94bea30 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApi.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApi.java @@ -8,6 +8,7 @@ import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.RequestHandlerSpec; import java.io.InputStream; +import java.security.Principal; import java.util.List; import java.util.Optional; import java.util.OptionalDouble; @@ -129,6 +130,8 @@ public interface RestApi { ObjectMapper jacksonJsonMapper(); UriBuilder uriBuilder(); AclMapping.Action aclAction(); + Optional<Principal> userPrincipal(); + Principal userPrincipalOrThrow(); interface Parameters { Optional<String> getString(String name); diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiException.java b/container-core/src/main/java/com/yahoo/restapi/RestApiException.java index 68e46a3a9b8..747cf1cedeb 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApiException.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiException.java @@ -78,4 +78,10 @@ public class RestApiException extends RuntimeException { public Conflict(String message) { this(message, null); } public Conflict(String message, Throwable cause) { super(ErrorResponse::conflict, message, cause); } } + + public static class Unauthorized extends RestApiException { + public Unauthorized() { this("Unauthorized", null); } + public Unauthorized(String message) { this(message, null); } + public Unauthorized(String message, Throwable cause) { super(ErrorResponse::unauthorized, message, cause); } + } } 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 646177e60db..fda08342ead 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java @@ -14,6 +14,7 @@ import com.yahoo.restapi.RestApiMappers.ResponseMapperHolder; import java.io.InputStream; import java.net.URI; +import java.security.Principal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -405,6 +406,12 @@ class RestApiImpl implements RestApi { : new UriBuilder(uri.getScheme() + "://" + uri.getHost()); } @Override public AclMapping.Action aclAction() { return aclAction; } + @Override public Optional<Principal> userPrincipal() { + return Optional.ofNullable(request.getJDiscRequest().getUserPrincipal()); + } + @Override public Principal userPrincipalOrThrow() { + return userPrincipal().orElseThrow(RestApiException.Unauthorized::new); + } private class PathParametersImpl implements RestApi.RequestContext.PathParameters { @Override diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java b/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java index 36d98421e6a..87574e6af9a 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiMappers.java @@ -29,7 +29,7 @@ public class RestApiMappers { private static final Logger log = Logger.getLogger(RestApiMappers.class.getName()); - public static List<RequestMapperHolder<?>> DEFAULT_REQUEST_MAPPERS = List.of( + 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), @@ -37,14 +37,14 @@ public class RestApiMappers { new RequestMapperHolder<>(InputStream .class, RestApiMappers::toInputStream), new RequestMapperHolder<>(Void.class, ctx -> Optional.empty())); - public static List<ResponseMapperHolder<?>> DEFAULT_RESPONSE_MAPPERS = List.of( + 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( + static List<ExceptionMapperHolder<?>> DEFAULT_EXCEPTION_MAPPERS = List.of( new ExceptionMapperHolder<>(RestApiException.class, (context, exception) -> exception.response())); private RestApiMappers() {} @@ -75,7 +75,7 @@ public class RestApiMappers { } } - public static class RequestMapperHolder<ENTITY> { + static class RequestMapperHolder<ENTITY> { final Class<ENTITY> type; final RestApi.RequestMapper<ENTITY> mapper; @@ -85,7 +85,7 @@ public class RestApiMappers { } } - public static class ResponseMapperHolder<ENTITY> { + static class ResponseMapperHolder<ENTITY> { final Class<ENTITY> type; final RestApi.ResponseMapper<ENTITY> mapper; @@ -99,7 +99,7 @@ public class RestApiMappers { } } - public static class ExceptionMapperHolder<EXCEPTION extends RuntimeException> { + static class ExceptionMapperHolder<EXCEPTION extends RuntimeException> { final Class<EXCEPTION> type; final RestApi.ExceptionMapper<EXCEPTION> mapper; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java index 9ac94d0208c..621ce9189e9 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java @@ -1,19 +1,14 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.restapi.athenz; +import com.google.inject.Inject; import com.yahoo.config.provision.SystemName; -import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; -import com.yahoo.container.jdisc.LoggingRequestHandler; -import com.yahoo.jdisc.http.HttpRequest.Method; -import com.yahoo.restapi.ErrorResponse; -import com.yahoo.restapi.MessageResponse; -import com.yahoo.restapi.Path; import com.yahoo.restapi.ResourceResponse; -import com.yahoo.restapi.SlimeJsonResponse; +import com.yahoo.restapi.RestApi; +import com.yahoo.restapi.RestApiRequestHandler; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; -import com.yahoo.text.Text; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzPrincipal; import com.yahoo.vespa.athenz.api.AthenzUser; @@ -22,20 +17,19 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; import com.yahoo.vespa.hosted.controller.api.integration.entity.EntityService; import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade; -import com.yahoo.yolean.Exceptions; import java.util.Map; -import java.util.Optional; -import java.util.logging.Level; import java.util.logging.Logger; +import static com.yahoo.restapi.RestApi.route; + /** * This API proxies requests to an Athenz server. * * @author jonmv */ @SuppressWarnings("unused") // Handler -public class AthenzApiHandler extends LoggingRequestHandler { +public class AthenzApiHandler extends RestApiRequestHandler<AthenzApiHandler> { private final static Logger log = Logger.getLogger(AthenzApiHandler.class.getName()); @@ -43,55 +37,32 @@ public class AthenzApiHandler extends LoggingRequestHandler { private final AthenzDomain sandboxDomain; private final EntityService properties; + @Inject public AthenzApiHandler(Context parentCtx, AthenzFacade athenz, Controller controller) { - super(parentCtx); + super(parentCtx, AthenzApiHandler::createRestApi); this.athenz = athenz; this.sandboxDomain = new AthenzDomain(sandboxDomainIn(controller.system())); this.properties = controller.serviceRegistry().entityService(); } - @Override - public HttpResponse handle(HttpRequest request) { - Method method = request.getMethod(); - try { - switch (method) { - case GET: return get(request); - case POST: return post(request); - default: return ErrorResponse.methodNotAllowed("Method '" + method + "' is unsupported"); - } - } - catch (IllegalArgumentException|IllegalStateException e) { - return ErrorResponse.badRequest(Exceptions.toMessageString(e)); - } - catch (RuntimeException e) { - log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e); - return ErrorResponse.internalServerError(Exceptions.toMessageString(e)); - } - } - - private HttpResponse get(HttpRequest request) { - Path path = new Path(request.getUri()); - if (path.matches("/athenz/v1")) return root(request); - if (path.matches("/athenz/v1/domains")) return domainList(request); - if (path.matches("/athenz/v1/properties")) return properties(); - - return ErrorResponse.notFoundError(Text.format("No '%s' handler at '%s'", request.getMethod(), - request.getUri().getPath())); + private static RestApi createRestApi(AthenzApiHandler self) { + return RestApi.builder() + .addRoute(route("/athenz/v1") + .get(self::root)) + .addRoute(route("/athenz/v1/domains") + .get(self::domainList)) + .addRoute(route("/athenz/v1/properties") + .get(self::properties)) + .addRoute(route("/athenz/v1/user") + .post(self::signup)) + .build(); } - private HttpResponse post(HttpRequest request) { - Path path = new Path(request.getUri()); - if (path.matches("/athenz/v1/user")) return signup(request); - return ErrorResponse.notFoundError(Text.format("No '%s' handler at '%s'", request.getMethod(), - request.getUri().getPath())); + private HttpResponse root(RestApi.RequestContext ctx) { + return new ResourceResponse(ctx.request(), "domains", "properties"); } - private HttpResponse root(HttpRequest request) { - return new ResourceResponse(request, "domains", "properties"); - } - - - private HttpResponse properties() { + private Slime properties(RestApi.RequestContext ctx) { Slime slime = new Slime(); Cursor response = slime.setObject(); Cursor array = response.setArray("properties"); @@ -100,26 +71,27 @@ public class AthenzApiHandler extends LoggingRequestHandler { propertyObject.setString("propertyid", entry.getKey().id()); propertyObject.setString("property", entry.getValue().id()); } - return new SlimeJsonResponse(slime); + return slime; } - private HttpResponse domainList(HttpRequest request) { + private Slime domainList(RestApi.RequestContext ctx) { Slime slime = new Slime(); Cursor array = slime.setObject().setArray("data"); - for (AthenzDomain athenzDomain : athenz.getDomainList(request.getProperty("prefix"))) + for (AthenzDomain athenzDomain : athenz.getDomainList(ctx.queryParameters().getString("prefix").orElse(null))) array.addString(athenzDomain.getName()); - return new SlimeJsonResponse(slime); + return slime; } - private HttpResponse signup(HttpRequest request) { - AthenzUser user = athenzUser(request); + private String signup(RestApi.RequestContext ctx) { + AthenzUser user = athenzUser(ctx); athenz.addTenantAdmin(sandboxDomain, user); - return new MessageResponse("User '" + user.getName() + "' added to admin role of '" + sandboxDomain.getName() + "'"); + return "User '" + user.getName() + "' added to admin role of '" + sandboxDomain.getName() + "'"; } - private static AthenzUser athenzUser(HttpRequest request) { - return Optional.ofNullable(request.getJDiscRequest().getUserPrincipal()).filter(AthenzPrincipal.class::isInstance) + private static AthenzUser athenzUser(RestApi.RequestContext ctx) { + return ctx.userPrincipal() + .filter(AthenzPrincipal.class::isInstance) .map(AthenzPrincipal.class::cast) .map(AthenzPrincipal::getIdentity) .filter(AthenzUser.class::isInstance) |