diff options
Diffstat (limited to 'controller-server')
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); |