diff options
11 files changed, 558 insertions, 108 deletions
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/AthenzFilterMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/AthenzFilterMock.java new file mode 100644 index 00000000000..02a7f63fbb8 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/AthenzFilterMock.java @@ -0,0 +1,42 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller; + +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.SecurityRequestFilter; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzPrincipal; +import com.yahoo.vespa.athenz.api.NToken; +import com.yahoo.vespa.athenz.utils.AthenzIdentities; +import com.yahoo.yolean.chain.Before; + +import static com.yahoo.vespa.hosted.controller.restapi.filter.SecurityFilterUtils.sendErrorResponse; + +/** + * @author bjorncs + */ +@Before("com.yahoo.vespa.hosted.controller.restapi.filter.ControllerAuthorizationFilter") +public class AthenzFilterMock implements SecurityRequestFilter { + + public static final String IDENTITY_HEADER_NAME = "Athenz-Identity"; + public static final String ATHENZ_NTOKEN_HEADER_NAME = "Athenz-NToken"; + + @Override + public void filter(DiscFilterRequest request, ResponseHandler handler) { + if (request.getMethod().equalsIgnoreCase("OPTIONS")) return; + String identityName = request.getHeader(IDENTITY_HEADER_NAME); + String nToken = request.getHeader(ATHENZ_NTOKEN_HEADER_NAME); + if (identityName == null) { + sendErrorResponse(handler, HttpResponse.Status.UNAUTHORIZED, "Not authenticated"); + } else { + AthenzIdentity identity = AthenzIdentities.from(identityName); + AthenzPrincipal principal = + nToken == null ? + new AthenzPrincipal(identity) : + new AthenzPrincipal(identity, new NToken(nToken)); + request.setUserPrincipal(principal); + } + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java new file mode 100644 index 00000000000..002ccb47e3b --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java @@ -0,0 +1,182 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.restapi; + +import com.yahoo.application.container.handler.Request; +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpRequest; + +import java.net.SocketAddress; +import java.net.URI; +import java.security.Principal; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Wraps an {@link Request} into a {@link DiscFilterRequest}. Only a few methods are supported. + * Changes are not propagated; updated request instance must be retrieved through {@link #getUpdatedRequest()}. + * + * @author bjorncs + */ +class ApplicationRequestToDiscFilterRequestWrapper extends DiscFilterRequest { + + private final Request request; + private Principal userPrincipal; + + public ApplicationRequestToDiscFilterRequestWrapper(Request request) { + super(new ServletOrJdiscHttpRequest() { + @Override + public void copyHeaders(HeaderFields target) { + request.getHeaders().forEach(target::add); + } + + @Override + public Map<String, List<String>> parameters() { + return Collections.emptyMap(); + } + + @Override + public URI getUri() { + return URI.create(request.getUri()); + } + + @Override + public HttpRequest.Version getVersion() { + throw new UnsupportedOperationException(); + } + + @Override + public String getRemoteHostAddress() { + throw new UnsupportedOperationException(); + } + + @Override + public String getRemoteHostName() { + throw new UnsupportedOperationException(); + } + + @Override + public int getRemotePort() { + throw new UnsupportedOperationException(); + } + + @Override + public void setRemoteAddress(SocketAddress remoteAddress) { + throw new UnsupportedOperationException(); + } + + @Override + public Map<String, Object> context() { + throw new UnsupportedOperationException(); + } + + @Override + public List<Cookie> decodeCookieHeader() { + throw new UnsupportedOperationException(); + } + + @Override + public void encodeCookieHeader(List<Cookie> cookies) { + throw new UnsupportedOperationException(); + } + + @Override + public long getConnectedAt(TimeUnit unit) { + throw new UnsupportedOperationException(); + } + }); + this.request = request; + this.userPrincipal = request.getUserPrincipal().orElse(null); + } + + public Request getUpdatedRequest() { + Request updatedRequest = new Request(this.request.getUri(), this.request.getBody(), this.request.getMethod(), this.userPrincipal); + this.request.getHeaders().forEach(updatedRequest.getHeaders()::put); + return updatedRequest; + } + + @Override + public String getMethod() { + return request.getMethod().name(); + } + + @Override + public void setUri(URI uri) { + throw new UnsupportedOperationException(); + } + + @Override + public String getParameter(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public Enumeration<String> getParameterNames() { + throw new UnsupportedOperationException(); + } + + @Override + public void addHeader(String name, String value) { + throw new UnsupportedOperationException(); + } + + @Override + public String getHeader(String name) { + return request.getHeaders().getFirst(name); + } + + @Override + public Enumeration<String> getHeaderNames() { + throw new UnsupportedOperationException(); + } + + @Override + public List<String> getHeaderNamesAsList() { + throw new UnsupportedOperationException(); + } + + @Override + public Enumeration<String> getHeaders(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public List<String> getHeadersAsList(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeHeaders(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void setHeaders(String name, String value) { + throw new UnsupportedOperationException(); + } + + @Override + public void setHeaders(String name, List<String> values) { + throw new UnsupportedOperationException(); + } + + @Override + public Principal getUserPrincipal() { + return this.userPrincipal; + } + + @Override + public void setUserPrincipal(Principal principal) { + this.userPrincipal = principal; + } + + @Override + public void clearCookies() { + throw new UnsupportedOperationException(); + } +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java index 95810e90cdb..be987e84cd8 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java @@ -5,8 +5,12 @@ import com.yahoo.application.container.JDisc; import com.yahoo.application.container.handler.Request; import com.yahoo.application.container.handler.Response; import com.yahoo.collections.Pair; +import com.yahoo.component.ComponentSpecification; import com.yahoo.component.Version; +import com.yahoo.container.http.filter.FilterChainRepository; import com.yahoo.io.IOUtils; +import com.yahoo.jdisc.http.filter.SecurityRequestFilter; +import com.yahoo.jdisc.http.filter.SecurityRequestFilterChain; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; @@ -21,6 +25,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Supplier; @@ -70,7 +75,9 @@ public class ContainerTester { public void assertResponse(Request request, File responseFile, int expectedStatusCode) throws IOException { String expectedResponse = IOUtils.readFile(new File(responseFilePath + responseFile.toString())); expectedResponse = include(expectedResponse); - Response response = container.handleRequest(request); + FilterResult filterResult = invokeSecurityFilters(request); + request = filterResult.request; + Response response = filterResult.response != null ? filterResult.response : container.handleRequest(request); Slime expectedSlime = SlimeUtils.jsonToSlime(expectedResponse.getBytes(StandardCharsets.UTF_8)); Set<String> fieldsToCensor = fieldsToCensor(null, expectedSlime.get(), new HashSet<>()); Slime responseSlime = SlimeUtils.jsonToSlime(response.getBody()); @@ -96,11 +103,31 @@ public class ContainerTester { } public void assertResponse(Request request, String expectedResponse, int expectedStatusCode) throws IOException { - Response response = container.handleRequest(request); + FilterResult filterResult = invokeSecurityFilters(request); + request = filterResult.request; + Response response = filterResult.response != null ? filterResult.response : container.handleRequest(request); assertEquals(expectedResponse, response.getBodyAsString()); assertEquals("Status code", expectedStatusCode, response.getStatus()); } + // Hack to run request filters as part of the request processing chain. + // Limitation: Bindings ignored, disc filter request wrapper only support limited set of methods. + private FilterResult invokeSecurityFilters(Request request) { + FilterChainRepository filterChainRepository = (FilterChainRepository) container.components().getComponent(FilterChainRepository.class.getName()); + SecurityRequestFilterChain chain = (SecurityRequestFilterChain) filterChainRepository.getFilter(ComponentSpecification.fromString("default")); + for (SecurityRequestFilter securityRequestFilter : chain.getFilters()) { + ApplicationRequestToDiscFilterRequestWrapper discFilterRequest = new ApplicationRequestToDiscFilterRequestWrapper(request); + ResponseHandlerToApplicationResponseWrapper responseHandlerWrapper = new ResponseHandlerToApplicationResponseWrapper(); + securityRequestFilter.filter(discFilterRequest, responseHandlerWrapper); + request = discFilterRequest.getUpdatedRequest(); + Optional<Response> filterResponse = responseHandlerWrapper.toResponse(); + if (filterResponse.isPresent()) { + return new FilterResult(request, filterResponse.get()); + } + } + return new FilterResult(request, null); + } + private Set<String> fieldsToCensor(String fieldNameOrNull, Inspector value, Set<String> fieldsToCensor) { switch (value.type()) { case ARRAY: value.traverse((ArrayTraverser)(int index, Inspector element) -> fieldsToCensor(null, element, fieldsToCensor)); break; @@ -157,5 +184,14 @@ public class ContainerTester { return prefix + includedContent + postFix; } + static class FilterResult { + final Request request; + final Response response; + + FilterResult(Request request, Response response) { + this.request = request; + this.response = response; + } + } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java index 94fef092a5b..413e9ff36bb 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java @@ -5,12 +5,18 @@ import com.yahoo.application.Networking; import com.yahoo.application.container.JDisc; import com.yahoo.application.container.handler.Request; import com.yahoo.application.container.handler.Response; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzUser; +import com.yahoo.vespa.athenz.api.NToken; +import com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock; import org.junit.After; import org.junit.Before; import java.io.UncheckedIOException; import java.nio.charset.CharacterCodingException; +import static com.yahoo.vespa.hosted.controller.AthenzFilterMock.ATHENZ_NTOKEN_HEADER_NAME; +import static com.yahoo.vespa.hosted.controller.AthenzFilterMock.IDENTITY_HEADER_NAME; import static org.junit.Assert.assertEquals; /** @@ -25,6 +31,7 @@ import static org.junit.Assert.assertEquals; */ public class ControllerContainerTest { + public static final AthenzUser USER = AthenzUser.fromUserId("bob"); protected JDisc container; @Before @@ -88,6 +95,16 @@ public class ControllerContainerTest { " <binding>http://*/zone/v2</binding>\n" + " <binding>http://*/zone/v2/*</binding>\n" + " </handler>\n" + + " <http>\n" + + " <server id='default' port='8080' />\n" + + " <filtering>\n" + + " <request-chain id='default'>\n" + + " <filter id='com.yahoo.vespa.hosted.controller.AthenzFilterMock'/>\n" + + " <filter id='com.yahoo.vespa.hosted.controller.restapi.filter.ControllerAuthorizationFilter'/>\n" + + " <binding>http://*/*</binding>\n" + + " </request-chain>\n" + + " </filtering>\n" + + " </http>\n" + "</jdisc>"; protected void assertResponse(Request request, int responseStatus, String responseMessage) { @@ -101,4 +118,32 @@ public class ControllerContainerTest { } } + protected static Request authenticatedRequest(String uri) { + return addIdentityToRequest(new Request(uri), USER); + } + + protected static Request authenticatedRequest(String uri, String body, Request.Method method) { + return addIdentityToRequest(new Request(uri, body, method), USER); + } + + protected static Request authenticatedRequest(String uri, byte[] body, Request.Method method) { + return addIdentityToRequest(new Request(uri, body, method), USER); + } + + protected static Request addIdentityToRequest(Request request, AthenzIdentity identity) { + request.getHeaders().put(IDENTITY_HEADER_NAME, identity.getFullName()); + return request; + } + + protected static Request addNTokenToRequest(Request request, NToken nToken) { + request.getHeaders().put(ATHENZ_NTOKEN_HEADER_NAME, nToken.getRawToken()); + return request; + } + + protected void addUserToHostedOperatorRole(AthenzIdentity athenzIdentity) { + AthenzClientFactoryMock mock = (AthenzClientFactoryMock) container.components() + .getComponent(AthenzClientFactoryMock.class.getName()); + mock.getSetup().addHostedOperator(athenzIdentity); + } + } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ResponseHandlerToApplicationResponseWrapper.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ResponseHandlerToApplicationResponseWrapper.java new file mode 100644 index 00000000000..783019fd187 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ResponseHandlerToApplicationResponseWrapper.java @@ -0,0 +1,77 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.restapi; + +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; + +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A {@link ResponseHandler} that caches the response content and + * converts a {@link Response} to {@link com.yahoo.application.container.handler.Response}. + * + * @author bjorncs + */ +class ResponseHandlerToApplicationResponseWrapper implements ResponseHandler { + + private Response response; + private SimpleContentChannel contentChannel; + + @Override + public ContentChannel handleResponse(Response response) { + this.response = response; + SimpleContentChannel contentChannel = new SimpleContentChannel(); + this.contentChannel = contentChannel; + return contentChannel; + } + + Optional<com.yahoo.application.container.handler.Response> toResponse() { + return Optional.ofNullable(this.response) + .map(r -> { + byte[] bytes = contentChannel.toByteArray(); + return new com.yahoo.application.container.handler.Response(response.getStatus(), bytes); + }); + } + + private class SimpleContentChannel implements ContentChannel { + + private final Queue<ByteBuffer> buffers = new ConcurrentLinkedQueue<>(); + private final AtomicBoolean closed = new AtomicBoolean(false); + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + buffers.add(buf); + handler.completed(); + } + + @Override + public void close(CompletionHandler handler) { + handler.completed(); + if (closed.getAndSet(true)) { + throw new IllegalStateException("Already closed"); + } + } + + byte[] toByteArray() { + if (!closed.get()) { + throw new IllegalStateException("Content channel not closed yet"); + } + int totalSize = 0; + for (ByteBuffer responseBuffer : buffers) { + totalSize += responseBuffer.remaining(); + } + ByteBuffer totalBuffer = ByteBuffer.allocate(totalSize); + for (ByteBuffer responseBuffer : buffers) { + totalBuffer.put(responseBuffer); + } + return totalBuffer.array(); + } + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java index 0e24a6d434f..ecc20110445 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java @@ -8,6 +8,7 @@ import com.yahoo.config.provision.Environment; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzUser; +import com.yahoo.vespa.athenz.api.NToken; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.ConfigServerClientMock; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; @@ -77,6 +78,8 @@ public class ApplicationApiTest extends ControllerContainerTest { private static final AthenzDomain ATHENZ_TENANT_DOMAIN = new AthenzDomain("domain1"); private static final ScrewdriverId SCREWDRIVER_ID = new ScrewdriverId("12345"); private static final UserId USER_ID = new UserId("myuser"); + private static final UserId HOSTED_VESPA_OPERATOR = new UserId("johnoperator"); + private static final NToken N_TOKEN = new NToken("dummy"); @Test public void testApplicationApi() throws Exception { @@ -87,32 +90,34 @@ public class ApplicationApiTest extends ControllerContainerTest { createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); // (Necessary but not provided in this API) // GET API root - tester.assertResponse(request("/application/v4/", GET), + tester.assertResponse(request("/application/v4/", GET).userIdentity(USER_ID), new File("root.json")); // GET athens domains - tester.assertResponse(request("/application/v4/athensDomain/", GET), + tester.assertResponse(request("/application/v4/athensDomain/", GET).userIdentity(USER_ID), new File("athensDomain-list.json")); // GET OpsDB properties - tester.assertResponse(request("/application/v4/property/", GET), + tester.assertResponse(request("/application/v4/property/", GET).userIdentity(USER_ID), new File("property-list.json")); // GET cookie freshness - tester.assertResponse(request("/application/v4/cookiefreshness/", GET), + tester.assertResponse(request("/application/v4/cookiefreshness/", GET).userIdentity(USER_ID), new File("cookiefreshness.json")); // POST (add) a tenant without property ID tester.assertResponse(request("/application/v4/tenant/tenant1", POST) .userIdentity(USER_ID) - .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"), + .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}") + .nToken(N_TOKEN), new File("tenant-without-applications.json")); // PUT (modify) a tenant tester.assertResponse(request("/application/v4/tenant/tenant1", PUT) .userIdentity(USER_ID) + .nToken(N_TOKEN) .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"), new File("tenant-without-applications.json")); // GET the authenticated user (with associated tenants) tester.assertResponse(request("/application/v4/user", GET).userIdentity(USER_ID), new File("user.json")); // GET all tenants - tester.assertResponse(request("/application/v4/tenant/", GET), + tester.assertResponse(request("/application/v4/tenant/", GET).userIdentity(USER_ID), new File("tenant-list.json")); @@ -123,15 +128,17 @@ public class ApplicationApiTest extends ControllerContainerTest { // POST (add) a tenant with property ID tester.assertResponse(request("/application/v4/tenant/tenant2", POST) .userIdentity(USER_ID) + .nToken(N_TOKEN) .data("{\"athensDomain\":\"domain2\", \"property\":\"property2\", \"propertyId\":\"1234\"}"), new File("tenant-without-applications-with-id.json")); // PUT (modify) a tenant with property ID tester.assertResponse(request("/application/v4/tenant/tenant2", PUT) .userIdentity(USER_ID) + .nToken(N_TOKEN) .data("{\"athensDomain\":\"domain2\", \"property\":\"property2\", \"propertyId\":\"1234\"}"), new File("tenant-without-applications-with-id.json")); // GET a tenant with property ID - tester.assertResponse(request("/application/v4/tenant/tenant2", GET), + tester.assertResponse(request("/application/v4/tenant/tenant2", GET).userIdentity(USER_ID), new File("tenant-without-applications-with-id.json")); // Test legacy OpsDB tenants @@ -148,28 +155,33 @@ public class ApplicationApiTest extends ControllerContainerTest { // POST (create) an application tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST) - .userIdentity(USER_ID), + .userIdentity(USER_ID) + .nToken(N_TOKEN), new File("application-reference.json")); // GET a tenant - tester.assertResponse(request("/application/v4/tenant/tenant1", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1", GET).userIdentity(USER_ID), new File("tenant-with-application.json")); // GET tenant applications - tester.assertResponse(request("/application/v4/tenant/tenant1/application/", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/", GET).userIdentity(USER_ID), new File("application-list.json")); + + addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR)); + // POST triggering of a full deployment to an application (if version is omitted, current system version is used) tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", POST) - .userIdentity(USER_ID) + .userIdentity(HOSTED_VESPA_OPERATOR) .data("6.1.0"), new File("application-deployment.json")); // DELETE (cancel) ongoing change tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", DELETE) - .userIdentity(USER_ID), + .userIdentity(HOSTED_VESPA_OPERATOR), new File("application-deployment-cancelled.json")); // DELETE (cancel) again is a no-op - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", DELETE), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", DELETE) + .userIdentity(HOSTED_VESPA_OPERATOR), new File("application-deployment-cancelled-no-op.json")); // POST (deploy) an application to a zone - manual user deployment @@ -233,14 +245,17 @@ public class ApplicationApiTest extends ControllerContainerTest { .submit(); // GET tenant screwdriver projects - tester.assertResponse(request("/application/v4/tenant-pipeline/", GET), + tester.assertResponse(request("/application/v4/tenant-pipeline/", GET) + .userIdentity(USER_ID), new File("tenant-pipelines.json")); setDeploymentMaintainedInfo(controllerTester); // GET tenant application deployments - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET) + .userIdentity(USER_ID), new File("application.json")); // GET an application deployment - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", GET) + .userIdentity(USER_ID), new File("deployment.json")); addIssues(controllerTester, ApplicationId.from("tenant1", "application1", "default")); @@ -275,16 +290,20 @@ public class ApplicationApiTest extends ControllerContainerTest { .screwdriverIdentity(SCREWDRIVER_ID), "Requested restart of tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default"); // POST a 'log' command - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/log", POST), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/log", POST) + .screwdriverIdentity(SCREWDRIVER_ID), new File("log-response.json")); // Proxied to config server, not sure about the expected return format // GET (wait for) convergence - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/converge", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/converge", GET) + .userIdentity(USER_ID), new File("convergence.json")); // GET services - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/service", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/service", GET) + .userIdentity(USER_ID), new File("services.json")); // GET service - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/service/storagenode-awe3slno6mmq2fye191y324jl/state/v1/", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/service/storagenode-awe3slno6mmq2fye191y324jl/state/v1/", GET) + .userIdentity(USER_ID), new File("service.json")); // DELETE application with active deployments fails @@ -293,7 +312,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // DELETE (deactivate) a deployment - dev tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default", DELETE) - .screwdriverIdentity(SCREWDRIVER_ID), + .userIdentity(USER_ID), "Deactivated tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default"); // DELETE (deactivate) a deployment - prod tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", DELETE) @@ -316,11 +335,13 @@ public class ApplicationApiTest extends ControllerContainerTest { ""); // GET global rotation status - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/global-rotation", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/global-rotation", GET) + .userIdentity(USER_ID), new File("global-rotation.json")); // GET global rotation override status - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/global-rotation/override", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/global-rotation/override", GET) + .userIdentity(USER_ID), new File("global-rotation-get.json")); // SET global rotation override status @@ -343,10 +364,12 @@ public class ApplicationApiTest extends ControllerContainerTest { "{\"message\":\"Successfully copied environment hosted-instance_tenant1_application1_placeholder_component_default to hosted-instance_tenant1_application1_us-west-1_prod_default\"}"); // DELETE an application - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE).userIdentity(USER_ID), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE).userIdentity(USER_ID) + .nToken(N_TOKEN), ""); // DELETE a tenant - tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE).userIdentity(USER_ID), + tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE).userIdentity(USER_ID) + .nToken(N_TOKEN), new File("tenant-without-applications.json")); controllerTester.controller().deconstruct(); @@ -369,12 +392,14 @@ public class ApplicationApiTest extends ControllerContainerTest { // Create tenant tester.assertResponse(request("/application/v4/tenant/tenant1", POST).userIdentity(USER_ID) - .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"), + .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}") + .nToken(N_TOKEN), new File("tenant-without-applications.json")); // Create application tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST) - .userIdentity(USER_ID), + .userIdentity(USER_ID) + .nToken(N_TOKEN), new File("application-reference.json")); // Grant deploy access @@ -401,12 +426,14 @@ public class ApplicationApiTest extends ControllerContainerTest { // Create tenant tester.assertResponse(request("/application/v4/tenant/tenant1", POST) .userIdentity(USER_ID) - .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"), + .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}") + .nToken(N_TOKEN), new File("tenant-without-applications.json")); // Create application tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST) - .userIdentity(USER_ID), + .userIdentity(USER_ID) + .nToken(N_TOKEN), new File("application-reference.json")); // Give Screwdriver project deploy access @@ -460,7 +487,8 @@ public class ApplicationApiTest extends ControllerContainerTest { .submit(); setDeploymentMaintainedInfo(controllerTester); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET) + .userIdentity(USER_ID), new File("application-without-change-multiple-deployments.json")); } @@ -472,35 +500,41 @@ public class ApplicationApiTest extends ControllerContainerTest { // PUT (update) non-existing tenant tester.assertResponse(request("/application/v4/tenant/tenant1", PUT) + .userIdentity(USER_ID) .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"), "{\"error-code\":\"NOT_FOUND\",\"message\":\"Tenant 'tenant1' does not exist\"}", 404); // GET non-existing tenant - tester.assertResponse(request("/application/v4/tenant/tenant1", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1", GET) + .userIdentity(USER_ID), "{\"error-code\":\"NOT_FOUND\",\"message\":\"Tenant 'tenant1' does not exist\"}", 404); // GET non-existing application - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET) + .userIdentity(USER_ID), "{\"error-code\":\"NOT_FOUND\",\"message\":\"tenant1.application1 not found\"}", 404); // GET non-existing deployment - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-east/instance/default", GET), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-east/instance/default", GET) + .userIdentity(USER_ID), "{\"error-code\":\"NOT_FOUND\",\"message\":\"tenant1.application1 not found\"}", 404); // POST (add) a tenant tester.assertResponse(request("/application/v4/tenant/tenant1", POST) .userIdentity(USER_ID) - .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"), + .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}") + .nToken(N_TOKEN), new File("tenant-without-applications.json")); // POST (add) another tenant under the same domain tester.assertResponse(request("/application/v4/tenant/tenant2", POST) .userIdentity(USER_ID) - .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"), + .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}") + .nToken(N_TOKEN), "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not create tenant 'tenant2': The Athens domain 'domain1' is already connected to tenant 'tenant1'\"}", 400); @@ -513,7 +547,8 @@ public class ApplicationApiTest extends ControllerContainerTest { // POST (create) an (empty) application tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST) - .userIdentity(USER_ID), + .userIdentity(USER_ID) + .nToken(N_TOKEN), new File("application-reference.json")); // Create the same application again @@ -561,7 +596,8 @@ public class ApplicationApiTest extends ControllerContainerTest { // DELETE application tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE) - .userIdentity(USER_ID), + .userIdentity(USER_ID) + .nToken(N_TOKEN), ""); // DELETE application again - should produce 404 tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE) @@ -570,15 +606,18 @@ public class ApplicationApiTest extends ControllerContainerTest { 404); // DELETE tenant tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE) - .userIdentity(USER_ID), + .userIdentity(USER_ID) + .nToken(N_TOKEN), new File("tenant-without-applications.json")); // DELETE tenant again - should produce 404 - tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE), + tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE) + .userIdentity(USER_ID), "{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not delete tenant 'tenant1': Tenant not found\"}", 404); // Promote application chef env for nonexistent tenant/application - tester.assertResponse(request("/application/v4/tenant/dontexist/application/dontexist/environment/prod/region/us-west-1/instance/default/promote", POST), + tester.assertResponse(request("/application/v4/tenant/dontexist/application/dontexist/environment/prod/region/us-west-1/instance/default/promote", POST) + .userIdentity(USER_ID), "{\"error-code\":\"INTERNAL_SERVER_ERROR\",\"message\":\"Unable to promote Chef environments for application\"}", 500); } @@ -589,14 +628,15 @@ public class ApplicationApiTest extends ControllerContainerTest { UserId authorizedUser = USER_ID; UserId unauthorizedUser = new UserId("othertenant"); - // Mutation without an authorized user is disallowed + // Mutation without an user is disallowed tester.assertResponse(request("/application/v4/tenant/tenant1", POST) .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"), - "{\"error-code\":\"FORBIDDEN\",\"message\":\"User is not authenticated\"}", - 403); + "{\n \"message\" : \"Not authenticated\"\n}", + 401); - // ... but read methods are allowed + // ... but read methods are allowed for authenticated user tester.assertResponse(request("/application/v4/tenant/", GET) + .userIdentity(USER_ID) .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"), "[]", 200); @@ -613,19 +653,22 @@ public class ApplicationApiTest extends ControllerContainerTest { // (Create it with the right tenant id) tester.assertResponse(request("/application/v4/tenant/tenant1", POST) .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}") - .userIdentity(authorizedUser), + .userIdentity(authorizedUser) + .nToken(N_TOKEN), new File("tenant-without-applications.json"), 200); // Creating an application for an Athens domain the user is not admin for is disallowed tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST) - .userIdentity(unauthorizedUser), - "{\"error-code\":\"FORBIDDEN\",\"message\":\"User user.othertenant does not have write access to tenant tenant1\"}", + .userIdentity(unauthorizedUser) + .nToken(N_TOKEN), + "{\n \"message\" : \"Tenant admin or Vespa operator role required\"\n}", 403); // (Create it with the right tenant id) tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST) - .userIdentity(authorizedUser), + .userIdentity(authorizedUser) + .nToken(N_TOKEN), new File("application-reference.json"), 200); @@ -634,18 +677,19 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/deploy", POST) .data(entity) .userIdentity(USER_ID), - "{\"error-code\":\"FORBIDDEN\",\"message\":\"Principal 'user.myuser' is not a Screwdriver principal. Excepted principal with Athenz domain 'cd.screwdriver.project', got 'user'.\"}", + "{\n \"message\" : \"'user.myuser' is not a Screwdriver identity. Only Screwdriver is allowed to deploy to this environment.\"\n}", 403); // Deleting an application for an Athens domain the user is not admin for is disallowed tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE) .userIdentity(unauthorizedUser), - "{\"error-code\":\"FORBIDDEN\",\"message\":\"User user.othertenant does not have write access to tenant tenant1\"}", + "{\n \"message\" : \"Tenant admin or Vespa operator role required\"\n}", 403); // (Deleting it with the right tenant id) tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE) - .userIdentity(authorizedUser), + .userIdentity(authorizedUser) + .nToken(N_TOKEN), "", 200); @@ -653,21 +697,22 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.assertResponse(request("/application/v4/tenant/tenant1", PUT) .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}") .userIdentity(unauthorizedUser), - "{\"error-code\":\"FORBIDDEN\",\"message\":\"User user.othertenant does not have write access to tenant tenant1\"}", + "{\n \"message\" : \"Tenant admin or Vespa operator role required\"\n}", 403); // Change Athens domain createAthenzDomainWithAdmin(new AthenzDomain("domain2"), USER_ID); tester.assertResponse(request("/application/v4/tenant/tenant1", PUT) .data("{\"athensDomain\":\"domain2\", \"property\":\"property1\"}") - .userIdentity(authorizedUser), + .userIdentity(authorizedUser) + .nToken(N_TOKEN), "{\"tenant\":\"tenant1\",\"type\":\"ATHENS\",\"athensDomain\":\"domain2\",\"property\":\"property1\",\"applications\":[]}", 200); // Deleting a tenant for an Athens domain the user is not admin for is disallowed tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE) .userIdentity(unauthorizedUser), - "{\"error-code\":\"FORBIDDEN\",\"message\":\"User user.othertenant does not have write access to tenant tenant1\"}", + "{\n \"message\" : \"Tenant admin or Vespa operator role required\"\n}", 403); } @@ -768,6 +813,7 @@ public class ApplicationApiTest extends ControllerContainerTest { private final Request.Method method; private byte[] data = new byte[0]; private AthenzIdentity identity; + private NToken nToken; private String contentType = "application/json"; private String recursive; @@ -789,6 +835,7 @@ public class ApplicationApiTest extends ControllerContainerTest { } private RequestBuilder userIdentity(UserId userId) { this.identity = HostedAthenzIdentities.from(userId); return this; } private RequestBuilder screwdriverIdentity(ScrewdriverId screwdriverId) { this.identity = HostedAthenzIdentities.from(screwdriverId); return this; } + private RequestBuilder nToken(NToken nToken) { this.nToken = nToken; return this; } private RequestBuilder contentType(String contentType) { this.contentType = contentType; return this; } private RequestBuilder recursive(String recursive) { this.recursive = recursive; return this; } @@ -800,8 +847,10 @@ public class ApplicationApiTest extends ControllerContainerTest { data, method); request.getHeaders().put("Content-Type", contentType); if (identity != null) { - request.getHeaders().put("Athenz-Identity-Domain", identity.getDomain().getName()); - request.getHeaders().put("Athenz-Identity-Name", identity.getName()); + addIdentityToRequest(request, identity); + } + if (nToken != null) { + addNTokenToRequest(request, nToken); } return request; } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java index 8047b0d48c9..c7128bb4cfc 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java @@ -2,12 +2,13 @@ package com.yahoo.vespa.hosted.controller.restapi.controller; import com.yahoo.application.container.handler.Request; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import org.junit.Test; import java.io.File; -import java.io.IOException; /** * @author bratseth @@ -15,60 +16,68 @@ import java.io.IOException; public class ControllerApiTest extends ControllerContainerTest { private final static String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/"; + private static final AthenzIdentity HOSTED_VESPA_OPERATOR = AthenzUser.fromUserId("johnoperator"); @Test public void testControllerApi() throws Exception { ContainerControllerTester tester = new ContainerControllerTester(container, responseFiles); - tester.assertResponse(new Request("http://localhost:8080/controller/v1/"), new File("root.json")); + tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/", new byte[0], Request.Method.GET), new File("root.json")); // POST deactivation of a maintenance job - assertResponse(new Request("http://localhost:8080/controller/v1/maintenance/inactive/DeploymentExpirer", - new byte[0], Request.Method.POST), + assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/maintenance/inactive/DeploymentExpirer", + new byte[0], Request.Method.POST), 200, "{\"message\":\"Deactivated job 'DeploymentExpirer'\"}"); // GET a list of all maintenance jobs - tester.assertResponse(new Request("http://localhost:8080/controller/v1/maintenance/"), + tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/maintenance/", new byte[0], Request.Method.GET), new File("maintenance.json")); // DELETE deactivation of a maintenance job - assertResponse(new Request("http://localhost:8080/controller/v1/maintenance/inactive/DeploymentExpirer", - new byte[0], Request.Method.DELETE), + assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/maintenance/inactive/DeploymentExpirer", + new byte[0], Request.Method.DELETE), 200, "{\"message\":\"Re-activated job 'DeploymentExpirer'\"}"); } @Test public void testUpgraderApi() throws Exception { + addUserToHostedOperatorRole(HOSTED_VESPA_OPERATOR); + ContainerControllerTester tester = new ContainerControllerTester(container, responseFiles); // Get current configuration - tester.assertResponse(new Request("http://localhost:8080/controller/v1/jobs/upgrader"), + tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/jobs/upgrader", new byte[0], Request.Method.GET), "{\"upgradesPerMinute\":0.5,\"ignoreConfidence\":false}", 200); // Set invalid configuration - tester.assertResponse(new Request("http://localhost:8080/controller/v1/jobs/upgrader", - "{\"upgradesPerMinute\":-1}", Request.Method.PATCH), - "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Upgrades per minute must be >= 0\"}", - 400); + ; + tester.assertResponse( + hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"upgradesPerMinute\":-1}", Request.Method.PATCH), + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Upgrades per minute must be >= 0\"}", + 400); // Unrecognized field - tester.assertResponse(new Request("http://localhost:8080/controller/v1/jobs/upgrader", - "{\"foo\":bar}", Request.Method.PATCH), - "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Unable to configure upgrader with data in request: '{\\\"foo\\\":bar}'\"}", - + 400); + tester.assertResponse( + hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader","{\"foo\":bar}", Request.Method.PATCH), + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Unable to configure upgrader with data in request: '{\\\"foo\\\":bar}'\"}", + 400); // Patch configuration - tester.assertResponse(new Request("http://localhost:8080/controller/v1/jobs/upgrader", - "{\"upgradesPerMinute\":42.0}", Request.Method.PATCH), - "{\"upgradesPerMinute\":42.0,\"ignoreConfidence\":false}", - 200); + tester.assertResponse( + hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"upgradesPerMinute\":42.0}", Request.Method.PATCH), + "{\"upgradesPerMinute\":42.0,\"ignoreConfidence\":false}", + 200); // Patch configuration - tester.assertResponse(new Request("http://localhost:8080/controller/v1/jobs/upgrader", - "{\"ignoreConfidence\":true}", Request.Method.PATCH), - "{\"upgradesPerMinute\":42.0,\"ignoreConfidence\":true}", - 200); + tester.assertResponse( + hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"ignoreConfidence\":true}", Request.Method.PATCH), + "{\"upgradesPerMinute\":42.0,\"ignoreConfidence\":true}", + 200); + } + + private static Request hostedOperatorRequest(String uri, String body, Request.Method method) { + return addIdentityToRequest(new Request(uri, body, method), HOSTED_VESPA_OPERATOR); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java index 538f2ac51e5..a56ab028233 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java @@ -2,7 +2,6 @@ package com.yahoo.vespa.hosted.controller.restapi.deployment; import com.google.common.collect.ImmutableSet; -import com.yahoo.application.container.handler.Request; import com.yahoo.component.Version; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; @@ -75,8 +74,7 @@ public class DeploymentApiTest extends ControllerContainerTest { tester.controller().updateVersionStatus(censorConfigServers(VersionStatus.compute(tester.controller()), tester.controller())); - tester.assertResponse(new Request("http://localhost:8080/deployment/v1/"), - new File("root.json")); + tester.assertResponse(authenticatedRequest("http://localhost:8080/deployment/v1/"), new File("root.json")); } private VersionStatus censorConfigServers(VersionStatus versionStatus, Controller controller) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java index 81470bac618..dfeabaf051c 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java @@ -8,6 +8,8 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; @@ -45,22 +47,24 @@ public class ScrewdriverApiTest extends ControllerContainerTest { private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/responses/"; private static final ZoneId testZone = ZoneId.from(Environment.test, RegionName.from("us-east-1")); private static final ZoneId stagingZone = ZoneId.from(Environment.staging, RegionName.from("us-east-3")); + private static final AthenzIdentity HOSTED_VESPA_OPERATOR = AthenzUser.fromUserId("johnoperator"); @Test public void testGetReleaseStatus() throws Exception { ContainerControllerTester tester = new ContainerControllerTester(container, responseFiles); - tester.containerTester().assertResponse(new Request("http://localhost:8080/screwdriver/v1/release/vespa"), + tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/screwdriver/v1/release/vespa"), "{\"error-code\":\"NOT_FOUND\",\"message\":\"Information about the current system version is not available at this time\"}", 404); tester.controller().updateVersionStatus(VersionStatus.compute(tester.controller())); - tester.containerTester().assertResponse(new Request("http://localhost:8080/screwdriver/v1/release/vespa"), + tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/screwdriver/v1/release/vespa"), new File("release-response.json"), 200); } @Test public void testJobStatusReporting() throws Exception { ContainerControllerTester tester = new ContainerControllerTester(container, responseFiles); + addUserToHostedOperatorRole(HOSTED_VESPA_OPERATOR); tester.containerTester().updateSystemVersion(); long projectId = 1; Application app = tester.createApplication(); @@ -79,11 +83,12 @@ public class ScrewdriverApiTest extends ControllerContainerTest { job.type(JobType.systemTest).submit(); // Notifying about unknown job fails - tester.containerTester().assertResponse(new Request("http://localhost:8080/application/v4/tenant/tenant1/application/application1/jobreport", - asJson(job.type(JobType.productionUsEast3).report()), - Request.Method.POST), - new File("unexpected-completion.json"), 400); - + Request request = new Request("http://localhost:8080/application/v4/tenant/tenant1/application/application1/jobreport", + asJson(job.type(JobType.productionUsEast3).report()), + Request.Method.POST); + addIdentityToRequest(request, HOSTED_VESPA_OPERATOR); + tester.containerTester().assertResponse(request, new File("unexpected-completion.json"), 400); + // ... and assert it was recorded JobStatus recordedStatus = tester.controller().applications().get(app.id()).get().deploymentJobs().jobStatus().get(JobType.component); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java index 6e87304774a..054e71465ad 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java @@ -1,11 +1,10 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.restapi.zone.v1; -import com.yahoo.application.container.handler.Request; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; -import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.ZoneRegistryMock; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import org.junit.Before; @@ -42,22 +41,22 @@ public class ZoneApiTest extends ControllerContainerTest { @Test public void test_requests() throws Exception { // GET /zone/v1 - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v1"), + tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/zone/v1"), new File("root.json")); // GET /zone/v1/environment/prod - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v1/environment/prod"), + tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/zone/v1/environment/prod"), new File("prod.json")); // GET /zone/v1/environment/dev/default - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v1/environment/dev/default"), + tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/zone/v1/environment/dev/default"), new File("default-for-region.json")); } @Test public void test_invalid_requests() throws Exception { // GET /zone/v1/environment/prod/default: No default region - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v1/environment/prod/default"), + tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/zone/v1/environment/prod/default"), new File("no-default-region.json"), 400); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java index c52266dfacc..5e9de74fe1b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java @@ -5,10 +5,12 @@ import com.yahoo.application.container.handler.Request; import com.yahoo.application.container.handler.Request.Method; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; -import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.text.Utf8; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.hosted.controller.ConfigServerProxyMock; import com.yahoo.vespa.hosted.controller.ZoneRegistryMock; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import org.junit.Before; @@ -26,6 +28,7 @@ import static org.junit.Assert.assertFalse; */ public class ZoneApiTest extends ControllerContainerTest { + private static final AthenzIdentity HOSTED_VESPA_OPERATOR = AthenzUser.fromUserId("johnoperator"); private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/"; private static final List<ZoneId> zones = Arrays.asList( ZoneId.from(Environment.prod, RegionName.from("us-north-1")), @@ -45,16 +48,17 @@ public class ZoneApiTest extends ControllerContainerTest { .setZones(zones); this.tester = new ContainerControllerTester(container, responseFiles); this.proxy = (ConfigServerProxyMock) container.components().getComponent(ConfigServerProxyMock.class.getName()); + addUserToHostedOperatorRole(HOSTED_VESPA_OPERATOR); } @Test public void test_requests() throws Exception { // GET /zone/v2 - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2"), + tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/zone/v2"), new File("root.json")); // GET /zone/v2/prod/us-north-1 - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1"), + tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/zone/v2/prod/us-north-1"), "ok"); assertEquals("prod", proxy.lastReceived().get().getEnvironment()); assertEquals("us-north-1", proxy.lastReceived().get().getRegion()); @@ -62,7 +66,7 @@ public class ZoneApiTest extends ControllerContainerTest { assertEquals("GET", proxy.lastReceived().get().getMethod()); // GET /zone/v2/nodes/v2/node/?recursive=true - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/?recursive=true"), + tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/?recursive=true"), "ok"); assertEquals("prod", proxy.lastReceived().get().getEnvironment()); @@ -71,7 +75,7 @@ public class ZoneApiTest extends ControllerContainerTest { assertEquals("GET", proxy.lastReceived().get().getMethod()); // POST /zone/v2/dev/us-north-2/nodes/v2/command/restart?hostname=node1 - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/dev/us-north-2/nodes/v2/command/restart?hostname=node1", + tester.containerTester().assertResponse(hostedOperatorRequest("http://localhost:8080/zone/v2/dev/us-north-2/nodes/v2/command/restart?hostname=node1", new byte[0], Method.POST), "ok"); assertEquals("dev", proxy.lastReceived().get().getEnvironment()); @@ -80,7 +84,7 @@ public class ZoneApiTest extends ControllerContainerTest { assertEquals("POST", proxy.lastReceived().get().getMethod()); // PUT /zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1 - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1", + tester.containerTester().assertResponse(hostedOperatorRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1", new byte[0], Method.PUT), "ok"); assertEquals("prod", proxy.lastReceived().get().getEnvironment()); assertEquals("us-north-1", proxy.lastReceived().get().getRegion()); @@ -88,7 +92,7 @@ public class ZoneApiTest extends ControllerContainerTest { assertEquals("PUT", proxy.lastReceived().get().getMethod()); // DELETE /zone/v2/prod/us-north-1/nodes/v2/node/node1 - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1", + tester.containerTester().assertResponse(hostedOperatorRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1", new byte[0], Method.DELETE), "ok"); assertEquals("prod", proxy.lastReceived().get().getEnvironment()); assertEquals("us-north-1", proxy.lastReceived().get().getRegion()); @@ -96,7 +100,7 @@ public class ZoneApiTest extends ControllerContainerTest { assertEquals("DELETE", proxy.lastReceived().get().getMethod()); // PATCH /zone/v2/prod/us-north-1/nodes/v2/node/node1 - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1", + tester.containerTester().assertResponse(hostedOperatorRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1", Utf8.toBytes("{\"currentRestartGeneration\": 1}"), Method.PATCH), "ok"); assertEquals("prod", proxy.lastReceived().get().getEnvironment()); @@ -108,11 +112,15 @@ public class ZoneApiTest extends ControllerContainerTest { @Test public void test_invalid_requests() throws Exception { - // GET /zone/v2/prod/us-north-34/nodes/v2 - tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-42/nodes/v2", + // POST /zone/v2/prod/us-north-34/nodes/v2 + tester.containerTester().assertResponse(hostedOperatorRequest("http://localhost:8080/zone/v2/prod/us-north-42/nodes/v2", new byte[0], Method.POST), new File("unknown-zone.json"), 400); assertFalse(proxy.lastReceived().isPresent()); } + private static Request hostedOperatorRequest(String uri, byte[] body, Request.Method method) { + return addIdentityToRequest(new Request(uri, body, method), HOSTED_VESPA_OPERATOR); + } + } |