diff options
author | Bjørn Christian Seime <bjorn.christian@seime.no> | 2018-02-08 10:04:25 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-02-08 10:04:25 +0100 |
commit | 6aadb56c082d1cb98f69152afb7c313ef214ce46 (patch) | |
tree | be26153d81baee379a76a7499c1f4920e6b39ef4 | |
parent | e4d79a49bf765fc17ff3427fa7463f74fe0028dc (diff) | |
parent | f3c392b3ea9551077c4e6f7c63cad9f077e90f2d (diff) |
Merge pull request #4945 from vespa-engine/bjorncs/controller-authorization-filter
Bjorncs/controller authorization filter
7 files changed, 155 insertions, 104 deletions
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 2105ea1d3d9..6fc65253da3 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 @@ -10,7 +10,6 @@ import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.TenantName; -import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; @@ -19,6 +18,10 @@ import com.yahoo.log.LogLevel; 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.AthenzUser; +import com.yahoo.vespa.athenz.api.NToken; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.controller.AlreadyExistsException; import com.yahoo.vespa.hosted.controller.Application; @@ -36,7 +39,6 @@ import com.yahoo.vespa.hosted.controller.api.application.v4.model.ScrewdriverBui import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.RefeedAction; import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.RestartAction; import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.ServiceInfo; -import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.identifiers.GitBranch; import com.yahoo.vespa.hosted.controller.api.identifiers.GitCommit; @@ -48,10 +50,13 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId; import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId; import com.yahoo.vespa.hosted.controller.api.identifiers.UserGroup; 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.athenz.ZmsException; 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.organization.User; import com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.ApplicationVersion; import com.yahoo.vespa.hosted.controller.application.Change; @@ -62,12 +67,6 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentCost; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.SourceRevision; -import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory; -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.NToken; -import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsException; import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse; import com.yahoo.vespa.hosted.controller.restapi.MessageResponse; import com.yahoo.vespa.hosted.controller.restapi.Path; @@ -85,7 +84,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; -import java.security.Principal; import java.time.Duration; import java.util.Collections; import java.util.List; @@ -107,6 +105,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private final Controller controller; private final Authorizer authorizer; private final AthenzClientFactory athenzClientFactory; + private final ApplicationInstanceAuthorizer applicationInstanceAuthorizer; @Inject public ApplicationApiHandler(LoggingRequestHandler.Context parentCtx, @@ -116,6 +115,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { this.controller = controller; this.authorizer = authorizer; this.athenzClientFactory = athenzClientFactory; + this.applicationInstanceAuthorizer = new ApplicationInstanceAuthorizer(controller.zoneRegistry(), athenzClientFactory); } @Override @@ -192,13 +192,13 @@ public class ApplicationApiHandler extends LoggingRequestHandler { Path path = new Path(request.getUri().getPath()); if (path.matches("/application/v4/tenant/{tenant}")) return createTenant(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return createApplication(path.get("tenant"), path.get("application"), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/promote")) return promoteApplication(path.get("tenant"), path.get("application")); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/promote")) return promoteApplication(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return deploy(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/deploy")) return deploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); // legacy synonym of the above if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/restart")) return restart(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/log")) return log(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/promote")) return promoteApplicationDeployment(path.get("tenant"), path.get("application"), path.get("environment"), path.get("region")); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/log")) return log(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/promote")) return promoteApplicationDeployment(path.get("tenant"), path.get("application"), path.get("environment"), path.get("region"), path.get("instance"), request); return ErrorResponse.notFoundError("Nothing at " + path); } @@ -207,7 +207,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}")) return deleteTenant(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return deleteApplication(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application")); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deactivate(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region")); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deactivate(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), true, request); return ErrorResponse.notFoundError("Nothing at " + path); @@ -238,7 +238,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private HttpResponse authenticatedUser(HttpRequest request) { String userIdString = request.getProperty("userOverride"); if (userIdString == null) - userIdString = userFrom(request) + userIdString = authorizer.getUserId(request) .map(UserId::id) .orElseThrow(() -> new ForbiddenException("You must be authenticated or specify userOverride")); UserId userId = new UserId(userIdString); @@ -594,8 +594,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } private HttpResponse createUser(HttpRequest request) { - Optional<UserId> user = userFrom(request); - if ( ! user.isPresent() ) throw new ForbiddenException("Not authenticated."); + Optional<UserId> user = authorizer.getUserId(request); + if ( ! user.isPresent() ) throw new ForbiddenException("Not authenticated or not an user."); try { controller.tenants().createUserTenant(user.get().id()); @@ -700,6 +700,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } /** Trigger deployment of the last built application package, on a given version */ + // TODO Add authorization + // TODO Consider move to API for maintenance related operations private HttpResponse deploy(String tenantName, String applicationName, HttpRequest request) { Version version = decideDeployVersion(request); if ( ! systemHasVersion(version)) @@ -719,6 +721,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } /** Cancel any ongoing change for given application */ + // TODO Add authorization + // TODO Consider move to API for maintenance related operations private HttpResponse cancelDeploy(String tenantName, String applicationName) { ApplicationId id = ApplicationId.from(tenantName, applicationName, "default"); Application application = controller.applications().require(id); @@ -736,8 +740,14 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private HttpResponse restart(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) { DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), ZoneId.from(environment, region)); + // TODO: Propagate all filters Optional<Hostname> hostname = Optional.ofNullable(request.getProperty("hostname")).map(Hostname::new); + + applicationInstanceAuthorizer.throwIfUnauthorized(authorizer.getPrincipal(request), + Environment.from(environment), + getTenantOrThrow(tenantName), + deploymentId.applicationId().application()); controller.applications().restart(deploymentId, hostname); // TODO: Change to return JSON @@ -753,10 +763,15 @@ public class ApplicationApiHandler extends LoggingRequestHandler { * the application is working. It is called for all production zones, also those in which the application is not present, * and possibly before it is present, so failures are normal and expected. */ - private HttpResponse log(String tenantName, String applicationName, String instanceName, String environment, String region) { + private HttpResponse log(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) { try { DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), ZoneId.from(environment, region)); + + applicationInstanceAuthorizer.throwIfUnauthorized(authorizer.getPrincipal(request), + Environment.from(environment), + getTenantOrThrow(tenantName), + deploymentId.applicationId().application()); return new JacksonJsonResponse(controller.grabLog(deploymentId)); } catch (RuntimeException e) { @@ -778,10 +793,11 @@ public class ApplicationApiHandler extends LoggingRequestHandler { Optional<ApplicationPackage> applicationPackage = Optional.ofNullable(dataParts.get("applicationZip")) .map(ApplicationPackage::new); - DeployAuthorizer deployAuthorizer = new DeployAuthorizer(controller.zoneRegistry(), athenzClientFactory); - Tenant tenant = controller.tenants().tenant(new TenantId(tenantName)).orElseThrow(() -> new NotExistsException(new TenantId(tenantName))); - Principal principal = authorizer.getPrincipal(request); - deployAuthorizer.throwIfUnauthorizedForDeploy(principal, Environment.from(environment), tenant, applicationId, applicationPackage); + applicationInstanceAuthorizer.throwIfUnauthorizedForDeploy(authorizer.getPrincipal(request), + Environment.from(environment), + getTenantOrThrow(tenantName), + ApplicationName.from(applicationName), + applicationPackage); // TODO: get rid of the json object DeployOptions deployOptionsJsonClass = new DeployOptions(screwdriverBuildJobFromSlime(deployOptions.field("screwdriverBuildJob")), @@ -814,11 +830,17 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return new EmptyJsonResponse(); // TODO: Replicates current behavior but should return a message response instead } - private HttpResponse deactivate(String tenantName, String applicationName, String instanceName, String environment, String region) { + private HttpResponse deactivate(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) { Application application = controller.applications().require(ApplicationId.from(tenantName, applicationName, instanceName)); ZoneId zone = ZoneId.from(environment, region); Deployment deployment = application.deployments().get(zone); + + applicationInstanceAuthorizer.throwIfUnauthorized(authorizer.getPrincipal(request), + Environment.from(environment), + getTenantOrThrow(tenantName), + ApplicationName.from(applicationName)); + if (deployment == null) { // Attempt to deactivate application even if the deployment is not known by the controller controller.applications().deactivate(application, zone); @@ -837,8 +859,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler { /** * Promote application Chef environments. To be used by component jobs only */ - private HttpResponse promoteApplication(String tenantName, String applicationName) { + private HttpResponse promoteApplication(String tenantName, String applicationName, HttpRequest request) { try{ + applicationInstanceAuthorizer.throwIfUnauthorized(authorizer.getPrincipal(request), + getTenantOrThrow(tenantName), + ApplicationName.from(applicationName)); + ApplicationChefEnvironment chefEnvironment = new ApplicationChefEnvironment(controller.system()); String sourceEnvironment = chefEnvironment.systemChefEnvironment(); String targetEnvironment = chefEnvironment.applicationSourceEnvironment(TenantName.from(tenantName), ApplicationName.from(applicationName)); @@ -853,8 +879,13 @@ public class ApplicationApiHandler extends LoggingRequestHandler { /** * Promote application Chef environments for jobs that deploy applications */ - private HttpResponse promoteApplicationDeployment(String tenantName, String applicationName, String environmentName, String regionName) { + private HttpResponse promoteApplicationDeployment(String tenantName, String applicationName, String environmentName, String regionName, String instanceName, HttpRequest request) { try { + applicationInstanceAuthorizer.throwIfUnauthorized(authorizer.getPrincipal(request), + Environment.from(environmentName), + getTenantOrThrow(tenantName), + ApplicationName.from(applicationName)); + ApplicationChefEnvironment chefEnvironment = new ApplicationChefEnvironment(controller.system()); String sourceEnvironment = chefEnvironment.applicationSourceEnvironment(TenantName.from(tenantName), ApplicationName.from(applicationName)); String targetEnvironment = chefEnvironment.applicationTargetEnvironment(TenantName.from(tenantName), ApplicationName.from(applicationName), Environment.from(environmentName), RegionName.from(regionName)); @@ -866,13 +897,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } } - private Optional<UserId> userFrom(HttpRequest request) { - return authorizer.getPrincipalIfAny(request) - .map(AthenzPrincipal::getIdentity) - .filter(AthenzUser.class::isInstance) - .map(AthenzUser.class::cast) - .map(AthenzUser::getName) - .map(UserId::new); + private Tenant getTenantOrThrow(String tenantName) { + return controller.tenants().tenant(new TenantId(tenantName)) + .orElseThrow(() -> new NotExistsException(new TenantId(tenantName))); } private void toSlime(Cursor object, Tenant tenant, HttpRequest request, boolean listApplications) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/DeployAuthorizer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationInstanceAuthorizer.java index af519439600..283d700c2bd 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/DeployAuthorizer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationInstanceAuthorizer.java @@ -2,7 +2,7 @@ package com.yahoo.vespa.hosted.controller.restapi.application; import com.yahoo.config.application.api.DeploymentSpec; -import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.Environment; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzPrincipal; @@ -16,7 +16,6 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import javax.ws.rs.ForbiddenException; import javax.ws.rs.NotAuthorizedException; -import java.security.Principal; import java.util.Objects; import java.util.Optional; import java.util.logging.Logger; @@ -25,56 +24,27 @@ import static com.yahoo.vespa.hosted.controller.api.integration.athenz.HostedAth import static com.yahoo.vespa.hosted.controller.restapi.application.Authorizer.environmentRequiresAuthorization; /** + * Validates that principal is allowed to perform a mutating operation on an application instance. + * * @author bjorncs * @author gjoranv */ -public class DeployAuthorizer { +public class ApplicationInstanceAuthorizer { - private static final Logger log = Logger.getLogger(DeployAuthorizer.class.getName()); + private static final Logger log = Logger.getLogger(ApplicationInstanceAuthorizer.class.getName()); private final ZoneRegistry zoneRegistry; private final AthenzClientFactory athenzClientFactory; - public DeployAuthorizer(ZoneRegistry zoneRegistry, AthenzClientFactory athenzClientFactory) { + public ApplicationInstanceAuthorizer(ZoneRegistry zoneRegistry, AthenzClientFactory athenzClientFactory) { this.zoneRegistry = zoneRegistry; this.athenzClientFactory = athenzClientFactory; } - public void throwIfUnauthorizedForDeploy(Principal principal, - Environment environment, - Tenant tenant, - ApplicationId applicationId, - Optional<ApplicationPackage> applicationPackage) { - // Validate that domain in identity configuration (deployment.xml) is same as tenant domain - applicationPackage.map(ApplicationPackage::deploymentSpec).flatMap(DeploymentSpec::athenzDomain) - .ifPresent(identityDomain -> { - AthenzDomain tenantDomain = tenant.getAthensDomain().orElseThrow(() -> new IllegalArgumentException("Identity provider only available to Athenz onboarded tenants")); - if (! Objects.equals(tenantDomain.getName(), identityDomain.value())) { - throw new ForbiddenException( - String.format( - "Athenz domain in deployment.xml: [%s] must match tenant domain: [%s]", - identityDomain.value(), - tenantDomain.getName() - )); - } - }); - - if (!environmentRequiresAuthorization(environment)) { - return; - } - - if (principal == null) { - throw loggedUnauthorizedException("Principal not authenticated!"); - } - - if (!(principal instanceof AthenzPrincipal)) { - throw loggedUnauthorizedException( - "Principal '%s' of type '%s' is not an Athenz principal, which is required for production deployments.", - principal.getName(), principal.getClass().getSimpleName()); - } - - AthenzPrincipal athenzPrincipal = (AthenzPrincipal) principal; - AthenzDomain principalDomain = athenzPrincipal.getDomain(); + public void throwIfUnauthorized(AthenzPrincipal principal, + Tenant tenant, + ApplicationName application) { + AthenzDomain principalDomain = principal.getDomain(); if (!principalDomain.equals(SCREWDRIVER_DOMAIN)) { throw loggedForbiddenException( @@ -89,16 +59,47 @@ public class DeployAuthorizer { // NOTE: no fine-grained deploy authorization for non-Athenz tenants if (tenant.isAthensTenant()) { AthenzDomain tenantDomain = tenant.getAthensDomain().get(); - if (!hasDeployAccessToAthenzApplication(athenzPrincipal, tenantDomain, applicationId)) { + if (!hasDeployAccessToAthenzApplication(principal, tenantDomain, application)) { throw loggedForbiddenException( "Screwdriver principal '%1$s' does not have deploy access to '%2$s'. " + - "Either the application has not been created at " + zoneRegistry.getDashboardUri() + " or " + - "'%1$s' is not added to the application's deployer role in Athenz domain '%3$s'.", - athenzPrincipal.getIdentity().getFullName(), applicationId, tenantDomain.getName()); + "Either the application has not been created at " + zoneRegistry.getDashboardUri() + " or " + + "'%1$s' is not added to the application's deployer role in Athenz domain '%3$s'.", + principal.getIdentity().getFullName(), application.value(), tenantDomain.getName()); } } } + public void throwIfUnauthorized(AthenzPrincipal principal, + Environment environment, + Tenant tenant, + ApplicationName application) { + if (!environmentRequiresAuthorization(environment)) { + return; + } + throwIfUnauthorized(principal, tenant, application); + } + + public void throwIfUnauthorizedForDeploy(AthenzPrincipal principal, + Environment environment, + Tenant tenant, + ApplicationName application, + Optional<ApplicationPackage> applicationPackage) { + // Validate that domain in identity configuration (deployment.xml) is same as tenant domain + applicationPackage.map(ApplicationPackage::deploymentSpec).flatMap(DeploymentSpec::athenzDomain) + .ifPresent(identityDomain -> { + AthenzDomain tenantDomain = tenant.getAthensDomain().orElseThrow(() -> new IllegalArgumentException("Identity provider only available to Athenz onboarded tenants")); + if (! Objects.equals(tenantDomain.getName(), identityDomain.value())) { + throw new ForbiddenException( + String.format( + "Athenz domain in deployment.xml: [%s] must match tenant domain: [%s]", + identityDomain.value(), + tenantDomain.getName() + )); + } + }); + throwIfUnauthorized(principal, environment, tenant, application); + } + private static ForbiddenException loggedForbiddenException(String message, Object... args) { String formattedMessage = String.format(message, args); log.info(formattedMessage); @@ -111,14 +112,14 @@ public class DeployAuthorizer { return new NotAuthorizedException(formattedMessage); } - private boolean hasDeployAccessToAthenzApplication(AthenzPrincipal principal, AthenzDomain domain, ApplicationId applicationId) { + private boolean hasDeployAccessToAthenzApplication(AthenzPrincipal principal, AthenzDomain domain, ApplicationName application) { try { return athenzClientFactory.createZmsClientWithServicePrincipal() .hasApplicationAccess( principal.getIdentity(), ApplicationAction.deploy, domain, - new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(applicationId.application().value())); + new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(application.value())); } catch (ZmsException e) { throw loggedForbiddenException( "Failed to authorize deployment through Athenz. If this problem persists, " + diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/Authorizer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/Authorizer.java index 06d078e8a36..9d45b9a6e09 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/Authorizer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/Authorizer.java @@ -66,18 +66,22 @@ public class Authorizer { /** Returns the principal or throws forbidden */ // TODO: Avoid REST exceptions public AthenzPrincipal getPrincipal(HttpRequest request) { - return getPrincipalIfAny(request).orElseThrow(() -> Authorizer.loggedForbiddenException("User is not authenticated")); + return Optional.ofNullable(request.getJDiscRequest().getUserPrincipal()) + .map(AthenzPrincipal.class::cast) + .orElseThrow(() -> loggedForbiddenException("User is not authenticated")); } - /** Returns the principal if there is any */ - public Optional<AthenzPrincipal> getPrincipalIfAny(HttpRequest request) { - return securityContextOf(request) - .map(SecurityContext::getUserPrincipal) - .map(AthenzPrincipal.class::cast); + public Optional<NToken> getNToken(HttpRequest request) { + return getPrincipal(request).getNToken(); } - public Optional<NToken> getNToken(HttpRequest request) { - return getPrincipalIfAny(request).flatMap(AthenzPrincipal::getNToken); + public Optional<UserId> getUserId(HttpRequest request) { + return Optional.of(getPrincipal(request)) + .map(AthenzPrincipal::getIdentity) + .filter(AthenzUser.class::isInstance) + .map(AthenzUser.class::cast) + .map(AthenzUser::getName) + .map(UserId::new); } public boolean isSuperUser(HttpRequest request) { @@ -147,6 +151,8 @@ public class Authorizer { return securityContext.get().isUserInRole(Authorizer.VESPA_HOSTED_ADMIN_ROLE); } + @Deprecated + // TODO: Remove once Bouncer filter is no longer needed protected Optional<SecurityContext> securityContextOf(HttpRequest request) { return Optional.ofNullable((SecurityContext)request.getJDiscRequest().context().get(ContextAttributes.SECURITY_CONTEXT_ATTRIBUTE)); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/CreateSecurityContextFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/CreateSecurityContextFilter.java index 6073307bafa..5fc15f4baa6 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/CreateSecurityContextFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/CreateSecurityContextFilter.java @@ -20,6 +20,8 @@ import java.security.Principal; @After("BouncerFilter") @Provides("SecurityContext") @SuppressWarnings("unused") // Injected +@Deprecated +// TODO Remove once Bouncer filter is gone public class CreateSecurityContextFilter implements SecurityRequestFilter { @Override diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/PropagateSecurityContextFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/PropagateSecurityContextFilter.java index 17c86e89362..23f94f2fc21 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/PropagateSecurityContextFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/PropagateSecurityContextFilter.java @@ -18,6 +18,8 @@ import java.io.IOException; */ @PreMatching @Provider +// TODO Remove once Bouncer filter is gone +@Deprecated public class PropagateSecurityContextFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext requestContext) throws IOException { 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 61a4a883904..e6fe7531fdc 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 @@ -197,7 +197,8 @@ public class ApplicationApiTest extends ControllerContainerTest { .data(createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId))) .screwdriverIdentity(SCREWDRIVER_ID), new File("deploy-result.json")); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default", DELETE), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default", DELETE) + .screwdriverIdentity(SCREWDRIVER_ID), "Deactivated tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default"); controllerTester.notifyJobCompletion(id, screwdriverProjectId, true, DeploymentJobs.JobType.systemTest); // Called through the separate screwdriver/v1 API @@ -206,7 +207,8 @@ public class ApplicationApiTest extends ControllerContainerTest { .data(createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId))) .screwdriverIdentity(SCREWDRIVER_ID), new File("deploy-result.json")); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/default", DELETE), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/default", DELETE) + .screwdriverIdentity(SCREWDRIVER_ID), "Deactivated tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/default"); controllerTester.notifyJobCompletion(id, screwdriverProjectId, true, DeploymentJobs.JobType.stagingTest); @@ -252,10 +254,12 @@ public class ApplicationApiTest extends ControllerContainerTest { // POST a 'restart application' command - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/restart", POST), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/restart", POST) + .screwdriverIdentity(SCREWDRIVER_ID), "Requested restart of tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default"); // POST a 'restart application' command with a host filter (other filters not supported yet) - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/restart?hostname=host1", POST), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/restart?hostname=host1", POST) + .screwdriverIdentity(SCREWDRIVER_ID), "Requested restart of tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default"); // POST a 'log' command tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/log", POST), @@ -275,14 +279,17 @@ public class ApplicationApiTest extends ControllerContainerTest { new File("delete-with-active-deployments.json"), 400); // DELETE (deactivate) a deployment - dev - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default", DELETE), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default", DELETE) + .screwdriverIdentity(SCREWDRIVER_ID), "Deactivated tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default"); // DELETE (deactivate) a deployment - prod - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", DELETE), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", DELETE) + .screwdriverIdentity(SCREWDRIVER_ID), "Deactivated tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default"); // DELETE (deactivate) a deployment is idempotent - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", DELETE), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", DELETE) + .screwdriverIdentity(SCREWDRIVER_ID), "Deactivated tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default"); // PUT (create) the authenticated user @@ -315,9 +322,11 @@ public class ApplicationApiTest extends ControllerContainerTest { .data("{\"reason\":\"because i can\"}"), new File("global-rotation-delete.json")); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/promote", POST), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/promote", POST) + .screwdriverIdentity(SCREWDRIVER_ID), "{\"message\":\"Successfully copied environment hosted-verified-prod to hosted-instance_tenant1_application1_placeholder_component_default\"}"); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/promote", POST), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/promote", POST) + .screwdriverIdentity(SCREWDRIVER_ID), "{\"message\":\"Successfully copied environment hosted-instance_tenant1_application1_placeholder_component_default to hosted-instance_tenant1_application1_us-west-1_prod_default\"}"); // DELETE an application @@ -816,7 +825,8 @@ public class ApplicationApiTest extends ControllerContainerTest { .data(deployData) .screwdriverIdentity(SCREWDRIVER_ID), new File("deploy-result.json")); - tester.assertResponse(request(testPath, DELETE), + tester.assertResponse(request(testPath, DELETE) + .screwdriverIdentity(SCREWDRIVER_ID), "Deactivated " + testPath.replaceFirst("/application/v4/", "")); controllerTester.notifyJobCompletion(application, projectId, true, DeploymentJobs.JobType.systemTest); @@ -827,7 +837,8 @@ public class ApplicationApiTest extends ControllerContainerTest { .data(deployData) .screwdriverIdentity(SCREWDRIVER_ID), new File("deploy-result.json")); - tester.assertResponse(request(stagingPath, DELETE), + tester.assertResponse(request(stagingPath, DELETE) + .screwdriverIdentity(SCREWDRIVER_ID), "Deactivated " + stagingPath.replaceFirst("/application/v4/", "")); controllerTester.notifyJobCompletion(application, projectId, true, DeploymentJobs.JobType.stagingTest); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MockAuthorizer.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MockAuthorizer.java index d0f5f4dbdb9..f2fc4b12096 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MockAuthorizer.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MockAuthorizer.java @@ -11,6 +11,7 @@ import com.yahoo.vespa.hosted.controller.TestIdentities; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory; import com.yahoo.vespa.hosted.controller.api.integration.entity.EntityService; +import javax.ws.rs.ForbiddenException; import javax.ws.rs.core.SecurityContext; import java.security.Principal; import java.util.Optional; @@ -31,14 +32,15 @@ public class MockAuthorizer extends Authorizer { /** Returns a principal given by the request parameters 'domain' and 'user' */ @Override - public Optional<AthenzPrincipal> getPrincipalIfAny(HttpRequest request) { + public AthenzPrincipal getPrincipal(HttpRequest request) { String domain = request.getHeader("Athenz-Identity-Domain"); String name = request.getHeader("Athenz-Identity-Name"); - if (domain == null || name == null) return Optional.empty(); - return Optional.of( - new AthenzPrincipal( - AthenzIdentities.from(new AthenzDomain(domain), name), - new NToken("dummy"))); + if (domain == null || name == null) { + throw new ForbiddenException("User is not authenticated"); + } + return new AthenzPrincipal( + AthenzIdentities.from(new AthenzDomain(domain), name), + new NToken("dummy")); } /** Returns the hardcoded NToken of {@link TestIdentities#userId} */ @@ -50,9 +52,9 @@ public class MockAuthorizer extends Authorizer { @Override protected Optional<SecurityContext> securityContextOf(HttpRequest request) { - return getPrincipalIfAny(request).map(MockSecurityContext::new); + return Optional.of(new MockSecurityContext(getPrincipal(request))); } - + private static final class MockSecurityContext implements SecurityContext { private final Principal principal; |