From 43ea2db071aac1434e2c05c81b9b5f76939eac00 Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Wed, 29 Sep 2021 14:41:30 +0200 Subject: Add feature flag for new tenant operator roles in public systems --- flags/src/main/java/com/yahoo/vespa/flags/Flags.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index 7e9681209f5..6190f4dc2f1 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -298,6 +298,14 @@ public class Flags { HOSTNAME ); + public static final UnboundBooleanFlag ENABLE_TENANT_OPERATOR_ROLE = defineFeatureFlag( + "enable-tenant-operator-role", false, + List.of("bjorncs"), "2021-09-29", "2021-12-31", + "Enable tenant specific operator roles in public systems. For controllers only.", + "Takes effect on subsequent maintainer invocation", + TENANT_ID + ); + /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ public static UnboundBooleanFlag defineFeatureFlag(String flagId, boolean defaultValue, List owners, String createdAt, String expiresAt, String description, -- cgit v1.2.3 From 58b3b16509b139470633460349dec26753a6160e Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Thu, 30 Sep 2021 09:40:52 +0200 Subject: Model policies with multiple assertions in AthenzDbMock --- .../api/integration/athenz/AthenzDbMock.java | 73 ++++++++++++++++++---- .../api/integration/athenz/ZmsClientMock.java | 17 +++-- 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java index 899e3174df9..e3c0f47625f 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; @@ -43,7 +44,7 @@ public class AthenzDbMock { public final Map applications = new HashMap<>(); public final Map services = new HashMap<>(); public final List roles = new ArrayList<>(); - public final List policies = new ArrayList<>(); + public final Map policies = new HashMap<>(); public boolean isVespaTenant = false; public Domain(AthenzDomain name) { @@ -52,7 +53,7 @@ public class AthenzDbMock { public Domain admin(AthenzIdentity identity) { admins.add(identity); - policies.add(new Policy("admin", identity.getFullName(), ".*", ".*")); + policies.put("admin", new Policy("admin", identity.getFullName(), ".*", ".*")); return this; } @@ -67,7 +68,7 @@ public class AthenzDbMock { } public Domain withPolicy(String principalRegex, String operation, String resource) { - policies.add(new Policy("admin", principalRegex, operation, resource)); + policies.put("admin", new Policy("admin", principalRegex, operation, resource)); return this; } @@ -107,31 +108,77 @@ public class AthenzDbMock { public static class Policy { private final String name; - private final Pattern principal; - private final Pattern action; - private final Pattern resource; + final List assertions = new ArrayList<>(); public Policy(String name, String principal, String action, String resource) { - this.name = name; - this.principal = Pattern.compile(principal); - this.action = Pattern.compile(action); - this.resource = Pattern.compile(resource); + this(name); + this.assertions.add(new Assertion("grant", principal, action, resource)); } + public Policy(String name) { this.name = name; } + public String name() { return name; } public boolean principalMatches(AthenzIdentity athenzIdentity) { - return this.principal.matcher(athenzIdentity.getFullName()).matches(); + return assertions.get(0).principalMatches(athenzIdentity); } public boolean actionMatches(String operation) { - return this.action.matcher(operation).matches(); + return assertions.get(0).actionMatches(operation); } public boolean resourceMatches(String resource) { - return this.resource.matcher(resource).matches(); + return assertions.get(0).resourceMatches(resource); + } + + public boolean hasAssertionMatching(String assertion) { + return assertions.stream().anyMatch(a -> a.asString().equals(assertion)); + } + } + + public static class Assertion { + private final String effect; + private final String role; + private final String action; + private final String resource; + + public Assertion(String effect, String role, String action, String resource) { + this.effect = effect; + this.role = role; + this.action = action; + this.resource = resource; + } + + public Assertion(String role, String action, String resource) { this("grant", role, action, resource); } + + public boolean principalMatches(AthenzIdentity athenzIdentity) { + return Pattern.compile(role).matcher(athenzIdentity.getFullName()).matches(); + } + + public boolean actionMatches(String operation) { + return Pattern.compile(action).matcher(operation).matches(); + } + + public boolean resourceMatches(String resource) { + return Pattern.compile(resource).matcher(resource).matches(); + } + + public String asString() { return String.format("%s %s to %s on %s", effect, action, role, resource).toLowerCase(); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Assertion assertion = (Assertion) o; + return Objects.equals(effect, assertion.effect) && Objects.equals(role, assertion.role) + && Objects.equals(action, assertion.action) && Objects.equals(resource, assertion.resource); + } + + @Override + public int hashCode() { + return Objects.hash(effect, role, action, resource); } } 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 42a5e2b42be..dd49f3a1e7c 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 @@ -158,7 +158,7 @@ public class ZmsClientMock implements ZmsClient { return false; } else { AthenzDbMock.Domain domain = getDomainOrThrow(resource.getDomain(), false); - return domain.policies.stream() + return domain.policies.values().stream() .anyMatch(policy -> policy.principalMatches(identity) && policy.actionMatches(action) && @@ -168,17 +168,18 @@ public class ZmsClientMock implements ZmsClient { @Override public void createPolicy(AthenzDomain athenzDomain, String athenzPolicy) { - List policies = athenz.getOrCreateDomain(athenzDomain).policies; - if (policies.stream().anyMatch(p -> p.name().equals(athenzPolicy))) { + Map policies = athenz.getOrCreateDomain(athenzDomain).policies; + if (policies.containsKey(athenzPolicy)) { throw new IllegalArgumentException("Policy already exists"); } - - // Policy will be created in the mock when an assertion is added + policies.put(athenzPolicy, new AthenzDbMock.Policy(athenzPolicy)); } @Override public void addPolicyRule(AthenzDomain athenzDomain, String athenzPolicy, String action, AthenzResourceName resourceName, AthenzRole athenzRole) { - athenz.getOrCreateDomain(athenzDomain).policies.add(new AthenzDbMock.Policy(athenzPolicy, athenzRole.roleName(), action, resourceName.toResourceNameString())); + AthenzDbMock.Policy policy = athenz.getOrCreateDomain(athenzDomain).policies.get(athenzPolicy); + if (policy == null) throw new IllegalArgumentException("No policy with name " + athenzPolicy); + policy.assertions.add(new AthenzDbMock.Assertion(athenzRole.roleName(), action, resourceName.toResourceNameString())); } @Override @@ -235,9 +236,7 @@ public class ZmsClientMock implements ZmsClient { @Override public Set listPolicies(AthenzDomain domain) { - return athenz.getOrCreateDomain(domain).policies.stream() - .map(AthenzDbMock.Policy::name) - .collect(Collectors.toSet()); + return athenz.getOrCreateDomain(domain).policies.keySet(); } @Override -- cgit v1.2.3 From 869209d6cf7bfd59780a08faba7c7ee14da2029d Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Thu, 30 Sep 2021 09:53:16 +0200 Subject: Add helpers to check for existence of role/policy --- .../vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java index e3c0f47625f..2b784a75760 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java @@ -79,6 +79,10 @@ public class AthenzDbMock { isVespaTenant = true; } + public boolean hasRole(String name) { return roles.stream().anyMatch(r -> r.name.equals(name)); } + + public boolean hasPolicy(String name) { return policies.containsKey(name); } + } public static class Application { -- cgit v1.2.3 From 2a43b4c0e6ddbf9acd64f1ff07ba5d4d9340c26c Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Fri, 1 Oct 2021 09:55:07 +0200 Subject: Improve policy matching. Don't reuse 'admin' policy name --- .../api/integration/athenz/AthenzDbMock.java | 37 ++++++++-------------- .../api/integration/athenz/ZmsClientMock.java | 6 +--- .../restapi/application/ApplicationApiTest.java | 5 +-- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java index 2b784a75760..a9b20040f20 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java @@ -67,8 +67,8 @@ public class AthenzDbMock { return this; } - public Domain withPolicy(String principalRegex, String operation, String resource) { - policies.put("admin", new Policy("admin", principalRegex, operation, resource)); + public Domain withPolicy(String name, String principalRegex, String operation, String resource) { + policies.put(name, new Policy(name, principalRegex, operation, resource)); return this; } @@ -83,6 +83,9 @@ public class AthenzDbMock { public boolean hasPolicy(String name) { return policies.containsKey(name); } + public boolean checkAccess(AthenzIdentity principal, String action, String resource) { + return policies.values().stream().anyMatch(a -> a.matches(principal, action, resource)); + } } public static class Application { @@ -125,20 +128,12 @@ public class AthenzDbMock { return name; } - public boolean principalMatches(AthenzIdentity athenzIdentity) { - return assertions.get(0).principalMatches(athenzIdentity); - } - - public boolean actionMatches(String operation) { - return assertions.get(0).actionMatches(operation); - } - - public boolean resourceMatches(String resource) { - return assertions.get(0).resourceMatches(resource); + public boolean matches(String assertion) { + return assertions.stream().anyMatch(a -> a.matches(assertion)); } - public boolean hasAssertionMatching(String assertion) { - return assertions.stream().anyMatch(a -> a.asString().equals(assertion)); + public boolean matches(AthenzIdentity principal, String action, String resource) { + return assertions.stream().anyMatch(a -> a.matches(principal, action, resource)); } } @@ -157,17 +152,13 @@ public class AthenzDbMock { public Assertion(String role, String action, String resource) { this("grant", role, action, resource); } - public boolean principalMatches(AthenzIdentity athenzIdentity) { - return Pattern.compile(role).matcher(athenzIdentity.getFullName()).matches(); - } - - public boolean actionMatches(String operation) { - return Pattern.compile(action).matcher(operation).matches(); + public boolean matches(AthenzIdentity principal, String action, String resource) { + return Pattern.compile(this.role).matcher(principal.getFullName()).matches() + && Pattern.compile(this.action).matcher(action).matches() + && Pattern.compile(this.resource).matcher(resource).matches(); } - public boolean resourceMatches(String resource) { - return Pattern.compile(resource).matcher(resource).matches(); - } + public boolean matches(String assertion) { return asString().equals(assertion); } public String asString() { return String.format("%s %s to %s on %s", effect, action, role, resource).toLowerCase(); } 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 dd49f3a1e7c..b362a0c7a47 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 @@ -158,11 +158,7 @@ public class ZmsClientMock implements ZmsClient { return false; } else { AthenzDbMock.Domain domain = getDomainOrThrow(resource.getDomain(), false); - return domain.policies.values().stream() - .anyMatch(policy -> - policy.principalMatches(identity) && - policy.actionMatches(action) && - policy.resourceMatches(resource.getEntityName())); + return domain.checkAccess(identity, action, resource.getEntityName()); } } 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 596a0b186db..d2eb43d31f8 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 @@ -1479,7 +1479,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // Allow developer launch privilege to domain1.service. Deployment now completes. AthenzDbMock.Domain domainMock = tester.athenzClientFactory().getSetup().getOrCreateDomain(ATHENZ_TENANT_DOMAIN); - domainMock.withPolicy("user." + developer.id(), "launch", "service.service"); + domainMock.withPolicy("launch-" +developer.id(), "user." + developer.id(), "launch", "service.service"); tester.assertResponse(request("/application/v4/tenant/sandbox/application/myapp/instance/default/deploy/dev-us-east-1", POST) @@ -1757,7 +1757,8 @@ public class ApplicationApiTest extends ControllerContainerTest { */ private void allowLaunchOfService(com.yahoo.vespa.athenz.api.AthenzService service) { AthenzDbMock.Domain domainMock = tester.athenzClientFactory().getSetup().getOrCreateDomain(service.getDomain()); - domainMock.withPolicy(tester.controller().zoneRegistry().accessControlDomain().value()+".provider.*","launch", "service." + service.getName()); + String principalRegex = tester.controller().zoneRegistry().accessControlDomain().value() + ".provider.*"; + domainMock.withPolicy("provider-launch", principalRegex,"launch", "service." + service.getName()); } /** -- cgit v1.2.3