From 0728f0a0aeb69bd4ee6bb76381734b5e4a6885bc Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 6 Nov 2025 18:09:17 -0500 Subject: [PATCH] Bug Fix UI: Await capabilities on KVv1 secret edit route to properly access methods on the promise proxy (#10548) (#10661) * the fix await the promose proxy and assign it to const to access * add changelog * add test coverage * return comment * remove uncessary await now that we await higher up Co-authored-by: Angel Garbarino --- changelog/_10548.txt | 3 ++ ui/app/components/secret-create-or-update.hbs | 10 +---- ui/app/components/secret-edit-toolbar.hbs | 2 +- .../cluster/secrets/backend/secret-edit.js | 8 ++-- .../secrets/backend/kv/secret-test.js | 42 ++++++++++++++++++- 5 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 changelog/_10548.txt diff --git a/changelog/_10548.txt b/changelog/_10548.txt new file mode 100644 index 0000000000..052b5758f1 --- /dev/null +++ b/changelog/_10548.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Resolved a regression that prevented users with create and update permissions on KV v1 secrets from opening the edit view. The UI now correctly recognizes these capabilities and allows editing without requiring full read access. +``` \ No newline at end of file diff --git a/ui/app/components/secret-create-or-update.hbs b/ui/app/components/secret-create-or-update.hbs index 827f40e9a6..4114e8f895 100644 --- a/ui/app/components/secret-create-or-update.hbs +++ b/ui/app/components/secret-create-or-update.hbs @@ -133,16 +133,10 @@ {{#if (eq @canReadSecret false)}} - + Warning -
    - {{#if (eq @canReadSecret false)}} -
  • - You do not have read permissions. If a secret exists at this path creating a new secret will overwrite it. -
  • - {{/if}} -
+ You do not have read permissions. If a secret exists at this path creating a new secret will overwrite it.
{{/if}} diff --git a/ui/app/components/secret-edit-toolbar.hbs b/ui/app/components/secret-edit-toolbar.hbs index 4dd3c0e4d8..c1146a25ad 100644 --- a/ui/app/components/secret-edit-toolbar.hbs +++ b/ui/app/components/secret-edit-toolbar.hbs @@ -44,7 +44,7 @@ {{#if (and (eq @mode "show") @canUpdateSecret)}} {{#let (concat "vault.cluster.secrets.backend." (if (eq @mode "show") "edit" "show")) as |targetRoute|}} - + Edit secret {{/let}} diff --git a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js index 88640e8a9a..b2932e2c11 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -145,7 +145,10 @@ export default Route.extend({ return types[engineType]; }, - handleSecretModelError(capabilities, secretId, modelType, error) { + async handleSecretModelError(capabilitiesPromise, secretId, modelType, error) { + // capabilities is a promise proxy, not a real object + // to work around this we explicitly assign it to a const and await it + const capabilities = await capabilitiesPromise; // can't read the path and don't have update capability, so re-throw if (!capabilities.canUpdate && modelType === 'secret') { throw error; @@ -186,8 +189,7 @@ export default Route.extend({ // we've failed the read request, but if it's a kv-v1 type backend, we want to // do additional checks of the capabilities if (err.httpStatus === 403 && modelType === 'secret') { - await capabilities; - secretModel = this.handleSecretModelError(capabilities, secret, modelType, err); + secretModel = await this.handleSecretModelError(capabilities, secret, modelType, err); } else { throw err; } diff --git a/ui/tests/acceptance/secrets/backend/kv/secret-test.js b/ui/tests/acceptance/secrets/backend/kv/secret-test.js index 9022bbf853..8ec1414dda 100644 --- a/ui/tests/acceptance/secrets/backend/kv/secret-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/secret-test.js @@ -14,7 +14,7 @@ import listPage from 'vault/tests/pages/secrets/backend/list'; import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { writeSecret, writeVersionedSecret } from 'vault/tests/helpers/kv/kv-run-commands'; -import { runCmd } from 'vault/tests/helpers/commands'; +import { runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands'; import { PAGE } from 'vault/tests/helpers/kv/kv-selectors'; import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; @@ -94,6 +94,7 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { hooks.afterEach(async function () { await runCmd([`delete sys/mounts/${this.backend}`]); }); + test('it can create a secret when check-and-set is required', async function (assert) { const secretPath = 'foo/bar'; const output = await runCmd(`write ${this.backend}/config cas_required=true`); @@ -110,6 +111,7 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { 'redirects to the overview page' ); }); + test('it navigates to version history and to a specific version', async function (assert) { assert.expect(4); const secretPath = `specific-version`; @@ -143,9 +145,11 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { await mountSecrets.version(1); await click(GENERAL.submitButton); }); + hooks.afterEach(async function () { await runCmd([`delete sys/mounts/${this.backend}`]); }); + test('version 1 performs the correct capabilities lookup', async function (assert) { // TODO: while this should pass it doesn't really do anything anymore for us as v1 and v2 are completely separate. const secretPath = 'foo/bar'; @@ -158,6 +162,41 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { ); assert.ok(showPage.editIsPresent, 'shows the edit button'); }); + + test('version 1 token without read permissions can create and update a secret', async function (assert) { + const updatePersonaToken = await runCmd( + tokenWithPolicyCmd( + 'read-all', + ` + path "${this.backend}/*" { + capabilities = ["create", "update", "list"] + } + # used to delete the engine after test done in afterEach hook + path "sys/mounts/${this.backend}" { + capabilities = ["delete"] + } + ` + ) + ); + + await login(updatePersonaToken); + await visit(`/vault/secrets-engines/${this.backend}/list`); + await click(SS.createSecretLink); + await createSecret('test', 'foo', 'bar'); + await click('[data-test-secret-edit]', 'can click edit button'); + // edit only without read permissions + assert + .dom('[data-test-secret-no-read-permissions] .hds-alert__description') + .hasText( + 'You do not have read permissions. If a secret exists at this path creating a new secret will overwrite it.', + 'Displays warning about no read permissions' + ); + await fillIn('[data-test-secret-key]', 'new'); + await fillIn('[data-test-secret-value] textarea', 'new'); + await click(GENERAL.submitButton); + assert.dom(GENERAL.latestFlashContent).includesText('Secret test updated successfully.'); + }); + // https://github.com/hashicorp/vault/issues/5960 test('version 1: nested paths creation maintains ability to navigate the tree', async function (assert) { const enginePath = this.backend; @@ -224,6 +263,7 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { 'redirected to the list page on delete' ); }); + test('paths are properly encoded', async function (assert) { const backend = this.backend; const paths = [