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 <Monkeychip@users.noreply.github.com>
This commit is contained in:
Vault Automation 2025-11-06 18:09:17 -05:00 committed by GitHub
parent 47a7482e42
commit 0728f0a0ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 52 additions and 13 deletions

3
changelog/_10548.txt Normal file
View file

@ -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.
```

View file

@ -133,16 +133,10 @@
<MessageError @model={{@modelForData}} @errorMessage={{this.error}} />
<NamespaceReminder @mode="edit" @noun="secret" />
{{#if (eq @canReadSecret false)}}
<Hds::Alert @type="inline" @color="warning" class="has-bottom-margin-s" as |A|>
<Hds::Alert @type="inline" @color="warning" class="has-bottom-margin-s" data-test-secret-no-read-permissions as |A|>
<A.Title>Warning</A.Title>
<A.Description>
<ul class={{if (eq @canReadSecret false) "bullet"}}>
{{#if (eq @canReadSecret false)}}
<li data-test-warning-no-read-permissions>
You do not have read permissions. If a secret exists at this path creating a new secret will overwrite it.
</li>
{{/if}}
</ul>
You do not have read permissions. If a secret exists at this path creating a new secret will overwrite it.
</A.Description>
</Hds::Alert>
{{/if}}

View file

@ -44,7 +44,7 @@
{{#if (and (eq @mode "show") @canUpdateSecret)}}
{{#let (concat "vault.cluster.secrets.backend." (if (eq @mode "show") "edit" "show")) as |targetRoute|}}
<ToolbarLink @route={{targetRoute}} @model={{@model.id}} @replace={{true}} data-test-secret-edit="true">
<ToolbarLink @route={{targetRoute}} @model={{@model.id}} @replace={{true}} data-test-secret-edit>
Edit secret
</ToolbarLink>
{{/let}}

View file

@ -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;
}

View file

@ -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 = [