From 85acabb8ac628985a60695a7021871d76d5cd7ef Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 7 Dec 2023 12:48:09 -0700 Subject: [PATCH] Add directory paths to KV capabilities checks (#24404) * add getter to metadata model * add changelog and data model fix * add test coverage * add nested create coverage * Update 24404.txt * remove from data model * return to how it was --- changelog/24404.txt | 3 ++ ui/app/models/kv/metadata.js | 9 ++++-- ui/lib/kv/addon/components/page/list.hbs | 7 ++++- .../backend/kv/kv-v2-workflow-create-test.js | 20 +++++++++++++ .../backend/kv/kv-v2-workflow-delete-test.js | 29 +++++++++++++++++++ ui/tests/helpers/kv/kv-selectors.js | 2 ++ ui/tests/helpers/policy-generator/kv.js | 20 ++++++++++++- 7 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 changelog/24404.txt diff --git a/changelog/24404.txt b/changelog/24404.txt new file mode 100644 index 0000000000..6fab70d0bf --- /dev/null +++ b/changelog/24404.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: fix issue where kv v2 capabilities checks were not passing in the full secret path if secret was inside a directory. +``` diff --git a/ui/app/models/kv/metadata.js b/ui/app/models/kv/metadata.js index 1f998f488a..43dd20fc91 100644 --- a/ui/app/models/kv/metadata.js +++ b/ui/app/models/kv/metadata.js @@ -95,9 +95,14 @@ export default class KvSecretMetadataModel extends Model { }; } + get permissionsPath() { + return this.fullSecretPath || this.path; + } + // permissions needed for the list view where kv/data has not yet been called. Allows us to conditionally show action items in the LinkedBlock popups. - @lazyCapabilities(apiPath`${'backend'}/data/${'path'}`, 'backend', 'path') dataPath; - @lazyCapabilities(apiPath`${'backend'}/metadata/${'path'}`, 'backend', 'path') metadataPath; + @lazyCapabilities(apiPath`${'backend'}/data/${'permissionsPath'}`, 'backend', 'permissionsPath') dataPath; + @lazyCapabilities(apiPath`${'backend'}/metadata/${'permissionsPath'}`, 'backend', 'permissionsPath') + metadataPath; get canDeleteMetadata() { return this.metadataPath.get('canDelete') !== false; diff --git a/ui/lib/kv/addon/components/page/list.hbs b/ui/lib/kv/addon/components/page/list.hbs index a7b5bb5d41..0accd8de08 100644 --- a/ui/lib/kv/addon/components/page/list.hbs +++ b/ui/lib/kv/addon/components/page/list.hbs @@ -95,7 +95,11 @@ {{/if}} {{#if metadata.canCreateVersionData}}
  • - + Create new version
  • @@ -106,6 +110,7 @@ @isInDropdown={{true}} @onConfirmAction={{fn this.onDelete metadata}} @confirmMessage="This will permanently delete this secret and all its versions." + data-test-popup-metadata-delete /> {{/if}} {{/if}} diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js index b6fb939e0e..49567b1f50 100644 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js @@ -1003,6 +1003,26 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook }); }); + module('secret-nested-creator persona', function (hooks) { + hooks.beforeEach(async function () { + const token = await runCmd( + tokenWithPolicyCmd('secret-nested-creator', personas.secretNestedCreator(this.backend)) + ); + await authPage.login(token); + clearRecords(this.store); + return; + }); + test('can create a secret from the nested list view (snc)', async function (assert) { + assert.expect(1); + // go to nested secret directory list view + await visit(`/vault/secrets/${this.backend}/kv/list/app/`); + // correct popup menu items appear on list view + const popupSelector = `${PAGE.list.item('first')} ${PAGE.popup}`; + await click(popupSelector); + assert.dom(PAGE.list.listMenuCreate).exists('shows the option to create new version'); + }); + }); + module('enterprise controlled access persona', function (hooks) { hooks.beforeEach(async function () { this.controlGroup = this.owner.lookup('service:control-group'); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js index db3a80f281..c96e70f061 100644 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js @@ -40,9 +40,11 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook this.store = this.owner.lookup('service:store'); this.backend = `kv-delete-${uuidv4()}`; this.secretPath = 'bad-secret'; + this.nestedSecretPath = 'app/nested/bad-secret'; await authPage.login(); await runCmd(mountEngineCmd('kv-v2', this.backend), false); await writeVersionedSecret(this.backend, this.secretPath, 'foo', 'bar', 4); + await writeVersionedSecret(this.backend, this.nestedSecretPath, 'foo', 'bar', 1); await writeVersionedSecret(this.backend, 'nuke', 'foo', 'bar', 2); // Delete latest version for testing undelete for users that can't delete await runCmd(deleteLatestCmd(this.backend, 'nuke')); @@ -353,6 +355,33 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook }); }); + module('secret-nested-creator persona', function (hooks) { + hooks.beforeEach(async function () { + const token = await runCmd( + tokenWithPolicyCmd('secret-nested-creator', personas.secretNestedCreator(this.backend)) + ); + await authPage.login(token); + clearRecords(this.store); + return; + }); + test('can delete all secret versions from the nested list view (snc)', async function (assert) { + assert.expect(1); + // go to nested secret directory list view + await visit(`/vault/secrets/${this.backend}/kv/list/app/nested`); + // correct popup menu items appear on list view + const popupSelector = `${PAGE.list.item('bad-secret')} ${PAGE.popup}`; + await click(popupSelector); + assert.dom(PAGE.list.listMenuDelete).exists('shows the option to permanently delete'); + }); + test('can not delete all secret versions from root list view (snc)', async function (assert) { + assert.expect(1); + // go to root secret directory list view + await visit(`/vault/secrets/${this.backend}/kv/list`); + // shows overview card and not list view + assert.dom(PAGE.list.overviewCard).exists('renders overview card'); + }); + }); + module('secret-creator persona', function (hooks) { hooks.beforeEach(async function () { const token = await runCmd(tokenWithPolicyCmd('secret-creator', personas.secretCreator(this.backend))); diff --git a/ui/tests/helpers/kv/kv-selectors.js b/ui/tests/helpers/kv/kv-selectors.js index 742d6217fa..82adf0df84 100644 --- a/ui/tests/helpers/kv/kv-selectors.js +++ b/ui/tests/helpers/kv/kv-selectors.js @@ -58,6 +58,8 @@ export const PAGE = { createSecret: '[data-test-toolbar-create-secret]', item: (secret) => (!secret ? '[data-test-list-item]' : `[data-test-list-item="${secret}"]`), filter: `[data-test-kv-list-filter]`, + listMenuDelete: `[data-test-popup-metadata-delete]`, + listMenuCreate: `[data-test-popup-create-new-version]`, overviewCard: '[data-test-overview-card-container="View secret"]', overviewInput: '[data-test-view-secret] input', overviewButton: '[data-test-get-secret-detail]', diff --git a/ui/tests/helpers/policy-generator/kv.js b/ui/tests/helpers/policy-generator/kv.js index 7794e0493d..bcd91ed0ee 100644 --- a/ui/tests/helpers/policy-generator/kv.js +++ b/ui/tests/helpers/policy-generator/kv.js @@ -25,6 +25,14 @@ export const dataPolicy = ({ backend, secretPath = '*', capabilities = root }) = `; }; +export const dataNestedPolicy = ({ backend, secretPath = '*', capabilities = root }) => { + return ` + path "${backend}/data/app/${secretPath}" { + capabilities = [${format(capabilities)}] + } + `; +}; + export const metadataPolicy = ({ backend, secretPath = '*', capabilities = root }) => { // "delete" capability on this path can destroy all versions return ` @@ -34,6 +42,14 @@ export const metadataPolicy = ({ backend, secretPath = '*', capabilities = root `; }; +export const metadataNestedPolicy = ({ backend, secretPath = '*', capabilities = root }) => { + return ` + path "${backend}/metadata/app/${secretPath}" { + capabilities = [${format(capabilities)}] + } + `; +}; + export const metadataListPolicy = (backend) => { return ` path "${backend}/metadata" { @@ -71,11 +87,13 @@ export const personas = { dataListReader: (backend) => dataPolicy({ backend, capabilities: ['read', 'delete'] }) + metadataListPolicy(backend), metadataMaintainer: (backend) => - metadataListPolicy(backend) + metadataPolicy({ backend, capabilities: ['create', 'read', 'update', 'list'] }) + deleteVersionsPolicy({ backend }) + undeleteVersionsPolicy({ backend }) + destroyVersionsPolicy({ backend }), + secretNestedCreator: (backend) => + dataNestedPolicy({ backend, capabilities: ['create', 'update'] }) + + metadataNestedPolicy({ backend, capabilities: ['list', 'delete'] }), secretCreator: (backend) => dataPolicy({ backend, capabilities: ['create', 'update'] }) + metadataPolicy({ backend, capabilities: ['delete'] }),