From da86ba0bda9284d0d8884a8e9220d4f317dccb77 Mon Sep 17 00:00:00 2001 From: Morten Tokle Date: Tue, 21 Dec 2021 12:43:15 +0100 Subject: Allow developers to deploy application in manual zones --- .../controller/athenz/impl/AthenzFacade.java | 11 ++++--- .../restapi/filter/AthenzRoleFilter.java | 37 ++++++++++++++++++++-- .../filter/ControllerAuthorizationFilterTest.java | 13 ++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) (limited to 'controller-server') 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 index d116ef3333c..28cf132af90 100644 --- 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 @@ -5,7 +5,9 @@ import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.inject.Inject; import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Zone; import com.yahoo.text.Text; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzIdentity; @@ -248,9 +250,9 @@ public class AthenzFacade implements AccessControl { } public boolean hasApplicationAccess( - AthenzIdentity identity, ApplicationAction action, AthenzDomain tenantDomain, ApplicationName applicationName) { + AthenzIdentity identity, ApplicationAction action, AthenzDomain tenantDomain, ApplicationName applicationName, Optional zone) { return hasAccess( - action.name(), applicationResourceString(tenantDomain, applicationName), identity); + action.name(), applicationResourceString(tenantDomain, applicationName, zone), identity); } public boolean hasTenantAdminAccess(AthenzIdentity identity, AthenzDomain tenantDomain) { @@ -325,8 +327,9 @@ public class AthenzFacade implements AccessControl { return resourceStringPrefix(tenantDomain) + ".wildcard"; } - private String applicationResourceString(AthenzDomain tenantDomain, ApplicationName applicationName) { - return resourceStringPrefix(tenantDomain) + "." + "res_group" + "." + applicationName.value() + ".wildcard"; + private String applicationResourceString(AthenzDomain tenantDomain, ApplicationName applicationName, Optional zone) { + String environment = zone.map(Zone::environment).map(Environment::value).orElse("*"); + return resourceStringPrefix(tenantDomain) + "." + "res_group" + "." + applicationName.value() + "." + environment; } private enum TenantAction { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java index c685390c7ed..7ab3b75a758 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java @@ -4,7 +4,10 @@ package com.yahoo.vespa.hosted.controller.restapi.filter; import com.auth0.jwt.JWT; import com.google.inject.Inject; 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.config.provision.Zone; import com.yahoo.jdisc.http.filter.DiscFilterRequest; import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase; @@ -16,6 +19,7 @@ import com.yahoo.restapi.Path; 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.client.zms.ZmsClientException; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.TenantController; @@ -94,6 +98,15 @@ public class AthenzRoleFilter extends JsonSecurityRequestFilterBase { path.matches("/application/v4/tenant/{tenant}/application/{application}/{*}"); Optional application = Optional.ofNullable(path.get("application")).map(ApplicationName::from); + final Optional zone; + if(path.matches("/application/v4/tenant/{tenant}/application/{application}/{*}/instance/{*}/environment/{environment}/region/{region}/{*}")) { + zone = Optional.of(new Zone(Environment.from(path.get("environment")), RegionName.from(path.get("region")))); + } else if(path.matches("/application/v4/tenant/{tenant}/application/{application}/{*}/environment/{environment}/region/{region}/{*}")) { + zone = Optional.of(new Zone(Environment.from(path.get("environment")), RegionName.from(path.get("region")))); + } else { + zone = Optional.empty(); + } + AthenzIdentity identity = principal.getIdentity(); Set roleMemberships = new CopyOnWriteArraySet<>(); @@ -121,10 +134,18 @@ public class AthenzRoleFilter extends JsonSecurityRequestFilterBase { && ! tenant.get().name().value().equals("sandbox")) futures.add(executor.submit(() -> { if ( tenant.get().type() == Tenant.Type.athenz - && hasDeployerAccess(identity, ((AthenzTenant) tenant.get()).domain(), application.get())) + && hasDeployerAccess(identity, ((AthenzTenant) tenant.get()).domain(), application.get(), zone)) roleMemberships.add(Role.buildService(tenant.get().name(), application.get())); })); + if (identity instanceof AthenzUser && zone.isPresent()) { + Zone z = zone.get(); + futures.add(executor.submit(() -> { + if (canDeployToManualZones(identity, ((AthenzTenant) tenant.get()).domain(), application.get(), z)) + roleMemberships.add(Role.hostedDeveloper(tenant.get().name())); + })); + } + futures.add(executor.submit(() -> { if (athenz.hasSystemFlagsAccess(identity, /*dryrun*/false)) roleMemberships.add(Role.systemFlagsDeployer()); @@ -167,12 +188,22 @@ public class AthenzRoleFilter extends JsonSecurityRequestFilterBase { } } - private boolean hasDeployerAccess(AthenzIdentity identity, AthenzDomain tenantDomain, ApplicationName application) { + private boolean hasDeployerAccess(AthenzIdentity identity, AthenzDomain tenantDomain, ApplicationName application, Optional zone) { try { return athenz.hasApplicationAccess(identity, ApplicationAction.deploy, tenantDomain, - application); + application, + zone); + } catch (ZmsClientException e) { + throw new RuntimeException("Failed to authorize operation: (" + e.getMessage() + ")", e); + } + } + + private boolean canDeployToManualZones(AthenzIdentity identity, AthenzDomain tenantDomain, ApplicationName application, Zone zone) { + if (! zone.environment().isManuallyDeployed()) return false; + try { + return athenz.hasApplicationAccess(identity, ApplicationAction.deploy, tenantDomain, application, Optional.of(zone)); } catch (ZmsClientException e) { throw new RuntimeException("Failed to authorize operation: (" + e.getMessage() + ")", e); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java index 9e17b44c9a6..eab3a37a9c3 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.restapi.filter; import com.fasterxml.jackson.databind.ObjectMapper; import com.yahoo.application.container.handler.Request; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.TenantName; import com.yahoo.jdisc.http.HttpRequest.Method; import com.yahoo.jdisc.http.filter.DiscFilterRequest; import com.yahoo.vespa.hosted.controller.ControllerTester; @@ -75,6 +76,18 @@ public class ControllerAuthorizationFilterTest { assertIsAllowed(invokeFilter(filter, createRequest(Method.GET, "/zone/v1/path", securityContext))); } + @Test + public void hostedDeveloper() { + ControllerTester tester = new ControllerTester(); + TenantName tenantName = TenantName.defaultName(); + SecurityContext securityContext = new SecurityContext(() -> "user", Set.of(Role.hostedDeveloper(tenantName))); + + ControllerAuthorizationFilter filter = createFilter(tester); + assertIsAllowed(invokeFilter(filter, createRequest(Method.POST, "/application/v4/tenant/" + tenantName.value() + "/application/app/instance/default/environment/dev/region/region/deploy", securityContext))); + assertIsForbidden(invokeFilter(filter, createRequest(Method.POST, "/application/v4/tenant/" + tenantName.value() + "/application/app/instance/default/environment/prod/region/region/deploy", securityContext))); + assertIsForbidden(invokeFilter(filter, createRequest(Method.POST, "/application/v4/tenant/" + tenantName.value() + "/application/app/submit", securityContext))); + } + private static void assertIsAllowed(Optional response) { assertFalse("Expected no response from filter, but got \"" + response.map(r -> r.message + "\" (" + r.statusCode + ")").orElse(""), -- cgit v1.2.3