summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorMorten Tokle <mortent@oath.com>2019-01-08 14:09:03 +0100
committerMorten Tokle <mortent@oath.com>2019-01-08 14:41:31 +0100
commit86b4db57152a47bea7669b9fbc95c958ba4229de (patch)
tree1fc45a684f9b26c8df9e2567641635d3c2b5b0b6 /controller-server
parent8191ade44977f2874d60fc6811bbb9427a60510d (diff)
Allow developers deploy applications with athenz credentials if they have admin access
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java60
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java41
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();