diff options
author | Morten Tokle <mortent@verizonmedia.com> | 2019-07-30 13:34:25 +0200 |
---|---|---|
committer | Morten Tokle <mortent@verizonmedia.com> | 2019-07-30 13:50:52 +0200 |
commit | c054c04cce7d2ba1b8daefe11e74308598a4b5f5 (patch) | |
tree | db0b161dd49db296a327b50f286fa3282d4d84ec | |
parent | b53df1fcba1dae72a6e78c7892bc7b51408d4348 (diff) |
Validate that config server is allowed to launch Athenz service
8 files changed, 116 insertions, 13 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 65ef270ffd5..cb449f01286 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 @@ -13,7 +13,9 @@ import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzPrincipal; +import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.flags.BooleanFlag; @@ -853,14 +855,17 @@ public class ApplicationController { /** * 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 + * 1. Verify that the Athenz service can be launched by the config server + * 2. If the principal is given, verify that the principal is tenant admin or admin of the tenant domain + * 3. 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 deployer Principal initiating the deployment, possibly empty */ public void verifyApplicationIdentityConfiguration(TenantName tenantName, ApplicationPackage applicationPackage, Optional<Principal> deployer) { + verifyAllowedLaunchAthenzService(applicationPackage.deploymentSpec()); + applicationPackage.deploymentSpec().athenzDomain().ifPresent(identityDomain -> { Tenant tenant = controller.tenants().require(tenantName); deployer.filter(AthenzPrincipal.class::isInstance) @@ -886,6 +891,25 @@ public class ApplicationController { }); } + /* + * Verifies that the configured athenz service (if any) can be launched. + */ + private void verifyAllowedLaunchAthenzService(DeploymentSpec deploymentSpec) { + deploymentSpec.athenzDomain().ifPresent(athenzDomain -> { + controller.zoneRegistry().zones().reachable().ids() + .forEach(zone -> { + AthenzIdentity configServerAthenzIdentity = controller.zoneRegistry().getConfigServerAthenzIdentity(zone); + deploymentSpec.athenzService(zone.environment(), zone.region()) + .map(service -> new AthenzService(athenzDomain.value(), service.value())) + .ifPresent(service -> { + boolean allowedToLaunch = ((AthenzFacade) accessControl).canLaunch(configServerAthenzIdentity, service); + if (!allowedToLaunch) + throw new IllegalArgumentException("Not allowed to launch Athenz service " + service.getFullName()); + }); + }); + }); + } + /** Returns the latest known version within the given major. */ private Optional<Version> lastCompatibleVersion(int targetMajorVersion) { return controller.versionStatus().versions().stream() 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 75b7e137998..9257855eb6c 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.AthenzIdentity; 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.OktaAccessToken; import com.yahoo.vespa.athenz.client.zms.RoleAction; import com.yahoo.vespa.athenz.client.zms.ZmsClient; @@ -192,6 +193,10 @@ public class AthenzFacade implements AccessControl { return hasAccess("modify", service.getDomain().getName() + ":hosted-vespa", identity); } + public boolean canLaunch(AthenzIdentity principal, AthenzService service) { + return hasAccess("launch", service.getDomain().getName() + ":service."+service.getName(), principal); + } + /** * Used when creating tenancies. As there are no tenancy policies at this point, * we cannot use {@link #hasTenantAdminAccess(AthenzIdentity, AthenzDomain)} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzDbMock.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzDbMock.java index a11426b9a23..4d9296ea18d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzDbMock.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/AthenzDbMock.java @@ -26,6 +26,10 @@ public class AthenzDbMock { return this; } + public Domain getOrCreateDomain(AthenzDomain domain) { + return domains.computeIfAbsent(domain, Domain::new); + } + public AthenzDbMock addHostedOperator(AthenzIdentity athenzIdentity) { hostedOperators.add(athenzIdentity); return this; @@ -37,6 +41,7 @@ public class AthenzDbMock { public final Set<AthenzIdentity> admins = new HashSet<>(); public final Set<AthenzIdentity> tenantAdmins = new HashSet<>(); public final Map<ApplicationId, Application> applications = new HashMap<>(); + public final Map<String, Service> services = new HashMap<>(); public boolean isVespaTenant = false; public Domain(AthenzDomain name) { @@ -78,4 +83,12 @@ public class AthenzDbMock { } } + public static class Service { + + public final boolean allowLaunch; + + public Service(boolean allowLaunch) { + this.allowLaunch = allowLaunch; + } + } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZmsClientMock.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZmsClientMock.java index 37926d944b7..01f77795c4b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZmsClientMock.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/mock/ZmsClientMock.java @@ -107,6 +107,12 @@ public class ZmsClientMock implements ZmsClient { return false; } return false; + } else if ("launch".equals(action)){ + AthenzDbMock.Domain domain = getDomainOrThrow(resource.getDomain(), false); + String serviceName = resource.getEntityName().replace("service.",""); + if(!domain.services.containsKey(serviceName)) return false; + AthenzDbMock.Service service = domain.services.get(serviceName); + return service.allowLaunch; } return false; } 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 d5935c752d9..40936dd8a68 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 @@ -248,8 +248,7 @@ public final class ControllerTester { public AthenzDomain createDomainWithAdmin(String domainName, AthenzUser user) { AthenzDomain domain = new AthenzDomain(domainName); - athenzDb.addDomain(new AthenzDbMock.Domain(domain)); - athenzDb.domains.get(domain).admin(user); + athenzDb.getOrCreateDomain(domain).admin(user); return domain; } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java index 6725e05dd6d..46e991a135a 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java @@ -19,6 +19,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockTesterCloud; import com.yahoo.config.provision.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.athenz.mock.AthenzDbMock; import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock; import com.yahoo.vespa.hosted.controller.integration.RoutingGeneratorMock; import com.yahoo.vespa.hosted.controller.maintenance.JobControl; @@ -39,8 +40,11 @@ import static org.junit.Assert.assertTrue; public class InternalDeploymentTester { + private static final String ATHENZ_DOMAIN = "domain"; + private static final String ATHENZ_SERVICE = "service"; + public static final ApplicationPackage applicationPackage = new ApplicationPackageBuilder() - .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service")) + .athenzIdentity(AthenzDomain.from(ATHENZ_DOMAIN), AthenzService.from(ATHENZ_SERVICE)) .upgradePolicy("default") .region("us-central-1") .parallel("us-west-1", "us-east-3") @@ -84,6 +88,10 @@ public class InternalDeploymentTester { Logger.getLogger(InternalStepRunner.class.getName()).setLevel(LogLevel.DEBUG); Logger.getLogger("").setLevel(LogLevel.DEBUG); tester.controllerTester().configureDefaultLogHandler(handler -> handler.setLevel(LogLevel.DEBUG)); + + // Mock Athenz domain to allow launch of service + AthenzDbMock.Domain domain = tester.controllerTester().athenzDb().getOrCreateDomain(new com.yahoo.vespa.athenz.api.AthenzDomain(ATHENZ_DOMAIN)); + domain.services.put(ATHENZ_SERVICE, new AthenzDbMock.Service(true)); } /** diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java index 427428a3a94..54fe575a365 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java @@ -31,7 +31,6 @@ import com.yahoo.vespa.hosted.controller.integration.ArtifactRepositoryMock; import com.yahoo.vespa.hosted.controller.maintenance.JobControl; import com.yahoo.vespa.hosted.controller.maintenance.Upgrader; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; -import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; import com.yahoo.vespa.hosted.controller.security.AthenzCredentials; import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec; @@ -159,10 +158,9 @@ public class ContainerControllerTester { AthenzClientFactoryMock mock = (AthenzClientFactoryMock) containerTester.container().components() .getComponent(AthenzClientFactoryMock.class.getName()); AthenzDomain athensDomain = new AthenzDomain(domainName); - AthenzDbMock.Domain domain = new AthenzDbMock.Domain(athensDomain); + AthenzDbMock.Domain domain = mock.getSetup().getOrCreateDomain(athensDomain); domain.markAsVespaTenant(); domain.admin(new AthenzUser(userName)); - mock.getSetup().addDomain(domain); return athensDomain; } 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 5087d5f83ad..8db820f6c83 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 @@ -28,10 +28,10 @@ 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.identifiers.ScrewdriverId; import com.yahoo.vespa.hosted.controller.api.identifiers.UserId; -import com.yahoo.vespa.hosted.controller.api.integration.metrics.MetricsService.ApplicationMetrics; import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; +import com.yahoo.vespa.hosted.controller.api.integration.metrics.MetricsService.ApplicationMetrics; import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.MockContactRetriever; @@ -562,6 +562,7 @@ public class ApplicationApiTest extends ControllerContainerTest { .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from(ATHENZ_TENANT_DOMAIN_2.getName()), AthenzService.from("service")) .region("us-west-1") .build(); + configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN_2, "service"), true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST) .screwdriverIdentity(SCREWDRIVER_ID) .data(createApplicationSubmissionData(packageWithServiceForWrongDomain)), @@ -573,6 +574,7 @@ public class ApplicationApiTest extends ControllerContainerTest { .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from(ATHENZ_TENANT_DOMAIN.getName()), AthenzService.from("service")) .region("us-west-1") .build(); + configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST) .screwdriverIdentity(SCREWDRIVER_ID) .data(createApplicationSubmissionData(packageWithService)), @@ -1127,12 +1129,13 @@ public class ApplicationApiTest extends ControllerContainerTest { public void deployment_fails_on_illegal_domain_in_deployment_spec() { ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .upgradePolicy("default") - .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("invalid.domain"), com.yahoo.config.provision.AthenzService.from("service")) + .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("another.domain"), com.yahoo.config.provision.AthenzService.from("service")) .environment(Environment.prod) .region("us-west-1") .build(); long screwdriverProjectId = 123; createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); + configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(new AthenzDomain("another.domain"), "service"), true); Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.getName(), "tenant1", "application1", "default"); ScrewdriverId screwdriverId = new ScrewdriverId(Long.toString(screwdriverProjectId)); @@ -1147,7 +1150,7 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/", POST) .data(createApplicationDeployData(applicationPackage, false)) .screwdriverIdentity(screwdriverId), - "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Athenz domain in deployment.xml: [invalid.domain] must match tenant domain: [domain1]\"}", + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Athenz domain in deployment.xml: [another.domain] must match tenant domain: [domain1]\"}", 400); } @@ -1164,6 +1167,7 @@ public class ApplicationApiTest extends ControllerContainerTest { ScrewdriverId screwdriverId = new ScrewdriverId(Long.toString(screwdriverProjectId)); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); + configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), true); Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.getName(), "tenant1", "application1", "default"); controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application); @@ -1188,6 +1192,7 @@ public class ApplicationApiTest extends ControllerContainerTest { UserId tenantAdmin = new UserId("tenant-admin"); UserId userId = new UserId("new-user"); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, tenantAdmin); + configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), true); // Create tenant // PUT (create) the authenticated user @@ -1222,6 +1227,7 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.computeVersionStatus(); UserId tenantAdmin = new UserId("new_user"); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, tenantAdmin); + configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), true); // Create tenant // PUT (create) the authenticated user @@ -1247,6 +1253,39 @@ public class ApplicationApiTest extends ControllerContainerTest { } @Test + public void deployment_fails_when_athenz_service_cannot_be_launched() { + ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .upgradePolicy("default") + .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service")) + .environment(Environment.prod) + .region("us-west-1") + .build(); + long screwdriverProjectId = 123; + ScrewdriverId screwdriverId = new ScrewdriverId(Long.toString(screwdriverProjectId)); + + createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); + configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), false); + + Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.getName(), "tenant1", "application1", "default"); + controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application); + + // Allow systemtest to succeed by notifying completion of system test + controllerTester.jobCompletion(JobType.component) + .application(application.id()) + .projectId(screwdriverProjectId) + .uploadArtifact(applicationPackage) + .submit(); + + String expectedResult="{\"error-code\":\"BAD_REQUEST\",\"message\":\"Not allowed to launch Athenz service domain1.service\"}"; + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/", POST) + .data(createApplicationDeployData(applicationPackage, false)) + .screwdriverIdentity(screwdriverId), + expectedResult, + 400); + + } + + @Test public void redeployment_succeeds_when_not_specifying_versions_or_application_package() { // Setup addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR)); @@ -1262,6 +1301,7 @@ public class ApplicationApiTest extends ControllerContainerTest { ScrewdriverId screwdriverId = new ScrewdriverId(Long.toString(screwdriverProjectId)); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); + configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), true); Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.getName(), "tenant1", "application1", "default"); controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application); @@ -1476,13 +1516,23 @@ public class ApplicationApiTest extends ControllerContainerTest { private void createAthenzDomainWithAdmin(AthenzDomain domain, UserId userId) { AthenzClientFactoryMock mock = (AthenzClientFactoryMock) container.components() .getComponent(AthenzClientFactoryMock.class.getName()); - AthenzDbMock.Domain domainMock = new AthenzDbMock.Domain(domain); + AthenzDbMock.Domain domainMock = mock.getSetup().getOrCreateDomain(domain); domainMock.markAsVespaTenant(); domainMock.admin(AthenzUser.fromUserId(userId.id())); - mock.getSetup().addDomain(domainMock); } /** + * Mock athenz service identity configuration. Simulates that configserver is allowed to launch a service + */ + private void configureAthenzIdentity(com.yahoo.vespa.athenz.api.AthenzService service, boolean allowLaunch) { + AthenzClientFactoryMock mock = (AthenzClientFactoryMock) container.components() + .getComponent(AthenzClientFactoryMock.class.getName()); + AthenzDbMock.Domain domainMock = mock.getSetup().domains.computeIfAbsent(service.getDomain(), AthenzDbMock.Domain::new); + domainMock.services.put(service.getName(), new AthenzDbMock.Service(allowLaunch)); + } + + + /** * In production this happens outside hosted Vespa, so there is no API for it and we need to reach down into the * mock setup to replicate the action. */ |