summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorOla Aunrønning <olaa@verizonmedia.com>2021-09-13 13:33:22 +0200
committerOla Aunrønning <olaa@verizonmedia.com>2021-09-13 13:33:22 +0200
commit15e9813f5b0ca000e19fba529a02dd695d5d00f4 (patch)
tree6e5c02f0f9ab8b7f3d7a37366f456982c26835d9 /controller-server
parentaa7ed17bc8f31e281d5d721c65188275e7bc79a0 (diff)
Control Horizon proxy usage through feature flag
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java57
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java18
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java64
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java19
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json9
6 files changed, 129 insertions, 45 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java
index 6f5b1f30592..19a5024833b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java
@@ -3,22 +3,33 @@ package com.yahoo.vespa.hosted.controller.restapi.horizon;
import com.google.inject.Inject;
import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
import com.yahoo.restapi.ErrorResponse;
import com.yahoo.restapi.Path;
+import com.yahoo.vespa.flags.BooleanFlag;
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.horizon.HorizonClient;
import com.yahoo.vespa.hosted.controller.api.integration.horizon.HorizonResponse;
+import com.yahoo.vespa.hosted.controller.api.role.Role;
+import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
+import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
import com.yahoo.yolean.Exceptions;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.EnumSet;
import java.util.Optional;
+import java.util.Set;
import java.util.logging.Level;
+import java.util.stream.Collectors;
/**
* Proxies metrics requests from Horizon UI
@@ -29,20 +40,32 @@ public class HorizonApiHandler extends LoggingRequestHandler {
private final SystemName systemName;
private final HorizonClient client;
+ private final BooleanFlag enabledHorizonDashboard;
+
+ private static final EnumSet<RoleDefinition> operatorRoleDefinitions =
+ EnumSet.of(RoleDefinition.hostedOperator, RoleDefinition.hostedSupporter);
@Inject
- public HorizonApiHandler(LoggingRequestHandler.Context parentCtx, Controller controller) {
+ public HorizonApiHandler(LoggingRequestHandler.Context parentCtx, Controller controller, FlagSource flagSource) {
super(parentCtx);
this.systemName = controller.system();
this.client = controller.serviceRegistry().horizonClient();
+ this.enabledHorizonDashboard = Flags.ENABLED_HORIZON_DASHBOARD.bindTo(flagSource);
}
@Override
public HttpResponse handle(HttpRequest request) {
+ var roles = getRoles(request);
+ var operator = roles.stream().map(Role::definition).anyMatch(operatorRoleDefinitions::contains);
+ var authorizedTenants = getAuthorizedTenants(roles);
+
+ if (!operator && authorizedTenants.isEmpty())
+ return ErrorResponse.forbidden("No tenant with enabled metrics view");
+
try {
switch (request.getMethod()) {
case GET: return get(request);
- case POST: return post(request);
+ case POST: return post(request, authorizedTenants, operator);
case PUT: return put(request);
default: return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
}
@@ -65,10 +88,10 @@ public class HorizonApiHandler extends LoggingRequestHandler {
return ErrorResponse.notFoundError("Nothing at " + path);
}
- private HttpResponse post(HttpRequest request) {
+ private HttpResponse post(HttpRequest request, Set<TenantName> authorizedTenants, boolean operator) {
Path path = new Path(request.getUri());
- if (path.matches("/horizon/v1/tsdb/api/query/graph")) return tsdbQuery(request, true);
- if (path.matches("/horizon/v1/meta/search/timeseries")) return tsdbQuery(request, false);
+ if (path.matches("/horizon/v1/tsdb/api/query/graph")) return tsdbQuery(request, authorizedTenants, operator, true);
+ if (path.matches("/horizon/v1/meta/search/timeseries")) return tsdbQuery(request, authorizedTenants, operator, false);
return ErrorResponse.notFoundError("Nothing at " + path);
}
@@ -78,10 +101,9 @@ public class HorizonApiHandler extends LoggingRequestHandler {
return ErrorResponse.notFoundError("Nothing at " + path);
}
- private HttpResponse tsdbQuery(HttpRequest request, boolean isMetricQuery) {
- SecurityContext securityContext = getAttribute(request, SecurityContext.ATTRIBUTE_NAME, SecurityContext.class);
+ private HttpResponse tsdbQuery(HttpRequest request, Set<TenantName> authorizedTenants, boolean operator, boolean isMetricQuery) {
try {
- byte[] data = TsdbQueryRewriter.rewrite(request.getData().readAllBytes(), securityContext.roles(), systemName);
+ byte[] data = TsdbQueryRewriter.rewrite(request.getData().readAllBytes(), authorizedTenants, operator, systemName);
return new JsonInputStreamResponse(isMetricQuery ? client.getMetrics(data) : client.getMetaData(data));
} catch (TsdbQueryRewriter.UnauthorizedException e) {
return ErrorResponse.forbidden("Access denied");
@@ -90,11 +112,20 @@ public class HorizonApiHandler extends LoggingRequestHandler {
}
}
- private static <T> T getAttribute(HttpRequest request, String attributeName, Class<T> clazz) {
- return Optional.ofNullable(request.getJDiscRequest().context().get(attributeName))
- .filter(clazz::isInstance)
- .map(clazz::cast)
- .orElseThrow(() -> new IllegalArgumentException("Attribute '" + attributeName + "' was not set on request"));
+ private static Set<Role> getRoles(HttpRequest request) {
+ return Optional.ofNullable(request.getJDiscRequest().context().get(SecurityContext.ATTRIBUTE_NAME))
+ .filter(SecurityContext.class::isInstance)
+ .map(SecurityContext.class::cast)
+ .map(SecurityContext::roles)
+ .orElseThrow(() -> new IllegalArgumentException("Attribute '" + SecurityContext.ATTRIBUTE_NAME + "' was not set on request"));
+ }
+
+ private Set<TenantName> getAuthorizedTenants(Set<Role> roles) {
+ return roles.stream()
+ .filter(TenantRole.class::isInstance)
+ .map(role -> ((TenantRole) role).tenant())
+ .filter(tenant -> enabledHorizonDashboard.with(FetchVector.Dimension.TENANT_ID, tenant.value()).value())
+ .collect(Collectors.toUnmodifiableSet());
}
private static class JsonInputStreamResponse extends HttpResponse {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java
index e034be46063..3e20584dbac 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java
@@ -7,12 +7,8 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
-import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
import java.io.IOException;
-import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@@ -23,20 +19,8 @@ import java.util.stream.Collectors;
public class TsdbQueryRewriter {
private static final ObjectMapper mapper = new ObjectMapper();
- private static final EnumSet<RoleDefinition> operatorRoleDefinitions =
- EnumSet.of(RoleDefinition.hostedOperator, RoleDefinition.hostedSupporter);
-
- public static byte[] rewrite(byte[] data, Set<Role> roles, SystemName systemName) throws IOException {
- boolean operator = roles.stream().map(Role::definition).anyMatch(operatorRoleDefinitions::contains);
-
- // Anyone with any tenant relation can view metrics for apps within those tenants
- Set<TenantName> authorizedTenants = roles.stream()
- .filter(TenantRole.class::isInstance)
- .map(role -> ((TenantRole) role).tenant())
- .collect(Collectors.toUnmodifiableSet());
- if (!operator && authorizedTenants.isEmpty())
- throw new UnauthorizedException();
+ public static byte[] rewrite(byte[] data, Set<TenantName> authorizedTenants, boolean operator, SystemName systemName) throws IOException {
JsonNode root = mapper.readTree(data);
requireLegalType(root);
getField(root, "executionGraph", ArrayNode.class)
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
index 7b0a2c9d6d6..0ecc8ac81df 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
@@ -22,6 +22,7 @@ import com.yahoo.text.Text;
import com.yahoo.vespa.flags.BooleanFlag;
import com.yahoo.vespa.flags.FetchVector;
import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.flags.IntFlag;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.controller.Controller;
@@ -71,6 +72,7 @@ public class UserApiHandler extends LoggingRequestHandler {
private final Controller controller;
private final BooleanFlag enable_public_signup_flow;
private final IntFlag maxTrialTenants;
+ private final BooleanFlag enabledHorizonDashboard;
@Inject
public UserApiHandler(Context parentCtx, UserManagement users, Controller controller, FlagSource flagSource) {
@@ -79,6 +81,7 @@ public class UserApiHandler extends LoggingRequestHandler {
this.controller = controller;
this.enable_public_signup_flow = PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.bindTo(flagSource);
this.maxTrialTenants = PermanentFlags.MAX_TRIAL_TENANTS.bindTo(flagSource);
+ this.enabledHorizonDashboard = Flags.ENABLED_HORIZON_DASHBOARD.bindTo(flagSource);
}
@Override
@@ -184,6 +187,10 @@ public class UserApiHandler extends LoggingRequestHandler {
Cursor tenantRolesObject = tenantObject.setArray("roles");
tenantRolesByTenantName.getOrDefault(tenant, List.of())
.forEach(role -> tenantRolesObject.addString(role.definition().name()));
+ if (controller.system().isPublic()) {
+ tenantObject.setBool(enabledHorizonDashboard.id().toString(),
+ enabledHorizonDashboard.with(FetchVector.Dimension.TENANT_ID, tenant.value()).value());
+ }
});
if (!operatorRoles.isEmpty()) {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java
new file mode 100644
index 00000000000..ae2e57d1529
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java
@@ -0,0 +1,64 @@
+package com.yahoo.vespa.hosted.controller.restapi.horizon;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.flags.Flags;
+import com.yahoo.vespa.flags.InMemoryFlagSource;
+import com.yahoo.vespa.hosted.controller.api.role.Role;
+import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
+import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest;
+import org.junit.Test;
+
+import java.util.Set;
+
+/**
+ * @author olaa
+ */
+public class HorizonApiTest extends ControllerContainerCloudTest {
+
+ @Test
+ public void only_operators_and_flag_enabled_tenants_allowed() {
+ ContainerTester tester = new ContainerTester(container, "");
+
+ tester.assertResponse(request("/horizon/v1/config/dashboard/topFolders")
+ .roles(Set.of(Role.hostedOperator())),
+ "", 200);
+
+ tester.assertResponse(request("/horizon/v1/config/dashboard/topFolders")
+ .roles(Set.of(Role.reader(TenantName.from("tenant")))),
+ "{\"error-code\":\"FORBIDDEN\",\"message\":\"No tenant with enabled metrics view\"}", 403);
+
+ ((InMemoryFlagSource) tester.controller().flagSource())
+ .withBooleanFlag(Flags.ENABLED_HORIZON_DASHBOARD.id(), true);
+
+ tester.assertResponse(request("/horizon/v1/config/dashboard/topFolders")
+ .roles(Set.of(Role.reader(TenantName.from("tenant")))),
+ "", 200);
+ }
+
+ @Override
+ protected SystemName system() {
+ return SystemName.PublicCd;
+ }
+
+ @Override
+ protected String variablePartXml() {
+ return " <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControlRequests'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControl'/>\n" +
+
+ " <handler id=\"com.yahoo.vespa.hosted.controller.restapi.horizon.HorizonApiHandler\" bundle=\"controller-server\">\n" +
+ " <binding>http://*/horizon/v1/*</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.restapi.filter.ControllerAuthorizationFilter'/>\n" +
+ " <binding>http://*/*</binding>\n" +
+ " </request-chain>\n" +
+ " </filtering>\n" +
+ " </http>\n";
+ }
+} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java
index ab9d50f8eae..d31d9c28c6c 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java
@@ -22,33 +22,28 @@ public class TsdbQueryRewriterTest {
@Test
public void rewrites_query() throws IOException {
- assertRewrite("filters-complex.json", "filters-complex.expected.json", Role.reader(TenantName.from("tenant2")));
+ assertRewrite("filters-complex.json", "filters-complex.expected.json", Set.of(TenantName.from("tenant2")), false);
assertRewrite("filter-in-execution-graph.json",
"filter-in-execution-graph.expected.json",
- Role.reader(TenantName.from("tenant2")), Role.athenzTenantAdmin(TenantName.from("tenant3")));
+ Set.of(TenantName.from("tenant2"), TenantName.from("tenant3")), false);
assertRewrite("filter-in-execution-graph.json",
"filter-in-execution-graph.expected.operator.json",
- Role.reader(TenantName.from("tenant2")), Role.athenzTenantAdmin(TenantName.from("tenant3")), Role.hostedOperator());
+ Set.of(TenantName.from("tenant2"), TenantName.from("tenant3")), true);
assertRewrite("no-filters.json",
"no-filters.expected.json",
- Role.reader(TenantName.from("tenant2")), Role.athenzTenantAdmin(TenantName.from("tenant3")));
+ Set.of(TenantName.from("tenant2"), TenantName.from("tenant3")), false);
assertRewrite("filters-meta-query.json",
"filters-meta-query.expected.json",
- Role.reader(TenantName.from("tenant2")), Role.athenzTenantAdmin(TenantName.from("tenant3")));
+ Set.of(TenantName.from("tenant2"), TenantName.from("tenant3")), false);
}
- @Test(expected = TsdbQueryRewriter.UnauthorizedException.class)
- public void throws_if_no_roles() throws IOException {
- assertRewrite("filters-complex.json", "filters-complex.expected.json");
- }
-
- private static void assertRewrite(String initialFilename, String expectedFilename, Role... roles) throws IOException {
+ private static void assertRewrite(String initialFilename, String expectedFilename, Set<TenantName> tenants, boolean operator) throws IOException {
byte[] data = Files.readAllBytes(Paths.get("src/test/resources/horizon", initialFilename));
- data = TsdbQueryRewriter.rewrite(data, Set.of(roles), SystemName.Public);
+ data = TsdbQueryRewriter.rewrite(data, tenants, operator, SystemName.Public);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new JsonFormat(false).encode(baos, SlimeUtils.jsonToSlime(data));
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json
index ae3dc68d9e3..e883993cb53 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json
@@ -14,17 +14,20 @@
"roles": [
"developer",
"reader"
- ]
+ ],
+ "enabled-horizon-dashboard":false
},
"tenant1": {
"roles": [
"administrator"
- ]
+ ],
+ "enabled-horizon-dashboard":false
},
"tenant2": {
"roles": [
"developer"
- ]
+ ],
+ "enabled-horizon-dashboard":false
}
}
}