aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorAndreas Eriksen <andreer@verizonmedia.com>2020-06-11 10:31:20 +0200
committerGitHub <noreply@github.com>2020-06-11 10:31:20 +0200
commit54828b73bcea1d086b145453b318bfdcdfef93b8 (patch)
tree958584a90f51ce2d6ea58c2f57b5da34a9a44878 /controller-server
parent1aaaa34cc41bc4bcf8f9448c637e8ee5fa551200 (diff)
tenant creation for everyone in public (with restrictions/gated by flag) (#13538)
* tenant creation for everyone in public (with restrictions/gated by flag) * always include feature flag value in user api Co-authored-by: Jon Marius Venstad <jonmv@users.noreply.github.com>
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Auth0Credentials.java30
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java45
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControlRequests.java20
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json1
10 files changed, 109 insertions, 13 deletions
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 df6b77ccb9e..1b3c216c1f3 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
@@ -157,10 +157,8 @@ public class UserApiHandler extends LoggingRequestHandler {
root.setBool("isPublic", controller.system().isPublic());
root.setBool("isCd", controller.system().isCd());
-
- if(enable_public_signup_flow.with(FetchVector.Dimension.CONSOLE_USER_EMAIL, user.email()).value()) {
- root.setBool(enable_public_signup_flow.id().toString(), true);
- }
+ root.setBool(enable_public_signup_flow.id().toString(),
+ enable_public_signup_flow.with(FetchVector.Dimension.CONSOLE_USER_EMAIL, user.email()).value());
toSlime(root.setObject("user"), user);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Auth0Credentials.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Auth0Credentials.java
new file mode 100644
index 00000000000..787f0f27d40
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Auth0Credentials.java
@@ -0,0 +1,30 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.security;
+
+import com.yahoo.vespa.hosted.controller.api.role.Role;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Like {@link Credentials}, but we know the principal is authenticated by Auth0.
+ * Also includes the set of roles for which the principal is a member.
+ *
+ * @author andreer
+ */
+public class Auth0Credentials extends Credentials {
+
+ private final Set<Role> roles;
+
+ public Auth0Credentials(Principal user, Set<Role> roles) {
+ super(user);
+ this.roles = Collections.unmodifiableSet(roles);
+ }
+
+ /** The set of roles set in the auth0 cookie, extracted by CloudAccessControlRequests. */
+ public Set<Role> getRoles() {
+ return roles;
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
index f34c9c67baa..af55da630c7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
@@ -3,6 +3,10 @@ package com.yahoo.vespa.hosted.controller.security;
import com.google.inject.Inject;
import com.yahoo.config.provision.TenantName;
+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.Application;
import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
import com.yahoo.vespa.hosted.controller.api.integration.user.Roles;
@@ -15,24 +19,32 @@ import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
+import javax.ws.rs.ForbiddenException;
import java.util.List;
+import static com.yahoo.vespa.hosted.controller.api.role.RoleDefinition.*;
+
/**
* @author jonmv
+ * @author andreer
*/
public class CloudAccessControl implements AccessControl {
private static final BillingInfo defaultBillingInfo = new BillingInfo("customer", "Vespa");
private final UserManagement userManagement;
+ private final BooleanFlag enablePublicSignup;
@Inject
- public CloudAccessControl(UserManagement userManagement) {
+ public CloudAccessControl(UserManagement userManagement, FlagSource flagSource) {
this.userManagement = userManagement;
+ this.enablePublicSignup = Flags.ENABLE_PUBLIC_SIGNUP_FLOW.bindTo(flagSource);
}
@Override
public CloudTenant createTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing) {
+ requireTenantCreationAllowed((Auth0Credentials) credentials);
+
CloudTenantSpec spec = (CloudTenantSpec) tenantSpec;
CloudTenant tenant = CloudTenant.create(spec.tenant(), defaultBillingInfo);
@@ -48,6 +60,35 @@ public class CloudAccessControl implements AccessControl {
return tenant;
}
+ private void requireTenantCreationAllowed(Auth0Credentials auth0Credentials) {
+ if (allowedByPrivilegedRole(auth0Credentials)) return;
+
+ if (!allowedByFeatureFlag(auth0Credentials)) {
+ throw new ForbiddenException("You are not currently permitted to create tenants. Please contact the Vespa team to request access.");
+ }
+
+ if(administeredTenants(auth0Credentials) >= 3) {
+ throw new ForbiddenException("You are already administering 3 tenants. If you need more, please contact the Vespa team.");
+ }
+ }
+
+ private boolean allowedByPrivilegedRole(Auth0Credentials auth0Credentials) {
+ return auth0Credentials.getRoles().stream()
+ .map(Role::definition)
+ .anyMatch(rd -> rd == hostedOperator || rd == hostedSupporter);
+ }
+
+ private boolean allowedByFeatureFlag(Auth0Credentials auth0Credentials) {
+ return enablePublicSignup.with(FetchVector.Dimension.CONSOLE_USER_EMAIL, auth0Credentials.user().getName()).value();
+ }
+
+ private long administeredTenants(Auth0Credentials auth0Credentials) {
+ return auth0Credentials.getRoles().stream()
+ .map(Role::definition)
+ .filter(rd -> rd == administrator)
+ .count();
+ }
+
@Override
public Tenant updateTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing, List<Application> applications) {
throw new UnsupportedOperationException("Update is not supported here, as it would entail changing the tenant name.");
@@ -55,7 +96,7 @@ public class CloudAccessControl implements AccessControl {
@Override
public void deleteTenant(TenantName tenant, Credentials credentials) {
- // Probably terminate customer subscription?
+ // TODO: allow only if 0 resources, 0 balance
for (TenantRole role : Roles.tenantRoles(tenant))
userManagement.deleteRole(role);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControlRequests.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControlRequests.java
index c0f8f585216..cc26ed1427a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControlRequests.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControlRequests.java
@@ -4,25 +4,37 @@ package com.yahoo.vespa.hosted.controller.security;
import com.yahoo.config.provision.TenantName;
import com.yahoo.jdisc.http.HttpRequest;
import com.yahoo.slime.Inspector;
+import com.yahoo.vespa.hosted.controller.api.role.Role;
+import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
+import java.util.Optional;
+import java.util.Set;
+
/**
* Extracts access control data for {@link CloudTenant}s from HTTP requests.
*
* @author jonmv
+ * @author andreer
*/
public class CloudAccessControlRequests implements AccessControlRequests {
@Override
public CloudTenantSpec specification(TenantName tenant, Inspector requestObject) {
- // TODO extract marketplace token.
- return new CloudTenantSpec(tenant, "token");
+ return new CloudTenantSpec(tenant, "token"); // TODO: remove token
}
@Override
public Credentials credentials(TenantName tenant, Inspector requestObject, HttpRequest request) {
- // TODO Include roles, if this is to be used for displaying accessible data.
- return new Credentials(request.getUserPrincipal());
+ return new Auth0Credentials(request.getUserPrincipal(), getUserRoles(request));
+ }
+
+ private static Set<Role> getUserRoles(HttpRequest request) {
+ var securityContext = Optional.ofNullable(request.context().get(SecurityContext.ATTRIBUTE_NAME))
+ .filter(SecurityContext.class::isInstance)
+ .map(SecurityContext.class::cast)
+ .orElseThrow(() -> new IllegalArgumentException("Attribute '" + SecurityContext.ATTRIBUTE_NAME + "' was not set on request"));
+ return securityContext.roles();
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
index 189615ad763..35093c22f42 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
@@ -41,6 +41,7 @@ import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
import com.yahoo.vespa.hosted.controller.security.AthenzCredentials;
import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec;
+import com.yahoo.vespa.hosted.controller.security.Auth0Credentials;
import com.yahoo.vespa.hosted.controller.security.CloudTenantSpec;
import com.yahoo.vespa.hosted.controller.security.Credentials;
import com.yahoo.vespa.hosted.controller.security.TenantSpec;
@@ -54,6 +55,7 @@ import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.OptionalLong;
@@ -302,7 +304,7 @@ public final class ControllerTester {
private TenantName createCloudTenant(String tenantName) {
TenantName tenant = TenantName.from(tenantName);
TenantSpec spec = new CloudTenantSpec(tenant, "token");
- controller().tenants().create(spec, new Credentials(new SimplePrincipal("dev")));
+ controller().tenants().create(spec, new Auth0Credentials(new SimplePrincipal("dev"), Collections.emptySet()));
return tenant;
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
index 5c5dc4b5fe6..a1b06262241 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
@@ -4,6 +4,8 @@ package com.yahoo.vespa.hosted.controller.restapi.application;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.Environment;
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.integration.user.User;
import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.role.Role;
@@ -12,11 +14,13 @@ import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest;
+import com.yahoo.vespa.hosted.controller.security.Auth0Credentials;
import com.yahoo.vespa.hosted.controller.security.CloudTenantSpec;
import com.yahoo.vespa.hosted.controller.security.Credentials;
import org.junit.Before;
import org.junit.Test;
+import java.util.Collections;
import java.util.Set;
import static com.yahoo.application.container.handler.Request.Method.POST;
@@ -36,6 +40,8 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
@Before
public void before() {
tester = new ContainerTester(container, responseFiles);
+ ((InMemoryFlagSource) tester.controller().flagSource())
+ .withBooleanFlag(Flags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true);
deploymentTester = new DeploymentTester(new ControllerTester(tester));
deploymentTester.controllerTester().computeVersionStatus();
}
@@ -73,6 +79,6 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
}
private static Credentials credentials(String name) {
- return new Credentials(() -> name);
+ return new Auth0Credentials(() -> name, Collections.emptySet());
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
index c066c50ca20..d9ad30020db 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
@@ -4,6 +4,8 @@ package com.yahoo.vespa.hosted.controller.restapi.user;
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.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.integration.user.User;
import com.yahoo.vespa.hosted.controller.api.role.Role;
@@ -62,7 +64,7 @@ public class UserApiTest extends ControllerContainerCloudTest {
// POST a tenant is not available to everyone.
tester.assertResponse(request("/application/v4/tenant/my-tenant", POST)
.data("{\"token\":\"hello\"}"),
- accessDenied, 403);
+ "{\"error-code\":\"FORBIDDEN\",\"message\":\"You are not currently permitted to create tenants. Please contact the Vespa team to request access.\"}", 403);
// POST a tenant is available to operators.
tester.assertResponse(request("/application/v4/tenant/my-tenant", POST)
@@ -200,6 +202,8 @@ public class UserApiTest extends ControllerContainerCloudTest {
@Test
public void userMetadataTest() {
ContainerTester tester = new ContainerTester(container, responseFiles);
+ ((InMemoryFlagSource) tester.controller().flagSource())
+ .withBooleanFlag(Flags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true);
ControllerTester controller = new ControllerTester(tester);
Set<Role> operator = Set.of(Role.hostedOperator(), Role.hostedSupporter(), Role.hostedAccountant());
User user = new User("dev@domail", "Joe Developer", "dev", null);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json
index a69b4d2a062..36918c743fa 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json
@@ -1,6 +1,7 @@
{
"isPublic": false,
"isCd": false,
+ "enable-public-signup-flow": (ignore),
"user": {
"name": "Joe Developer",
"email": "dev@domail",
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 7d4e01073e0..27398352e53 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
@@ -1,6 +1,7 @@
{
"isPublic": true,
"isCd": false,
+"enable-public-signup-flow": (ignore),
"user": {
"name": "Joe Developer",
"email": "dev@domail",
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json
index bc702f02150..b62a70d1871 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json
@@ -1,6 +1,7 @@
{
"isPublic": (ignore),
"isCd": (ignore),
+ "enable-public-signup-flow": (ignore),
"user": {
"name": "Joe Developer",
"email": "dev@domail",