summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@oath.com>2018-02-21 15:44:45 +0100
committerBjørn Christian Seime <bjorncs@oath.com>2018-02-22 09:15:25 +0100
commit78f3fbef93109ef2b395574d87490ac287f1cdd0 (patch)
tree0914bdcca6684cedf800a0fa2440aa7167333e8c /controller-server
parenta3284e95e5b18ec72659caf934d18a44b05f3f5f (diff)
Include ControllerAuthorizationFilter in all container api tests
Test access control logic as part of all ContainerTester based unit tests.
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/AthenzFilterMock.java42
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java182
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java40
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java45
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ResponseHandlerToApplicationResponseWrapper.java77
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java161
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java57
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java19
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java28
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);
+ }
+
}