From a5a4966e4800154a69e4cdb20f380d5f3c448131 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 30 Sep 2019 19:11:07 +0000 Subject: [PATCH] Bug 1584321 - Handle static client scopes internally With this change, each service declares its scopes in `services//scopes.yml`, and that data is gathered during generation and placed in a convenient place for the auth service. The Auth service then interpolates those scopes into the configured STATIC_CLIENTS, preserving the supplied accessToken and applying the service's configured azure accountId. --- changelog/bug1584321-b.md | 6 + dev-docs/dev-config-example.yml | 82 ------------- generated/docs-table-of-contents.json | 30 ++++- infrastructure/tooling/src/dev/helm.js | 11 +- .../tooling/src/generate/generators/k8s.js | 51 ++------ .../src/generate/generators/metadata.js | 43 +++++++ .../src/generate/generators/static-clients.js | 46 +++++++ infrastructure/tooling/static-clients.yml | 72 ----------- services/auth/config.yml | 9 +- services/auth/src/data.js | 52 +++++++- services/auth/src/main.js | 2 +- services/auth/src/static-scopes.json | 116 ++++++++++++++++++ services/auth/test/azure_test.js | 2 +- services/auth/test/client_test.js | 2 +- services/auth/test/gcp_test.js | 2 +- services/auth/test/helper.js | 11 +- .../auth/test/purgeExpiredClients_test.js | 2 +- services/auth/test/remotevalidation_test.js | 2 +- services/auth/test/role_test.js | 2 +- services/auth/test/rolelogic_test.js | 2 +- services/auth/test/s3_test.js | 2 +- services/auth/test/sentry_test.js | 2 +- services/auth/test/staticclients_test.js | 51 ++++++-- services/auth/test/statsum_test.js | 2 +- services/auth/test/stories_test.js | 2 +- services/auth/test/testauth_test.js | 3 +- services/auth/test/websocktunnel_test.js | 2 +- services/built-in-workers/scopes.yml | 5 + services/github/scopes.yml | 9 ++ services/hooks/scopes.yml | 7 ++ services/index/scopes.yml | 6 + services/notify/scopes.yml | 3 + services/purge-cache/scopes.yml | 2 + services/queue/scopes.yml | 2 + services/secrets/scopes.yml | 2 + services/web-server/scopes.yml | 21 ++++ services/worker-manager/scopes.yml | 11 ++ ui/docs/manual/deploying/static-clients.mdx | 41 +++++++ 38 files changed, 477 insertions(+), 241 deletions(-) create mode 100644 changelog/bug1584321-b.md create mode 100644 infrastructure/tooling/src/generate/generators/metadata.js create mode 100644 infrastructure/tooling/src/generate/generators/static-clients.js delete mode 100644 infrastructure/tooling/static-clients.yml create mode 100644 services/auth/src/static-scopes.json create mode 100644 services/built-in-workers/scopes.yml create mode 100644 services/github/scopes.yml create mode 100644 services/hooks/scopes.yml create mode 100644 services/index/scopes.yml create mode 100644 services/notify/scopes.yml create mode 100644 services/purge-cache/scopes.yml create mode 100644 services/queue/scopes.yml create mode 100644 services/secrets/scopes.yml create mode 100644 services/web-server/scopes.yml create mode 100644 services/worker-manager/scopes.yml create mode 100644 ui/docs/manual/deploying/static-clients.mdx diff --git a/changelog/bug1584321-b.md b/changelog/bug1584321-b.md new file mode 100644 index 00000000000..74b9d5407f0 --- /dev/null +++ b/changelog/bug1584321-b.md @@ -0,0 +1,6 @@ +level: major +reference: bug 1584321 +--- +Scopes for the Taskcluster services themselves are now handled internally to the platform, although access tokens must still be managed as part of the deployment process. +When deploying this version, remove all `scopes` and `description` properties from `static/taskcluster/..` clients in the array in the Auth service's `STATIC_CLIENTS` configuration. +See [the new docs on static clients](https://docs.taskcluster.net/docs/manual/deploying/static-clients) for more background on this setting. diff --git a/dev-docs/dev-config-example.yml b/dev-docs/dev-config-example.yml index 9ec95616dcf..35607f8e293 100644 --- a/dev-docs/dev-config-example.yml +++ b/dev-docs/dev-config-example.yml @@ -15,108 +15,26 @@ auth: roles_container_name: ... static_clients: - clientId: static/taskcluster/built-in-workers - description: 'Autogenerated, do not edit. Client for built-in-workers' - scopes: - - 'queue:claim-work:built-in/*' - - 'assume:worker-id:built-in/*' - - 'queue:worker-id:built-in/*' - - 'queue:resolve-task' accessToken: ... - clientId: static/taskcluster/github - description: 'Autogenerated, do not edit. Client for github' - scopes: - - 'assume:repo:github.com/*' - - 'assume:scheduler-id:taskcluster-github/*' - - 'auth:azure-table-access:${azureAccountId}/TaskclusterGithubBuilds' - - 'auth:azure-table-access:${azureAccountId}/TaskclusterIntegrationOwners' - - 'auth:azure-table:read-write:${azureAccountId}/TaskclusterGithubBuilds' - - 'auth:azure-table:read-write:${azureAccountId}/TaskclusterIntegrationOwners' - - 'auth:azure-table:read-write:${azureAccountId}/TaskclusterChecksToTasks' - - 'auth:azure-table:read-write:${azureAccountId}/TaskclusterCheckRuns' accessToken: ... - clientId: static/taskcluster/hooks - description: 'Autogenerated, do not edit. Client for hooks' - scopes: - - 'auth:azure-table:read-write:${azureAccountId}/Hooks' - - 'auth:azure-table:read-write:${azureAccountId}/Queue' - - 'auth:azure-table:read-write:${azureAccountId}/LastFire3' - - 'assume:hook-id:*' - - 'notify:email:*' - - 'queue:create-task:*' accessToken: ... - clientId: static/taskcluster/index - description: 'Autogenerated, do not edit. Client for index' - scopes: - - 'auth:azure-table-access:${azureAccountId}/IndexedTasks' - - 'auth:azure-table-access:${azureAccountId}/Namespaces' - - 'auth:azure-table:read-write:${azureAccountId}/IndexedTasks' - - 'auth:azure-table:read-write:${azureAccountId}/Namespaces' - - 'queue:get-artifact:*' accessToken: ... - clientId: static/taskcluster/notify - description: 'Autogenerated, do not edit. Client for notify' - scopes: - - 'auth:azure-table:read-write:${azureAccountId}/DenylistedNotification' - - 'auth:azure-table:read-write:${azureAccountId}/Denylist' accessToken: ... - clientId: static/taskcluster/purge-cache - description: 'Autogenerated, do not edit. Client for purge-cache' - scopes: - - 'auth:azure-table:read-write:${azureAccountId}/CachePurges' accessToken: ... - clientId: static/taskcluster/queue - description: 'Autogenerated, do not edit. Client for queue' - scopes: - - '*' accessToken: ... - clientId: static/taskcluster/secrets - description: 'Autogenerated, do not edit. Client for secrets' - scopes: - - 'auth:azure-table-access:${azureAccountId}/Secrets' - - 'auth:azure-table:read-write:${azureAccountId}/Secrets' accessToken: ... - clientId: static/taskcluster/web-server - description: 'Autogenerated, do not edit. Client for web-server' - scopes: - - 'assume:mozilla-group:*' - - 'assume:mozilla-user:*' - - 'assume:mozillians-group:*' - - 'assume:mozillians-user:*' - - 'auth:create-client:mozilla-auth0/*' - - 'auth:delete-client:mozilla-auth0/*' - - 'auth:disable-client:mozilla-auth0/*' - - 'auth:enable-client:mozilla-auth0/*' - - 'auth:reset-access-token:mozilla-auth0/*' - - 'auth:update-client:mozilla-auth0/*' - - 'auth:create-client:github/*' - - 'auth:delete-client:github/*' - - 'auth:disable-client:github/*' - - 'auth:enable-client:github/*' - - 'auth:reset-access-token:github/*' - - 'auth:update-client:github/*' - - 'auth:azure-table:read-write:${azureAccountId}/AccessTokenTable' - - 'auth:azure-table:read-write:${azureAccountId}/AuthorizationCodesTable' - - 'auth:azure-table:read-write:${azureAccountId}/GithubAccessTokenTable' - - 'assume:login-identity:*' accessToken: ... - clientId: static/taskcluster/worker-manager - description: 'Autogenerated, do not edit. Client for worker-manager' - scopes: - - 'auth:create-client:worker/*' - - 'assume:worker-type:*' - - 'assume:worker-pool:*' - - 'assume:worker-id:*' - - 'auth:azure-table:read-write:${azureAccountId}/WM*' - - 'notify:email:*' - - 'secrets:get:worker-type:*' - - 'secrets:get:worker-pool:*' - - 'queue:claim-work:*' - - 'queue:worker-id:*' accessToken: ... - clientId: static/taskcluster/root - description: 'Autogenerated, do not edit. Client for administering the deployment.' - scopes: - - '*' accessToken: ... azure_accounts: {} sentry_organization: ... diff --git a/generated/docs-table-of-contents.json b/generated/docs-table-of-contents.json index e6299bb8152..6ca0d6da0cb 100644 --- a/generated/docs-table-of-contents.json +++ b/generated/docs-table-of-contents.json @@ -1585,8 +1585,8 @@ }, "name": "pulse", "next": { - "path": "manual/deploying/third-party", - "title": "Third Party" + "path": "manual/deploying/static-clients", + "title": "static-clients" }, "path": "manual/deploying/pulse", "prev": { @@ -1598,6 +1598,28 @@ "title": "Deploying Taskcluster" } }, + { + "children": [ + ], + "data": { + "order": 1000, + "title": "static-clients" + }, + "name": "static-clients", + "next": { + "path": "manual/deploying/third-party", + "title": "Third Party" + }, + "path": "manual/deploying/static-clients", + "prev": { + "path": "manual/deploying/pulse", + "title": "Pulse" + }, + "up": { + "path": "manual/deploying", + "title": "Deploying Taskcluster" + } + }, { "children": [ ], @@ -1612,8 +1634,8 @@ }, "path": "manual/deploying/third-party", "prev": { - "path": "manual/deploying/pulse", - "title": "Pulse" + "path": "manual/deploying/static-clients", + "title": "static-clients" }, "up": { "path": "manual/deploying", diff --git a/infrastructure/tooling/src/dev/helm.js b/infrastructure/tooling/src/dev/helm.js index ecdf39f9cb8..bfc373a8eae 100644 --- a/infrastructure/tooling/src/dev/helm.js +++ b/infrastructure/tooling/src/dev/helm.js @@ -22,6 +22,15 @@ const actions = [ if (!config.meta || !config.meta.deploymentPrefix) { throw new Error('Must have configured dev-config.yml to deploy.'); } + + if (config.auth && config.auth.static_clients) { + if (config.auth.static_clients.some(({clientId, scopes}) => clientId.startsWith('static/taskcluster/') && scopes)) { + throw new Error('`static/taskcluster/..` clients in auth.static_clients in `dev-config.yml` should not contain scopes'); + } + if (config.auth.static_clients.some(({clientId, scopes}) => !clientId.startsWith('static/taskcluster/') && !scopes)) { + throw new Error('non-taskcluster static clients in auth.static_clients in `dev-config.yml` should scopes'); + } + } return {'dev-config': config}; }, }, @@ -58,7 +67,7 @@ const actions = [ if (requirements['helm-version'] === 2) { command.push('-n'); } - command = command.concat(['taskcluster', '-f', './dev-config.yml', 'infrastructure/k8s']); + command = command.concat(['taskcluster', '-f', 'dev-config.yml', 'infrastructure/k8s']); return { 'target-templates': await execCommand({ command, diff --git a/infrastructure/tooling/src/generate/generators/k8s.js b/infrastructure/tooling/src/generate/generators/k8s.js index 4b67d541b40..9b9e53da700 100644 --- a/infrastructure/tooling/src/generate/generators/k8s.js +++ b/infrastructure/tooling/src/generate/generators/k8s.js @@ -4,7 +4,6 @@ const glob = require('glob'); const util = require('util'); const yaml = require('js-yaml'); const jsone = require('json-e'); -const config = require('taskcluster-lib-config'); const rimraf = util.promisify(require('rimraf')); const mkdirp = util.promisify(require('mkdirp')); const {listServices, writeRepoFile, readRepoYAML, writeRepoYAML, writeRepoJSON, REPO_ROOT, configToSchema, configToExample} = require('../../utils'); @@ -140,34 +139,16 @@ exports.tasks.push({ }); SERVICES.forEach(name => { - exports.tasks.push({ - title: `Get config options for ${name}`, - requires: [], - provides: [`configs-${name}`], - run: async (requirements, utils) => { - const envVars = config({ - files: [{ - path: path.join(REPO_ROOT, 'services', name, 'config.yml'), - required: true, - }], - getEnvVars: true, - }); - return { - [`configs-${name}`]: envVars, - }; - }, - }); exports.tasks.push({ title: `Generate helm templates for ${name}`, - requires: [`configs-${name}`, 'k8s-templates'], - provides: [`ingresses-${name}`, `procslist-${name}`], + requires: [`configs-${name}`, `procslist-${name}`, 'k8s-templates'], + provides: [`ingresses-${name}`], run: async (requirements, utils) => { - const procs = await readRepoYAML(path.join('services', name, 'procs.yml')); + const procs = requirements[`procslist-${name}`]; const templates = requirements['k8s-templates']; const vars = requirements[`configs-${name}`].map(v => v.var); return { [`ingresses-${name}`]: await renderTemplates(name, vars, procs, templates), - [`procslist-${name}`]: procs, }; }, }); @@ -254,6 +235,7 @@ exports.tasks.push({ requires: [ ...SERVICES.map(name => `configs-${name}`), ...SERVICES.map(name => `procslist-${name}`), + 'static-clients', ], provides: [], run: async (requirements, utils) => { @@ -362,9 +344,6 @@ exports.tasks.push({ ...cfg, }))); - const staticClients = []; - const serviceScopes = await readRepoYAML(path.join('infrastructure', 'tooling', 'static-clients.yml')); - configs.forEach(cfg => { const confName = cfg.name.replace(/-/g, '_'); exampleConfig[confName] = {}; @@ -393,14 +372,6 @@ exports.tasks.push({ additionalProperties: false, }; - if (serviceScopes[cfg.name]) { - staticClients.push({ - clientId: `static/taskcluster/${cfg.name}`, - description: `Autogenerated, do not edit. Client for ${cfg.name}`, - scopes: serviceScopes[cfg.name], - }); - } - // Some services actually duplicate their config env vars in multiple places // so we de-dupe first. We use the variable name for this. If they've asked // for the same variable twice with different types then this is not our fault @@ -462,17 +433,9 @@ exports.tasks.push({ }); }); - // Now push root as well - staticClients.push({ - clientId: 'static/taskcluster/root', - description: `Autogenerated, do not edit. Client for administering the deployment.`, - scopes: ['*'], - }); - - exampleConfig.auth.static_clients = staticClients.map(c => { - c.accessToken = '...'; - return c; - }); + // omit scopes and add a placeholder accessToken to each client + exampleConfig.auth.static_clients = requirements['static-clients'] + .map(({scopes, ...c}) => ({...c, accessToken: '...'})); await writeRepoJSON(path.join(CHART_DIR, 'values.schema.json'), schema); await writeRepoYAML(path.join(CHART_DIR, 'values.yaml'), valuesYAML); // helm requires this to be "yaml" diff --git a/infrastructure/tooling/src/generate/generators/metadata.js b/infrastructure/tooling/src/generate/generators/metadata.js new file mode 100644 index 00000000000..29f9c985d7f --- /dev/null +++ b/infrastructure/tooling/src/generate/generators/metadata.js @@ -0,0 +1,43 @@ +const _ = require('lodash'); +const path = require('path'); +const config = require('taskcluster-lib-config'); +const {listServices, readRepoYAML, REPO_ROOT} = require('../../utils'); + +// We're not going to deploy login into k8s +const SERVICES = listServices().filter(s => !['login'].includes(s)); + +exports.tasks = []; + +SERVICES.forEach(name => { + exports.tasks.push({ + title: `Fetch service metadata for ${name}`, + requires: [], + provides: [`configs-${name}`, `procslist-${name}`, `scopes-${name}`], + run: async (requirements, utils) => { + const envVars = config({ + files: [{ + path: path.join(REPO_ROOT, 'services', name, 'config.yml'), + required: true, + }], + getEnvVars: true, + }); + + const procs = await readRepoYAML(path.join('services', name, 'procs.yml')); + + const scopesPath = path.join('services', name, 'scopes.yml'); + let scopes = null; + try { + scopes = await readRepoYAML(scopesPath); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + return { + [`configs-${name}`]: envVars, + [`procslist-${name}`]: procs, + [`scopes-${name}`]: scopes, + }; + }, + }); +}); diff --git a/infrastructure/tooling/src/generate/generators/static-clients.js b/infrastructure/tooling/src/generate/generators/static-clients.js new file mode 100644 index 00000000000..06061382b46 --- /dev/null +++ b/infrastructure/tooling/src/generate/generators/static-clients.js @@ -0,0 +1,46 @@ +const _ = require('lodash'); +const {listServices, writeRepoJSON} = require('../../utils'); + +// We're not going to deploy login into k8s +const SERVICES = listServices().filter(s => !['login'].includes(s)); + +exports.tasks = []; + +exports.tasks.push({ + title: 'Assemble static clients', + requires: [ + ...SERVICES.map(name => `scopes-${name}`), + ], + provides: ['static-clients'], + run: async (requirements, utils) => { + const staticClients = []; + SERVICES.forEach(name => { + const scopes = requirements[`scopes-${name}`]; + if (scopes) { + staticClients.push({ + clientId: `static/taskcluster/${name}`, + scopes: scopes, + }); + } + }); + + staticClients.push({ + clientId: 'static/taskcluster/root', + scopes: ['*'], + }); + + return {'static-clients': staticClients}; + }, +}); + +exports.tasks.push({ + title: 'Configure static client scopes', + requires: ['static-clients'], + provides: [], + run: async (requirements, utils) => { + const staticClients = requirements['static-clients']; + const staticScopes = staticClients.map(({clientId, scopes}) => ({clientId, scopes})); + + writeRepoJSON('services/auth/src/static-scopes.json', staticScopes); + }, +}); diff --git a/infrastructure/tooling/static-clients.yml b/infrastructure/tooling/static-clients.yml deleted file mode 100644 index fa14e9ad689..00000000000 --- a/infrastructure/tooling/static-clients.yml +++ /dev/null @@ -1,72 +0,0 @@ ---- -web-server: - - assume:mozilla-group:* - - assume:mozilla-user:* - - assume:mozillians-group:* - - assume:mozillians-user:* - - auth:create-client:mozilla-auth0/* - - auth:delete-client:mozilla-auth0/* - - auth:disable-client:mozilla-auth0/* - - auth:enable-client:mozilla-auth0/* - - auth:reset-access-token:mozilla-auth0/* - - auth:update-client:mozilla-auth0/* - - auth:create-client:github/* - - auth:delete-client:github/* - - auth:disable-client:github/* - - auth:enable-client:github/* - - auth:reset-access-token:github/* - - auth:update-client:github/* - - auth:azure-table:read-write:${azureAccountId}/AccessTokenTable - - auth:azure-table:read-write:${azureAccountId}/AuthorizationCodesTable - - auth:azure-table:read-write:${azureAccountId}/GithubAccessTokenTable - - assume:login-identity:* -secrets: - - auth:azure-table-access:${azureAccountId}/Secrets - - auth:azure-table:read-write:${azureAccountId}/Secrets -index: - - auth:azure-table-access:${azureAccountId}/IndexedTasks - - auth:azure-table-access:${azureAccountId}/Namespaces - - auth:azure-table:read-write:${azureAccountId}/IndexedTasks - - auth:azure-table:read-write:${azureAccountId}/Namespaces - - queue:get-artifact:* -worker-manager: - - auth:create-client:worker/* - - assume:worker-type:* - - assume:worker-pool:* - - assume:worker-id:* - - auth:azure-table:read-write:${azureAccountId}/WM* - - notify:email:* - - secrets:get:worker-type:* - - secrets:get:worker-pool:* - - queue:claim-work:* - - queue:worker-id:* -github: - - assume:repo:github.com/* - - assume:scheduler-id:taskcluster-github/* - - auth:azure-table-access:${azureAccountId}/TaskclusterGithubBuilds - - auth:azure-table-access:${azureAccountId}/TaskclusterIntegrationOwners - - auth:azure-table:read-write:${azureAccountId}/TaskclusterGithubBuilds - - auth:azure-table:read-write:${azureAccountId}/TaskclusterIntegrationOwners - - auth:azure-table:read-write:${azureAccountId}/TaskclusterChecksToTasks - - auth:azure-table:read-write:${azureAccountId}/TaskclusterCheckRuns -hooks: - - auth:azure-table:read-write:${azureAccountId}/Hooks - - auth:azure-table:read-write:${azureAccountId}/Queue - - auth:azure-table:read-write:${azureAccountId}/LastFire3 - - assume:hook-id:* - - notify:email:* - - queue:create-task:* -notify: - - auth:azure-table:read-write:${azureAccountId}/DenylistedNotification - - auth:azure-table:read-write:${azureAccountId}/Denylist -purge-cache: - - auth:azure-table:read-write:${azureAccountId}/CachePurges -built-in-workers: - - queue:claim-work:built-in/* - - assume:worker-id:built-in/* - - queue:worker-id:built-in/* - - queue:resolve-task -queue: - - '*' -root: - - '*' diff --git a/services/auth/config.yml b/services/auth/config.yml index f8f3292794b..d2902e92000 100644 --- a/services/auth/config.yml +++ b/services/auth/config.yml @@ -144,14 +144,7 @@ test: app: statsComponent: auth-tests clientTableName: Clients - # hardcoded into helper.js - staticClients: - - clientId: static/taskcluster/root - accessToken: -test-access-token-that-is-at-least-22-chars-long- - description: | - Root client used for creating other clients (bootstrapping). - scopes: - - '*' + staticClients: # overridden in helper.js # Special value for tests, as we don't want to wait forever maxLastUsedDelay: '- 3 seconds' sentry: diff --git a/services/auth/src/data.js b/services/auth/src/data.js index 54efc84c9cc..2c3cd3688ab 100644 --- a/services/auth/src/data.js +++ b/services/auth/src/data.js @@ -2,6 +2,7 @@ const Entity = require('azure-entities'); const assert = require('assert'); const _ = require('lodash'); const taskcluster = require('taskcluster-client'); +const staticScopes = require('./static-scopes.json'); const Client = Entity.configure({ version: 1, @@ -112,20 +113,61 @@ Client.prototype.json = function(resolver) { * , where description will be amended with a section explaining that this * client is static and can't be modified at runtime. */ -Client.syncStaticClients = async function(clients = []) { +Client.syncStaticClients = async function(clients = [], azureAccountId) { // Validate input for sanity (we hardly need perfect validation here...) assert(clients instanceof Array, 'Expected clients to be am array'); for (const client of clients) { assert(typeof client.clientId === 'string', 'expected clientId to be a string'); assert(typeof client.accessToken === 'string', 'expected accessToken to be a string'); assert(client.accessToken.length >= 22, 'accessToken must have at least 22 chars'); - assert(client.accessToken.length <= 66, 'accessToken must have at least 66 chars'); - assert(typeof client.description === 'string', 'expected description to be a string'); - assert(client.scopes instanceof Array, 'expected scopes to be an array of strings'); + assert(client.accessToken.length <= 66, 'accessToken must have at most 66 chars'); assert(client.clientId.startsWith('static/'), 'static clients must have clientId = "static/..."'); - assert(client.scopes.every(s => typeof s === 'string'), 'scopes must be strings'); + if (client.clientId.startsWith('static/taskcluster')) { + assert(!client.scopes, 'scopes are not allowed in configuration for static/taskcluster clients'); + } else { + assert(client.scopes instanceof Array, 'expected scopes to be an array of strings'); + assert(typeof client.description === 'string', 'expected description to be a string'); + assert(client.scopes.every(s => typeof s === 'string'), 'scopes must be strings'); + } + } + + // check that we have all of the expected static/taskcluster clients, and no more. The staticClients + // are generated from `services/*/scopes.yml` for all of the other services. + const seenTCClients = clients + .map(({clientId}) => clientId) + .filter(c => c.startsWith('static/taskcluster/')); + const expectedTCClients = staticScopes + .map(({clientId}) => clientId); + const extraTCClients = _.difference(seenTCClients, expectedTCClients); + const missingTCClients = _.difference(expectedTCClients, seenTCClients); + + if (extraTCClients.length > 0 || missingTCClients.length > 0) { + let msg = 'Incorrect `static/taskcluster` static clients in STATIC_CLIENTS'; + if (extraTCClients.length > 0) { + msg = msg + `; extra clients ${JSON.stringify(extraTCClients)}`; + } + if (missingTCClients.length > 0) { + msg = msg + `; missing clients ${JSON.stringify(missingTCClients)}`; + } + throw new Error(msg); } + // put the configured scopes into place + clients = clients.map(client => { + if (client.clientId.startsWith('static/taskcluster/')) { + const {scopes} = _.find(staticScopes, {clientId: client.clientId}); + return {...client, description: 'Internal client', scopes}; + } else { + return client; + } + }); + + // substitute the azureAccountId into the scopes + clients = clients.map(client => ({ + ...client, + scopes: client.scopes.map(sc => sc.replace(/\${azureAccountId}/g, azureAccountId)), + })); + // description suffix to use for all static clients const descriptionSuffix = [ '\n---\n', diff --git a/services/auth/src/main.js b/services/auth/src/main.js index 53b9a2ccb62..862e94d6aec 100755 --- a/services/auth/src/main.js +++ b/services/auth/src/main.js @@ -132,7 +132,7 @@ const load = Loader({ await Client.ensureTable(); // set up the static clients - await Client.syncStaticClients(cfg.app.staticClients || []); + await Client.syncStaticClients(cfg.app.staticClients || [], cfg.azure.accountId); // Load everything for resolver await resolver.setup({ diff --git a/services/auth/src/static-scopes.json b/services/auth/src/static-scopes.json new file mode 100644 index 00000000000..0dd665d0132 --- /dev/null +++ b/services/auth/src/static-scopes.json @@ -0,0 +1,116 @@ +[ + { + "clientId": "static/taskcluster/built-in-workers", + "scopes": [ + "queue:claim-work:built-in/*", + "assume:worker-id:built-in/*", + "queue:worker-id:built-in/*", + "queue:resolve-task" + ] + }, + { + "clientId": "static/taskcluster/github", + "scopes": [ + "assume:repo:github.com/*", + "assume:scheduler-id:taskcluster-github/*", + "auth:azure-table-access:${azureAccountId}/TaskclusterGithubBuilds", + "auth:azure-table-access:${azureAccountId}/TaskclusterIntegrationOwners", + "auth:azure-table:read-write:${azureAccountId}/TaskclusterGithubBuilds", + "auth:azure-table:read-write:${azureAccountId}/TaskclusterIntegrationOwners", + "auth:azure-table:read-write:${azureAccountId}/TaskclusterChecksToTasks", + "auth:azure-table:read-write:${azureAccountId}/TaskclusterCheckRuns" + ] + }, + { + "clientId": "static/taskcluster/hooks", + "scopes": [ + "auth:azure-table:read-write:${azureAccountId}/Hooks", + "auth:azure-table:read-write:${azureAccountId}/Queue", + "auth:azure-table:read-write:${azureAccountId}/LastFire3", + "assume:hook-id:*", + "notify:email:*", + "queue:create-task:*" + ] + }, + { + "clientId": "static/taskcluster/index", + "scopes": [ + "auth:azure-table-access:${azureAccountId}/IndexedTasks", + "auth:azure-table-access:${azureAccountId}/Namespaces", + "auth:azure-table:read-write:${azureAccountId}/IndexedTasks", + "auth:azure-table:read-write:${azureAccountId}/Namespaces", + "queue:get-artifact:*" + ] + }, + { + "clientId": "static/taskcluster/notify", + "scopes": [ + "auth:azure-table:read-write:${azureAccountId}/DenylistedNotification", + "auth:azure-table:read-write:${azureAccountId}/Denylist" + ] + }, + { + "clientId": "static/taskcluster/purge-cache", + "scopes": [ + "auth:azure-table:read-write:${azureAccountId}/CachePurges" + ] + }, + { + "clientId": "static/taskcluster/queue", + "scopes": [ + "*" + ] + }, + { + "clientId": "static/taskcluster/secrets", + "scopes": [ + "auth:azure-table:read-write:${azureAccountId}/Secrets" + ] + }, + { + "clientId": "static/taskcluster/web-server", + "scopes": [ + "assume:mozilla-group:*", + "assume:mozilla-user:*", + "assume:mozillians-group:*", + "assume:mozillians-user:*", + "auth:create-client:mozilla-auth0/*", + "auth:delete-client:mozilla-auth0/*", + "auth:disable-client:mozilla-auth0/*", + "auth:enable-client:mozilla-auth0/*", + "auth:reset-access-token:mozilla-auth0/*", + "auth:update-client:mozilla-auth0/*", + "auth:create-client:github/*", + "auth:delete-client:github/*", + "auth:disable-client:github/*", + "auth:enable-client:github/*", + "auth:reset-access-token:github/*", + "auth:update-client:github/*", + "auth:azure-table:read-write:${azureAccountId}/AccessTokenTable", + "auth:azure-table:read-write:${azureAccountId}/AuthorizationCodesTable", + "auth:azure-table:read-write:${azureAccountId}/GithubAccessTokenTable", + "assume:login-identity:*" + ] + }, + { + "clientId": "static/taskcluster/worker-manager", + "scopes": [ + "auth:create-client:worker/*", + "assume:worker-type:*", + "assume:worker-pool:*", + "assume:worker-id:*", + "auth:azure-table:read-write:${azureAccountId}/WM*", + "notify:email:*", + "secrets:get:worker-type:*", + "secrets:get:worker-pool:*", + "queue:claim-work:*", + "queue:worker-id:*" + ] + }, + { + "clientId": "static/taskcluster/root", + "scopes": [ + "*" + ] + } +] \ No newline at end of file diff --git a/services/auth/test/azure_test.js b/services/auth/test/azure_test.js index 1883b8ff476..6b8c841f135 100644 --- a/services/auth/test/azure_test.js +++ b/services/auth/test/azure_test.js @@ -9,11 +9,11 @@ helper.secrets.mockSuite(testing.suiteName(), ['app', 'azure', 'gcp'], function( if (mock) { return; // We only test this with real creds } + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping); helper.withRoles(mock, skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); let testaccount; suiteSetup('get azure test account name', async function() { diff --git a/services/auth/test/client_test.js b/services/auth/test/client_test.js index b4dfa9c4c57..e68a7598604 100644 --- a/services/auth/test/client_test.js +++ b/services/auth/test/client_test.js @@ -6,11 +6,11 @@ const testing = require('taskcluster-lib-testing'); const taskcluster = require('taskcluster-client'); helper.secrets.mockSuite(testing.suiteName(), ['app', 'azure', 'gcp'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping); helper.withRoles(mock, skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); test('ping', async () => { await helper.apiClient.ping(); diff --git a/services/auth/test/gcp_test.js b/services/auth/test/gcp_test.js index b6dfba6b1ea..62880fb696a 100644 --- a/services/auth/test/gcp_test.js +++ b/services/auth/test/gcp_test.js @@ -4,12 +4,12 @@ const helper = require('./helper'); const testing = require('taskcluster-lib-testing'); helper.secrets.mockSuite(testing.suiteName(), ['app', 'gcp', 'azure'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withGcp(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping); helper.withRoles(mock, skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); test('gcpCredentials invalid account', async () => { try { diff --git a/services/auth/test/helper.js b/services/auth/test/helper.js index 6fc9f870eb9..7d93e57e6a9 100644 --- a/services/auth/test/helper.js +++ b/services/auth/test/helper.js @@ -9,6 +9,7 @@ const slugid = require('slugid'); const uuid = require('uuid'); const Builder = require('taskcluster-lib-api'); const SchemaSet = require('taskcluster-lib-validate'); +const staticScopes = require('../src/static-scopes.json'); const {stickyLoader, Secrets, withEntity, withPulse, withMonitor} = require('taskcluster-lib-testing'); exports.load = stickyLoader(load); @@ -24,6 +25,7 @@ suiteSetup(async function() { exports.rootUrl = `http://localhost:60552`; exports.containerName = `auth-test-${uuid.v4()}`; +exports.rootAccessToken = '-test-access-token-that-is-at-least-22-chars-long-'; withMonitor(exports); @@ -62,6 +64,13 @@ exports.withCfg = (mock, skipping) => { } suiteSetup(async function() { exports.cfg = await exports.load('cfg'); + + // override app.staticClients based on the static scopes + exports.load.cfg('app.staticClients', staticScopes.map(({clientId}) => ({ + clientId, + accessToken: clientId === 'static/taskcluster/root' ? exports.rootAccessToken : 'must-be-at-least-22-characters', + description: 'testing', + }))); }); }; @@ -242,7 +251,6 @@ exports.withServers = (mock, skipping) => { await exports.load('cfg'); exports.load.cfg('taskcluster.rootUrl', exports.rootUrl); - exports.rootAccessToken = '-test-access-token-that-is-at-least-22-chars-long-'; // First set up the auth service exports.AuthClient = taskcluster.createClient(builder.reference()); @@ -407,6 +415,7 @@ exports.withGcp = (mock, skipping) => { }; } else { const {credentials, allowedServiceAccounts} = await exports.load('gcp'); + console.log(credentials); exports.gcpAccount = { email: allowedServiceAccounts[0], project_id: credentials.project_id, diff --git a/services/auth/test/purgeExpiredClients_test.js b/services/auth/test/purgeExpiredClients_test.js index 911e1cc19fe..721a7b278d5 100644 --- a/services/auth/test/purgeExpiredClients_test.js +++ b/services/auth/test/purgeExpiredClients_test.js @@ -4,11 +4,11 @@ const taskcluster = require('taskcluster-client'); const testing = require('taskcluster-lib-testing'); helper.secrets.mockSuite(testing.suiteName(), ['app', 'azure', 'gcp'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping); helper.withRoles(mock, skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); const CLIENT_ID = 'nobody/sds:ad_asd/df-sAdSfchsdfsdfs'; diff --git a/services/auth/test/remotevalidation_test.js b/services/auth/test/remotevalidation_test.js index fcd29fa4207..d311d42029e 100644 --- a/services/auth/test/remotevalidation_test.js +++ b/services/auth/test/remotevalidation_test.js @@ -6,11 +6,11 @@ const request = require('superagent'); const testing = require('taskcluster-lib-testing'); helper.secrets.mockSuite(testing.suiteName(), ['app', 'azure', 'gcp'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping); helper.withRoles(mock, skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); let rootCredentials; diff --git a/services/auth/test/role_test.js b/services/auth/test/role_test.js index 3667360ced4..bb31d99b725 100644 --- a/services/auth/test/role_test.js +++ b/services/auth/test/role_test.js @@ -7,11 +7,11 @@ const testing = require('taskcluster-lib-testing'); const taskcluster = require('taskcluster-client'); helper.secrets.mockSuite(testing.suiteName(), ['app', 'azure', 'gcp'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping, {orderedTests: true}); helper.withRoles(mock, skipping, {orderedTests: true}); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); let sorted = (arr) => { arr.sort(); diff --git a/services/auth/test/rolelogic_test.js b/services/auth/test/rolelogic_test.js index c5b5330309a..7b3f71d8bbd 100644 --- a/services/auth/test/rolelogic_test.js +++ b/services/auth/test/rolelogic_test.js @@ -5,11 +5,11 @@ const mocha = require('mocha'); const testing = require('taskcluster-lib-testing'); helper.secrets.mockSuite(testing.suiteName(), ['app', 'azure', 'gcp'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping); helper.withRoles(mock, skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); /** * Customized test function, taking an object as follows: diff --git a/services/auth/test/s3_test.js b/services/auth/test/s3_test.js index 5a8a29d4bf7..a649933c907 100644 --- a/services/auth/test/s3_test.js +++ b/services/auth/test/s3_test.js @@ -10,11 +10,11 @@ helper.secrets.mockSuite(testing.suiteName(), ['app', 'aws', 'gcp'], function(mo return; // This is actually testing sts tokens and we are not going to mock those } // pulse/azure aren't under test, so we always mock them out + helper.withCfg(mock, skipping); helper.withPulse('mock', skipping); helper.withEntities('mock', skipping); helper.withRoles('mock', skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); let bucket; setup(function() { diff --git a/services/auth/test/sentry_test.js b/services/auth/test/sentry_test.js index 4df5813c7d5..8199dfb8088 100644 --- a/services/auth/test/sentry_test.js +++ b/services/auth/test/sentry_test.js @@ -7,12 +7,12 @@ helper.secrets.mockSuite(testing.suiteName(), ['app', 'gcp'], function(mock, ski if (!mock) { return; // We don't test this with real credentials for now! } + helper.withCfg(mock, skipping); helper.withSentry(mock, skipping); helper.withPulse('mock', skipping); helper.withEntities('mock', skipping); helper.withRoles('mock', skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); test('sentryDSN', async () => { await helper.apiClient.sentryDSN('playground'); diff --git a/services/auth/test/staticclients_test.js b/services/auth/test/staticclients_test.js index c3d181bf719..78117c02392 100644 --- a/services/auth/test/staticclients_test.js +++ b/services/auth/test/staticclients_test.js @@ -5,11 +5,11 @@ const assume = require('assume'); const testing = require('taskcluster-lib-testing'); helper.secrets.mockSuite(testing.suiteName(), ['app', 'azure', 'gcp'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping); helper.withRoles(mock, skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); test('static/taskcluster/root exists', async () => { await helper.apiClient.client('static/taskcluster/root'); @@ -24,29 +24,60 @@ helper.secrets.mockSuite(testing.suiteName(), ['app', 'azure', 'gcp'], function( } }); - test('static/taskcluster/test-static-client can be created and removed', async () => { + test('other clients can be created and removed, and azureAccountId is substituted', async () => { debug('test that we can create static clients'); await helper.Client.syncStaticClients([ ...helper.cfg.app.staticClients, { - clientId: 'static/taskcluster/test-static-client', + clientId: 'static/mystuff/foo', accessToken: 'test-secret-12345678910', description: 'Just testing, you should never see this in production!!!', - scopes: ['dummy-scope'], + scopes: ['dummy-scope:${azureAccountId}:foo'], }, - ]); + ], 'pamplemousse'); debug('test that static client was created'); - const c = await helper.apiClient.client('static/taskcluster/test-static-client'); - assume(c.clientId).equals('static/taskcluster/test-static-client'); - assume(c.scopes).includes('dummy-scope'); + const c = await helper.apiClient.client('static/mystuff/foo'); + assume(c.clientId).equals('static/mystuff/foo'); + assume(c.scopes).includes('dummy-scope:pamplemousse:foo'); debug('test that we delete the static client again'); - await helper.Client.syncStaticClients(helper.cfg.app.staticClients); + await helper.Client.syncStaticClients(helper.cfg.app.staticClients, 'pamplemousse'); try { - await helper.apiClient.client('static/taskcluster/test-static-client'); + await helper.apiClient.client('static/mystuff/foo'); assert(false, 'expected an error'); } catch (err) { assume(err.code).equals('ResourceNotFound'); } }); + + test('adding scopes for a static/taskcluster client is an error', async () => { + await assert.rejects(() => helper.Client.syncStaticClients([ + ...helper.cfg.app.staticClients + .filter(({clientId}) => clientId !== 'static/taskcluster/queue'), + { + clientId: 'static/taskcluster/queue', + accessToken: 'test-secret-12345678910', + description: 'testing', + scopes: ['new-queue-scope'], + }, + ], 'pamplemousse'), /not allowed/); + }); + + test('omitting a static/taskcluster client is an error', async () => { + await assert.rejects(() => helper.Client.syncStaticClients( + helper.cfg.app.staticClients + .filter(({clientId}) => clientId !== 'static/taskcluster/queue')), + /missing clients/); + }); + + test('adding extra static/taskcluster clients is an error', async () => { + await assert.rejects(() => helper.Client.syncStaticClients([ + ...helper.cfg.app.staticClients, + { + clientId: 'static/taskcluster/newthing', + accessToken: 'test-secret-12345678910', + description: 'testing', + }, + ], 'pamplemousse'), /extra clients/); + }); }); diff --git a/services/auth/test/statsum_test.js b/services/auth/test/statsum_test.js index 592d17d4679..2e41713d580 100644 --- a/services/auth/test/statsum_test.js +++ b/services/auth/test/statsum_test.js @@ -3,11 +3,11 @@ const assert = require('assert'); const testing = require('taskcluster-lib-testing'); helper.secrets.mockSuite(testing.suiteName(), ['app', 'azure', 'gcp'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping); helper.withRoles(mock, skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); test('statsumToken', async () => { let result = await helper.apiClient.statsumToken('test'); diff --git a/services/auth/test/stories_test.js b/services/auth/test/stories_test.js index 6f316e95b2c..64bb9f7936c 100644 --- a/services/auth/test/stories_test.js +++ b/services/auth/test/stories_test.js @@ -5,11 +5,11 @@ const taskcluster = require('taskcluster-client'); const testing = require('taskcluster-lib-testing'); helper.secrets.mockSuite(testing.suiteName(), ['app', 'azure', 'gcp'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping, {orderedTests: true}); helper.withRoles(mock, skipping, {orderedTests: true}); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); suite('charlene creates permanent credentials for a test runner', function() { suiteSetup(async function() { diff --git a/services/auth/test/testauth_test.js b/services/auth/test/testauth_test.js index ec38d8e67d0..d7f81744586 100644 --- a/services/auth/test/testauth_test.js +++ b/services/auth/test/testauth_test.js @@ -14,11 +14,11 @@ const badcreds = { suite(testing.suiteName(), function() { helper.secrets.mockSuite('testAuth', ['app', 'azure', 'gcp'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping); helper.withRoles(mock, skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); let testAuth = (name, {config, requiredScopes, clientScopes, errorCode}) => { test(name, async () => { @@ -101,6 +101,7 @@ suite(testing.suiteName(), function() { }); helper.secrets.mockSuite('testAuthGet', ['app', 'azure', 'gcp'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping); helper.withRoles(mock, skipping); diff --git a/services/auth/test/websocktunnel_test.js b/services/auth/test/websocktunnel_test.js index b534ed0107a..c4d60f73927 100644 --- a/services/auth/test/websocktunnel_test.js +++ b/services/auth/test/websocktunnel_test.js @@ -4,11 +4,11 @@ const jwt = require('jsonwebtoken'); const testing = require('taskcluster-lib-testing'); helper.secrets.mockSuite(testing.suiteName(), ['app', 'azure', 'gcp'], function(mock, skipping) { + helper.withCfg(mock, skipping); helper.withPulse(mock, skipping); helper.withEntities(mock, skipping); helper.withRoles(mock, skipping); helper.withServers(mock, skipping); - helper.withCfg(mock, skipping); test('websocktunnelToken', async () => { const wstAudience = 'websocktunnel-usw2'; diff --git a/services/built-in-workers/scopes.yml b/services/built-in-workers/scopes.yml new file mode 100644 index 00000000000..44fcc030b38 --- /dev/null +++ b/services/built-in-workers/scopes.yml @@ -0,0 +1,5 @@ +# NOTE: ${azureAccountId} will be substituted with the azure account at runtime +- queue:claim-work:built-in/* +- assume:worker-id:built-in/* +- queue:worker-id:built-in/* +- queue:resolve-task diff --git a/services/github/scopes.yml b/services/github/scopes.yml new file mode 100644 index 00000000000..3feb82fcafe --- /dev/null +++ b/services/github/scopes.yml @@ -0,0 +1,9 @@ +# NOTE: ${azureAccountId} will be substituted with the azure account at runtime +- assume:repo:github.com/* +- assume:scheduler-id:taskcluster-github/* +- auth:azure-table-access:${azureAccountId}/TaskclusterGithubBuilds +- auth:azure-table-access:${azureAccountId}/TaskclusterIntegrationOwners +- auth:azure-table:read-write:${azureAccountId}/TaskclusterGithubBuilds +- auth:azure-table:read-write:${azureAccountId}/TaskclusterIntegrationOwners +- auth:azure-table:read-write:${azureAccountId}/TaskclusterChecksToTasks +- auth:azure-table:read-write:${azureAccountId}/TaskclusterCheckRuns diff --git a/services/hooks/scopes.yml b/services/hooks/scopes.yml new file mode 100644 index 00000000000..2b12e35573f --- /dev/null +++ b/services/hooks/scopes.yml @@ -0,0 +1,7 @@ +# NOTE: ${azureAccountId} will be substituted with the azure account at runtime +- auth:azure-table:read-write:${azureAccountId}/Hooks +- auth:azure-table:read-write:${azureAccountId}/Queue +- auth:azure-table:read-write:${azureAccountId}/LastFire3 +- assume:hook-id:* +- notify:email:* +- queue:create-task:* diff --git a/services/index/scopes.yml b/services/index/scopes.yml new file mode 100644 index 00000000000..1c95c3d4141 --- /dev/null +++ b/services/index/scopes.yml @@ -0,0 +1,6 @@ +# NOTE: ${azureAccountId} will be substituted with the azure account at runtime +- auth:azure-table-access:${azureAccountId}/IndexedTasks +- auth:azure-table-access:${azureAccountId}/Namespaces +- auth:azure-table:read-write:${azureAccountId}/IndexedTasks +- auth:azure-table:read-write:${azureAccountId}/Namespaces +- queue:get-artifact:* diff --git a/services/notify/scopes.yml b/services/notify/scopes.yml new file mode 100644 index 00000000000..c9b553d869b --- /dev/null +++ b/services/notify/scopes.yml @@ -0,0 +1,3 @@ +# NOTE: ${azureAccountId} will be substituted with the azure account at runtime +- auth:azure-table:read-write:${azureAccountId}/DenylistedNotification +- auth:azure-table:read-write:${azureAccountId}/Denylist diff --git a/services/purge-cache/scopes.yml b/services/purge-cache/scopes.yml new file mode 100644 index 00000000000..5c3ab275356 --- /dev/null +++ b/services/purge-cache/scopes.yml @@ -0,0 +1,2 @@ +# NOTE: ${azureAccountId} will be substituted with the azure account at runtime +- auth:azure-table:read-write:${azureAccountId}/CachePurges diff --git a/services/queue/scopes.yml b/services/queue/scopes.yml new file mode 100644 index 00000000000..fa376714417 --- /dev/null +++ b/services/queue/scopes.yml @@ -0,0 +1,2 @@ +# NOTE: ${azureAccountId} will be substituted with the azure account at runtime +- '*' diff --git a/services/secrets/scopes.yml b/services/secrets/scopes.yml new file mode 100644 index 00000000000..aedca2c18e2 --- /dev/null +++ b/services/secrets/scopes.yml @@ -0,0 +1,2 @@ +# NOTE: ${azureAccountId} will be substituted with the azure account at runtime +- auth:azure-table:read-write:${azureAccountId}/Secrets diff --git a/services/web-server/scopes.yml b/services/web-server/scopes.yml new file mode 100644 index 00000000000..1cea637479d --- /dev/null +++ b/services/web-server/scopes.yml @@ -0,0 +1,21 @@ +# NOTE: ${azureAccountId} will be substituted with the azure account at runtime +- assume:mozilla-group:* +- assume:mozilla-user:* +- assume:mozillians-group:* +- assume:mozillians-user:* +- auth:create-client:mozilla-auth0/* +- auth:delete-client:mozilla-auth0/* +- auth:disable-client:mozilla-auth0/* +- auth:enable-client:mozilla-auth0/* +- auth:reset-access-token:mozilla-auth0/* +- auth:update-client:mozilla-auth0/* +- auth:create-client:github/* +- auth:delete-client:github/* +- auth:disable-client:github/* +- auth:enable-client:github/* +- auth:reset-access-token:github/* +- auth:update-client:github/* +- auth:azure-table:read-write:${azureAccountId}/AccessTokenTable +- auth:azure-table:read-write:${azureAccountId}/AuthorizationCodesTable +- auth:azure-table:read-write:${azureAccountId}/GithubAccessTokenTable +- assume:login-identity:* diff --git a/services/worker-manager/scopes.yml b/services/worker-manager/scopes.yml new file mode 100644 index 00000000000..a818467d8b8 --- /dev/null +++ b/services/worker-manager/scopes.yml @@ -0,0 +1,11 @@ +# NOTE: ${azureAccountId} will be substituted with the azure account at runtime +- auth:create-client:worker/* +- assume:worker-type:* +- assume:worker-pool:* +- assume:worker-id:* +- auth:azure-table:read-write:${azureAccountId}/WM* +- notify:email:* +- secrets:get:worker-type:* +- secrets:get:worker-pool:* +- queue:claim-work:* +- queue:worker-id:* diff --git a/ui/docs/manual/deploying/static-clients.mdx b/ui/docs/manual/deploying/static-clients.mdx new file mode 100644 index 00000000000..ff321319b31 --- /dev/null +++ b/ui/docs/manual/deploying/static-clients.mdx @@ -0,0 +1,41 @@ +title: Static Clients +--- + +# Static Clients + +As a collection of microservices, Taskcluster services must be able to communicate with one another. +They use the same [authorization system](/docs/manual/design/apis/hawk) as external clients. +This means that each service needs a clientId and accessToken, and that clientId must be associated with specific scopes. + +## Static Clients + +The Auth service supports this with a feature called "static clients". +All clientIds beginning with `static/` are considered static clients. +These are configured in the Auth service's `STATIC_CLIENTS` and cannot be modified via the API. +The configuration is a JSON blob with the form + +```json +[ + {clientId: 'static/taskcluster/index', accessToken: ''}, + {clientId: 'static/my-client', accessToken: '', description: 'Client for index', scopes: [ + 'some-scope', 'another-scope']]}, + ..., +] +``` + +Note that clients beginning with `static/taskcluster/` must not specify scopes or description (those are determined automatically), while those not beginning with `static/taskcluster/` *must* specify scopes and description. +Most deployments do not use non-taskcluster static credentials, but they are available to support some advanced "locked-down" scenarios. + +## Configuring AccessTokens + +Taskcluster expects the person configuring the deployment to configure the same accessToken for each service both in that service's settings and in `STATIC_CLIENTS`. +An access token must be a random string from 22 to 66 characters in length. + +An access token is required for each Taskcluster service, as well as one for `static/taskcluster/root`, which is an account with `*` scope, used for bootstrapping administration of a deployment. +The Auth service will fail to start up if `STATIC_CLIENTS` is missing any `static/taskcluster/...` clients, or if it contains unrecognized clients. + +## Changes when Upgrading + +When a service is added or removed from Taskcluster, the change is considered major. +In this case, `STATIC_CLIENTS` will need to be modified to add or remove the accessToken for the added or removed service. +Failure to do so will result in the Auth service failing to start up.