diff options
author | Bjørn Christian Seime <bjorncs@oath.com> | 2018-02-21 15:44:45 +0100 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@oath.com> | 2018-02-21 15:44:45 +0100 |
commit | f1623e981f61fe9c615e6871257c1c5ae0bfcfa5 (patch) | |
tree | bf56d3301f33ace9896eff93f64cd5a971d1aba2 | |
parent | ebac8e20ecf8a55f79374fdc32848d9aaf70187e (diff) |
Include ControllerAuthorizationFilter in all container api tests
Test access control logic as part of all ContainerTester based unit tests.
11 files changed, 561 insertions, 110 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 1818bf97430..32053cce7d2 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,11 +5,17 @@ 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.IOException; +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; /** @@ -24,6 +30,7 @@ import static org.junit.Assert.assertEquals; */ public class ControllerContainerTest { + public static final AthenzUser USER = AthenzUser.fromUserId("bob"); protected JDisc container; @Before @@ -87,6 +94,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) throws IOException { @@ -96,4 +113,32 @@ public class ControllerContainerTest { "status: " + response.getStatus() + "\nmessage: " + response.getBodyAsString()); } + 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 e6fe7531fdc..9cd28e92228 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 @@ -189,6 +201,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // Trigger deployment tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", POST) + .userIdentity(HOSTED_VESPA_OPERATOR) .data("6.1.0"), new File("application-deployment.json")); @@ -220,14 +233,17 @@ public class ApplicationApiTest extends ControllerContainerTest { controllerTester.notifyJobCompletion(id, screwdriverProjectId, false, DeploymentJobs.JobType.productionCorpUsEast1); // 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")); @@ -262,16 +278,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 @@ -280,7 +300,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) @@ -303,11 +323,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 @@ -330,10 +352,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(); @@ -356,12 +380,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 @@ -388,12 +414,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 @@ -440,7 +468,8 @@ public class ApplicationApiTest extends ControllerContainerTest { controllerTester.notifyJobCompletion(id, projectId, true, DeploymentJobs.JobType.productionUsEast3); 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")); } @@ -452,35 +481,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); @@ -493,7 +528,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 @@ -541,7 +577,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) @@ -550,15 +587,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); } @@ -569,14 +609,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); @@ -593,19 +634,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); @@ -614,18 +658,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); @@ -633,21 +678,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); } @@ -740,6 +786,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; @@ -761,6 +808,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; } @@ -772,8 +820,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 f3c8e57b1b5..f70da6583ed 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,13 +2,12 @@ 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; -import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; @@ -79,8 +78,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 679b0114721..9bca164cb9c 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 @@ -7,6 +7,8 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.controller.Application; @@ -42,22 +44,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(); @@ -74,12 +78,13 @@ public class ScrewdriverApiTest extends ControllerContainerTest { notifyCompletion(app.id(), projectId, JobType.systemTest, Optional.empty()); // Notifying about unknown job fails - tester.containerTester().assertResponse(new Request("http://localhost:8080/application/v4/tenant/tenant1/application/application1/jobreport", - jsonReport(app.id(), JobType.productionUsEast3, projectId, 1L, - Optional.empty()) - .getBytes(StandardCharsets.UTF_8), - Request.Method.POST), - new File("unexpected-completion.json"), 400); + Request request = new Request("http://localhost:8080/application/v4/tenant/tenant1/application/application1/jobreport", + jsonReport(app.id(), JobType.productionUsEast3, projectId, 1L, + Optional.empty()) + .getBytes(StandardCharsets.UTF_8), + 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 = 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); + } + } |