diff options
author | Valerij Fredriksen <valerijf@verizonmedia.com> | 2019-12-02 13:52:20 +0100 |
---|---|---|
committer | Valerij Fredriksen <valerijf@verizonmedia.com> | 2019-12-02 13:52:20 +0100 |
commit | e25eba3551b703b884d9deb5169b090b58188e1a (patch) | |
tree | 0cf3e8c58d667a64aae16ef1fa91238504449860 | |
parent | c68656dd94e71ec552f9a44d04eb55d99e7cdb5b (diff) |
Include changes from #11407
15 files changed, 108 insertions, 37 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java index a80843ad252..e44038d0185 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java @@ -74,6 +74,13 @@ public class ZmsClientMock implements ZmsClient { } @Override + public void addRoleMember(AthenzRole role, AthenzIdentity member) { + if ( ! role.roleName().equals("tenancy.vespa.hosting.admin")) + throw new IllegalArgumentException("Mock only supports adding tenant admins, not " + role.roleName()); + getDomainOrThrow(role.domain(), true).tenantAdmin(member); + } + + @Override public boolean getMembership(AthenzRole role, AthenzIdentity identity) { if (role.roleName().equals("admin")) { return getDomainOrThrow(role.domain(), false).admins.contains(identity); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java index 6e16035156f..95669f7f05d 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java @@ -32,8 +32,9 @@ enum PathGroup { "/provision/v2/{*}", "/zone/v2/{*}"), - /** Paths used for creating user tenants. */ - user("/application/v4/user"), + /** Paths used for creating and reading user resources. */ + user("/application/v4/user", + "/athenz/v1/{*}"), /** Paths used for creating tenants with proper access control. */ tenant(Matcher.tenant, @@ -95,6 +96,7 @@ enum PathGroup { "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/global-rotation/{*}", "/application/v4/tenant/{tenant}/application/{application}/metering"), + // TODO jonmv: remove /** Path used to restart development nodes. */ developmentRestart(Matcher.tenant, Matcher.application, @@ -105,6 +107,7 @@ enum PathGroup { "/application/v4/tenant/{tenant}/application/{application}/environment/dev/region/{region}/instance/{instance}/restart", "/application/v4/tenant/{tenant}/application/{application}/environment/perf/region/{region}/instance/{instance}/restart"), + // TODO jonmv: remove /** Path used to restart production nodes. */ productionRestart(Matcher.tenant, Matcher.application, @@ -131,6 +134,7 @@ enum PathGroup { "/application/v4/tenant/{tenant}/application/{application}/environment/perf/region/{region}/instance/{instance}", "/application/v4/tenant/{tenant}/application/{application}/environment/perf/region/{region}/instance/{instance}/deploy"), + // TODO jonmv: remove /** Paths used for production deployments. */ productionDeployment(Matcher.tenant, Matcher.application, @@ -168,8 +172,7 @@ enum PathGroup { "/application/v4/tenant/"), /** Paths which contain (not very strictly) classified information about, e.g., customers. */ - classifiedInfo("/athenz/v1/{*}", - "/cost/v1/{*}", + classifiedInfo("/cost/v1/{*}", "/deployment/v1/{*}", "/", "/d/{*}", diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java index b1587575909..e27fb0fbf27 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java @@ -34,9 +34,9 @@ enum Policy { .in(SystemName.all())), /** Access to create a user tenant in select systems. */ - userCreate(Privilege.grant(Action.update) - .on(PathGroup.user) - .in(SystemName.main, SystemName.cd, SystemName.dev)), + user(Privilege.grant(Action.create, Action.update) + .on(PathGroup.user) + .in(SystemName.main, SystemName.cd, SystemName.dev)), /** Access to create a tenant in select systems. */ tenantCreate(Privilege.grant(Action.create) diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java index 6edce9fe0db..10df7604667 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java @@ -24,7 +24,7 @@ public enum RoleDefinition { everyone(Policy.classifiedRead, Policy.classifiedApiRead, Policy.publicRead, - Policy.userCreate, + Policy.user, Policy.tenantCreate), /** Application reader which can see all information about an application, its tenant and deployments. */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java index d1a6e39a1dd..e794334232f 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller; import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.curator.Lock; +import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade; import com.yahoo.vespa.hosted.controller.concurrent.Once; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import com.yahoo.vespa.hosted.controller.security.AccessControl; @@ -61,9 +62,10 @@ public class TenantController { .collect(Collectors.toList()); } + // TODO jonmv: Remove. /** Returns the list of tenants accessible to the given user. */ public List<Tenant> asList(Credentials credentials) { - return accessControl.accessibleTenants(asList(), credentials); + return ((AthenzFacade) accessControl).accessibleTenants(asList(), credentials); } /** Locks a tenant for modification and applies the given action. */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java index bb6777b9e27..7ace62ab44d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java @@ -11,6 +11,7 @@ import com.yahoo.vespa.athenz.api.AthenzPrincipal; import com.yahoo.vespa.athenz.api.AthenzResourceName; import com.yahoo.vespa.athenz.api.AthenzRole; import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.athenz.api.OktaAccessToken; import com.yahoo.vespa.athenz.api.OktaIdentityToken; import com.yahoo.vespa.athenz.client.zms.RoleAction; @@ -174,7 +175,13 @@ public class AthenzFacade implements AccessControl { athenzCredentials.identityToken(), athenzCredentials.accessToken()); } - @Override + /** + * Returns the list of tenants to which a user has access. + * @param tenants the list of all known tenants + * @param credentials the credentials of user whose tenants to list + * @return the list of tenants the given user has access to + */ + // TODO jonmv: Remove public List<Tenant> accessibleTenants(List<Tenant> tenants, Credentials credentials) { AthenzIdentity identity = ((AthenzPrincipal) credentials.user()).getIdentity(); List<AthenzDomain> userDomains = ztsClient.getTenantDomains(service, identity, "admin"); @@ -184,6 +191,10 @@ public class AthenzFacade implements AccessControl { .collect(Collectors.toUnmodifiableList()); } + public void addTenantAdmin(AthenzDomain tenantDomain, AthenzUser user) { + zmsClient.addRoleMember(new AthenzRole(tenantDomain, "tenancy." + service.getFullName() + ".admin"), user); + } + private void deleteApplication(AthenzDomain domain, ApplicationName application, OktaIdentityToken identityToken, OktaAccessToken accessToken) { log("deleteProviderResourceGroup(tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s)", domain, service.getDomain().getName(), service.getName(), application); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 114df26015f..42471cb9001 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -325,6 +325,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { : new ResourceResponse(request, "user", "tenant"); } + // TODO jonmv: Move to Athenz API. private HttpResponse authenticatedUser(HttpRequest request) { Principal user = requireUserPrincipal(request); if (user == null) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java index d36a7487e59..26c4bf6292a 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java @@ -1,25 +1,30 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.restapi.athenz; +import com.yahoo.config.provision.SystemName; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; import com.yahoo.jdisc.http.HttpRequest.Method; +import com.yahoo.restapi.ErrorResponse; +import com.yahoo.restapi.MessageResponse; import com.yahoo.restapi.Path; +import com.yahoo.restapi.ResourceResponse; +import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzPrincipal; +import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; import com.yahoo.vespa.hosted.controller.api.integration.entity.EntityService; import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade; -import com.yahoo.restapi.ErrorResponse; -import com.yahoo.restapi.ResourceResponse; -import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.yolean.Exceptions; import java.util.Map; +import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -34,11 +39,13 @@ public class AthenzApiHandler extends LoggingRequestHandler { private final static Logger log = Logger.getLogger(AthenzApiHandler.class.getName()); private final AthenzFacade athenz; + private final AthenzDomain sandboxDomain; private final EntityService properties; public AthenzApiHandler(Context parentCtx, AthenzFacade athenz, Controller controller) { super(parentCtx); this.athenz = athenz; + this.sandboxDomain = new AthenzDomain(sandboxDomainIn(controller.system())); this.properties = controller.serviceRegistry().entityService(); } @@ -48,6 +55,7 @@ public class AthenzApiHandler extends LoggingRequestHandler { try { switch (method) { case GET: return get(request); + case POST: return post(request); default: return ErrorResponse.methodNotAllowed("Method '" + method + "' is unsupported"); } } @@ -70,6 +78,13 @@ public class AthenzApiHandler extends LoggingRequestHandler { request.getUri().getPath())); } + private HttpResponse post(HttpRequest request) { + Path path = new Path(request.getUri()); + if (path.matches("/athenz/v1/user")) return signup(request); + return ErrorResponse.notFoundError(String.format("No '%s' handler at '%s'", request.getMethod(), + request.getUri().getPath())); + } + private HttpResponse root(HttpRequest request) { return new ResourceResponse(request, "domains", "properties"); } @@ -96,4 +111,27 @@ public class AthenzApiHandler extends LoggingRequestHandler { return new SlimeJsonResponse(slime); } + private HttpResponse signup(HttpRequest request) { + AthenzUser user = athenzUser(request); + athenz.addTenantAdmin(sandboxDomain, user); + return new MessageResponse("User '" + user.getName() + "' added to admin role of '" + sandboxDomain.getName() + "'"); + } + + private static AthenzUser athenzUser(HttpRequest request) { + return Optional.ofNullable(request.getJDiscRequest().getUserPrincipal()).filter(AthenzPrincipal.class::isInstance) + .map(AthenzPrincipal.class::cast) + .map(AthenzPrincipal::getIdentity) + .filter(AthenzUser.class::isInstance) + .map(AthenzUser.class::cast) + .orElseThrow(() -> new IllegalArgumentException("No Athenz user principal on request")); + } + + static String sandboxDomainIn(SystemName system) { + switch (system) { + case main: return "vespa.vespa.tenants.sandbox"; + case cd: return "vespa.vespa.cd.tenants.sandbox"; + default: throw new IllegalArgumentException("No sandbox domain in system '" + system + "'"); + } + } + } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java index 66c87a8eefd..8db96801a36 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java @@ -1,6 +1,5 @@ package com.yahoo.vespa.hosted.controller.security; -import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; @@ -63,12 +62,4 @@ public interface AccessControl { */ void deleteApplication(TenantAndApplicationId id, Credentials credentials); - /** - * Returns the list of tenants to which a user has access. - * @param tenants the list of all known tenants - * @param credentials the credentials of user whose tenants to list - * @return the list of tenants the given user has access to - */ - List<Tenant> accessibleTenants(List<Tenant> tenants, Credentials credentials); - } 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 363dc348ad3..4f3a55dbea0 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 @@ -14,7 +14,6 @@ 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 java.util.Collections; import java.util.List; /** @@ -73,11 +72,4 @@ public class CloudAccessControl implements AccessControl { userManagement.deleteRole(role); } - @Override - public List<Tenant> accessibleTenants(List<Tenant> tenants, Credentials credentials) { - // TODO: Get credential things (token with roles or something) and check what it's good for. - // TODO ... or ignore this here, and compute it somewhere else. - return Collections.emptyList(); - } - } 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 591146cbc1c..d37df2cc313 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 @@ -34,8 +34,8 @@ import static org.junit.Assert.assertEquals; */ public class ControllerContainerTest { - private static final AthenzUser hostedOperator = AthenzUser.fromUserId("alice"); - private static final AthenzUser defaultUser = AthenzUser.fromUserId("bob"); + protected static final AthenzUser hostedOperator = AthenzUser.fromUserId("alice"); + protected static final AthenzUser defaultUser = AthenzUser.fromUserId("bob"); protected JDisc container; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java index e3216a0038b..c90dcbf7e2b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java @@ -1,8 +1,9 @@ package com.yahoo.vespa.hosted.controller.restapi.athenz; +import com.yahoo.application.container.handler.Request; +import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.athenz.api.AthenzDomain; -import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock; -import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock; +import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import org.junit.Test; @@ -19,8 +20,20 @@ public class AthenzApiTest extends ControllerContainerTest { @Test public void testAthenzApi() { ContainerTester tester = new ContainerTester(container, responseFiles); - ((AthenzClientFactoryMock) tester.container().components().getComponent(AthenzClientFactoryMock.class.getName())) - .getSetup().addDomain(new AthenzDbMock.Domain(new AthenzDomain("domain1"))); + ControllerTester controllerTester = new ControllerTester(tester); + + controllerTester.createTenant("sandbox", AthenzApiHandler.sandboxDomainIn(tester.controller().system()), 123L); + controllerTester.createApplication("sandbox", "app", "default"); + tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", hostedOperator.getName())); + tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", defaultUser.getName())); + controllerTester.createApplication("sandbox", "opp", "default"); + + controllerTester.createTenant("tenant1", "domain1", 123L); + controllerTester.createApplication("tenant1", "app", "default"); + tester.athenzClientFactory().getSetup().getOrCreateDomain(new AthenzDomain("domain1")).admin(defaultUser); + + controllerTester.createTenant("tenant2", "domain2", 123L); + controllerTester.createApplication("tenant2", "app", "default"); // GET root tester.assertResponse(authenticatedRequest("http://localhost:8080/athenz/v1/"), @@ -33,6 +46,10 @@ public class AthenzApiTest extends ControllerContainerTest { // GET properties tester.assertResponse(authenticatedRequest("http://localhost:8080/athenz/v1/properties/"), new File("property-list.json")); + + // POST user signup + tester.assertResponse(authenticatedRequest("http://localhost:8080/athenz/v1/user", "", Request.Method.POST), + "{\"message\":\"User 'bob' added to admin role of 'vespa.vespa.tenants.sandbox'\"}"); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json index 3a1cc9c6582..913b8fab62f 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json @@ -1,5 +1,7 @@ { "data": [ - "domain1" + "domain1", + "domain2", + "vespa.vespa.tenants.sandbox" ] }
\ No newline at end of file diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java index 8129763a6d6..d0b0de45a0b 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java @@ -99,6 +99,11 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient { } @Override + public void addRoleMember(AthenzRole role, AthenzIdentity member) { + + } + + @Override public boolean getMembership(AthenzRole role, AthenzIdentity identity) { URI uri = zmsUrl.resolve(String.format("domain/%s/role/%s/member/%s", role.domain().getName(), role.roleName(), identity.getFullName())); HttpUriRequest request = RequestBuilder.get() diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java index 6a11a69a797..635cd30605d 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java @@ -28,6 +28,8 @@ public interface ZmsClient extends AutoCloseable { void deleteProviderResourceGroup(AthenzDomain tenantDomain, AthenzIdentity providerService, String resourceGroup, OktaIdentityToken identityToken, OktaAccessToken accessToken); + void addRoleMember(AthenzRole role, AthenzIdentity member); + boolean getMembership(AthenzRole role, AthenzIdentity identity); List<AthenzDomain> getDomainList(String prefix); |