diff options
author | Bjørn Christian Seime <bjorncs@oath.com> | 2018-02-20 14:42:46 +0100 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@oath.com> | 2018-02-21 11:15:22 +0100 |
commit | b23445f6489045794a219a087bca2b8af15989cf (patch) | |
tree | 0096c9548f2520c8d41a8dfb410210e099c11e86 /controller-server | |
parent | bcaa910f1cdd10815ec7bd30d67332af3751b759 (diff) |
Remove Authorizer and ApplicationInstanceAuthorizer
Diffstat (limited to 'controller-server')
6 files changed, 54 insertions, 453 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java index 1dc46ed81ab..f59e0fbce5c 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java @@ -2,18 +2,16 @@ package com.yahoo.vespa.hosted.controller.athenz.filter; import com.google.inject.Inject; -import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.jdisc.Response; import com.yahoo.jdisc.handler.ResponseHandler; import com.yahoo.jdisc.http.filter.DiscFilterRequest; import com.yahoo.log.LogLevel; -import com.yahoo.vespa.hosted.controller.api.identifiers.UserId; 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.identifiers.UserId; import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsKeystore; import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig; -import com.yahoo.vespa.hosted.controller.restapi.application.Authorizer; import java.security.Principal; import java.util.Optional; @@ -81,10 +79,6 @@ public class UserAuthWithAthenzPrincipalFilter extends AthenzPrincipalFilter { .orElseThrow(() -> new IllegalStateException("Invalid status code: " + statusCode)); } - /** - * NOTE: The Bouncer user roles ({@link DiscFilterRequest#roles} are still intact as they are required - * for {@link Authorizer#isMemberOfVespaBouncerGroup(HttpRequest)}. - */ private void rewriteUserPrincipalToAthenz(DiscFilterRequest request) { Principal userPrincipal = request.getUserPrincipal(); log.log(LogLevel.DEBUG, () -> "Original user principal: " + userPrincipal.toString()); 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 a99b50f0980..c161374d753 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 @@ -5,6 +5,7 @@ import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; import com.yahoo.component.Version; +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; @@ -20,6 +21,7 @@ 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.NToken; import com.yahoo.vespa.config.SlimeUtils; @@ -80,6 +82,7 @@ import com.yahoo.yolean.Exceptions; import javax.ws.rs.BadRequestException; import javax.ws.rs.ForbiddenException; +import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.NotAuthorizedException; import java.io.IOException; import java.io.InputStream; @@ -89,6 +92,7 @@ import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Scanner; import java.util.logging.Level; @@ -104,19 +108,15 @@ import java.util.logging.Level; 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, - Controller controller, Authorizer authorizer, + Controller controller, AthenzClientFactory athenzClientFactory) { super(parentCtx); this.controller = controller; - this.authorizer = authorizer; this.athenzClientFactory = athenzClientFactory; - this.applicationInstanceAuthorizer = new ApplicationInstanceAuthorizer(controller.zoneRegistry(), athenzClientFactory); } @Override @@ -240,7 +240,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private HttpResponse authenticatedUser(HttpRequest request) { String userIdString = request.getProperty("userOverride"); if (userIdString == null) - userIdString = authorizer.getUserId(request) + userIdString = getUserId(request) .map(UserId::id) .orElseThrow(() -> new ForbiddenException("You must be authenticated or specify userOverride")); UserId userId = new UserId(userIdString); @@ -501,13 +501,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (!existingTenant.isPresent()) return ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist"); - authorizer.throwIfUnauthorized(existingTenant.get().getId(), request); // Decode payload (reason) and construct parameter to the configserver Inspector requestData = toSlime(request.getData()).get(); String reason = mandatory("reason", requestData).asString(); - String agent = authorizer.getIdentity(request).getFullName(); + String agent = getUserPrincipal(request).getIdentity().getFullName(); long timestamp = controller.clock().instant().getEpochSecond(); EndpointStatus.Status status = inService ? EndpointStatus.Status.in : EndpointStatus.Status.out; EndpointStatus endPointStatus = new EndpointStatus(status, reason, agent, timestamp); @@ -596,7 +595,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } private HttpResponse createUser(HttpRequest request) { - Optional<UserId> user = authorizer.getUserId(request); + Optional<UserId> user = getUserId(request); if ( ! user.isPresent() ) throw new ForbiddenException("Not authenticated or not an user."); try { @@ -614,7 +613,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler { Inspector requestData = toSlime(request.getData()).get(); - authorizer.throwIfUnauthorized(existingTenant.get().getId(), request); Tenant updatedTenant; switch (existingTenant.get().tenantType()) { case USER: { @@ -626,8 +624,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { userGroup, new Property(mandatory("property", requestData).asString()), optional("propertyId", requestData).map(PropertyId::new)); - throwIfNotSuperUserOrPartOfOpsDbGroup(userGroup, request); - controller.tenants().updateTenant(updatedTenant, authorizer.getNToken(request)); + controller.tenants().updateTenant(updatedTenant, getUserPrincipal(request).getNToken()); break; } case ATHENS: { @@ -637,7 +634,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { new AthenzDomain(mandatory("athensDomain", requestData).asString()), new Property(mandatory("property", requestData).asString()), optional("propertyId", requestData).map(PropertyId::new)); - controller.tenants().updateTenant(updatedTenant, authorizer.getNToken(request)); + controller.tenants().updateTenant(updatedTenant, getUserPrincipal(request).getNToken()); break; } default: { @@ -658,12 +655,10 @@ public class ApplicationApiHandler extends LoggingRequestHandler { optional("property", requestData).map(Property::new), optional("athensDomain", requestData).map(AthenzDomain::new), optional("propertyId", requestData).map(PropertyId::new)); - if (tenant.isOpsDbTenant()) - throwIfNotSuperUserOrPartOfOpsDbGroup(new UserGroup(mandatory("userGroup", requestData).asString()), request); if (tenant.isAthensTenant()) throwIfNotAthenzDomainAdmin(new AthenzDomain(mandatory("athensDomain", requestData).asString()), request); - controller.tenants().addTenant(tenant, authorizer.getNToken(request)); + controller.tenants().addTenant(tenant, getUserPrincipal(request).getNToken()); return tenant(tenant, request, true); } @@ -674,9 +669,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler { Property property = new Property(mandatory("property", requestData).asString()); PropertyId propertyId = new PropertyId(mandatory("propertyId", requestData).asString()); - authorizer.throwIfUnauthorized(tenantid, request); throwIfNotAthenzDomainAdmin(tenantDomain, request); - NToken nToken = authorizer.getNToken(request) + NToken nToken = getUserPrincipal(request).getNToken() .orElseThrow(() -> new BadRequestException("The NToken for a domain admin is required to migrate tenant to Athens")); Tenant tenant = controller.tenants().migrateTenantToAthenz(tenantid, tenantDomain, propertyId, property, nToken); @@ -684,10 +678,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } private HttpResponse createApplication(String tenantName, String applicationName, HttpRequest request) { - authorizer.throwIfUnauthorized(new TenantId(tenantName), request); Application application; try { - application = controller.applications().createApplication(ApplicationId.from(tenantName, applicationName, "default"), authorizer.getNToken(request)); + application = controller.applications().createApplication(ApplicationId.from(tenantName, applicationName, "default"), getUserPrincipal(request).getNToken()); } catch (ZmsException e) { // TODO: Push conversion down if (e.getCode() == com.yahoo.jdisc.Response.Status.FORBIDDEN) @@ -702,7 +695,6 @@ 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); @@ -723,7 +715,6 @@ 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"); @@ -746,11 +737,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler { // 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 return new StringResponse("Requested restart of " + path(TenantResource.API_PATH, tenantName, @@ -770,10 +756,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler { 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) { @@ -795,11 +777,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler { Optional<ApplicationPackage> applicationPackage = Optional.ofNullable(dataParts.get("applicationZip")) .map(ApplicationPackage::new); - applicationInstanceAuthorizer.throwIfUnauthorizedForDeploy(authorizer.getPrincipal(request), - Environment.from(environment), - getTenantOrThrow(tenantName), - ApplicationName.from(applicationName), - applicationPackage); + + verifyApplicationIdentityConfiguration(tenantName, applicationPackage); // TODO: get rid of the json object DeployOptions deployOptionsJsonClass = new DeployOptions(screwdriverBuildJobFromSlime(deployOptions.field("screwdriverBuildJob")), @@ -813,22 +792,36 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return new SlimeJsonResponse(toSlime(result)); } + private void verifyApplicationIdentityConfiguration(String tenantName, 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 -> { + Tenant tenant = controller.tenants().tenant(new TenantId(tenantName)).orElseThrow(() -> new IllegalArgumentException("Tenant does not exist")); + 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() + )); + } + }); + } + private HttpResponse deleteTenant(String tenantName, HttpRequest request) { Optional<Tenant> tenant = controller.tenants().tenant(new TenantId(tenantName)); if ( ! tenant.isPresent()) return ErrorResponse.notFoundError("Could not delete tenant '" + tenantName + "': Tenant not found"); // NOTE: The Jersey implementation would silently ignore this - authorizer.throwIfUnauthorized(new TenantId(tenantName), request); - controller.tenants().deleteTenant(new TenantId(tenantName), authorizer.getNToken(request)); + controller.tenants().deleteTenant(new TenantId(tenantName), getUserPrincipal(request).getNToken()); // TODO: Change to a message response saying the tenant was deleted return tenant(tenant.get(), request, false); } private HttpResponse deleteApplication(String tenantName, String applicationName, HttpRequest request) { - authorizer.throwIfUnauthorized(new TenantId(tenantName), request); - ApplicationId id = ApplicationId.from(tenantName, applicationName, "default"); - controller.applications().deleteApplication(id, authorizer.getNToken(request)); + controller.applications().deleteApplication(id, getUserPrincipal(request).getNToken()); return new EmptyJsonResponse(); // TODO: Replicates current behavior but should return a message response instead } @@ -838,11 +831,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler { 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); @@ -863,10 +851,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler { */ 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)); @@ -883,11 +867,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler { */ 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)); @@ -1046,26 +1025,31 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } } - private void throwIfNotSuperUserOrPartOfOpsDbGroup(UserGroup userGroup, HttpRequest request) { - AthenzIdentity identity = authorizer.getIdentity(request); - if (!(identity instanceof AthenzUser)) { - throw new ForbiddenException("Identity not an user: " + identity.getFullName()); - } - AthenzUser user = (AthenzUser) identity; - if (!authorizer.isSuperUser(request) && !authorizer.isGroupMember(new UserId(user.getName()), userGroup) ) { - throw new ForbiddenException(String.format("User '%s' is not super user or part of the OpsDB user group '%s'", - user.getName(), userGroup.id())); - } - } - private void throwIfNotAthenzDomainAdmin(AthenzDomain tenantDomain, HttpRequest request) { - AthenzIdentity identity = authorizer.getIdentity(request); - if ( ! authorizer.isAthenzDomainAdmin(identity, tenantDomain)) { + AthenzIdentity identity = getUserPrincipal(request).getIdentity(); + boolean isDomainAdmin = athenzClientFactory.createZmsClientWithServicePrincipal() + .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) + .filter(AthenzUser.class::isInstance) + .map(AthenzUser.class::cast) + .map(AthenzUser::getName) + .map(UserId::new); + } + + private static AthenzPrincipal getUserPrincipal(HttpRequest request) { + return Optional.ofNullable(request.getJDiscRequest().getUserPrincipal()) + .map(AthenzPrincipal.class::cast) + .orElseThrow(() -> new InternalServerErrorException("Expected user principal")); + } + private Inspector mandatory(String key, Inspector object) { if ( ! object.field(key).valid()) throw new IllegalArgumentException("'" + key + "' is missing"); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationInstanceAuthorizer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationInstanceAuthorizer.java deleted file mode 100644 index 2deef474f7c..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationInstanceAuthorizer.java +++ /dev/null @@ -1,136 +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.restapi.application; - -import com.yahoo.config.application.api.DeploymentSpec; -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.AthenzIdentity; -import com.yahoo.vespa.athenz.api.AthenzPrincipal; -import com.yahoo.vespa.hosted.controller.api.Tenant; -import com.yahoo.vespa.hosted.controller.api.application.v4.model.TenantType; -import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction; -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.zone.ZoneRegistry; -import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; - -import javax.ws.rs.ForbiddenException; -import javax.ws.rs.NotAuthorizedException; -import java.util.Objects; -import java.util.Optional; -import java.util.logging.Logger; - -import static com.yahoo.vespa.hosted.controller.api.integration.athenz.HostedAthenzIdentities.SCREWDRIVER_DOMAIN; -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 ApplicationInstanceAuthorizer { - - private static final Logger log = Logger.getLogger(ApplicationInstanceAuthorizer.class.getName()); - - private final ZoneRegistry zoneRegistry; - private final AthenzClientFactory athenzClientFactory; - - public ApplicationInstanceAuthorizer(ZoneRegistry zoneRegistry, AthenzClientFactory athenzClientFactory) { - this.zoneRegistry = zoneRegistry; - this.athenzClientFactory = athenzClientFactory; - } - - public void throwIfUnauthorized(AthenzPrincipal principal, - Tenant tenant, - ApplicationName application) { - AthenzDomain principalDomain = principal.getDomain(); - if (isHostedOperator(principal.getIdentity())) return; - - if (!principalDomain.equals(SCREWDRIVER_DOMAIN)) { - throw loggedForbiddenException( - "Principal '%s' is not a Screwdriver principal. Excepted principal with Athenz domain '%s', got '%s'.", - principal.getName(), SCREWDRIVER_DOMAIN.getName(), principalDomain.getName()); - } - - if (tenant.tenantType() == TenantType.USER) { - throw loggedForbiddenException("User tenants are only allowed to deploy to 'dev' and 'perf' environment"); - } - - // NOTE: no fine-grained deploy authorization for non-Athenz tenants - if (tenant.isAthensTenant()) { - AthenzDomain tenantDomain = tenant.getAthensDomain().get(); - 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'.", - 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); - return new ForbiddenException(formattedMessage); - } - - private static NotAuthorizedException loggedUnauthorizedException(String message, Object... args) { - String formattedMessage = String.format(message, args); - log.info(formattedMessage); - return new NotAuthorizedException(formattedMessage); - } - - private boolean isHostedOperator(AthenzIdentity identity) { - return athenzClientFactory.createZmsClientWithServicePrincipal() - .hasHostedOperatorAccess(identity); - } - - 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(application.value())); - } catch (ZmsException e) { - throw loggedForbiddenException( - "Failed to authorize deployment through Athenz. If this problem persists, " + - "please create ticket at yo/vespa-support. (" + e.getMessage() + ")"); - } - } -} 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 deleted file mode 100644 index 6a268ce8fda..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/Authorizer.java +++ /dev/null @@ -1,160 +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.restapi.application; - -import com.yahoo.config.provision.Environment; -import com.yahoo.container.jdisc.HttpRequest; -import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.api.Tenant; -import com.yahoo.vespa.athenz.api.AthenzDomain; -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.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.entity.EntityService; -import com.yahoo.vespa.hosted.controller.common.ContextAttributes; - -import javax.ws.rs.ForbiddenException; -import javax.ws.rs.HttpMethod; -import javax.ws.rs.core.SecurityContext; -import java.util.Optional; -import java.util.logging.Logger; - - -/** - * @author Stian Kristoffersen - * @author Tony Vaagenes - * @author bjorncs - */ -// TODO: Make this an interface -public class Authorizer { - - private static final Logger log = Logger.getLogger(Authorizer.class.getName()); - - // Must be kept in sync with bouncer filter configuration. - private static final String VESPA_HOSTED_ADMIN_ROLE = "10707.A"; - - private final Controller controller; - private final AthenzClientFactory athenzClientFactory; - private final EntityService entityService; - - public Authorizer(Controller controller, EntityService entityService, AthenzClientFactory athenzClientFactory) { - this.controller = controller; - this.athenzClientFactory = athenzClientFactory; - this.entityService = entityService; - } - - public void throwIfUnauthorized(TenantId tenantId, HttpRequest request) throws ForbiddenException { - if (isReadOnlyMethod(request.getMethod().name())) return; - if (isSuperUser(request)) return; - - Optional<Tenant> tenant = controller.tenants().tenant(tenantId); - if ( ! tenant.isPresent()) return; - - AthenzIdentity identity = getIdentity(request); - if (isTenantAdmin(identity, tenant.get())) return; - - throw loggedForbiddenException("User " + identity.getFullName() + " does not have write access to tenant " + tenantId); - } - - public AthenzIdentity getIdentity(HttpRequest request) { - return getPrincipal(request).getIdentity(); - } - - /** Returns the principal or throws forbidden */ // TODO: Avoid REST exceptions - public AthenzPrincipal getPrincipal(HttpRequest request) { - return Optional.ofNullable(request.getJDiscRequest().getUserPrincipal()) - .map(AthenzPrincipal.class::cast) - .orElseThrow(() -> loggedForbiddenException("User is not authenticated")); - } - - public Optional<NToken> getNToken(HttpRequest request) { - return getPrincipal(request).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) { - // TODO Replace check with membership of a dedicated 'hosted Vespa super-user' role in Vespa's Athenz domain - return isMemberOfVespaBouncerGroup(request); - } - - private static ForbiddenException loggedForbiddenException(String message, Object... args) { - String formattedMessage = String.format(message, args); - log.info(formattedMessage); - return new ForbiddenException(formattedMessage); - } - - public boolean isTenantAdmin(AthenzIdentity identity, Tenant tenant) { - switch (tenant.tenantType()) { - case ATHENS: - return isAthenzTenantAdmin(identity, tenant.getAthensDomain().get()); - case OPSDB: { - if (!(identity instanceof AthenzUser)) { - return false; - } - AthenzUser user = (AthenzUser) identity; - return isGroupMember(new UserId(user.getName()), tenant.getUserGroup().get()); - } - case USER: { - if (!(identity instanceof AthenzUser)) { - return false; - } - AthenzUser user = (AthenzUser) identity; - return isUserTenantOwner(tenant.getId(), new UserId(user.getName())); - } - } - throw new IllegalArgumentException("Unknown tenant type: " + tenant.tenantType()); - } - - private boolean isAthenzTenantAdmin(AthenzIdentity athenzIdentity, AthenzDomain tenantDomain) { - return athenzClientFactory.createZmsClientWithServicePrincipal() - .hasTenantAdminAccess(athenzIdentity, tenantDomain); - } - - public boolean isAthenzDomainAdmin(AthenzIdentity identity, AthenzDomain tenantDomain) { - return athenzClientFactory.createZmsClientWithServicePrincipal() - .isDomainAdmin(identity, tenantDomain); - } - - public boolean isGroupMember(UserId userId, UserGroup userGroup) { - return entityService.isGroupMember(userId, userGroup); - } - - private static boolean isUserTenantOwner(TenantId tenantId, UserId userId) { - return tenantId.equals(userId.toTenantId()); - } - - public static boolean environmentRequiresAuthorization(Environment environment) { - return environment != Environment.dev && environment != Environment.perf; - } - - private static boolean isReadOnlyMethod(String method) { - return method.equals(HttpMethod.GET) || method.equals(HttpMethod.HEAD) || method.equals(HttpMethod.OPTIONS); - } - - @Deprecated - // TODO Remove this method. Stop using Bouncer for authorization and use Athenz instead - private boolean isMemberOfVespaBouncerGroup(HttpRequest request) { - Optional<SecurityContext> securityContext = securityContextOf(request); - if ( ! securityContext.isPresent() ) throw Authorizer.loggedForbiddenException("User is not authenticated"); - 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/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 abc5f9f8aa1..1818bf97430 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 @@ -65,7 +65,6 @@ public class ControllerContainerTest { " <component id='com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.maintenance.JobControl'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.persistence.MemoryControllerDb'/>\n" + - " <component id='com.yahoo.vespa.hosted.controller.restapi.application.MockAuthorizer'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.routing.MockRoutingGenerator'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.ArtifactRepositoryMock'/>\n" + " <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>\n" + 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 deleted file mode 100644 index f2fc4b12096..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MockAuthorizer.java +++ /dev/null @@ -1,80 +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.restapi.application; - -import com.yahoo.container.jdisc.HttpRequest; -import com.yahoo.vespa.athenz.api.AthenzDomain; -import com.yahoo.vespa.athenz.api.AthenzPrincipal; -import com.yahoo.vespa.athenz.api.NToken; -import com.yahoo.vespa.athenz.utils.AthenzIdentities; -import com.yahoo.vespa.hosted.controller.Controller; -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; - -/** - * This overrides methods in Authorizer which relies on properties set by jdisc HTTP filters. - * This is necessary because filters are not currently executed when executing requests with Application. - * - * @author bratseth - * @author bjorncs - */ -@SuppressWarnings("unused") // injected -public class MockAuthorizer extends Authorizer { - - public MockAuthorizer(Controller controller, EntityService entityService, AthenzClientFactory athenzClientFactory) { - super(controller, entityService, athenzClientFactory); - } - - /** Returns a principal given by the request parameters 'domain' and 'user' */ - @Override - public AthenzPrincipal getPrincipal(HttpRequest request) { - String domain = request.getHeader("Athenz-Identity-Domain"); - String name = request.getHeader("Athenz-Identity-Name"); - 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} */ - @Override - public Optional<NToken> getNToken(HttpRequest request) { - return Optional.of(TestIdentities.userNToken); - } - - - @Override - protected Optional<SecurityContext> securityContextOf(HttpRequest request) { - return Optional.of(new MockSecurityContext(getPrincipal(request))); - } - - private static final class MockSecurityContext implements SecurityContext { - - private final Principal principal; - - private MockSecurityContext(Principal principal) { - this.principal = principal; - } - - @Override - public Principal getUserPrincipal() { return principal; } - - @Override - public boolean isUserInRole(String role) { return false; } - - @Override - public boolean isSecure() { return true; } - - @Override - public String getAuthenticationScheme() { throw new UnsupportedOperationException(); } - - } - -} |