diff options
author | Andreas Eriksen <andreer@verizonmedia.com> | 2020-06-11 10:31:20 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-11 10:31:20 +0200 |
commit | 54828b73bcea1d086b145453b318bfdcdfef93b8 (patch) | |
tree | 958584a90f51ce2d6ea58c2f57b5da34a9a44878 /controller-server | |
parent | 1aaaa34cc41bc4bcf8f9448c637e8ee5fa551200 (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')
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", |