diff options
author | Morten Tokle <mortent@verizonmedia.com> | 2022-02-07 10:27:39 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-07 10:27:39 +0100 |
commit | 49d631e46af560b732d4032c99d9b592095d801e (patch) | |
tree | a4bfd497bfe2c794995ae2c028abc8f8cc682fe2 | |
parent | 7bbbe036755eb775d19b47393f7a6ab3efbd0a07 (diff) | |
parent | 4f7b790882fe1deea3f1dc1f181fb7478d08a16a (diff) |
Merge pull request #21070 from vespa-engine/mortent/reapply-developer-role
reapply developer role
7 files changed, 78 insertions, 9 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java index 4679f660319..1b00368b73e 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java @@ -40,7 +40,7 @@ public class ZmsClientMock implements ZmsClient { private final AthenzDbMock athenz; private final AthenzIdentity controllerIdentity; private static final Pattern TENANT_RESOURCE_PATTERN = Pattern.compile("service\\.hosting\\.tenant\\.(?<tenantDomain>[\\w\\-_]+)\\..*"); - private static final Pattern APPLICATION_RESOURCE_PATTERN = Pattern.compile("service\\.hosting\\.tenant\\.[\\w\\-_]+\\.res_group\\.(?<resourceGroup>[\\w\\-_]+)\\.wildcard"); + private static final Pattern APPLICATION_RESOURCE_PATTERN = Pattern.compile("service\\.hosting\\.tenant\\.[\\w\\-_]+\\.res_group\\.(?<resourceGroup>[\\w\\-_]+)\\.(?<environment>[\\w\\-_]+)"); public ZmsClientMock(AthenzDbMock athenz, AthenzIdentity controllerIdentity) { this.athenz = athenz; diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java index 5cdd12ecb1c..c40c2d4db01 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java @@ -52,6 +52,11 @@ public abstract class Role { return new TenantRole(RoleDefinition.developer, tenant); } + /** Returns a {@link RoleDefinition#hostedDeveloper} for the current system and given tenant. */ + public static TenantRole hostedDeveloper(TenantName tenant) { + return new TenantRole(RoleDefinition.hostedDeveloper, tenant); + } + /** Returns a {@link RoleDefinition#administrator} for the current system and given tenant. */ public static TenantRole administrator(TenantName tenant) { return new TenantRole(RoleDefinition.administrator, tenant); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java index eeb3bae4431..aed5c08f0db 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java @@ -60,6 +60,9 @@ public enum RoleDefinition { Policy.billingInformationRead, Policy.secretStoreOperations), + /** Developer for manual deployments for a tenant */ + hostedDeveloper(Policy.developmentDeployment), + /** Admin — the administrative function for user management etc. */ administrator(Policy.tenantUpdate, Policy.tenantManager, 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..a0b70eb88ab 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> 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,10 @@ 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> zone) { + // If environment is not provided, add .wildcard to match .* in the policy resource (* is not allowed in the request) + String environment = zone.map(Zone::environment).map(Environment::value).orElse("wildcard"); + 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..411e9ec6070 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<ApplicationName> application = Optional.ofNullable(path.get("application")).map(ApplicationName::from); + final Optional<Zone> zone; + if(path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{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<Role> roleMemberships = new CopyOnWriteArraySet<>(); @@ -121,10 +134,21 @@ 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() + && tenant.isPresent() + && application.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 +191,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> 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<AuthorizationResponse> response) { assertFalse("Expected no response from filter, but got \"" + response.map(r -> r.message + "\" (" + r.statusCode + ")").orElse(""), diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/PolicyEntity.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/PolicyEntity.java index 4be82daee43..84d522ee85d 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/PolicyEntity.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/PolicyEntity.java @@ -6,12 +6,15 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * @author olaa */ @JsonIgnoreProperties(ignoreUnknown = true) public class PolicyEntity { + private static final Pattern namePattern = Pattern.compile("^(?<domain>[^:]+):policy\\.(?<name>.*)$"); @JsonInclude(JsonInclude.Include.NON_EMPTY) private final List<AssertionEntity> assertions; @@ -19,10 +22,17 @@ public class PolicyEntity { public PolicyEntity(@JsonProperty("name") String name, @JsonProperty("assertions") List<AssertionEntity> assertions) { - this.name = name; + this.name = nameFromResourceString(name); this.assertions = assertions; } + private static String nameFromResourceString(String resource) { + Matcher matcher = namePattern.matcher(resource); + if (!matcher.matches()) + throw new IllegalArgumentException("Could not find policy name from resource string: " + resource); + return matcher.group("name"); + } + public String getName() { return name; } |