diff options
author | Jon Marius Venstad <jonmv@users.noreply.github.com> | 2019-04-01 11:04:29 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-01 11:04:29 +0200 |
commit | ae909b3a59e2f947b94a243ea62d34a4392ad2d0 (patch) | |
tree | 49f6f323b0334f2dce7e04987244efc761f040e6 | |
parent | b012478bd6aed5cf4d2f62854b6d800b8c020b0e (diff) | |
parent | 65c0ced3e0c6ed15c563636dc20fc6ec9957b2b2 (diff) |
Merge pull request #8944 from vespa-engine/jvenstad/more-general-roles
Jvenstad/more general roles
13 files changed, 475 insertions, 141 deletions
diff --git a/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java b/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java index f1eb03d88a9..8858465e846 100644 --- a/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java +++ b/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java @@ -23,7 +23,11 @@ import org.json.JSONObject; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.io.PrintStream; import java.io.ByteArrayOutputStream; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolver.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolver.java index 8a5e70af6c0..21c9875bb8b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolver.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolver.java @@ -93,7 +93,7 @@ public class AthenzRoleResolver implements RoleMembership.Resolver { memberships.add(Role.hostedOperator); } if (tenant.isPresent() && isTenantAdmin(identity, tenant.get())) { - memberships.add(Role.tenantAdmin).limitedTo(tenant.get().name()); + memberships.add(Role.athenzTenantAdmin).limitedTo(tenant.get().name()); } AthenzDomain principalDomain = identity.getDomain(); if (principalDomain.equals(SCREWDRIVER_DOMAIN)) { @@ -102,11 +102,11 @@ public class AthenzRoleResolver implements RoleMembership.Resolver { if (tenant.get() instanceof AthenzTenant) { AthenzDomain tenantDomain = ((AthenzTenant) tenant.get()).domain(); if (hasDeployerAccess(identity, tenantDomain, application.get())) { - memberships.add(Role.tenantPipelineOperator).limitedTo(tenant.get().name(), application.get()); + memberships.add(Role.tenantPipeline).limitedTo(tenant.get().name(), application.get()); } } else { - memberships.add(Role.tenantPipelineOperator).limitedTo(tenant.get().name(), application.get()); + memberships.add(Role.tenantPipeline).limitedTo(tenant.get().name(), application.get()); } } } 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 index 3776b1d2a13..e3d81e04591 100644 --- 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 @@ -17,10 +17,8 @@ import java.util.Set; */ public enum PathGroup { - /** Paths used for system management by operators */ + /** Paths used for system management by operators. */ operator("/controller/v1/{*}", - "/cost/v1/{*}", - "/deployment/v1/{*}", "/flags/v1/{*}", "/nodes/v2/{*}", "/orchestrator/v1/{*}", @@ -28,57 +26,84 @@ public enum PathGroup { "/provision/v2/{*}", "/zone/v2/{*}"), - /** Paths used for creating user tenants */ - onboardingUser("/application/v4/user"), - - /** Paths used for creating tenants with access control */ - onboarding("/application/v4/tenant/{ignored}"), - - /** Read-only paths used when onboarding tenants */ - onboardingInfo("/athenz/v1/", - "/athenz/v1/domains", - "/athenz/v1/properties"), - - /** Paths used for user management */ + /** Paths used for user management. */ userManagement("/user/v1/{*}"), // TODO probably add tenant and application levels. - /** Paths used by tenant administrators */ - tenantInfo("/application/v4/", // TODO move - "/application/v4/tenant/"), // TODO move + /** Paths used for creating user tenants. */ + user("/application/v4/user"), - /** Paths used by tenant administrators */ + /** Paths used for creating tenants with proper access control. */ tenant(Matcher.tenant, - "/application/v4/tenant/{tenant}", - "/application/v4/tenant/{tenant}/application/"), + "/application/v4/tenant/{tenant}"), - /** Paths used by application administrators */ + /** Paths used by tenant administrators. */ + tenantInfo(Matcher.tenant, + "/application/v4/tenant/{tenant}/application/"), + + /** Path for the base application resource. */ application(Matcher.tenant, Matcher.application, - "/application/v4/tenant/{tenant}/application/{application}", - "/application/v4/tenant/{tenant}/application/{application}/deploying/{*}", - "/application/v4/tenant/{tenant}/application/{application}/instance/{*}", - "/application/v4/tenant/{tenant}/application/{application}/environment/dev/{*}", - "/application/v4/tenant/{tenant}/application/{application}/environment/perf/{*}", - "/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{instance}/global-rotation/override"), - - /** Paths used for deployments by build service(s) */ + "/application/v4/tenant/{tenant}/application/{application}"), + + /** Paths used by application administrators. */ + applicationInfo(Matcher.tenant, + Matcher.application, + "/application/v4/tenant/{tenant}/application/{application}/deploying/{*}", + "/application/v4/tenant/{tenant}/application/{application}/instance/{*}", + "/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{instance}/logs", + "/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{instance}/suspended", + "/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{instance}/service/{*}", + "/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{instance}/global-rotation/{*}"), + + /** Path used to restart application nodes. */ // TODO move to the above when everyone is on new pipeline. + applicationRestart(Matcher.tenant, + Matcher.application, + "/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{ignored}/restart"), + + /** Paths used for development deployments. */ + developmentDeployment(Matcher.tenant, + Matcher.application, + "/application/v4/tenant/{tenant}/application/{application}/environment/dev/region/{region}/instance/{instance}", + "/application/v4/tenant/{tenant}/application/{application}/environment/dev/region/{region}/instance/{instance}/deploy", + "/application/v4/tenant/{tenant}/application/{application}/environment/perf/region/{region}/instance/{instance}", + "/application/v4/tenant/{tenant}/application/{application}/environment/perf/region/{region}/instance/{instance}/deploy"), + + /** Paths used for production deployments. */ + productionDeployment(Matcher.tenant, + Matcher.application, + "/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{instance}", + "/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{instance}/deploy", + "/application/v4/tenant/{tenant}/application/{application}/environment/test/region/{region}/instance/{instance}", + "/application/v4/tenant/{tenant}/application/{application}/environment/test/region/{region}/instance/{instance}/deploy", + "/application/v4/tenant/{tenant}/application/{application}/environment/staging/region/{region}/instance/{instance}", + "/application/v4/tenant/{tenant}/application/{application}/environment/staging/region/{region}/instance/{instance}/deploy"), + + /** Paths used for continuous deployment to production. */ + submission(Matcher.tenant, + Matcher.application, + "/application/v4/tenant/{tenant}/application/{application}/submit"), + + /** Paths used for other tasks by build services. */ // TODO: This will vanish. buildService(Matcher.tenant, Matcher.application, "/application/v4/tenant/{tenant}/application/{application}/jobreport", - "/application/v4/tenant/{tenant}/application/{application}/submit", "/application/v4/tenant/{tenant}/application/{application}/promote", - "/application/v4/tenant/{tenant}/application/{application}/environment/prod/{*}", - "/application/v4/tenant/{tenant}/application/{application}/environment/test/{*}", - "/application/v4/tenant/{tenant}/application/{application}/environment/staging/{*}"), - - /** Read-only paths providing information related to deployments */ - deploymentStatus("/badge/v1/{*}", - "/zone/v1/{*}"), - - /** Paths used by some dashboard */ - dashboard("/", - "/d/{*}", - "/statuspage/v1/{*}"); + "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/promote"), + + /** Paths which contain (not very strictly) classified information about, e.g., customers. */ + classifiedInfo("/athenz/v1/{*}", + "/cost/v1/{*}", + "/deployment/v1/{*}", + "/application/v4/", + "/application/v4/tenant/", + "/", + "/d/{*}", + "/statuspage/v1/{*}" + ), + + /** Paths providing public information. */ + publicInfo("/badge/v1/{*}", + "/zone/v1/{*}"); final List<String> pathSpecs; final List<Matcher> matchers; @@ -118,11 +143,11 @@ public enum PathGroup { public boolean matches(String path, Context context) { return get(path).map(p -> { boolean match = true; - String tenant = p.get("tenant"); + String tenant = p.get(Matcher.tenant.name); if (tenant != null && context.tenant().isPresent()) { match = context.tenant().get().value().equals(tenant); } - String application = p.get("application"); + String application = p.get(Matcher.application.name); if (application != null && context.application().isPresent()) { match &= context.application().get().value().equals(application); } @@ -138,8 +163,12 @@ public enum PathGroup { application("{application}"); final String pattern; + final String name; - Matcher(String pattern) { this.pattern = pattern; } + Matcher(String pattern) { + this.pattern = pattern; + this.name = pattern.substring(1, pattern.length() - 1); + } } 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 index 9c09cb193da..85702ac1b89 100644 --- 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 @@ -1,51 +1,111 @@ // 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.Set; /** * Policies for REST APIs in the controller. A policy is only considered when defined in a {@link Role}. + * A policy describes a set of {@link Privilege}s, which are valid for a set of {@link SystemName}s. + * A policy is evaluated with a {@link Context}, which provides the {@link SystemName} the policy is + * evaluated in, and any limitations to a specific {@link TenantName} or {@link ApplicationName}. * * @author mpolden */ public enum Policy { - /** Operator policy allows access to everything in all systems */ + /** Full access to everything. */ 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.tenantInfo, PathGroup.tenant, PathGroup.application) - .in(SystemName.all())), - - /** 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.onboarding) - .in(SystemName.main, SystemName.cd, SystemName.dev), // TODO System.all() - Privilege.grant(Action.read) - .on(PathGroup.onboardingInfo) - .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.deploymentStatus) - .in(SystemName.all())); + /** Full access to user management in select systems. */ + manager(Privilege.grant(Action.all()) + .on(PathGroup.userManagement) + .in(SystemName.Public)), + + /** Access to create a user tenant in select systems. */ + userCreate(Privilege.grant(Action.update) + .on(PathGroup.user) + .in(SystemName.main, SystemName.cd, SystemName.dev)), + + /** Access to create a tenant in select systems. */ + tenantCreate(Privilege.grant(Action.create) + .on(PathGroup.tenant) + .in(SystemName.main, SystemName.cd, SystemName.dev)), // TODO SystemName.all() + + /** Full access to tenant information and settings. */ + tenantWrite(Privilege.grant(Action.write()) + .on(PathGroup.tenant) + .in(SystemName.all())), + + /** Read access to tenant information and settings. */ + tenantRead(Privilege.grant(Action.read) + .on(PathGroup.tenant, PathGroup.tenantInfo) + .in(SystemName.all())), + + /** Access to create application under a certain tenant. */ + applicationCreate(Privilege.grant(Action.create) + .on(PathGroup.application) + .in(SystemName.all())), + + /** Read access to application information and settings. */ + applicationRead(Privilege.grant(Action.read) + .on(PathGroup.application, PathGroup.applicationInfo) + .in(SystemName.all())), + + /** Read access to application information and settings. */ + applicationUpdate(Privilege.grant(Action.update) + .on(PathGroup.application, PathGroup.applicationInfo) + .in(SystemName.all())), + + /** Access to delete a certain application. */ + applicationDelete(Privilege.grant(Action.delete) + .on(PathGroup.application) + .in(SystemName.all())), + + /** Full access to application information and settings. */ + applicationOperations(Privilege.grant(Action.write()) + .on(PathGroup.applicationInfo, PathGroup.applicationRestart) + .in(SystemName.all())), + + /** Full access to application development deployments. */ + developmentDeployment(Privilege.grant(Action.all()) + .on(PathGroup.developmentDeployment) + .in(SystemName.all())), + + /** Full access to application production deployments. */ + productionDeployment(Privilege.grant(Action.all()) + .on(PathGroup.productionDeployment) + .in(SystemName.all())), + + /** Read access to all application deployments. */ + deploymentRead(Privilege.grant(Action.read) + .on(PathGroup.developmentDeployment, PathGroup.productionDeployment) + .in(SystemName.all())), + + /** Full access to submissions for continuous deployment. */ + submission(Privilege.grant(Action.all()) + .on(PathGroup.submission) + .in(SystemName.all())), + + /** Full access to the additional tasks needed for continuous deployment. */ + deploymentPipeline(Privilege.grant(Action.all()) // TODO remove when everyone is on new pipeline. + .on(PathGroup.buildService, PathGroup.applicationRestart) + .in(SystemName.all())), + + /** Read access to all information in select systems. */ + classifiedRead(Privilege.grant(Action.read) + .on(PathGroup.all()) + .in(SystemName.main, SystemName.cd, SystemName.dev)), + + /** Read access to public info. */ + publicRead(Privilege.grant(Action.read) + .on(PathGroup.publicInfo) + .in(SystemName.all())); private final Set<Privilege> privileges; 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 index 01d1c7e0eb8..cae143a92a2 100644 --- 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 @@ -5,17 +5,70 @@ 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 + * 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. * + * Optionally, some role definition also inherit all policies from a "lower ranking" role. Read the list of roles + * from {@code everyone} to {@code tenantAdmin}, in order, to see what policies these roles. + * * @author mpolden + * @author jonmv */ public enum Role { + /** Deus ex machina. */ hostedOperator(Policy.operator), - tenantAdmin(Policy.tenant), - tenantPipelineOperator(Policy.buildService), - everyone(Policy.unauthorized); + + /** Build service which may submit new applications for continuous deployment. */ + buildService(Policy.submission, + Policy.applicationRead), + + /** Base role which every user is part of. */ + everyone(Policy.classifiedRead, + Policy.publicRead, + Policy.userCreate, + Policy.tenantCreate), + + /** Application reader which can see all information about an application, its tenant and deployments. */ + applicationReader(everyone, + Policy.tenantRead, + Policy.applicationRead, + Policy.deploymentRead), + + /** Application developer with access to deploy to development zones. */ + applicationDeveloper(applicationReader, + Policy.developmentDeployment), + + /** Application operator with access to normal, operational tasks of an application. */ + applicationOperator(applicationDeveloper, + Policy.applicationOperations), + + /** Application administrator with full access to an already existing application, including emergency operations. */ + applicationAdmin(applicationOperator, + Policy.applicationUpdate, + Policy.productionDeployment, + Policy.submission), + + /** Tenant admin with full access to all tenant resources, including the ability to create new applications. */ + tenantAdmin(applicationAdmin, + Policy.applicationCreate, + Policy.applicationDelete, + Policy.manager, + Policy.tenantWrite), + + /** Build and continuous delivery service. */ // TODO replace with buildService, when everyone is on new pipeline. + tenantPipeline(Policy.submission, + Policy.deploymentPipeline, + Policy.productionDeployment), + + /** Tenant administrator with full access to all child resources. */ + athenzTenantAdmin(Policy.tenantWrite, + Policy.tenantRead, + Policy.applicationCreate, + Policy.applicationUpdate, + Policy.applicationDelete, + Policy.applicationOperations, + Policy.developmentDeployment); // TODO remove, as it is covered by applicationAdmin. private final Set<Policy> policies; @@ -23,6 +76,11 @@ public enum Role { this.policies = EnumSet.copyOf(Set.of(policies)); } + Role(Role inherited, Policy... policies) { + this.policies = EnumSet.copyOf(Set.of(policies)); + this.policies.addAll(inherited.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. diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java index ecf849fc68c..ca8a2e34fdf 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalDeploymentTester.java @@ -89,10 +89,7 @@ public class InternalDeploymentTester { * Submits a new application, and returns the version of the new submission. */ public ApplicationVersion newSubmission() { - ApplicationVersion version = jobs.submit(appId, BuildJob.defaultSourceRevision, "a@b", 2, applicationPackage, new byte[0]); - tester.applicationStore().put(appId, version, applicationPackage.zippedContent()); - tester.applicationStore().put(testerId, version, new byte[0]); - return version; + return jobs.submit(appId, BuildJob.defaultSourceRevision, "a@b", 2, applicationPackage, new byte[0]); } /** 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 26cf9c3127e..e8d5c8b8ca4 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 @@ -453,6 +453,11 @@ public class ApplicationApiTest extends ControllerContainerTest { // POST a 'restart application' command tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/default/restart", POST) + .userIdentity(USER_ID), + "Requested restart of tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/default"); + + // POST a 'restart application' command + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/default/restart", POST) .screwdriverIdentity(SCREWDRIVER_ID), "Requested restart of tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/default"); @@ -539,10 +544,28 @@ public class ApplicationApiTest extends ControllerContainerTest { "to the old pipeline, please file a ticket at yo/vespa-support and request this.\"}", 400); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/default/job/production-us-west-1", DELETE) + // GET deployment job overview, after triggering system and staging test jobs. + assertEquals(2, tester.controller().applications().deploymentTrigger().triggerReadyJobs()); + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/default/job", GET) + .userIdentity(USER_ID), + new File("jobs.json")); + + // GET system test job overview. + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/default/job/system-test", GET) + .userIdentity(USER_ID), + new File("system-test-job.json")); + + // GET system test run 1 details. + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/default/job/system-test/run/1", GET) + .userIdentity(USER_ID), + new File("system-test-details.json")); + + // DELETE a running job to have it aborted. + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/default/job/staging-test", DELETE) .userIdentity(USER_ID), - "{\"message\":\"Nothing to abort.\"}"); + "{\"message\":\"Aborting run 1 of stagingTest for tenant1.application1\"}"); + // DELETE submission to unsubscribe from continuous deployment. tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit", DELETE) .userIdentity(HOSTED_VESPA_OPERATOR), "{\"message\":\"Unregistered 'tenant1.application1' from internal deployment pipeline.\"}"); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json new file mode 100644 index 00000000000..1c8da68e138 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json @@ -0,0 +1,130 @@ +{ + "lastVersions": { + "platform": { + "platform": "6.1", + "at": 0, + "pending": "Waiting for current deployment to complete" + }, + "application": { + "application": { + "hash": "1.0.44-d00d", + "build": 44, + "source": { + "gitRepository": "repo", + "gitBranch": "master", + "gitCommit": "d00d" + } + }, + "at": (ignore), + "deploying": "0 of 1 complete" + } + }, + "deploying": { + "application": { + "hash": "1.0.44-d00d", + "build": 44, + "source": { + "gitRepository": "repo", + "gitBranch": "master", + "gitCommit": "d00d" + } + } + }, + "deployments": [ + {} + ], + "jobs": { + "system-test": { + "runs": [ + { + "id": 1, + "status": "running", + "start": (ignore), + "wantedPlatform": "6.1", + "wantedApplication": { + "hash": "1.0.44-d00d", + "build": 44, + "source": { + "gitRepository": "repo", + "gitBranch": "master", + "gitCommit": "d00d" + } + }, + "steps": { + "deployReal": "unfinished", + "installReal": "unfinished", + "deployTester": "unfinished", + "installTester": "unfinished", + "startTests": "unfinished", + "endTests": "unfinished", + "deactivateReal": "unfinished", + "deactivateTester": "unfinished", + "report": "unfinished" + }, + "tasks": { + "deploy": "running" + }, + "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/default/job/system-test/run/1" + } + ], + "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/default/job/system-test" + }, + "staging-test": { + "runs": [ + { + "id": 1, + "status": "running", + "start": (ignore), + "wantedPlatform": "6.1", + "wantedApplication": { + "hash": "1.0.44-d00d", + "build": 44, + "source": { + "gitRepository": "repo", + "gitBranch": "master", + "gitCommit": "d00d" + } + }, + "steps": { + "deployInitialReal": "unfinished", + "installInitialReal": "unfinished", + "deployReal": "unfinished", + "installReal": "unfinished", + "deployTester": "unfinished", + "installTester": "unfinished", + "startTests": "unfinished", + "endTests": "unfinished", + "deactivateReal": "unfinished", + "deactivateTester": "unfinished", + "report": "unfinished" + }, + "tasks": {}, + "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/default/job/staging-test/run/1" + } + ], + "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/default/job/staging-test" + }, + "us-west-1": { + "runs": [ + { + "status": "pending", + "wantedPlatform": "6.1", + "wantedApplication": { + "hash": "1.0.44-d00d", + "build": 44, + "source": { + "gitRepository": "repo", + "gitBranch": "master", + "gitCommit": "d00d" + } + }, + "tasks": { + "system-test": "running", + "staging-test": "running" + } + } + ], + "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/default/job/production-us-west-1" + } + } +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-details.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-details.json new file mode 100644 index 00000000000..98d4e2db612 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-details.json @@ -0,0 +1,4 @@ +{ + "active": true, + "log": {} +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json new file mode 100644 index 00000000000..71f647ead11 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json @@ -0,0 +1,32 @@ +{ + "1": { + "id": 1, + "status": "running", + "start": (ignore), + "wantedPlatform": "6.1", + "wantedApplication": { + "hash": "1.0.44-d00d", + "build": 44, + "source": { + "gitRepository": "repo", + "gitBranch": "master", + "gitCommit": "d00d" + } + }, + "steps": { + "deployReal": "unfinished", + "installReal": "unfinished", + "deployTester": "unfinished", + "installTester": "unfinished", + "startTests": "unfinished", + "endTests": "unfinished", + "deactivateReal": "unfinished", + "deactivateTester": "unfinished", + "report": "unfinished" + }, + "tasks": { + "deploy": "running" + }, + "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/default/job/system-test/run/1" + } +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolverTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolverTest.java index 5c86301d037..4628b95ad3c 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolverTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleResolverTest.java @@ -92,29 +92,29 @@ public class AthenzRoleResolverTest { // Operators and tenant admins are tenant admins of their tenants. assertEquals(Set.of(Context.limitedTo(TENANT, tester.controller().system())), - resolver.membership(HOSTED_OPERATOR, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantAdmin)); + resolver.membership(HOSTED_OPERATOR, APPLICATION_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); assertEquals(emptySet(), // TODO this is wrong, but we can't do better until we ask ZMS for roles. - resolver.membership(TENANT_ADMIN, NO_CONTEXT_PATH).contextsFor(Role.tenantAdmin)); + resolver.membership(TENANT_ADMIN, NO_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); assertEquals(Set.of(Context.limitedTo(TENANT, tester.controller().system())), - resolver.membership(TENANT_ADMIN, TENANT_CONTEXT_PATH).contextsFor(Role.tenantAdmin)); + resolver.membership(TENANT_ADMIN, TENANT_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); assertEquals(emptySet(), - resolver.membership(TENANT_ADMIN, TENANT2_CONTEXT_PATH).contextsFor(Role.tenantAdmin)); + resolver.membership(TENANT_ADMIN, TENANT2_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); assertEquals(emptySet(), - resolver.membership(TENANT_PIPELINE, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantAdmin)); + resolver.membership(TENANT_PIPELINE, APPLICATION_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); assertEquals(emptySet(), - resolver.membership(USER, TENANT_CONTEXT_PATH).contextsFor(Role.tenantAdmin)); + resolver.membership(USER, TENANT_CONTEXT_PATH).contextsFor(Role.athenzTenantAdmin)); // Only build services are pipeline operators of their applications. assertEquals(emptySet(), - resolver.membership(HOSTED_OPERATOR, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipelineOperator)); + resolver.membership(HOSTED_OPERATOR, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipeline)); assertEquals(emptySet(), - resolver.membership(TENANT_ADMIN, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipelineOperator)); + resolver.membership(TENANT_ADMIN, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipeline)); assertEquals(Set.of(Context.limitedTo(TENANT, APPLICATION, tester.controller().system())), - resolver.membership(TENANT_PIPELINE, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipelineOperator)); + resolver.membership(TENANT_PIPELINE, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipeline)); assertEquals(emptySet(), - resolver.membership(TENANT_PIPELINE, APPLICATION2_CONTEXT_PATH).contextsFor(Role.tenantPipelineOperator)); + resolver.membership(TENANT_PIPELINE, APPLICATION2_CONTEXT_PATH).contextsFor(Role.tenantPipeline)); assertEquals(emptySet(), - resolver.membership(USER, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipelineOperator)); + resolver.membership(USER, APPLICATION_CONTEXT_PATH).contextsFor(Role.tenantPipeline)); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/PathGroupTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/PathGroupTest.java index 4cf46933dcb..b4a3e674594 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/PathGroupTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/role/PathGroupTest.java @@ -2,10 +2,12 @@ package com.yahoo.vespa.hosted.controller.role; import org.junit.Test; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import java.util.regex.Pattern; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; /** @@ -15,45 +17,40 @@ import static org.junit.Assert.fail; public class PathGroupTest { @Test - public void nonOverlappingGroups() { - for (PathGroup pg : PathGroup.all()) { - for (PathGroup pg2 : PathGroup.all()) { - if (pg == pg2) continue; - Set<String> overlapping = new LinkedHashSet<>(pg.pathSpecs); - overlapping.retainAll(pg2.pathSpecs); - if ( ! overlapping.isEmpty()) { - fail("The following path specs overlap in " + pg + " and " + pg2 + ": " + overlapping); - } - } - } - } - - @Test public void uniqueMatches() { // Ensure that each path group contains at most one match for any given path, to avoid undefined context extraction. - for (PathGroup group : PathGroup.values()) - for (String path1 : group.pathSpecs) - for (String path2 : group.pathSpecs) { - if (path1 == path2) continue; + Set<String> checkedAgainstSelf = new HashSet<>(); + for (PathGroup group1 : PathGroup.values()) + for (PathGroup group2 : PathGroup.values()) + for (String path1 : group1.pathSpecs) + for (String path2 : group2.pathSpecs) { + if (path1.equals(path2)) { + if (checkedAgainstSelf.add(path1)) continue; + fail("Path '" + path1 + "' appears in both '" + group1 + "' and '" + group2 + "'."); + } - String[] parts1 = path1.split("/"); - String[] parts2 = path2.split("/"); + String[] parts1 = path1.split("/"); + String[] parts2 = path2.split("/"); - int end = Math.min(parts1.length, parts2.length); - // If one path has more parts than the other ... - // and the other doesn't end with a wildcard matcher ... - // and the longest one isn't just one wildcard longer ... - if (end < parts1.length && (end == 0 || ! parts2[end - 1].equals("{*}")) && ! parts1[end].equals("{*}")) continue; - if (end < parts2.length && (end == 0 || ! parts1[end - 1].equals("{*}")) && ! parts2[end].equals("{*}")) continue; + int end = Math.min(parts1.length, parts2.length); + // If one path has more parts than the other ... + // and the other doesn't end with a wildcard matcher ... + // and the longest one isn't just one wildcard longer ... + // then one is strictly longer than the other, and it's not a match. + if (end < parts1.length && (end == 0 || ! parts2[end - 1].equals("{*}")) && ! parts1[end].equals("{*}")) continue; + if (end < parts2.length && (end == 0 || ! parts1[end - 1].equals("{*}")) && ! parts2[end].equals("{*}")) continue; - int i; - for (i = 0; i < end; i++) - if ( ! parts1[i].equals(parts2[i]) - && ! (parts1[i].startsWith("{") && parts1[i].endsWith("}")) - && ! (parts2[i].startsWith("{") && parts2[i].endsWith("}"))) break; + int i; + for (i = 0; i < end; i++) + if ( ! parts1[i].equals(parts2[i]) + && ! (parts1[i].startsWith("{") && parts1[i].endsWith("}")) + && ! (parts2[i].startsWith("{") && parts2[i].endsWith("}"))) break; - if (i == end) fail("Paths '" + path1 + "' and '" + path2 + "' overlap."); - } + if (i == end) fail("Paths '" + path1 + "' and '" + path2 + "' overlap."); + } + + assertEquals(PathGroup.all().stream().mapToInt(group -> group.pathSpecs.size()).sum(), + checkedAgainstSelf.size()); } @Test 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 index 2638e3f74e6..972ddefb1a5 100644 --- 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 @@ -31,7 +31,7 @@ public class RoleMembershipTest { @Test public void tenant_membership() { RoleMembership roles = RoleMembership.in(SystemName.main) - .add(Role.tenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) + .add(Role.athenzTenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) .build(); assertFalse(roles.allows(Action.create, "/not/explicitly/defined")); assertFalse("Deny access to operator API", roles.allows(Action.create, "/controller/v1/foo")); @@ -40,15 +40,15 @@ public class RoleMembershipTest { assertTrue(roles.allows(Action.update, "/application/v4/tenant/t1/application/a1")); RoleMembership multiContext = RoleMembership.in(SystemName.main) - .add(Role.tenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) - .add(Role.tenantAdmin).limitedTo(TenantName.from("t2"), ApplicationName.from("a2")) + .add(Role.athenzTenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) + .add(Role.athenzTenantAdmin).limitedTo(TenantName.from("t2"), ApplicationName.from("a2")) .build(); assertFalse("Deny access to other tenant and app", multiContext.allows(Action.update, "/application/v4/tenant/t3/application/a3")); assertTrue(multiContext.allows(Action.update, "/application/v4/tenant/t2/application/a2")); assertTrue(multiContext.allows(Action.update, "/application/v4/tenant/t1/application/a1")); RoleMembership publicSystem = RoleMembership.in(SystemName.vaas) - .add(Role.tenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) + .add(Role.athenzTenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) .build(); assertFalse(publicSystem.allows(Action.read, "/controller/v1/foo")); assertTrue(multiContext.allows(Action.update, "/application/v4/tenant/t1/application/a1")); @@ -57,7 +57,7 @@ public class RoleMembershipTest { @Test public void build_service_membership() { RoleMembership roles = RoleMembership.in(SystemName.main) - .add(Role.tenantPipelineOperator).build(); + .add(Role.tenantPipeline).build(); assertFalse(roles.allows(Action.create, "/not/explicitly/defined")); assertFalse(roles.allows(Action.update, "/application/v4/tenant/t1/application/a1")); assertTrue(roles.allows(Action.create, "/application/v4/tenant/t1/application/a1/jobreport")); @@ -67,8 +67,8 @@ public class RoleMembershipTest { @Test public void multi_role_membership() { RoleMembership roles = RoleMembership.in(SystemName.main) - .add(Role.tenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) - .add(Role.tenantPipelineOperator) + .add(Role.athenzTenantAdmin).limitedTo(TenantName.from("t1"), ApplicationName.from("a1")) + .add(Role.tenantPipeline) .add(Role.everyone) .build(); assertFalse(roles.allows(Action.create, "/not/explicitly/defined")); |