aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMorten Tokle <mortent@verizonmedia.com>2022-02-07 10:27:39 +0100
committerGitHub <noreply@github.com>2022-02-07 10:27:39 +0100
commit49d631e46af560b732d4032c99d9b592095d801e (patch)
treea4bfd497bfe2c794995ae2c028abc8f8cc682fe2
parent7bbbe036755eb775d19b47393f7a6ab3efbd0a07 (diff)
parent4f7b790882fe1deea3f1dc1f181fb7478d08a16a (diff)
Merge pull request #21070 from vespa-engine/mortent/reapply-developer-role
reapply developer role
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java12
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java40
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java13
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/bindings/PolicyEntity.java12
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;
}