From c05f9707e51856e24f41a996d56f3de39fb0ac3f Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Fri, 5 Aug 2022 10:45:22 -0400 Subject: [PATCH] [ui] "can list variables" capability refactor (#13996) * Check against all your policies' namespaces' secvars' paths' capabilities to see if you can list vars * Changelog and lintfix * Unit tests for list-vars * Removed unused computed dep * Changelog removed --- ui/app/abilities/variable.js | 49 ++++++++- ui/tests/unit/abilities/variable-test.js | 123 ++++++++++++++++++++++- 2 files changed, 163 insertions(+), 9 deletions(-) diff --git a/ui/app/abilities/variable.js b/ui/app/abilities/variable.js index caa0ae5ec..94148eff3 100644 --- a/ui/app/abilities/variable.js +++ b/ui/app/abilities/variable.js @@ -1,3 +1,4 @@ +// @ts-check import { computed, get } from '@ember/object'; import { or } from '@ember/object/computed'; import AbstractAbility from './abstract'; @@ -20,7 +21,7 @@ export default class Variable extends AbstractAbility { @or( 'bypassAuthorization', 'selfTokenIsManagement', - 'policiesSupportVariableView' + 'policiesSupportVariableList' ) canList; @@ -38,10 +39,48 @@ export default class Variable extends AbstractAbility { ) canDestroy; - @computed('rulesForNamespace.@each.capabilities') - get policiesSupportVariableView() { - return this.rulesForNamespace.some((rules) => { - return get(rules, 'SecureVariables'); + @computed('token.selfTokenPolicies') + get policiesSupportVariableList() { + return this.policyNamespacesIncludeSecureVariablesCapabilities( + this.token.selfTokenPolicies, + ['list', 'read', 'write', 'destroy'] + ); + } + + /** + * + * Map to your policy's namespaces, + * and each of their SecureVariables blocks' paths, + * and each of their capabilities. + * Then, check to see if any of the permissions you're looking for + * are contained within at least one of them. + * + * @param {Object} policies + * @param {string[]} capabilities + * @returns {boolean} + */ + policyNamespacesIncludeSecureVariablesCapabilities( + policies = [], + capabilities = [] + ) { + const namespacesWithSecureVariableCapabilities = policies + .toArray() + .map((policy) => get(policy, 'rulesJSON.Namespaces')) + .flat() + .map((namespace = {}) => { + return namespace.SecureVariables?.Paths; + }) + .flat() + .compact() + .map((secVarsBlock = {}) => { + return secVarsBlock.Capabilities; + }) + .flat() + .compact(); + + // Check for requested permissions + return namespacesWithSecureVariableCapabilities.some((abilityList) => { + return capabilities.includes(abilityList); }); } diff --git a/ui/tests/unit/abilities/variable-test.js b/ui/tests/unit/abilities/variable-test.js index 909d72365..742c62db0 100644 --- a/ui/tests/unit/abilities/variable-test.js +++ b/ui/tests/unit/abilities/variable-test.js @@ -60,9 +60,7 @@ module('Unit | Ability | variable', function (hooks) { Name: 'default', Capabilities: [], SecureVariables: { - 'Path "*"': { - Capabilities: ['list'], - }, + Paths: [{ Capabilities: ['list'], PathSpec: '*' }], }, }, ], @@ -76,7 +74,7 @@ module('Unit | Ability | variable', function (hooks) { assert.ok(this.ability.canList); }); - test('it permits listing variables when token has SecureVariables alone in its rules', function (assert) { + test('it does not permit listing variables when token has SecureVariables alone in its rules', function (assert) { const mockToken = Service.extend({ aclEnabled: true, selfToken: { type: 'client' }, @@ -97,6 +95,123 @@ module('Unit | Ability | variable', function (hooks) { this.owner.register('service:token', mockToken); + assert.notOk(this.ability.canList); + }); + + test('it does not permit listing variables when token has a null SecureVariables block', function (assert) { + const mockToken = Service.extend({ + aclEnabled: true, + selfToken: { type: 'client' }, + selfTokenPolicies: [ + { + rulesJSON: { + Namespaces: [ + { + Name: 'default', + Capabilities: [], + SecureVariables: null, + }, + ], + }, + }, + ], + }); + + this.owner.register('service:token', mockToken); + + assert.notOk(this.ability.canList); + }); + + test('it does not permit listing variables when token has a SecureVariables block where paths are without capabilities', function (assert) { + const mockToken = Service.extend({ + aclEnabled: true, + selfToken: { type: 'client' }, + selfTokenPolicies: [ + { + rulesJSON: { + Namespaces: [ + { + Name: 'default', + Capabilities: [], + SecureVariables: { + Paths: [ + { Capabilities: [], PathSpec: '*' }, + { Capabilities: [], PathSpec: 'foo' }, + { Capabilities: [], PathSpec: 'foo/bar' }, + ], + }, + }, + ], + }, + }, + ], + }); + + this.owner.register('service:token', mockToken); + + assert.notOk(this.ability.canList); + }); + + test('it does not permit listing variables when token has no SecureVariables block', function (assert) { + const mockToken = Service.extend({ + aclEnabled: true, + selfToken: { type: 'client' }, + selfTokenPolicies: [ + { + rulesJSON: { + Namespaces: [ + { + Name: 'default', + Capabilities: [], + }, + ], + }, + }, + ], + }); + + this.owner.register('service:token', mockToken); + + assert.notOk(this.ability.canList); + }); + + test('it permits listing variables when token multiple namespaces, only one of which having a SecureVariables block', function (assert) { + const mockToken = Service.extend({ + aclEnabled: true, + selfToken: { type: 'client' }, + selfTokenPolicies: [ + { + rulesJSON: { + Namespaces: [ + { + Name: 'default', + Capabilities: [], + SecureVariables: null, + }, + { + Name: 'nonsense', + Capabilities: [], + SecureVariables: { + Paths: [{ Capabilities: [], PathSpec: '*' }], + }, + }, + { + Name: 'shenanigans', + Capabilities: [], + SecureVariables: { + Paths: [ + { Capabilities: ['list'], PathSpec: 'foo/bar/baz' }, + ], + }, + }, + ], + }, + }, + ], + }); + + this.owner.register('service:token', mockToken); + assert.ok(this.ability.canList); }); });