diff options
author | Morten Tokle <mortent@oath.com> | 2019-01-08 14:09:03 +0100 |
---|---|---|
committer | Morten Tokle <mortent@oath.com> | 2019-01-08 14:41:31 +0100 |
commit | 86b4db57152a47bea7669b9fbc95c958ba4229de (patch) | |
tree | 1fc45a684f9b26c8df9e2567641635d3c2b5b0b6 | |
parent | 8191ade44977f2874d60fc6811bbb9427a60510d (diff) |
Allow developers deploy applications with athenz credentials if they have admin access
3 files changed, 82 insertions, 24 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index eb86f0c2919..4b695d531a3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java @@ -10,6 +10,8 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.TenantName; 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.OktaAccessToken; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.api.ActivateResult; @@ -28,6 +30,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.configserver.NoInstance import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node; import com.yahoo.vespa.hosted.controller.api.integration.configserver.PrepareResponse; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationStore; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId; @@ -40,7 +43,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingEndpoint import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGenerator; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.JobList; import com.yahoo.vespa.hosted.controller.application.JobStatus; @@ -298,7 +300,7 @@ public class ApplicationController { public ActivateResult deploy(ApplicationId applicationId, ZoneId zone, Optional<ApplicationPackage> applicationPackageFromDeployer, DeployOptions options) { - return deploy(applicationId, zone, applicationPackageFromDeployer, Optional.empty(), options); + return deploy(applicationId, zone, applicationPackageFromDeployer, Optional.empty(), options, Optional.empty()); } /** Deploys an application. If the application does not exist it is created. */ @@ -307,7 +309,8 @@ public class ApplicationController { public ActivateResult deploy(ApplicationId applicationId, ZoneId zone, Optional<ApplicationPackage> applicationPackageFromDeployer, Optional<ApplicationVersion> applicationVersionFromDeployer, - DeployOptions options) { + DeployOptions options, + Optional<AthenzIdentity> deployingIdentity) { if (applicationId.instance().isTester()) throw new IllegalArgumentException("'" + applicationId + "' is a tester application!"); @@ -350,7 +353,7 @@ public class ApplicationController { } // TODO: Remove this when all packages are validated upon submission, as in ApplicationApiHandler.submit(...). - verifyApplicationIdentityConfiguration(applicationId.tenant(), applicationPackage); + verifyApplicationIdentityConfiguration(applicationId.tenant(), applicationPackage, deployingIdentity); // Update application with information from application package if ( ! preferOldestVersion && ! application.get().deploymentJobs().deployedInternally()) @@ -733,28 +736,49 @@ public class ApplicationController { return applications.stream().sorted(Comparator.comparing(Application::id)).collect(Collectors.toList()); } - public void verifyApplicationIdentityConfiguration(TenantName tenantName, ApplicationPackage applicationPackage) { + /** + * Verifies that the application can be deployed to the tenant, following these rules: + * + * 1. If the principal is given, verify that the principal is tenant admin or admin of the tenant domain + * 2. If the principal is not given, verify that the Athenz domain of the tenant equals Athenz domain given in deployment.xml + * + * @param tenantName Tenant where application should be deployed + * @param applicationPackage Application package + * @param deployingIdentity Principal initiating the deployment, possibly empty + */ + public void verifyApplicationIdentityConfiguration(TenantName tenantName, ApplicationPackage applicationPackage, Optional<AthenzIdentity> deployingIdentity) { applicationPackage.deploymentSpec().athenzDomain() .ifPresent(identityDomain -> { Optional<Tenant> tenant = controller.tenants().tenant(tenantName); if(!tenant.isPresent()) { throw new IllegalArgumentException("Tenant does not exist"); } else { - AthenzDomain tenantDomain = tenant.filter(t -> t instanceof AthenzTenant) - .map(t -> (AthenzTenant) t) - .orElseThrow(() -> new IllegalArgumentException( - String.format("Athenz domain defined in deployment.xml, but no Athenz domain for tenant (%s). " + - "It is currently not possible to launch Athenz services from personal tenants, use " + - "Athenz tenant instead.", - tenantName.value()))) - .domain(); - - if (!Objects.equals(tenantDomain.getName(), identityDomain.value())) - throw new IllegalArgumentException(String.format("Athenz domain in deployment.xml: [%s] must match tenant domain: [%s]", - identityDomain.value(), - tenantDomain.getName())); + if (isUserDeployment(deployingIdentity)) { + deployingIdentity + .filter(user -> zmsClient.hasTenantAdminAccess(user, new AthenzDomain(identityDomain.value()))) + .orElseThrow(() -> new IllegalArgumentException( + String.format("User %s is not allowed to launch services in Athenz domain %s. Please reach out to the domain admin.", deployingIdentity.get().getFullName(), identityDomain.value()) + )); + } else { + AthenzDomain tenantDomain = tenant.filter(t -> t instanceof AthenzTenant) + .map(t -> (AthenzTenant) t) + .orElseThrow(() -> new IllegalArgumentException( + String.format("Athenz domain defined in deployment.xml, but no Athenz domain for tenant (%s). " + + tenantName.value()))) + .domain(); + + if (!Objects.equals(tenantDomain.getName(), identityDomain.value())) + throw new IllegalArgumentException(String.format("Athenz domain in deployment.xml: [%s] must match tenant domain: [%s]", + identityDomain.value(), + tenantDomain.getName())); + } } }); } + private boolean isUserDeployment(Optional<AthenzIdentity> identity) { + return identity + .filter(id -> id instanceof AthenzUser) + .isPresent(); + } } 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 cfd36a25de8..eeb1606d415 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 @@ -857,7 +857,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler { zone, applicationPackage, applicationVersion, - deployOptionsJsonClass); + deployOptionsJsonClass, + Optional.of(getUserPrincipal(request).getIdentity())); return new SlimeJsonResponse(toSlime(result)); } @@ -1287,7 +1288,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { ApplicationPackage applicationPackage = new ApplicationPackage(dataParts.get(EnvironmentResource.APPLICATION_ZIP)); if ( ! applicationPackage.deploymentSpec().athenzDomain().isPresent()) throw new IllegalArgumentException("Application must define an Athenz service in deployment.xml!"); - controller.applications().verifyApplicationIdentityConfiguration(TenantName.from(tenant), applicationPackage); + controller.applications().verifyApplicationIdentityConfiguration(TenantName.from(tenant), applicationPackage, Optional.of(getUserPrincipal(request).getIdentity())); return JobControllerApiHandlerHelper.submitResponse(controller.jobController(), tenant, 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 22ef61839be..702f50ca19d 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 @@ -1007,11 +1007,12 @@ public class ApplicationApiTest extends ControllerContainerTest { } @Test - public void deployment_fails_for_personal_tenants_when_athenzdomain_specified() { + public void deployment_fails_for_personal_tenants_when_athenzdomain_specified_and_user_not_admin() { // Setup tester.computeVersionStatus(); - UserId userId = new UserId("new_user"); - createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, userId); + UserId tenantAdmin = new UserId("tenant-admin"); + UserId userId = new UserId("new-user"); + createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, tenantAdmin); // Create tenant // PUT (create) the authenticated user @@ -1029,7 +1030,7 @@ public class ApplicationApiTest extends ControllerContainerTest { .build(); // POST (deploy) an application to a dev zone - String expectedResult="{\"error-code\":\"BAD_REQUEST\",\"message\":\"Athenz domain defined in deployment.xml, but no Athenz domain for tenant (by-new-user). It is currently not possible to launch Athenz services from personal tenants, use Athenz tenant instead.\"}"; + String expectedResult="{\"error-code\":\"BAD_REQUEST\",\"message\":\"User user.new-user is not allowed to launch services in Athenz domain domain1. Please reach out to the domain admin.\"}"; HttpEntity entity = createApplicationDeployData(applicationPackage, true); tester.assertResponse(request("/application/v4/tenant/by-new-user/application/application1/environment/dev/region/us-west-1/instance/default", POST) .data(entity) @@ -1040,6 +1041,38 @@ public class ApplicationApiTest extends ControllerContainerTest { } @Test + public void deployment_succeeds_for_personal_tenants_when_user_is_tenant_admin() { + + // Setup + tester.computeVersionStatus(); + UserId tenantAdmin = new UserId("new_user"); + createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, tenantAdmin); + + // Create tenant + // PUT (create) the authenticated user + byte[] data = new byte[0]; + tester.assertResponse(request("/application/v4/user?user=new_user&domain=by", PUT) + .data(data) + .userIdentity(tenantAdmin), // Normalized to by-new-user by API + new File("create-user-response.json")); + + ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .upgradePolicy("default") + .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service")) + .environment(Environment.dev) + .region("us-west-1") + .build(); + + // POST (deploy) an application to a dev zone + HttpEntity entity = createApplicationDeployData(applicationPackage, true); + tester.assertResponse(request("/application/v4/tenant/by-new-user/application/application1/environment/dev/region/us-west-1/instance/default", POST) + .data(entity) + .userIdentity(tenantAdmin), + new File("deploy-result.json")); + } + + + @Test public void testJobStatusReporting() { addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR)); tester.computeVersionStatus(); |