summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2019-03-22 10:33:40 +0100
committerMartin Polden <mpolden@mpolden.no>2019-03-22 10:57:00 +0100
commit4e17ebaea7a20506d5edd4d26baa68767a0c59b7 (patch)
tree26292a68cd00b691a6bd5332a09a0a30f268539a
parent519d59334ca8c3e314e71f83de618e375a7c2d6c (diff)
Define roles and policies
-rw-r--r--config-provisioning/abi-spec.json3
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/SystemName.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Action.java43
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Context.java74
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/PathGroup.java100
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Policy.java64
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Privilege.java99
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Role.java35
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/RoleMembership.java43
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/RoleMembershipTest.java89
10 files changed, 556 insertions, 1 deletions
diff --git a/config-provisioning/abi-spec.json b/config-provisioning/abi-spec.json
index 80680a4d095..6e110b708cf 100644
--- a/config-provisioning/abi-spec.json
+++ b/config-provisioning/abi-spec.json
@@ -707,7 +707,8 @@
"public static com.yahoo.config.provision.SystemName[] values()",
"public static com.yahoo.config.provision.SystemName valueOf(java.lang.String)",
"public static com.yahoo.config.provision.SystemName defaultSystem()",
- "public static com.yahoo.config.provision.SystemName from(java.lang.String)"
+ "public static com.yahoo.config.provision.SystemName from(java.lang.String)",
+ "public static java.util.Set all()"
],
"fields": [
"public static final enum com.yahoo.config.provision.SystemName dev",
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/SystemName.java b/config-provisioning/src/main/java/com/yahoo/config/provision/SystemName.java
index 891c213e9aa..2de11be08f2 100644
--- a/config-provisioning/src/main/java/com/yahoo/config/provision/SystemName.java
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/SystemName.java
@@ -1,6 +1,9 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.config.provision;
+import java.util.EnumSet;
+import java.util.Set;
+
/**
* Systems in hosted Vespa
*
@@ -39,4 +42,8 @@ public enum SystemName {
}
}
+ public static Set<SystemName> all() {
+ return EnumSet.allOf(SystemName.class);
+ }
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Action.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Action.java
new file mode 100644
index 00000000000..533c28905a9
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Action.java
@@ -0,0 +1,43 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.role;
+
+import com.yahoo.jdisc.http.HttpRequest;
+
+import java.util.EnumSet;
+
+/**
+ * Action defines an operation, typically a HTTP method, that may be performed on an entity in the controller
+ * (e.g. tenant or application).
+ *
+ * @author mpolden
+ */
+public enum Action {
+
+ create,
+ read,
+ update,
+ delete;
+
+ public static EnumSet<Action> all() {
+ return EnumSet.allOf(Action.class);
+ }
+
+ public static EnumSet<Action> write() {
+ return EnumSet.of(create, update, delete);
+ }
+
+ /** Returns the appropriate action for given HTTP method */
+ public static Action from(HttpRequest.Method method) {
+ switch (method) {
+ case POST: return Action.create;
+ case GET:
+ case OPTIONS:
+ case HEAD: return Action.read;
+ case PUT:
+ case PATCH: return Action.update;
+ case DELETE: return Action.delete;
+ default: throw new IllegalArgumentException("No action defined for method " + method);
+ }
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Context.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Context.java
new file mode 100644
index 00000000000..71948540667
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Context.java
@@ -0,0 +1,74 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.role;
+
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * The context in which a role is valid.
+ *
+ * @author mpolden
+ */
+public class Context {
+
+ private final Optional<TenantName> tenant;
+ private final Optional<ApplicationName> application;
+ private final SystemName system;
+
+ private Context(Optional<TenantName> tenant, Optional<ApplicationName> application, SystemName system) {
+ this.tenant = Objects.requireNonNull(tenant, "tenant must be non-null");
+ this.application = Objects.requireNonNull(application, "application must be non-null");
+ this.system = Objects.requireNonNull(system, "system must be non-null");
+ }
+
+ /** A specific tenant this is valid for, if any */
+ public Optional<TenantName> tenant() {
+ return tenant;
+ }
+
+ /** A specific application this is valid for, if any */
+ public Optional<ApplicationName> application() {
+ return application;
+ }
+
+ /** System in which this is valid */
+ public SystemName system() {
+ return system;
+ }
+
+ /** Returns an empty context in given system */
+ public static Context empty(SystemName system) {
+ return new Context(Optional.empty(), Optional.empty(), system);
+ }
+
+ /** Returns context for a tenant and an optional application in system */
+ public static Context of(TenantName tenant, Optional<ApplicationName> application, SystemName system) {
+ return new Context(Optional.of(tenant), application, system);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Context context = (Context) o;
+ return tenant.equals(context.tenant) &&
+ application.equals(context.application) &&
+ system == context.system;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tenant, application, system);
+ }
+
+ @Override
+ public String toString() {
+ return "tenant " + tenant.map(TenantName::value).orElse("[none]") + ", application " +
+ application.map(ApplicationName::value).orElse("[none]") + ", system " + system;
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/PathGroup.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/PathGroup.java
new file mode 100644
index 00000000000..6ccc2112010
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/PathGroup.java
@@ -0,0 +1,100 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.role;
+
+import com.yahoo.restapi.Path;
+
+import java.util.EnumSet;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * This declares and groups all known REST API paths in the controller.
+ *
+ * When creating a new API, its paths must be added here and a policy must be declared in {@link Policy}.
+ *
+ * @author mpolden
+ */
+public enum PathGroup {
+
+ /** Paths used for system management by operators */
+ operator("/controller/v1/{*}",
+ "/provision/v2/{*}",
+ "/flags/v1/{*}",
+ "/os/v1/{*}",
+ "/cost/v1/{*}",
+ "/zone/v2/{*}",
+ "/nodes/v2/{*}",
+ "/orchestrator/v1/{*}"),
+
+ /** Paths used when onboarding and creating a new tenants */
+ onboardingUser("/application/v4/user"),
+
+ onboardingTenant("/application/v4/tenant/{ignored}"),
+
+
+ /** Paths used by tenant/application administrators */
+ tenant("/athenz/v1/",
+ "/athenz/v1/domains",
+ "/application/v4/",
+ "/application/v4/athensDomain/",
+ "/application/v4/property/",
+ "/application/v4/tenant/",
+ "/application/v4/tenant-pipeline/",
+ "/application/v4/tenant/{tenant}",
+ "/application/v4/tenant/tenant1/application/",
+ "/application/v4/tenant/{tenant}/application/{application}",
+ "/application/v4/tenant/{tenant}/application/{application}/deploying/{*}",
+ "/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{job}/{*}",
+ "/application/v4/tenant/{tenant}/application/{application}/environment/dev/{*}",
+ "/application/v4/tenant/{tenant}/application/{application}/environment/perf/{*}",
+ "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation/override"),
+
+ /**
+ * Paths used for deployments by build service(s). Note that context is ignored in these paths as build service
+ * roles are not granted in specific contexts.
+ */
+ buildService("/zone/v1/{*}",
+ "/application/v4/tenant/{ignored}/application/{ignored2}/jobreport",
+ "/application/v4/tenant/{ignored}/application/{ignored2}/submit",
+ "/application/v4/tenant/{ignored}/application/{ignored2}/promote",
+ "/application/v4/tenant/{ignored}/application/{ignored2}/environment/prod/{*}",
+ "/application/v4/tenant/{ignored}/application/{ignored2}/environment/test/{*}",
+ "/application/v4/tenant/{ignored}/application/{ignored2}/environment/staging/{*}"),
+
+ /** Paths providing information about deployment status */
+ deployment("/badge/v1/{*}",
+ "/deployment/v1/{*}");
+
+ private final Set<String> pathSpecs;
+
+ PathGroup(String... pathSpecs) {
+ this.pathSpecs = Set.of(pathSpecs);
+ }
+
+ /** Returns path if it matches any spec in this group */
+ private Optional<Path> get(String path) {
+ return Optional.of(path).map(Path::new).filter(p -> pathSpecs.stream().anyMatch(p::matches));
+ }
+
+ /** All known path groups */
+ public static Set<PathGroup> all() {
+ return EnumSet.allOf(PathGroup.class);
+ }
+
+ /** Returns whether this group matches path in given context */
+ public boolean matches(String path, Context context) {
+ return get(path).filter(p -> {
+ boolean match = true;
+ String tenant = p.get("tenant");
+ if (tenant != null && context.tenant().isPresent()) {
+ match = context.tenant().get().value().equals(tenant);
+ }
+ String application = p.get("application");
+ if (application != null && context.application().isPresent()) {
+ match &= context.application().get().value().equals(application);
+ }
+ return match;
+ }).isPresent();
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Policy.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Policy.java
new file mode 100644
index 00000000000..968f6e39797
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Policy.java
@@ -0,0 +1,64 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.role;
+
+import com.yahoo.config.provision.SystemName;
+
+import java.util.Set;
+
+/**
+ * Policies for REST APIs in the controller. A policy is only considered when defined in a {@link Role}.
+ *
+ * @author mpolden
+ */
+public enum Policy {
+
+ /** Operator policy allows access to everything in all systems */
+ operator(Privilege.grant(Action.all())
+ .on(PathGroup.all())
+ .in(SystemName.all())),
+
+ /**
+ * Tenant policy allows tenants to access their own tenant, in all systems, and allows global read access in
+ * selected systems
+ */
+ tenant(Privilege.grant(Action.all())
+ .on(PathGroup.tenant)
+ .in(SystemName.all()),
+ Privilege.grant(Action.read)
+ .on(PathGroup.all())
+ .in(SystemName.main, SystemName.cd, SystemName.dev)),
+
+ /** Build service policy only allows access relevant for build service(s) */
+ buildService(Privilege.grant(Action.all())
+ .on(PathGroup.buildService)
+ .in(SystemName.all())),
+
+ /** Unauthorized policy allows creation of tenants and read of everything in selected systems */
+ unauthorized(Privilege.grant(Action.update)
+ .on(PathGroup.onboardingUser)
+ .in(SystemName.main, SystemName.cd, SystemName.dev),
+ Privilege.grant(Action.create)
+ .on(PathGroup.onboardingTenant)
+ .in(SystemName.main, SystemName.cd, SystemName.dev),
+ Privilege.grant(Action.read)
+ .on(PathGroup.all())
+ .in(SystemName.main, SystemName.cd, SystemName.dev),
+ Privilege.grant(Action.read)
+ .on(PathGroup.deployment)
+ .in(SystemName.all()));
+
+ private final Set<Privilege> privileges;
+
+ Policy(Privilege... privileges) {
+ this.privileges = Set.of(privileges);
+ }
+
+ /** Returns whether action is allowed on path in given context */
+ public boolean evaluate(Action action, String path, Context context) {
+ return privileges.stream().anyMatch(privilege -> privilege.actions().contains(action) &&
+ privilege.systems().contains(context.system()) &&
+ privilege.pathGroups().stream()
+ .anyMatch(pg -> pg.matches(path, context)));
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Privilege.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Privilege.java
new file mode 100644
index 00000000000..4c5ad136f56
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Privilege.java
@@ -0,0 +1,99 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.role;
+
+import com.yahoo.config.provision.SystemName;
+
+import java.util.EnumSet;
+import java.util.LinkedHashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This describes a privilege in the controller. A privilege expresses the actions (e.g. create or read) granted
+ * for a particular group of REST API paths. A privilege is valid in one or more systems.
+ *
+ * @author mpolden
+ */
+public class Privilege {
+
+ private final Set<SystemName> systems;
+ private final Set<Action> actions;
+ private final Set<PathGroup> pathGroups;
+
+ private Privilege(Set<SystemName> systems, Set<Action> actions, Set<PathGroup> pathGroups) {
+ this.systems = EnumSet.copyOf(Objects.requireNonNull(systems, "system must be non-null"));
+ this.actions = EnumSet.copyOf(Objects.requireNonNull(actions, "actions must be non-null"));
+ this.pathGroups = EnumSet.copyOf(Objects.requireNonNull(pathGroups, "pathGroups must be non-null"));
+ if (systems.isEmpty()) {
+ throw new IllegalArgumentException("systems must be non-empty");
+ }
+ }
+
+ /** Systems where this applies */
+ public Set<SystemName> systems() {
+ return systems;
+ }
+
+ /** Actions allowed by this */
+ public Set<Action> actions() {
+ return actions;
+ }
+
+ /** Path groups where this applies */
+ public Set<PathGroup> pathGroups() {
+ return pathGroups;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Privilege privilege = (Privilege) o;
+ return systems.equals(privilege.systems) &&
+ actions.equals(privilege.actions) &&
+ pathGroups.equals(privilege.pathGroups);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(systems, actions, pathGroups);
+ }
+
+ public static PrivilegeBuilder grant(Action... actions) {
+ return grant(Set.of(actions));
+ }
+
+ public static PrivilegeBuilder grant(Set<Action> actions) {
+ return new PrivilegeBuilder(actions);
+ }
+
+ public static class PrivilegeBuilder {
+
+ private Set<Action> actions;
+ private Set<PathGroup> pathGroups;
+
+ private PrivilegeBuilder(Set<Action> actions) {
+ this.actions = EnumSet.copyOf(actions);
+ this.pathGroups = new LinkedHashSet<>();
+ }
+
+ public PrivilegeBuilder on(PathGroup... pathGroups) {
+ return on(Set.of(pathGroups));
+ }
+
+ public PrivilegeBuilder on(Set<PathGroup> pathGroups) {
+ this.pathGroups.addAll(pathGroups);
+ return this;
+ }
+
+ public Privilege in(SystemName... systems) {
+ return in(Set.of(systems));
+ }
+
+ public Privilege in(Set<SystemName> systems) {
+ return new Privilege(systems, actions, pathGroups);
+ }
+
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Role.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Role.java
new file mode 100644
index 00000000000..6d1f8287405
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/Role.java
@@ -0,0 +1,35 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.role;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+/**
+ * This declares all tenant roles known to the controller. A role contains one or more {@link Policy}'s which decide
+ * what actions a member of a role can perform.
+ *
+ * @author mpolden
+ */
+public enum Role {
+
+ hostedOperator(Policy.operator),
+ tenantAdmin(Policy.tenant),
+ tenantPipelineOperator(Policy.buildService),
+ everyone(Policy.unauthorized);
+
+ private final Set<Policy> policies;
+
+ Role(Policy... policies) {
+ this.policies = EnumSet.copyOf(Set.of(policies));
+ }
+
+ /**
+ * Returns whether this role is allowed to perform action in given role context. Action is allowed if at least one
+ * policy evaluates to true.
+ */
+ public boolean allow(Action action, String path, Context context) {
+ return policies.stream().anyMatch(policy -> policy.evaluate(action, path, context));
+ }
+
+}
+
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/RoleMembership.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/RoleMembership.java
new file mode 100644
index 00000000000..167c1128dbe
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/role/RoleMembership.java
@@ -0,0 +1,43 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.role;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A list of roles and their associated contexts. This defines the role membership of a tenant, and in which contexts
+ * (see {@link Context}) those roles apply.
+ *
+ * @author mpolden
+ */
+public class RoleMembership {
+
+ private final Map<Role, Set<Context>> roles;
+
+ public RoleMembership(Map<Role, Set<Context>> roles) {
+ this.roles = Map.copyOf(roles);
+ }
+
+ /** Returns whether any role in this allows action to take place in path */
+ public boolean allow(Action action, String path) {
+ return roles.entrySet().stream().anyMatch(kv -> {
+ Role role = kv.getKey();
+ Set<Context> contexts = kv.getValue();
+ return contexts.stream().anyMatch(context -> role.allow(action, path, context));
+ });
+ }
+
+ @Override
+ public String toString() {
+ return "roles " + roles;
+ }
+
+ /**
+ * A role resolver. Identity providers can implement this to translate their internal representation of role
+ * membership to a {@link RoleMembership}.
+ */
+ public interface Resolver {
+ RoleMembership membership();
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/RoleMembershipTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/RoleMembershipTest.java
new file mode 100644
index 00000000000..051860331ee
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/RoleMembershipTest.java
@@ -0,0 +1,89 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.role;
+
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author mpolden
+ */
+public class RoleMembershipTest {
+
+ @Test
+ public void operator_membership() {
+ RoleMembership roles = new RoleMembership(Map.of(Role.hostedOperator, Set.of(Context.empty(SystemName.main))));
+
+ // Operator actions
+ assertFalse(roles.allow(Action.create, "/not/explictly/defined"));
+ assertTrue(roles.allow(Action.create, "/controller/v1/foo"));
+ assertTrue(roles.allow(Action.update, "/os/v1/bar"));
+ assertTrue(roles.allow(Action.update, "/application/v4/tenant/t1/application/a1"));
+ assertTrue(roles.allow(Action.update, "/application/v4/tenant/t2/application/a2"));
+ }
+
+ @Test
+ public void tenant_membership() {
+ RoleMembership roles = new RoleMembership(Map.of(Role.tenantAdmin,
+ Set.of(Context.of(TenantName.from("t1"),
+ Optional.of(ApplicationName.from("a1")),
+ SystemName.main))));
+
+ assertFalse(roles.allow(Action.create, "/not/explictly/defined"));
+ assertFalse("Deny access to operator API", roles.allow(Action.create, "/controller/v1/foo"));
+ assertFalse("Deny access to other tenant and app", roles.allow(Action.update, "/application/v4/tenant/t2/application/a2"));
+ assertFalse("Deny access to other app", roles.allow(Action.update, "/application/v4/tenant/t1/application/a2"));
+ assertTrue(roles.allow(Action.update, "/application/v4/tenant/t1/application/a1"));
+ assertTrue("Global read access", roles.allow(Action.read, "/controller/v1/foo"));
+
+ RoleMembership multiContext = new RoleMembership(Map.of(Role.tenantAdmin,
+ Set.of(Context.of(TenantName.from("t1"),
+ Optional.of(ApplicationName.from("a1")),
+ SystemName.main),
+ Context.of(TenantName.from("t2"),
+ Optional.of(ApplicationName.from("a2")),
+ SystemName.main))));
+ assertFalse("Deny access to other tenant and app", multiContext.allow(Action.update, "/application/v4/tenant/t3/application/a3"));
+ assertTrue(multiContext.allow(Action.update, "/application/v4/tenant/t2/application/a2"));
+ assertTrue(multiContext.allow(Action.update, "/application/v4/tenant/t1/application/a1"));
+ assertTrue("Global read access", roles.allow(Action.read, "/controller/v1/foo"));
+
+ RoleMembership publicSystem = new RoleMembership(Map.of(Role.tenantAdmin,
+ Set.of(Context.of(TenantName.from("t1"),
+ Optional.of(ApplicationName.from("a1")),
+ SystemName.vaas))));
+ assertFalse(publicSystem.allow(Action.read, "/controller/v1/foo"));
+ assertTrue(multiContext.allow(Action.update, "/application/v4/tenant/t1/application/a1"));
+ }
+
+ @Test
+ public void build_service_membership() {
+ RoleMembership roles = new RoleMembership(Map.of(Role.tenantPipelineOperator, Set.of(Context.empty(SystemName.main))));
+ assertFalse(roles.allow(Action.create, "/not/explictly/defined"));
+ assertFalse(roles.allow(Action.update, "/application/v4/tenant/t1/application/a1"));
+ assertTrue(roles.allow(Action.create, "/application/v4/tenant/t1/application/a1/jobreport"));
+ assertFalse("No global read access", roles.allow(Action.read, "/controller/v1/foo"));
+ }
+
+ @Test
+ public void multi_role_membership() {
+ RoleMembership roles = new RoleMembership(Map.of(Role.tenantAdmin, Set.of(Context.of(TenantName.from("t1"),
+ Optional.of(ApplicationName.from("a1")),
+ SystemName.main)),
+ Role.tenantPipelineOperator, Set.of(Context.empty(SystemName.main))));
+ assertFalse(roles.allow(Action.create, "/not/explictly/defined"));
+ assertFalse(roles.allow(Action.create,"/controller/v1/foo"));
+ assertTrue(roles.allow(Action.create, "/application/v4/tenant/t1/application/a1/jobreport"));
+ assertTrue(roles.allow(Action.update, "/application/v4/tenant/t1/application/a1"));
+ assertTrue("Global read access", roles.allow(Action.read, "/controller/v1/foo"));
+ }
+
+}