aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java102
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java19
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java132
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java213
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsClientFacade.java122
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/ApplicationPermit.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzApplicationPermit.java17
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzPermitExtractor.java66
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzTenantPermit.java38
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/CloudApplicationPermit.java (renamed from controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/OktaApplicationPermit.java)11
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/CloudTenantPermit.java26
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/OktaTenantPermit.java26
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/PermitExtractor.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/PermitStore.java54
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/TenantPermit.java22
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java154
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java16
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java39
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/ZipStreamReaderTest.java10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java15
28 files changed, 664 insertions, 478 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 d53d3137991..a9b0ff421b9 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
@@ -12,7 +12,6 @@ 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;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
@@ -22,7 +21,6 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.identifiers.Hostname;
import com.yahoo.vespa.hosted.controller.api.identifiers.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.BuildService;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Log;
@@ -48,10 +46,12 @@ import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.JobStatus.JobRun;
import com.yahoo.vespa.hosted.controller.application.RoutingPolicy;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.athenz.impl.ZmsClientFacade;
+import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
import com.yahoo.vespa.hosted.controller.concurrent.Once;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentSteps;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
+import com.yahoo.vespa.hosted.controller.permits.ApplicationPermit;
+import com.yahoo.vespa.hosted.controller.permits.PermitStore;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.rotation.Rotation;
import com.yahoo.vespa.hosted.controller.rotation.RotationLock;
@@ -104,7 +104,7 @@ public class ApplicationController {
private final ArtifactRepository artifactRepository;
private final ApplicationStore applicationStore;
private final RotationRepository rotationRepository;
- private final ZmsClientFacade zmsClient;
+ private final PermitStore permits;
private final NameService nameService;
private final ConfigServer configServer;
private final RoutingGenerator routingGenerator;
@@ -113,13 +113,13 @@ public class ApplicationController {
private final DeploymentTrigger deploymentTrigger;
ApplicationController(Controller controller, CuratorDb curator,
- AthenzClientFactory zmsClientFactory, RotationsConfig rotationsConfig,
+ PermitStore permits, RotationsConfig rotationsConfig,
NameService nameService, ConfigServer configServer,
ArtifactRepository artifactRepository, ApplicationStore applicationStore,
RoutingGenerator routingGenerator, BuildService buildService, Clock clock) {
this.controller = controller;
this.curator = curator;
- this.zmsClient = new ZmsClientFacade(zmsClientFactory.createZmsClient(), zmsClientFactory.getControllerIdentity());
+ this.permits = permits;
this.nameService = nameService;
this.configServer = configServer;
this.routingGenerator = routingGenerator;
@@ -217,7 +217,7 @@ public class ApplicationController {
*
* @throws IllegalArgumentException if the application already exists
*/
- public Application createApplication(ApplicationId id, Optional<OktaAccessToken> token) {
+ public Application createApplication(ApplicationId id, Optional<ApplicationPermit> permit) {
if ( ! (id.instance().isDefault())) // TODO: Support instances properly
throw new IllegalArgumentException("Only the instance name 'default' is supported at the moment");
if (id.instance().isTester())
@@ -234,12 +234,12 @@ public class ApplicationController {
throw new IllegalArgumentException("Could not create '" + id + "': Application already exists");
if (get(dashToUnderscore(id)).isPresent()) // VESPA-1945
throw new IllegalArgumentException("Could not create '" + id + "': Application " + dashToUnderscore(id) + " already exists");
- if (id.instance().isDefault() && tenant.get() instanceof AthenzTenant) { // Only create the athenz application for "default" instances.
- if ( ! token.isPresent())
- throw new IllegalArgumentException("Could not create '" + id + "': No Okta Access Token provided");
+ if (tenant.get().type() != Tenant.Type.user) {
+ if ( ! permit.isPresent())
+ throw new IllegalArgumentException("Could not create '" + id + "': No permit provided");
- zmsClient.addApplication(((AthenzTenant) tenant.get()).domain(),
- new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(id.application().value()), token.get());
+ if (id.instance().isDefault()) // Only store the application permits for non-user applications.
+ permits.createApplication(permit.get());
}
LockedApplication application = new LockedApplication(new Application(id, clock.instant()), lock);
store(application);
@@ -265,6 +265,10 @@ public class ApplicationController {
if (applicationId.instance().isTester())
throw new IllegalArgumentException("'" + applicationId + "' is a tester application!");
+ Tenant tenant = controller.tenants().require(applicationId.tenant());
+ if (tenant.type() == Tenant.Type.user && ! get(applicationId).isPresent())
+ createApplication(applicationId, Optional.empty());
+
try (Lock deploymentLock = lockForDeployment(applicationId, zone)) {
Version platformVersion;
ApplicationVersion applicationVersion;
@@ -273,9 +277,7 @@ public class ApplicationController {
Set<String> cnames = new HashSet<>();
try (Lock lock = lock(applicationId)) {
- LockedApplication application = get(applicationId)
- .map(app -> new LockedApplication(app, lock))
- .orElseGet(() -> new LockedApplication(createApplication(applicationId, Optional.empty()), lock));
+ LockedApplication application = new LockedApplication(require(applicationId), lock);
boolean manuallyDeployed = options.deployDirectly || zone.environment().isManuallyDeployed();
boolean preferOldestVersion = options.deployCurrentVersion;
@@ -540,7 +542,11 @@ public class ApplicationController {
* @throws IllegalArgumentException if the application has deployments or the caller is not authorized
* @throws NotExistsException if no instances of the application exist
*/
- public void deleteApplication(ApplicationId applicationId, Optional<OktaAccessToken> token) {
+ public void deleteApplication(ApplicationId applicationId, Optional<ApplicationPermit> permit) {
+ Tenant tenant = controller.tenants().require(applicationId.tenant());
+ if (tenant.type() != Tenant.Type.user && ! permit.isPresent())
+ throw new IllegalArgumentException("Could not delete application '" + applicationId + "': No permit provided");
+
// Find all instances of the application
List<ApplicationId> instances = asList(applicationId.tenant()).stream()
.map(Application::id)
@@ -555,21 +561,16 @@ public class ApplicationController {
if ( ! application.get().deployments().isEmpty())
throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments");
- Tenant tenant = controller.tenants().get(id.tenant()).get();
- if (tenant instanceof AthenzTenant && ! token.isPresent())
- throw new IllegalArgumentException("Could not delete '" + application + "': No Okta Access Token provided");
-
- // Only delete in Athenz once
- if (id.instance().isDefault() && tenant instanceof AthenzTenant) {
- zmsClient.deleteApplication(((AthenzTenant) tenant).domain(),
- new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(id.application().value()), token.get());
- }
curator.removeApplication(id);
applicationStore.removeAll(id);
applicationStore.removeAll(TesterId.of(id));
log.info("Deleted " + application);
}));
+
+ // Only delete permits once.
+ if (tenant.type() != Tenant.Type.user)
+ permits.deleteApplication(permit.get());
}
/**
@@ -723,36 +724,29 @@ public class ApplicationController {
*
* @param tenantName Tenant where application should be deployed
* @param applicationPackage Application package
- * @param deployingIdentity Principal initiating the deployment, possibly empty
+ * @param deployer 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().get(tenantName);
- if(!tenant.isPresent()) {
- throw new IllegalArgumentException("Tenant does not exist");
- } else {
- 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()));
- }
- }
- });
+ public void verifyApplicationIdentityConfiguration(TenantName tenantName, ApplicationPackage applicationPackage, Optional<AthenzIdentity> deployer) {
+ applicationPackage.deploymentSpec().athenzDomain().ifPresent(identityDomain -> {
+ Tenant tenant = controller.tenants().require(tenantName);
+ deployer.filter(AthenzUser.class::isInstance)
+ .ifPresentOrElse(user -> {
+ if ( ! ((AthenzFacade) permits).hasTenantAdminAccess(user, new AthenzDomain(identityDomain.value())))
+ throw new IllegalArgumentException("User " + user.getFullName() + " is not allowed to launch " +
+ "services in Athenz domain " + identityDomain.value() + ". " +
+ "Please reach out to the domain admin.");
+ },
+ () -> {
+ if (tenant.type() != Tenant.Type.athenz)
+ throw new IllegalArgumentException("Athenz domain defined in deployment.xml, but no " +
+ "Athenz domain for tenant " + tenantName.value());
+
+ AthenzDomain tenantDomain = ((AthenzTenant) tenant).domain();
+ if ( ! Objects.equals(tenantDomain.getName(), identityDomain.value()))
+ throw new IllegalArgumentException("Athenz domain in deployment.xml: [" + identityDomain.value() + "] " +
+ "must match tenant domain: [" + tenantDomain.getName() + "]");
+ });
+ });
}
/** Returns the latest known version within the given major. */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
index 2f7ff97ce26..8be4bb689cc 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
@@ -28,7 +28,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer;
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.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.athenz.impl.ZmsClientFacade;
+import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
import com.yahoo.vespa.hosted.controller.auditlog.AuditLogger;
import com.yahoo.vespa.hosted.controller.deployment.JobController;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
@@ -79,7 +79,7 @@ public class Controller extends AbstractComponent {
private final ConfigServer configServer;
private final MetricsService metricsService;
private final Chef chef;
- private final ZmsClientFacade zmsClient;
+ private final AthenzFacade zmsClient;
private final Mailer mailer;
private final AuditLogger auditLogger;
@@ -89,11 +89,10 @@ public class Controller extends AbstractComponent {
* @param curator the curator instance storing the persistent state of the controller.
*/
@Inject
- public Controller(CuratorDb curator, RotationsConfig rotationsConfig,
- GitHub gitHub, EntityService entityService,
- ZoneRegistry zoneRegistry, ConfigServer configServer,
- MetricsService metricsService, NameService nameService,
- RoutingGenerator routingGenerator, Chef chef, AthenzClientFactory athenzClientFactory,
+ public Controller(CuratorDb curator, RotationsConfig rotationsConfig, GitHub gitHub, EntityService entityService,
+ ZoneRegistry zoneRegistry, ConfigServer configServer, MetricsService metricsService,
+ NameService nameService, RoutingGenerator routingGenerator, Chef chef,
+ AthenzClientFactory athenzClientFactory,
ArtifactRepository artifactRepository, ApplicationStore applicationStore, TesterCloud testerCloud,
BuildService buildService, RunDataStore runDataStore, Mailer mailer) {
this(curator, rotationsConfig,
@@ -122,11 +121,11 @@ public class Controller extends AbstractComponent {
this.metricsService = Objects.requireNonNull(metricsService, "MetricsService cannot be null");
this.chef = Objects.requireNonNull(chef, "Chef cannot be null");
this.clock = Objects.requireNonNull(clock, "Clock cannot be null");
- this.zmsClient = new ZmsClientFacade(athenzClientFactory.createZmsClient(), athenzClientFactory.getControllerIdentity());
+ this.zmsClient = new AthenzFacade(athenzClientFactory);
this.mailer = Objects.requireNonNull(mailer, "Mailer cannot be null");
jobController = new JobController(this, runDataStore, Objects.requireNonNull(testerCloud));
- applicationController = new ApplicationController(this, curator, athenzClientFactory,
+ applicationController = new ApplicationController(this, curator, zmsClient,
Objects.requireNonNull(rotationsConfig, "RotationsConfig cannot be null"),
Objects.requireNonNull(nameService, "NameService cannot be null"),
configServer,
@@ -135,7 +134,7 @@ public class Controller extends AbstractComponent {
Objects.requireNonNull(routingGenerator, "RoutingGenerator cannot be null"),
Objects.requireNonNull(buildService, "BuildService cannot be null"),
clock);
- tenantController = new TenantController(this, curator, athenzClientFactory);
+ tenantController = new TenantController(this, curator, zmsClient);
auditLogger = new AuditLogger(curator, clock);
// Record the version of this controller
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 3b6317efa52..f92b30af0dd 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
@@ -2,34 +2,32 @@
package com.yahoo.vespa.hosted.controller;
import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-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.client.zts.ZtsClient;
import com.yahoo.vespa.curator.Lock;
-import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.athenz.impl.ZmsClientFacade;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
import com.yahoo.vespa.hosted.controller.concurrent.Once;
+import com.yahoo.vespa.hosted.controller.permits.PermitStore;
+import com.yahoo.vespa.hosted.controller.permits.TenantPermit;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
+import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.Comparator;
-import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
+import static com.yahoo.vespa.hosted.controller.tenant.Tenant.Type.cloud;
+
/**
* A singleton owned by the Controller which contains the methods and state for controlling tenants.
*
@@ -42,16 +40,12 @@ public class TenantController {
private final Controller controller;
private final CuratorDb curator;
- private final ZmsClientFacade zmsClient;
- private final ZtsClient ztsClient;
- private final AthenzService controllerIdentity;
+ private final PermitStore permits;
- public TenantController(Controller controller, CuratorDb curator, AthenzClientFactory athenzClientFactory) {
+ public TenantController(Controller controller, CuratorDb curator, PermitStore permits) {
this.controller = Objects.requireNonNull(controller, "controller must be non-null");
this.curator = Objects.requireNonNull(curator, "curator must be non-null");
- this.controllerIdentity = athenzClientFactory.getControllerIdentity();
- this.zmsClient = new ZmsClientFacade(athenzClientFactory.createZmsClient(), controllerIdentity);
- this.ztsClient = athenzClientFactory.createZtsClient();
+ this.permits = permits;
// Update serialization format of all tenants
Once.after(Duration.ofMinutes(1), () -> {
@@ -73,14 +67,9 @@ public class TenantController {
.collect(Collectors.toList());
}
- /** Returns a list of all tenants accessible by the given user */
- public List<Tenant> asList(UserId user) {
- AthenzUser athenzUser = AthenzUser.fromUserId(user.id());
- Set<AthenzDomain> userDomains = new HashSet<>(ztsClient.getTenantDomains(controllerIdentity, athenzUser, "admin"));
- return asList().stream()
- .filter(tenant -> isUser(tenant, user) ||
- userDomains.stream().anyMatch(domain -> inDomain(tenant, domain)))
- .collect(Collectors.toList());
+ /** Returns the lsit of tenants accessible to the given user. */
+ public List<Tenant> asList(Principal user) {
+ return permits.accessibleTenants(asList(), user);
}
/** Locks a tenant for modification and applies the given action. */
@@ -110,38 +99,21 @@ public class TenantController {
}
/** Create an user tenant with given username */
- public void create(UserTenant tenant) {
+ public void createUser(UserTenant tenant) {
try (Lock lock = lock(tenant.name())) {
requireNonExistent(tenant.name());
curator.writeTenant(tenant);
}
}
- /** Create an Athenz tenant */
- public void create(AthenzTenant tenant, OktaAccessToken token) {
- try (Lock lock = lock(tenant.name())) {
- requireNonExistent(tenant.name());
- AthenzDomain domain = tenant.domain();
- Optional<Tenant> existingTenantWithDomain = tenantIn(domain);
- if (existingTenantWithDomain.isPresent()) {
- throw new IllegalArgumentException("Could not create tenant '" + tenant.name().value() +
- "': The Athens domain '" +
- domain.getName() + "' is already connected to tenant '" +
- existingTenantWithDomain.get().name().value() +
- "'");
- }
- zmsClient.createTenant(domain, token);
- curator.writeTenant(tenant);
+ /** Create a tenant, provided the given permit is valid. */
+ public void create(TenantPermit permit) {
+ try (Lock lock = lock(permit.tenant())) {
+ requireNonExistent(permit.tenant());
+ curator.writeTenant(permits.createTenant(permit, asList(), Collections.emptyList()));
}
}
- /** Returns the tenant in the given Athenz domain, or empty if none */
- private Optional<Tenant> tenantIn(AthenzDomain domain) {
- return asList().stream()
- .filter(tenant -> inDomain(tenant, domain))
- .findFirst();
- }
-
/** Find tenant by name */
public Optional<Tenant> get(TenantName name) {
return curator.readTenant(name);
@@ -164,47 +136,37 @@ public class TenantController {
return athenzTenant(name).orElseThrow(() -> new IllegalArgumentException("Tenant '" + name + "' not found"));
}
- /** Update Athenz domain for tenant. Returns the updated tenant which must be explicitly stored */
- public LockedTenant.Athenz withDomain(LockedTenant.Athenz tenant, AthenzDomain newDomain, OktaAccessToken token) {
- AthenzTenant athenzTenant = tenant.get();
- AthenzDomain existingDomain = athenzTenant.domain();
- if (existingDomain.equals(newDomain)) return tenant;
- Optional<Tenant> existingTenantWithNewDomain = tenantIn(newDomain);
- if (existingTenantWithNewDomain.isPresent())
- throw new IllegalArgumentException("Could not set domain of " + tenant + " to '" + newDomain +
- "':" + existingTenantWithNewDomain.get() + " already has this domain");
-
- zmsClient.createTenant(newDomain, token);
- List<Application> applications = controller.applications().asList(tenant.get().name());
- applications.forEach(a -> zmsClient.addApplication(newDomain, new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(a.id().application().value()), token));
- applications.forEach(a -> zmsClient.deleteApplication(existingDomain, new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(a.id().application().value()), token));
- zmsClient.deleteTenant(existingDomain, token);
- log.info("Set Athenz domain for '" + tenant + "' from '" + existingDomain + "' to '" + newDomain + "'");
-
- return tenant.with(newDomain);
- }
+ /** Updates the tenant contained in the given permit with new data. */
+ public void update(TenantPermit permit) {
+ try (Lock lock = lock(permit.tenant())) {
+ Tenant tenant = require(permit.tenant());
+ List<Tenant> otherTenants = new ArrayList<>(asList());
+ otherTenants.remove(tenant);
- /** Delete an user tenant */
- public void deleteTenant(UserTenant tenant) {
- try (Lock lock = lock(tenant.name())) {
- deleteTenant(tenant.name());
+ List<Application> applications = controller.applications().asList(permit.tenant());
+ permits.deleteTenant(permit, tenant, applications);
+ curator.writeTenant(permits.createTenant(permit, otherTenants, applications));
}
}
- /** Delete an Athenz tenant */
- public void deleteTenant(AthenzTenant tenant, OktaAccessToken token) {
- try (Lock lock = lock(tenant.name())) {
- deleteTenant(tenant.name());
- zmsClient.deleteTenant(tenant.domain(), token);
+ /** Deletes the tenant in the given permit. */
+ public void delete(TenantPermit permit) {
+ try (Lock lock = lock(permit.tenant())) {
+ Tenant tenant = require(permit.tenant());
+ if ( ! controller.applications().asList(tenant.name()).isEmpty())
+ throw new IllegalArgumentException("Could not delete tenant '" + tenant.name().value()
+ + "': This tenant has active applications");
+
+ curator.removeTenant(tenant.name());
+ permits.deleteTenant(permit, tenant, controller.applications().asList(permit.tenant()));
}
}
- private void deleteTenant(TenantName name) {
- if (!controller.applications().asList(name).isEmpty()) {
- throw new IllegalArgumentException("Could not delete tenant '" + name.value()
- + "': This tenant has active applications");
+ /** Deletes the given user tenant. */
+ public void deleteUser(UserTenant tenant) {
+ try (Lock lock = lock(tenant.name())) {
+ curator.removeTenant(tenant.name());
}
- curator.removeTenant(name);
}
private void requireNonExistent(TenantName name) {
@@ -225,14 +187,6 @@ public class TenantController {
return curator.lock(tenant);
}
- private static boolean inDomain(Tenant tenant, AthenzDomain domain) {
- return tenant instanceof AthenzTenant && ((AthenzTenant) tenant).in(domain);
- }
-
- private static boolean isUser(Tenant tenant, UserId userId) {
- return tenant instanceof UserTenant && ((UserTenant) tenant).is(userId.id());
- }
-
private static String dashToUnderscore(String s) {
return s.replace('-', '_');
}
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
new file mode 100644
index 00000000000..cd97d18ff02
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
@@ -0,0 +1,213 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.athenz.impl;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.log.LogLevel;
+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.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.client.zms.RoleAction;
+import com.yahoo.vespa.athenz.client.zms.ZmsClient;
+import com.yahoo.vespa.athenz.client.zts.ZtsClient;
+import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.athenz.ApplicationAction;
+import com.yahoo.vespa.hosted.controller.permits.ApplicationPermit;
+import com.yahoo.vespa.hosted.controller.permits.AthenzApplicationPermit;
+import com.yahoo.vespa.hosted.controller.permits.AthenzTenantPermit;
+import com.yahoo.vespa.hosted.controller.permits.PermitStore;
+import com.yahoo.vespa.hosted.controller.permits.TenantPermit;
+import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
+import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
+
+import javax.ws.rs.ForbiddenException;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+/**
+ * @author bjorncs
+ */
+public class AthenzFacade implements PermitStore {
+
+ private static final Logger log = Logger.getLogger(AthenzFacade.class.getName());
+ private final ZmsClient zmsClient;
+ private final ZtsClient ztsClient;
+ private final AthenzService service;
+
+ @Inject
+ public AthenzFacade(AthenzClientFactory factory) {
+ this(factory.createZmsClient(), factory.createZtsClient(), factory.getControllerIdentity());
+ }
+
+ public AthenzFacade(ZmsClient zmsClient, ZtsClient ztsClient, AthenzService identity) {
+ this.zmsClient = zmsClient;
+ this.ztsClient = ztsClient;
+ this.service = identity;
+ }
+
+ @Override
+ public Tenant createTenant(TenantPermit permit, List<Tenant> existing, List<Application> applications) {
+ AthenzTenantPermit athenzPermit = (AthenzTenantPermit) permit;
+ AthenzDomain domain = athenzPermit.domain()
+ .orElseThrow(() -> new IllegalArgumentException("Must provide Athenz domain."));
+
+ Tenant tenant = AthenzTenant.create(athenzPermit.tenant(),
+ athenzPermit.domain()
+ .orElseThrow(() -> new IllegalArgumentException("Must provide Athenz domain.")),
+ athenzPermit.property()
+ .orElseThrow(() -> new IllegalArgumentException("Must provide property.")),
+ athenzPermit.propertyId());
+
+ verifyIsDomainAdmin(((AthenzPrincipal) athenzPermit.user()).getIdentity(), domain);
+
+ Optional<Tenant> existingWithSameDomain = existing.stream()
+ .filter(existingTenant -> existingTenant instanceof AthenzTenant
+ && domain.equals(((AthenzTenant) existingTenant).domain()))
+ .findAny();
+
+ if (existingWithSameDomain.isPresent()) { // Throw if domain taken by someone else, or do nothing if taken by this tenant.
+ if ( ! existingWithSameDomain.get().name().equals(permit.tenant()))
+ throw new IllegalArgumentException("Could not create tenant '" + athenzPermit.tenant().value() +
+ "': The Athens domain '" +
+ domain.getName() + "' is already connected to tenant '" +
+ existingWithSameDomain.get().name().value() + "'");
+ }
+ else { // Create tenant, and optionally application, resources in Athenz if domain is not already taken.
+ log("createTenancy(tenantDomain=%s, service=%s)", athenzPermit.domain(), service);
+ zmsClient.createTenancy(domain, service, athenzPermit.token());
+
+ for (Application application : applications)
+ createApplication(domain, application.id().application(), athenzPermit.token());
+ }
+
+ return tenant;
+ }
+
+ @Override
+ public void deleteTenant(TenantPermit permit, Tenant tenant, List<Application> applications) {
+ AthenzTenantPermit athenzPermit = (AthenzTenantPermit) permit;
+ AthenzDomain domain = ((AthenzTenant) tenant).domain();
+
+ for (Application application : applications)
+ deleteApplication(domain,
+ application.id().application(),
+ athenzPermit.token());
+
+ log("deleteTenancy(tenantDomain=%s, service=%s)", athenzPermit.domain(), service);
+ zmsClient.deleteTenancy(domain, service, athenzPermit.token());
+ }
+
+ @Override
+ public void createApplication(ApplicationPermit permit) {
+ AthenzApplicationPermit athenzPermit = (AthenzApplicationPermit) permit;
+ createApplication(athenzPermit.domain(), athenzPermit.application().application(), athenzPermit.token());
+ }
+
+ private void createApplication(AthenzDomain domain, ApplicationName application, OktaAccessToken token) {
+ Set<RoleAction> tenantRoleActions = createTenantRoleActions();
+ log("createProviderResourceGroup(" +
+ "tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s, roleActions=%s)",
+ domain, service.getDomain().getName(), service.getName(), application, tenantRoleActions);
+ zmsClient.createProviderResourceGroup(domain, service, application.value(), tenantRoleActions, token);
+ }
+
+ @Override
+ public void deleteApplication(ApplicationPermit permit) {
+ AthenzApplicationPermit athenzPermit = (AthenzApplicationPermit) permit;
+ log("deleteProviderResourceGroup(tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s)",
+ athenzPermit.domain(), service.getDomain().getName(), service.getName(), athenzPermit.application());
+ zmsClient.deleteProviderResourceGroup(athenzPermit.domain(), service, athenzPermit.application().application().value(), athenzPermit.token());
+ }
+
+ @Override
+ public List<Tenant> accessibleTenants(List<Tenant> tenants, Principal principal) {
+ List<AthenzDomain> userDomains = ztsClient.getTenantDomains(service, ((AthenzPrincipal) principal).getIdentity(), "admin");
+ return tenants.stream()
+ .filter(tenant -> tenant instanceof UserTenant && ((UserTenant) tenant).is(principal.getName())
+ || tenant instanceof AthenzTenant && userDomains.contains(((AthenzTenant) tenant).domain()))
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ private void deleteApplication(AthenzDomain domain, ApplicationName application, OktaAccessToken token) {
+ log("deleteProviderResourceGroup(tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s)",
+ domain, service.getDomain().getName(), service.getName(), application);
+ zmsClient.deleteProviderResourceGroup(domain, service, application.value(), token);
+ }
+
+ public boolean hasApplicationAccess(
+ AthenzIdentity identity, ApplicationAction action, AthenzDomain tenantDomain, ApplicationName applicationName) {
+ return hasAccess(
+ action.name(), applicationResourceString(tenantDomain, applicationName), identity);
+ }
+
+ public boolean hasTenantAdminAccess(AthenzIdentity identity, AthenzDomain tenantDomain) {
+ return hasAccess(TenantAction._modify_.name(), tenantResourceString(tenantDomain), identity);
+ }
+
+ public boolean hasHostedOperatorAccess(AthenzIdentity identity) {
+ return hasAccess("modify", service.getDomain().getName() + ":hosted-vespa", identity);
+ }
+
+ /**
+ * Used when creating tenancies. As there are no tenancy policies at this point,
+ * we cannot use {@link #hasTenantAdminAccess(AthenzIdentity, AthenzDomain)}
+ */
+ private void verifyIsDomainAdmin(AthenzIdentity identity, AthenzDomain domain) {
+ log("getMembership(domain=%s, role=%s, principal=%s)", domain, "admin", identity);
+ if ( ! zmsClient.getMembership(new AthenzRole(domain, "admin"), identity))
+ throw new ForbiddenException(
+ String.format("The user '%s' is not admin in Athenz domain '%s'", identity.getFullName(), domain.getName()));
+ }
+
+ public List<AthenzDomain> getDomainList(String prefix) {
+ log.log(LogLevel.DEBUG, String.format("getDomainList(prefix=%s)", prefix));
+ return zmsClient.getDomainList(prefix);
+ }
+
+ private static Set<RoleAction> createTenantRoleActions() {
+ return Arrays.stream(ApplicationAction.values())
+ .map(action -> new RoleAction(action.roleName, action.name()))
+ .collect(Collectors.toSet());
+ }
+
+ private boolean hasAccess(String action, String resource, AthenzIdentity identity) {
+ log("getAccess(action=%s, resource=%s, principal=%s)", action, resource, identity);
+ return zmsClient.hasAccess(AthenzResourceName.fromString(resource), action, identity);
+ }
+
+ private static void log(String format, Object... args) {
+ log.log(LogLevel.DEBUG, String.format(format, args));
+ }
+
+ private String resourceStringPrefix(AthenzDomain tenantDomain) {
+ return String.format("%s:service.%s.tenant.%s",
+ service.getDomain().getName(), service.getName(), tenantDomain.getName());
+ }
+
+ private String tenantResourceString(AthenzDomain tenantDomain) {
+ return resourceStringPrefix(tenantDomain) + ".wildcard";
+ }
+
+ private String applicationResourceString(AthenzDomain tenantDomain, ApplicationName applicationName) {
+ return resourceStringPrefix(tenantDomain) + "." + "res_group" + "." + applicationName.value() + ".wildcard";
+ }
+
+ private enum TenantAction {
+ // This is meant to match only the '*' action of the 'admin' role.
+ // If needed, we can replace it with 'create', 'delete' etc. later.
+ _modify_
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsClientFacade.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsClientFacade.java
deleted file mode 100644
index 09619a33cc4..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/ZmsClientFacade.java
+++ /dev/null
@@ -1,122 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.athenz.impl;
-
-import com.yahoo.log.LogLevel;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-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;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
-import com.yahoo.vespa.hosted.controller.athenz.ApplicationAction;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * @author bjorncs
- */
-public class ZmsClientFacade {
-
- private static final Logger log = Logger.getLogger(ZmsClientFacade.class.getName());
- private final ZmsClient zmsClient;
- private final AthenzService service;
-
- public ZmsClientFacade(ZmsClient zmsClient, AthenzService identity) {
- this.zmsClient = zmsClient;
- this.service = identity;
- }
-
- public void createTenant(AthenzDomain tenantDomain, OktaAccessToken token) {
- log("createTenancy(tenantDomain=%s, service=%s)", tenantDomain, service);
- zmsClient.createTenancy(tenantDomain, service, token);
- }
-
- public void deleteTenant(AthenzDomain tenantDomain, OktaAccessToken token) {
- log("deleteTenancy(tenantDomain=%s, service=%s)", tenantDomain, service);
- zmsClient.deleteTenancy(tenantDomain, service, token);
- }
-
- public void addApplication(AthenzDomain tenantDomain, ApplicationId applicationName, OktaAccessToken token) {
- Set<RoleAction> tenantRoleActions = createTenantRoleActions();
- log("createProviderResourceGroup(" +
- "tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s, roleActions=%s)",
- tenantDomain, service.getDomain().getName(), service.getName(), applicationName, tenantRoleActions);
- zmsClient.createProviderResourceGroup(tenantDomain, service, applicationName.id(), tenantRoleActions, token);
- }
-
- public void deleteApplication(AthenzDomain tenantDomain, ApplicationId applicationName, OktaAccessToken token) {
- log("deleteProviderResourceGroup(tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s)",
- tenantDomain, service.getDomain().getName(), service.getName(), applicationName);
- zmsClient.deleteProviderResourceGroup(tenantDomain, service, applicationName.id(), token);
- }
-
- public boolean hasApplicationAccess(
- AthenzIdentity identity, ApplicationAction action, AthenzDomain tenantDomain, ApplicationId applicationName) {
- return hasAccess(
- action.name(), applicationResourceString(tenantDomain, applicationName), identity);
- }
-
- public boolean hasTenantAdminAccess(AthenzIdentity identity, AthenzDomain tenantDomain) {
- return hasAccess(TenantAction._modify_.name(), tenantResourceString(tenantDomain), identity);
- }
-
- public boolean hasHostedOperatorAccess(AthenzIdentity identity) {
- return hasAccess("modify", service.getDomain().getName() + ":hosted-vespa", identity);
- }
-
- /**
- * Used when creating tenancies. As there are no tenancy policies at this point,
- * we cannot use {@link #hasTenantAdminAccess(AthenzIdentity, AthenzDomain)}
- */
- public boolean isDomainAdmin(AthenzIdentity identity, AthenzDomain domain) {
- log("getMembership(domain=%s, role=%s, principal=%s)", domain, "admin", identity);
- return zmsClient.getMembership(new AthenzRole(domain, "admin"), identity);
- }
-
- public List<AthenzDomain> getDomainList(String prefix) {
- log.log(LogLevel.DEBUG, String.format("getDomainList(prefix=%s)", prefix));
- return zmsClient.getDomainList(prefix);
- }
-
- private static Set<RoleAction> createTenantRoleActions() {
- return Arrays.stream(ApplicationAction.values())
- .map(action -> new RoleAction(action.roleName, action.name()))
- .collect(Collectors.toSet());
- }
-
- private boolean hasAccess(String action, String resource, AthenzIdentity identity) {
- log("getAccess(action=%s, resource=%s, principal=%s)", action, resource, identity);
- return zmsClient.hasAccess(AthenzResourceName.fromString(resource), action, identity);
- }
-
- private static void log(String format, Object... args) {
- log.log(LogLevel.DEBUG, String.format(format, args));
- }
-
- private String resourceStringPrefix(AthenzDomain tenantDomain) {
- return String.format("%s:service.%s.tenant.%s",
- service.getDomain().getName(), service.getName(), tenantDomain.getName());
- }
-
- private String tenantResourceString(AthenzDomain tenantDomain) {
- return resourceStringPrefix(tenantDomain) + ".wildcard";
- }
-
- private String applicationResourceString(AthenzDomain tenantDomain, ApplicationId applicationName) {
- return resourceStringPrefix(tenantDomain) + "." + "res_group" + "." + applicationName.id() + ".wildcard";
- }
-
- private enum TenantAction {
- // This is meant to match only the '*' action of the 'admin' role.
- // If needed, we can replace it with 'create', 'delete' etc. later.
- _modify_
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
index cf706c0a1a4..c8ba3f31316 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
@@ -22,6 +22,7 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.persistence.BufferedLogStore;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import java.net.URI;
import java.util.ArrayList;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/ApplicationPermit.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/ApplicationPermit.java
index 272fae5ca65..20492ae16ab 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/ApplicationPermit.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/ApplicationPermit.java
@@ -1,11 +1,24 @@
package com.yahoo.vespa.hosted.controller.permits;
+import com.yahoo.config.provision.ApplicationId;
+
+import static java.util.Objects.requireNonNull;
+
/**
* Data that relates identities to permissions to an application.
*
* @author jonmv
*/
-public interface ApplicationPermit extends TenantPermit {
+public abstract class ApplicationPermit {
+
+ private final ApplicationId application;
+
+ protected ApplicationPermit(ApplicationId application) {
+ this.application = requireNonNull(application);
+ }
+
+ /** The application this permit concerns. */
+ public ApplicationId application() { return application; }
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzApplicationPermit.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzApplicationPermit.java
index fff860465ba..084d13fe128 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzApplicationPermit.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzApplicationPermit.java
@@ -4,27 +4,28 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.athenz.api.OktaAccessToken;
-import java.util.Objects;
+import static java.util.Objects.requireNonNull;
/**
* Wraps the permit data of an Athenz application modification.
*
* @author jonmv
*/
-public class AthenzApplicationPermit implements ApplicationPermit {
+public class AthenzApplicationPermit extends ApplicationPermit {
private final AthenzDomain domain;
- private final ApplicationId application;
private final OktaAccessToken token;
- public AthenzApplicationPermit(AthenzDomain domain, ApplicationId application, OktaAccessToken token) {
- this.domain = Objects.requireNonNull(domain);
- this.application = Objects.requireNonNull(application);
- this.token = Objects.requireNonNull(token);
+ public AthenzApplicationPermit(ApplicationId application, AthenzDomain domain, OktaAccessToken token) {
+ super(application);
+ this.domain = requireNonNull(domain);
+ this.token = requireNonNull(token);
}
+ /** The athenz domain to create this application under. */
public AthenzDomain domain() { return domain; }
- public ApplicationId application() { return application; }
+
+ /** The Okta issued token proving the user's access to Athenz. */
public OktaAccessToken token() { return token; }
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzPermitExtractor.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzPermitExtractor.java
new file mode 100644
index 00000000000..b4e96f32d87
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzPermitExtractor.java
@@ -0,0 +1,66 @@
+package com.yahoo.vespa.hosted.controller.permits;
+
+import com.google.inject.Inject;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.slime.Inspector;
+import com.yahoo.vespa.athenz.api.AthenzDomain;
+import com.yahoo.vespa.athenz.api.OktaAccessToken;
+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.tenant.AthenzTenant;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import static com.yahoo.io.IOUtils.readBytes;
+import static com.yahoo.vespa.config.SlimeUtils.jsonToSlime;
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * Extracts permits for Athenz or user tenants from HTTP requests.
+ */
+public class AthenzPermitExtractor implements PermitExtractor {
+
+ private final Controller controller;
+
+ @Inject
+ public AthenzPermitExtractor(Controller controller) {
+ this.controller = Objects.requireNonNull(controller);
+ }
+
+ @Override
+ public TenantPermit getTenantPermit(TenantName tenant, HttpRequest request) {
+ Inspector root = jsonToSlime(uncheck(() -> readBytes(request.getData(), 1 << 20))).get();
+ return new AthenzTenantPermit(tenant,
+ request.getJDiscRequest().getUserPrincipal(),
+ optional("athensDomain", root).map(AthenzDomain::new),
+ optional("property", root).map(Property::new),
+ optional("propertyId", root).map(PropertyId::new),
+ requireOktaAccessToken(request));
+ }
+
+ @Override
+ public ApplicationPermit getApplicationPermit(ApplicationId application, HttpRequest request) {
+ return new AthenzApplicationPermit(application,
+ ((AthenzTenant) controller.tenants().require(application.tenant())).domain(),
+ requireOktaAccessToken(request));
+ }
+
+ private static OktaAccessToken requireOktaAccessToken(HttpRequest request) {
+ return Optional.ofNullable(request.getJDiscRequest().context().get("okta.access-token"))
+ .map(attribute -> new OktaAccessToken((String) attribute))
+ .orElseThrow(() -> new IllegalArgumentException("No Okta Access Token provided"));
+ }
+
+ private static String required(String fieldName, Inspector object) {
+ return optional(fieldName, object) .orElseThrow(() -> new IllegalArgumentException("Missing required field '" + fieldName + "'."));
+ }
+
+ private static Optional<String> optional(String fieldName, Inspector object) {
+ return object.field(fieldName).valid() ? Optional.of(object.field(fieldName).asString()) : Optional.empty();
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzTenantPermit.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzTenantPermit.java
index 73a61f8fb0b..d9bf8815c74 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzTenantPermit.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/AthenzTenantPermit.java
@@ -1,26 +1,46 @@
package com.yahoo.vespa.hosted.controller.permits;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.athenz.api.OktaAccessToken;
+import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
+import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import java.util.Objects;
+import java.security.Principal;
+import java.util.Optional;
+
+import static java.util.Objects.requireNonNull;
/**
- * Wraps the permit data of an Athenz tenancy modification.
+ * Wraps the permit data for creating an Athenz tenant.
*
* @author jonmv
*/
-public class AthenzTenantPermit implements TenantPermit {
+public class AthenzTenantPermit extends TenantPermit {
- private final AthenzDomain domain;
+ private final Optional<Property> property;
+ private final Optional<PropertyId> propertyId;
+ private final Optional<AthenzDomain> domain;
private final OktaAccessToken token;
- public AthenzTenantPermit(AthenzDomain domain, OktaAccessToken token) {
- this.domain = Objects.requireNonNull(domain);
- this.token = Objects.requireNonNull(token);
+ public AthenzTenantPermit(TenantName tenant, Principal user, Optional<AthenzDomain> domain,
+ Optional<Property> property, Optional<PropertyId> propertyId, OktaAccessToken token) {
+ super(tenant, user);
+ this.domain = requireNonNull(domain);
+ this.token = requireNonNull(token);
+ this.property = requireNonNull(property);
+ this.propertyId = requireNonNull(propertyId);
}
- public AthenzDomain domain() { return domain; }
- public OktaAccessToken token() { return token; }
+ /** The property name of the tenant to create. */
+ public Optional<Property> property() { return property; }
+
+ /** The ID of the property of the tenant to create. */
+ public Optional<PropertyId> propertyId() { return propertyId; }
+ /** The Athens domain of the concerned tenant. */
+ public Optional<AthenzDomain> domain() { return domain; }
+
+ /** The Okta issued token proving the user's access to Athenz. */
+ public OktaAccessToken token() { return token; }
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/OktaApplicationPermit.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/CloudApplicationPermit.java
index 633d1dfb393..a9d63c418f3 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/OktaApplicationPermit.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/CloudApplicationPermit.java
@@ -3,19 +3,20 @@ package com.yahoo.vespa.hosted.controller.permits;
import com.yahoo.config.provision.ApplicationId;
import java.security.Principal;
-import java.util.Objects;
+
+import static java.util.Objects.requireNonNull;
/**
* Wraps the permit data of an Okta application modification.
*/
-public class OktaApplicationPermit {
+public class CloudApplicationPermit {
private final ApplicationId application;
private final Principal user;
- public OktaApplicationPermit(ApplicationId application, Principal user) {
- this.application = Objects.requireNonNull(application);
- this.user = Objects.requireNonNull(user);
+ public CloudApplicationPermit(ApplicationId application, Principal user) {
+ this.application = requireNonNull(application);
+ this.user = requireNonNull(user);
}
public ApplicationId application() { return application; }
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/CloudTenantPermit.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/CloudTenantPermit.java
new file mode 100644
index 00000000000..01b43397b13
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/CloudTenantPermit.java
@@ -0,0 +1,26 @@
+package com.yahoo.vespa.hosted.controller.permits;
+
+import com.yahoo.config.provision.TenantName;
+
+import java.security.Principal;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Wraps the permit data of an Okta tenancy modification.
+ *
+ * @author jonmv
+ */
+public class CloudTenantPermit extends TenantPermit {
+
+ private final String registrationToken;
+
+ public CloudTenantPermit(TenantName tenant, Principal user, String registrationToken) {
+ super(tenant, user);
+ this.registrationToken = requireNonNull(registrationToken);
+ }
+
+ /** The cloud issued token proving the user intends to register the given tenant. */
+ public String getRegistrationToken() { return registrationToken; }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/OktaTenantPermit.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/OktaTenantPermit.java
deleted file mode 100644
index 1501971cac6..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/OktaTenantPermit.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.yahoo.vespa.hosted.controller.permits;
-
-import com.yahoo.config.provision.TenantName;
-
-import java.security.Principal;
-import java.util.Objects;
-
-/**
- * Wraps the permit data of an Okta tenancy modification.
- *
- * @author jonmv
- */
-public class OktaTenantPermit implements TenantPermit {
-
- private final TenantName tenant;
- private final Principal user;
-
- public OktaTenantPermit(TenantName tenant, Principal user) {
- this.tenant = Objects.requireNonNull(tenant);
- this.user = Objects.requireNonNull(user);
- }
-
- public TenantName tenant() { return tenant; }
- public Principal user() { return user; }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/PermitExtractor.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/PermitExtractor.java
index 0ca92a1f57a..465e5b26216 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/PermitExtractor.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/PermitExtractor.java
@@ -1,5 +1,7 @@
package com.yahoo.vespa.hosted.controller.permits;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.TenantName;
import com.yahoo.container.jdisc.HttpRequest;
/**
@@ -10,9 +12,9 @@ import com.yahoo.container.jdisc.HttpRequest;
public interface PermitExtractor {
/** Extracts permit data for a tenant, from the given request. */
- TenantPermit getTenantPermit(HttpRequest request);
+ TenantPermit getTenantPermit(TenantName tenant, HttpRequest request);
/** Extracts permit data for an application, from the given request. */
- ApplicationPermit getApplication(HttpRequest request);
+ ApplicationPermit getApplicationPermit(ApplicationId application, HttpRequest request);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/PermitStore.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/PermitStore.java
index 78bc869d68d..bfc6ba47857 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/PermitStore.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/PermitStore.java
@@ -1,22 +1,62 @@
package com.yahoo.vespa.hosted.controller.permits;
+import com.yahoo.config.application.api.DeploymentSpec;
+import com.yahoo.vespa.athenz.api.AthenzDomain;
+import com.yahoo.vespa.athenz.api.AthenzIdentity;
+import com.yahoo.vespa.athenz.api.AthenzService;
+import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
+
+import java.security.Principal;
+import java.util.List;
+import java.util.Optional;
+
/**
- * Stores permits for accessing tenant and application resources.
+ * Keeps permits for tenant and application resources.
*
* @author jonmv
*/
public interface PermitStore {
- /** Creates a tenant with permissions given by the permit. */
- void createTenant(TenantPermit tenantPermit);
+ /**
+ * Sets up permissions for a tenant, based on the given permit, or throws.
+ *
+ * @param tenantPermit permit for the tenant to create
+ * @param existing list of existing tenants, to check for conflicts
+ * @param applications list of applications this tenant already owns
+ * @return the created tenant, for keeping
+ */
+ Tenant createTenant(TenantPermit tenantPermit, List<Tenant> existing, List<Application> applications);
- /** Deletes the tenant and all permissions related to it. */
- void deleteTenant(TenantPermit tenantPermit);
+ /**
+ * Removes all permissions for tenant in the given permit, and for any applications it owns, or throws.
+ *
+ * @param tenantPermit permit for the tenant to delete
+ * @param tenant the tenant to delete
+ * @param applications list of applications this tenant owns
+ */
+ void deleteTenant(TenantPermit tenantPermit, Tenant tenant, List<Application> applications);
- /** Creates an application resource with permissions given by the permit. */
+ /**
+ * Sets up permissions for an application, based on the given permit, or throws.
+ *
+ * @param applicationPermit permit for the application to create
+ */
void createApplication(ApplicationPermit applicationPermit);
- /** Deletes the application and all permissions related to it. */
+ /**
+ * Removes permissions for the application in the given permit, or throws.
+ *
+ * @param applicationPermit permit for the application to delete
+ */
void deleteApplication(ApplicationPermit applicationPermit);
+ /**
+ * Returns the list of tenants to which this principal has access.
+ * @param tenants the list of all known tenants
+ * @param principal the user whose tenants to return
+ * @return the list of tenants the given user has access to
+ */
+ List<Tenant> accessibleTenants(List<Tenant> tenants, Principal principal);
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/TenantPermit.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/TenantPermit.java
index fa821814b45..4f92b75d669 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/TenantPermit.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/permits/TenantPermit.java
@@ -1,10 +1,30 @@
package com.yahoo.vespa.hosted.controller.permits;
+import com.yahoo.config.provision.TenantName;
+
+import java.security.Principal;
+
+import static java.util.Objects.requireNonNull;
+
/**
* Data that relates identities to permissions to a tenant.
*
* @author jonmv
*/
-public interface TenantPermit {
+public abstract class TenantPermit {
+
+ private final TenantName tenant;
+ private final Principal user;
+
+ protected TenantPermit(TenantName tenant, Principal user) {
+ this.user = requireNonNull(user);
+ this.tenant = requireNonNull(tenant);
+ }
+
+ /** The tenant this permit concerns. */
+ public TenantName tenant() { return tenant; }
+
+ /** The user handling this permit. */
+ public Principal user() { return user; }
}
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 37c0847f167..965827fb9e4 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
@@ -21,7 +21,6 @@ import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
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.AthenzUser;
import com.yahoo.vespa.athenz.api.OktaAccessToken;
@@ -30,7 +29,6 @@ import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.AlreadyExistsException;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
import com.yahoo.vespa.hosted.controller.NotExistsException;
import com.yahoo.vespa.hosted.controller.api.ActivateResult;
import com.yahoo.vespa.hosted.controller.api.application.v4.ApplicationResource;
@@ -47,7 +45,6 @@ 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.TenantId;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Log;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Logs;
@@ -70,9 +67,10 @@ import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.RotationStatus;
import com.yahoo.vespa.hosted.controller.application.RoutingPolicy;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.athenz.impl.ZmsClientFacade;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel;
+import com.yahoo.vespa.hosted.controller.permits.ApplicationPermit;
+import com.yahoo.vespa.hosted.controller.permits.PermitExtractor;
import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse;
import com.yahoo.vespa.hosted.controller.restapi.MessageResponse;
import com.yahoo.vespa.hosted.controller.restapi.ResourceResponse;
@@ -117,15 +115,15 @@ import static java.util.stream.Collectors.joining;
public class ApplicationApiHandler extends LoggingRequestHandler {
private final Controller controller;
- private final ZmsClientFacade zmsClient;
+ private final PermitExtractor permits;
@Inject
public ApplicationApiHandler(LoggingRequestHandler.Context parentCtx,
Controller controller,
- AthenzClientFactory athenzClientFactory) {
+ PermitExtractor permits) {
super(parentCtx);
this.controller = controller;
- this.zmsClient = new ZmsClientFacade(athenzClientFactory.createZmsClient(), athenzClientFactory.getControllerIdentity());
+ this.permits = permits;
}
@Override
@@ -254,7 +252,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Slime slime = new Slime();
Cursor tenantArray = slime.setArray();
for (Tenant tenant : controller.tenants().asList())
- toSlime(tenantArray.addObject(), tenant, request, true);
+ toSlime(tenantArray.addObject(), tenant, request);
return new SlimeJsonResponse(slime);
}
@@ -265,23 +263,20 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse authenticatedUser(HttpRequest request) {
- String userIdString = request.getProperty("userOverride");
- if (userIdString == null)
- userIdString = getUserId(request)
- .map(UserId::id)
- .orElseThrow(() -> new ForbiddenException("You must be authenticated or specify userOverride"));
- UserId userId = new UserId(userIdString);
+ AthenzPrincipal user = getUserPrincipal(request);
+ if (user == null)
+ throw new NotAuthorizedException("You must be authenticated.");
- List<Tenant> tenants = controller.tenants().asList(userId);
+ List<Tenant> tenants = controller.tenants().asList(user);
Slime slime = new Slime();
Cursor response = slime.setObject();
- response.setString("user", userId.id());
+ response.setString("user", user.getIdentity().getName());
Cursor tenantsArray = response.setArray("tenants");
for (Tenant tenant : tenants)
tenantInTenantsListToSlime(tenant, request.getUri(), tenantsArray.addObject());
- response.setBool("tenantExists", tenants.stream().anyMatch(tenant -> tenant instanceof UserTenant &&
- ((UserTenant) tenant).is(userId.id())));
+ response.setBool("tenantExists", tenants.stream().anyMatch(tenant -> tenant instanceof UserTenant && // TODO jvenstad: No.
+ ((UserTenant) tenant).is(user.getName())));
return new SlimeJsonResponse(slime);
}
@@ -335,13 +330,13 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse tenant(String tenantName, HttpRequest request) {
return controller.tenants().get(TenantName.from(tenantName))
- .map(tenant -> tenant(tenant, request, true))
+ .map(tenant -> tenant(tenant, request))
.orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist"));
}
- private HttpResponse tenant(Tenant tenant, HttpRequest request, boolean listApplications) {
+ private HttpResponse tenant(Tenant tenant, HttpRequest request) {
Slime slime = new Slime();
- toSlime(slime.setObject(), tenant, request, listApplications);
+ toSlime(slime.setObject(), tenant, request);
return new SlimeJsonResponse(slime);
}
@@ -738,11 +733,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse createUser(HttpRequest request) {
Optional<UserId> user = getUserId(request);
- if ( ! user.isPresent() ) throw new ForbiddenException("Not authenticated or not an user.");
+ if ( ! user.isPresent()) throw new ForbiddenException("Not authenticated or not a user.");
String username = UserTenant.normalizeUser(user.get().id());
+ UserTenant tenant = UserTenant.create(username);
try {
- controller.tenants().create(UserTenant.create(username));
+ controller.tenants().createUser(tenant);
return new MessageResponse("Created user '" + username + "'");
} catch (AlreadyExistsException e) {
// Ok
@@ -751,45 +747,26 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse updateTenant(String tenantName, HttpRequest request) {
- Optional<AthenzTenant> tenant = controller.tenants().athenzTenant(TenantName.from(tenantName));
- if ( ! tenant.isPresent()) return ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist");
-
- Inspector requestData = toSlime(request.getData()).get();
- OktaAccessToken token = requireOktaAccessToken(request, "Could not update " + tenantName);
-
- controller.tenants().lockOrThrow(tenant.get().name(), LockedTenant.Athenz.class, lockedTenant -> {
- lockedTenant = lockedTenant.with(new Property(mandatory("property", requestData).asString()));
- lockedTenant = controller.tenants().withDomain(
- lockedTenant,
- new AthenzDomain(mandatory("athensDomain", requestData).asString()),
- token
- );
- Optional<PropertyId> propertyId = optional("propertyId", requestData).map(PropertyId::new);
- if (propertyId.isPresent()) {
- lockedTenant = lockedTenant.with(propertyId.get());
- }
- controller.tenants().store(lockedTenant);
- });
-
- return tenant(controller.tenants().requireAthenzTenant(tenant.get().name()), request, true);
+ getTenantOrThrow(tenantName);
+ controller.tenants().update(permits.getTenantPermit(TenantName.from(tenantName), request));
+ return tenant(controller.tenants().require(TenantName.from(tenantName)), request);
}
private HttpResponse createTenant(String tenantName, HttpRequest request) {
- Inspector requestData = toSlime(request.getData()).get();
-
- AthenzTenant tenant = AthenzTenant.create(TenantName.from(tenantName),
- new AthenzDomain(mandatory("athensDomain", requestData).asString()),
- new Property(mandatory("property", requestData).asString()),
- optional("propertyId", requestData).map(PropertyId::new));
- throwIfNotAthenzDomainAdmin(tenant.domain(), request);
- controller.tenants().create(tenant, requireOktaAccessToken(request, "Could not create " + tenantName));
- return tenant(tenant, request, true);
+ controller.tenants().create(permits.getTenantPermit(TenantName.from(tenantName), request));
+ return tenant(controller.tenants().require(TenantName.from(tenantName)), request);
}
private HttpResponse createApplication(String tenantName, String applicationName, HttpRequest request) {
- Application application;
+ ApplicationId id = ApplicationId.from(tenantName, applicationName, "default");
try {
- application = controller.applications().createApplication(ApplicationId.from(tenantName, applicationName, "default"), getOktaAccessToken(request));
+ Optional<ApplicationPermit> permit = controller.tenants().require(id.tenant()).type() != Tenant.Type.user
+ ? Optional.of(permits.getApplicationPermit(id, request)) : Optional.empty();
+ Application application = controller.applications().createApplication(id, permit);
+
+ Slime slime = new Slime();
+ toSlime(application, slime.setObject(), request);
+ return new SlimeJsonResponse(slime);
}
catch (ZmsClientException e) { // TODO: Push conversion down
if (e.getErrorCode() == com.yahoo.jdisc.Response.Status.FORBIDDEN)
@@ -797,10 +774,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
else
throw e;
}
-
- Slime slime = new Slime();
- toSlime(application, slime.setObject(), request);
- return new SlimeJsonResponse(slime);
}
/** Trigger deployment of the given Vespa version if a valid one is given, e.g., "7.8.9". */
@@ -897,7 +870,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
* this might be handy later to handle emergency downgrades.
*/
boolean isZoneApplication = SystemApplication.zone.id().equals(applicationId);
- if (isZoneApplication) {
+ if (isZoneApplication) { // TODO jvenstad: Separate out.
// Make it explicit that version is not yet supported here
String versionStr = deployOptions.field("vespaVersion").asString();
boolean versionPresent = !versionStr.isEmpty() && !versionStr.equals("null");
@@ -972,7 +945,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
deployOptions.field("ignoreValidationErrors").asBool(),
deployOptions.field("deployCurrentVersion").asBool());
-
ActivateResult result = controller.applications().deploy(applicationId,
zone,
applicationPackage,
@@ -985,26 +957,23 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse deleteTenant(String tenantName, HttpRequest request) {
Optional<Tenant> tenant = controller.tenants().get(tenantName);
- if ( ! tenant.isPresent()) return ErrorResponse.notFoundError("Could not delete tenant '" + tenantName + "': Tenant not found"); // NOTE: The Jersey implementation would silently ignore this
-
+ if ( ! tenant.isPresent())
+ return ErrorResponse.notFoundError("Could not delete tenant '" + tenantName + "': Tenant not found");
- if (tenant.get() instanceof AthenzTenant) {
- controller.tenants().deleteTenant((AthenzTenant) tenant.get(),
- requireOktaAccessToken(request, "Could not delete " + tenantName));
- } else if (tenant.get() instanceof UserTenant) {
- controller.tenants().deleteTenant((UserTenant) tenant.get());
- } else {
- throw new IllegalArgumentException("Unknown tenant type:" + tenant.get().getClass().getSimpleName() +
- ", for " + tenant.get());
- }
+ if (tenant.get().type() == Tenant.Type.user)
+ controller.tenants().deleteUser((UserTenant) tenant.get());
+ else
+ controller.tenants().delete(permits.getTenantPermit(tenant.get().name(), request));
// TODO: Change to a message response saying the tenant was deleted
- return tenant(tenant.get(), request, false);
+ return tenant(tenant.get(), request);
}
private HttpResponse deleteApplication(String tenantName, String applicationName, HttpRequest request) {
ApplicationId id = ApplicationId.from(tenantName, applicationName, "default");
- controller.applications().deleteApplication(id, getOktaAccessToken(request));
+ Optional<ApplicationPermit> permit = controller.tenants().require(id.tenant()).type() != Tenant.Type.user
+ ? Optional.of(permits.getApplicationPermit(id, request)) : Optional.empty();
+ controller.applications().deleteApplication(id, permit);
return new EmptyJsonResponse(); // TODO: Replicates current behavior but should return a message response instead
}
@@ -1104,7 +1073,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
.orElseThrow(() -> new NotExistsException(new TenantId(tenantName)));
}
- private void toSlime(Cursor object, Tenant tenant, HttpRequest request, boolean listApplications) {
+ private void toSlime(Cursor object, Tenant tenant, HttpRequest request) {
object.setString("tenant", tenant.name().value());
object.setString("type", tentantType(tenant));
if (tenant instanceof AthenzTenant) {
@@ -1114,14 +1083,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
athenzTenant.propertyId().ifPresent(id -> object.setString("propertyId", id.toString()));
}
Cursor applicationArray = object.setArray("applications");
- if (listApplications) { // This cludge is needed because we call this after deleting the tenant. As this call makes another tenant lookup it will fail. TODO is to support lookup on tenant
- for (Application application : controller.applications().asList(tenant.name())) {
- if (application.id().instance().isDefault()) {// TODO: Skip non-default applications until supported properly
- if (recurseOverApplications(request))
- toSlime(applicationArray.addObject(), application, request);
- else
- toSlime(application, applicationArray.addObject(), request);
- }
+ for (Application application : controller.applications().asList(tenant.name())) {
+ if (application.id().instance().isDefault()) {// TODO: Skip non-default applications until supported properly
+ if (recurseOverApplications(request))
+ toSlime(applicationArray.addObject(), application, request);
+ else
+ toSlime(application, applicationArray.addObject(), request);
}
}
if (tenant instanceof AthenzTenant) {
@@ -1190,15 +1157,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
}
- private void throwIfNotAthenzDomainAdmin(AthenzDomain tenantDomain, HttpRequest request) {
- AthenzIdentity identity = getUserPrincipal(request).getIdentity();
- boolean isDomainAdmin = zmsClient.isDomainAdmin(identity, tenantDomain);
- if ( ! isDomainAdmin) {
- throw new ForbiddenException(
- String.format("The user '%s' is not admin in Athenz domain '%s'", identity.getFullName(), tenantDomain.getName()));
- }
- }
-
private static Optional<UserId> getUserId(HttpRequest request) {
return Optional.of(getUserPrincipal(request))
.map(AthenzPrincipal::getIdentity)
@@ -1208,7 +1166,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
.map(UserId::new);
}
- private static AthenzPrincipal getUserPrincipal(HttpRequest request) {
+ private static AthenzPrincipal getUserPrincipal(HttpRequest request) { // TODO jvenstad: Not necessarily Athenz ...
Principal principal = request.getJDiscRequest().getUserPrincipal();
if (principal == null) throw new InternalServerErrorException("Expected a user principal");
if (!(principal instanceof AthenzPrincipal))
@@ -1376,16 +1334,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
throw new IllegalArgumentException("Unknown tenant type: " + tenant.getClass().getSimpleName());
}
- private static OktaAccessToken requireOktaAccessToken(HttpRequest request, String message) {
- return getOktaAccessToken(request)
- .orElseThrow(() -> new IllegalArgumentException(message + ": No Okta Access Token provided"));
- }
-
- private static Optional<OktaAccessToken> getOktaAccessToken(HttpRequest request) {
- return Optional.ofNullable(request.getJDiscRequest().context().get("okta.access-token"))
- .map(attribute -> new OktaAccessToken((String) attribute));
- }
-
private static ApplicationId appIdFromPath(Path path) {
return ApplicationId.from(path.get("tenant"), path.get("application"), path.get("instance"));
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java
index 3d0e21617c5..643bf462f13 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java
@@ -19,7 +19,7 @@ import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.TenantController;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
import com.yahoo.vespa.hosted.controller.athenz.ApplicationAction;
-import com.yahoo.vespa.hosted.controller.athenz.impl.ZmsClientFacade;
+import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
@@ -55,7 +55,7 @@ public class ControllerAuthorizationFilter extends CorsRequestFilterBase {
private static final Logger log = Logger.getLogger(ControllerAuthorizationFilter.class.getName());
- private final ZmsClientFacade zmsClient;
+ private final AthenzFacade athenz;
private final TenantController tenantController;
@Inject
@@ -63,7 +63,7 @@ public class ControllerAuthorizationFilter extends CorsRequestFilterBase {
Controller controller,
CorsFilterConfig corsConfig) {
super(corsConfig);
- this.zmsClient = new ZmsClientFacade(clientFactory.createZmsClient(), clientFactory.getControllerIdentity());
+ this.athenz = new AthenzFacade(clientFactory);
this.tenantController = controller.tenants();
}
@@ -71,7 +71,7 @@ public class ControllerAuthorizationFilter extends CorsRequestFilterBase {
TenantController tenantController,
Set<String> allowedUrls) {
super(allowedUrls);
- this.zmsClient = new ZmsClientFacade(clientFactory.createZmsClient(), clientFactory.getControllerIdentity());;
+ this.athenz = new AthenzFacade(clientFactory);;
this.tenantController = tenantController;
}
@@ -154,7 +154,7 @@ public class ControllerAuthorizationFilter extends CorsRequestFilterBase {
}
private boolean isHostedOperator(AthenzIdentity identity) {
- return zmsClient.hasHostedOperatorAccess(identity);
+ return athenz.hasHostedOperatorAccess(identity);
}
private void verifyIsTenantAdmin(AthenzPrincipal principal, TenantName name) {
@@ -168,7 +168,7 @@ public class ControllerAuthorizationFilter extends CorsRequestFilterBase {
private boolean isTenantAdmin(AthenzIdentity identity, Tenant tenant) {
if (tenant instanceof AthenzTenant) {
- return zmsClient.hasTenantAdminAccess(identity, ((AthenzTenant) tenant).domain());
+ return athenz.hasTenantAdminAccess(identity, ((AthenzTenant) tenant).domain());
} else if (tenant instanceof UserTenant) {
if (!(identity instanceof AthenzUser)) {
return false;
@@ -211,12 +211,12 @@ public class ControllerAuthorizationFilter extends CorsRequestFilterBase {
private boolean hasDeployerAccess(AthenzIdentity identity, AthenzDomain tenantDomain, ApplicationName application) {
try {
- return zmsClient
+ return athenz
.hasApplicationAccess(
identity,
ApplicationAction.deploy,
tenantDomain,
- new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(application.value()));
+ application);
} catch (ZmsClientException e) {
throw new InternalServerErrorException("Failed to authorize operation: (" + e.getMessage() + ")", e);
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
index cf5f8fac69d..38f26427558 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
@@ -12,6 +12,7 @@ import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.athenz.api.OktaAccessToken;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
@@ -29,6 +30,7 @@ import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.BuildJob;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
+import com.yahoo.vespa.hosted.controller.permits.AthenzApplicationPermit;
import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import com.yahoo.vespa.hosted.controller.rotation.RotationLock;
import org.junit.Test;
@@ -348,7 +350,7 @@ public class ControllerTest {
tester.deployAndNotify(app1, applicationPackage, true, systemTest);
tester.applications().deactivate(app1.id(), ZoneId.from(Environment.test, RegionName.from("us-east-1")));
tester.applications().deactivate(app1.id(), ZoneId.from(Environment.staging, RegionName.from("us-east-3")));
- tester.applications().deleteApplication(app1.id(), Optional.of(new OktaAccessToken("okta-token")));
+ tester.applications().deleteApplication(app1.id(), tester.controllerTester().permitFor(app1.id()));
try (RotationLock lock = tester.applications().rotationRepository().lock()) {
assertTrue("Rotation is unassigned",
tester.applications().rotationRepository().availableRotations(lock)
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 e573c12af3b..c7fc4732368 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
@@ -8,6 +8,8 @@ import com.yahoo.config.provision.TenantName;
import com.yahoo.slime.Slime;
import com.yahoo.test.ManualClock;
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.athenz.api.OktaAccessToken;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.curator.mock.MockCurator;
@@ -41,6 +43,9 @@ import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock;
import com.yahoo.vespa.hosted.controller.integration.MetricsServiceMock;
import com.yahoo.vespa.hosted.controller.integration.RoutingGeneratorMock;
import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock;
+import com.yahoo.vespa.hosted.controller.permits.ApplicationPermit;
+import com.yahoo.vespa.hosted.controller.permits.AthenzApplicationPermit;
+import com.yahoo.vespa.hosted.controller.permits.AthenzTenantPermit;
import com.yahoo.vespa.hosted.controller.persistence.ApplicationSerializer;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
@@ -241,21 +246,33 @@ public final class ControllerTester {
}
}
- public AthenzDomain createDomain(String domainName) {
+ public AthenzDomain createDomainWithAdmin(String domainName, AthenzUser user) {
AthenzDomain domain = new AthenzDomain(domainName);
athenzDb.addDomain(new AthenzDbMock.Domain(domain));
+ athenzDb.domains.get(domain).admin(user);
return domain;
}
+ public Optional<AthenzDomain> domainOf(ApplicationId id) {
+ Tenant tenant = controller().tenants().require(id.tenant());
+ return tenant.type() == Tenant.Type.athenz ? Optional.of(((AthenzTenant) tenant).domain()) : Optional.empty();
+ }
+
public TenantName createTenant(String tenantName, String domainName, Long propertyId, Optional<Contact> contact) {
TenantName name = TenantName.from(tenantName);
Optional<Tenant> existing = controller().tenants().get(name);
if (existing.isPresent()) return name;
- AthenzTenant tenant = AthenzTenant.create(name, createDomain(domainName), new Property("Property"+propertyId),
- Optional.ofNullable(propertyId)
- .map(Object::toString)
- .map(PropertyId::new), contact);
- controller().tenants().create(tenant, new OktaAccessToken("okta-token"));
+ AthenzUser user = new AthenzUser("user");
+ AthenzTenantPermit permit = new AthenzTenantPermit(name,
+ new AthenzPrincipal(user),
+ Optional.of(createDomainWithAdmin(domainName, user)),
+ Optional.of(new Property("Property" + propertyId)),
+ Optional.ofNullable(propertyId).map(Object::toString).map(PropertyId::new),
+ new OktaAccessToken("okta-token"));
+ controller().tenants().create(permit);
+ if (contact.isPresent())
+ controller().tenants().lockOrThrow(name, LockedTenant.Athenz.class, tenant ->
+ controller().tenants().store(tenant.with(contact.get())));
assertNotNull(controller().tenants().get(name));
return name;
}
@@ -264,14 +281,22 @@ public final class ControllerTester {
return createTenant(tenantName, domainName, propertyId, Optional.empty());
}
+ public Optional<ApplicationPermit> permitFor(ApplicationId id) {
+ return domainOf(id).map(domain -> new AthenzApplicationPermit(id, domain, new OktaAccessToken("okta-token")));
+ }
+
public Application createApplication(TenantName tenant, String applicationName, String instanceName, long projectId) {
ApplicationId applicationId = ApplicationId.from(tenant.value(), applicationName, instanceName);
- controller().applications().createApplication(applicationId, Optional.of(new OktaAccessToken("okta-token")));
+ controller().applications().createApplication(applicationId, permitFor(applicationId));
controller().applications().lockOrThrow(applicationId, lockedApplication ->
controller().applications().store(lockedApplication.withProjectId(OptionalLong.of(projectId))));
return controller().applications().require(applicationId);
}
+ public void deleteApplication(ApplicationId id) {
+ controller().applications().deleteApplication(id, permitFor(id));
+ }
+
public void deploy(Application application, ZoneId zone) {
deploy(application, zone, new ApplicationPackage(new byte[0]));
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/ZipStreamReaderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/ZipStreamReaderTest.java
index fa78ce7bb12..fe5680e2a58 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/ZipStreamReaderTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/ZipStreamReaderTest.java
@@ -8,7 +8,6 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Path;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -75,13 +74,4 @@ public class ZipStreamReaderTest {
return zip.toByteArray();
}
- @Test
- public void lul() {
- String name = "./artif/../yolo/../../hi/";
- Path path = Path.of(name);
- System.err.println(name);
- System.err.println(path);
- System.err.println(path.normalize());
- }
-
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java
index 9eac6e61b99..c84f8ed7c58 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java
@@ -6,6 +6,7 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.TenantName;
import com.yahoo.test.ManualClock;
+import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ApplicationController;
import com.yahoo.vespa.hosted.controller.Controller;
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java
index 75c287e700f..1ca8f7ba2b4 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java
@@ -46,7 +46,7 @@ public class ApplicationOwnershipConfirmerTest {
Supplier<Application> propertyApp = () -> tester.controller().applications().require(ApplicationId.from("property", "application", "default"));
UserTenant user = UserTenant.create("by-user", contact);
- tester.controller().tenants().create(user);
+ tester.controller().tenants().createUser(user);
tester.createAndDeploy(user.name(), "application", 2, "default");
Supplier<Application> userApp = () -> tester.controller().applications().require(ApplicationId.from("by-user", "application", "default"));
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java
index 578e7824913..23c7ec537f5 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DnsMaintainerTest.java
@@ -100,7 +100,7 @@ public class DnsMaintainerTest {
tester.deployAndNotify(application, applicationPackage, true, systemTest);
tester.applications().deactivate(application.id(), ZoneId.from(Environment.test, RegionName.from("us-east-1")));
tester.applications().deactivate(application.id(), ZoneId.from(Environment.staging, RegionName.from("us-east-3")));
- tester.applications().deleteApplication(application.id(), Optional.of(new OktaAccessToken("okta-token")));
+ tester.controllerTester().deleteApplication(application.id());
// DnsMaintainer removes records
for (int i = 0; i < ControllerTester.availableRotations; i++) {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java
index 2539687ea4d..843a4cfedd6 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java
@@ -212,7 +212,7 @@ public class JobRunnerTest {
// Thread is still trying to deploy tester -- delete application, and see all data is garbage collected.
assertEquals(Collections.singletonList(runId), jobs.active().stream().map(run -> run.id()).collect(Collectors.toList()));
- tester.controller().applications().deleteApplication(id, Optional.of(new OktaAccessToken("okta-token")));
+ tester.controllerTester().deleteApplication(id);
assertEquals(Collections.emptyList(), jobs.active());
assertEquals(runId, jobs.last(id, systemTest).get().id());
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 11e8a82dd42..e9136ad3adf 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
@@ -6,6 +6,8 @@ import com.yahoo.application.container.handler.Request;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.TenantName;
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.athenz.api.OktaAccessToken;
import com.yahoo.vespa.athenz.utils.AthenzIdentities;
import com.yahoo.vespa.hosted.controller.Application;
@@ -27,9 +29,10 @@ import com.yahoo.vespa.hosted.controller.deployment.BuildJob;
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.permits.AthenzApplicationPermit;
+import com.yahoo.vespa.hosted.controller.permits.AthenzTenantPermit;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
import java.io.File;
import java.time.Duration;
@@ -73,13 +76,20 @@ public class ContainerControllerTester {
}
public Application createApplication(String athensDomain, String tenant, String application) {
- AthenzDomain domain1 = addTenantAthenzDomain(athensDomain, "mytenant");
- controller().tenants().create(AthenzTenant.create(TenantName.from(tenant), domain1,
- new Property("property1"),
- Optional.of(new PropertyId("1234"))),
- new OktaAccessToken("okta-token"));
+ AthenzDomain domain1 = addTenantAthenzDomain(athensDomain, "user");
+ AthenzTenantPermit tenantPermit = new AthenzTenantPermit(TenantName.from(tenant),
+ new AthenzPrincipal(new AthenzUser("user")),
+ Optional.of(domain1),
+ Optional.of(new Property("property1")),
+ Optional.of(new PropertyId("1234")),
+ new OktaAccessToken("okta-token"));
+ controller().tenants().create(tenantPermit);
+
ApplicationId app = ApplicationId.from(tenant, application, "default");
- return controller().applications().createApplication(app, Optional.of(new OktaAccessToken("okta-token")));
+ AthenzApplicationPermit applicationPermit = new AthenzApplicationPermit(app,
+ domain1,
+ new OktaAccessToken("okta-token"));
+ return controller().applications().createApplication(app, Optional.of(applicationPermit));
}
public Application deploy(Application application, ApplicationPackage applicationPackage, ZoneId zone) {
@@ -132,7 +142,7 @@ public class ContainerControllerTester {
AthenzDomain athensDomain = new AthenzDomain(domainName);
AthenzDbMock.Domain domain = new AthenzDbMock.Domain(athensDomain);
domain.markAsVespaTenant();
- domain.admin(AthenzIdentities.from(new AthenzDomain("domain"), userName));
+ domain.admin(AthenzIdentities.from(new AthenzDomain("user"), userName));
mock.getSetup().addDomain(domain);
return athensDomain;
}
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 f051818a12f..b9a59a34664 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
@@ -93,6 +93,7 @@ public class ControllerContainerTest {
" <component id='com.yahoo.vespa.hosted.controller.integration.ApplicationStoreMock'/>\n" +
" <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockTesterCloud'/>\n" +
" <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer'/>\n" +
+ " <component id='com.yahoo.vespa.hosted.controller.permits.AthenzPermitExtractor'/>\n" +
" <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>\n" +
" <binding>http://*/application/v4/*</binding>\n" +
" </handler>\n" +
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 705fc8adbac..e077ad0c1c9 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
@@ -56,6 +56,8 @@ import com.yahoo.vespa.hosted.controller.deployment.BuildJob;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock;
import com.yahoo.vespa.hosted.controller.integration.MetricsServiceMock;
+import com.yahoo.vespa.hosted.controller.permits.AthenzTenantPermit;
+import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester;
import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
@@ -473,7 +475,9 @@ public class ApplicationApiTest extends ControllerContainerTest {
new File("service.json"));
// DELETE application with active deployments fails
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE).userIdentity(USER_ID),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE)
+ .userIdentity(USER_ID)
+ .oktaAccessToken(OKTA_AT),
new File("delete-with-active-deployments.json"), 400);
// DELETE (deactivate) a deployment - dev
@@ -806,6 +810,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
// PUT (update) non-existing tenant
tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
.userIdentity(USER_ID)
+ .oktaAccessToken(OKTA_AT)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
"{\"error-code\":\"NOT_FOUND\",\"message\":\"Tenant 'tenant1' does not exist\"}",
404);
@@ -875,6 +880,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
// Create the same application again
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST)
+ .oktaAccessToken(OKTA_AT)
.userIdentity(USER_ID),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not create 'tenant1.application1': Application already exists\"}",
400);
@@ -924,6 +930,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
"");
// DELETE application again - should produce 404
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE)
+ .oktaAccessToken(OKTA_AT)
.userIdentity(USER_ID),
"{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not delete application 'tenant1.application1': Application not found\"}",
404);
@@ -945,9 +952,8 @@ public class ApplicationApiTest extends ControllerContainerTest {
500);
// Create legancy tenant name containing underscores
- tester.controller().tenants().create(new AthenzTenant(TenantName.from("my_tenant"), ATHENZ_TENANT_DOMAIN,
- new Property("property1"), Optional.empty(), Optional.empty()),
- OKTA_AT);
+ tester.controller().curator().writeTenant(new AthenzTenant(TenantName.from("my_tenant"), ATHENZ_TENANT_DOMAIN,
+ new Property("property1"), Optional.empty(), Optional.empty()));
// POST (add) a Athenz tenant with dashes duplicates existing one with underscores
tester.assertResponse(request("/application/v4/tenant/my-tenant", POST)
.userIdentity(USER_ID)
@@ -980,6 +986,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
// Creating a tenant for an Athens domain the user is not admin for is disallowed
tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
.data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
+ .oktaAccessToken(OKTA_AT)
.userIdentity(unauthorizedUser),
"{\"error-code\":\"FORBIDDEN\",\"message\":\"The user 'user.othertenant' is not admin in Athenz domain 'domain1'\"}",
403);