diff --git a/ui/lib/kv/addon/components/page/secret/patch.js b/ui/lib/kv/addon/components/page/secret/patch.js
index b49ae842ef..fb1a4db152 100644
--- a/ui/lib/kv/addon/components/page/secret/patch.js
+++ b/ui/lib/kv/addon/components/page/secret/patch.js
@@ -40,6 +40,7 @@ export default class KvSecretPatch extends Component {
@service router;
@service store;
+ @tracked controlGroupError;
@tracked errorMessage;
@tracked invalidFormAlert;
@tracked patchMethod = 'UI';
@@ -60,21 +61,19 @@ export default class KvSecretPatch extends Component {
const { backend, path, metadata, subkeysMeta } = this.args;
// if no metadata permission, use subkey metadata as backup
- const version = metadata.currentVersion || subkeysMeta.version;
+ const version = metadata?.currentVersion || subkeysMeta?.version;
const adapter = this.store.adapterFor('kv/data');
try {
yield adapter.patchSecret(backend, path, patchData, version);
this.flashMessages.success(`Successfully patched new version of ${path}.`);
this.router.transitionTo('vault.cluster.secrets.backend.kv.secret.index');
} catch (error) {
- // TODO test...this is copy pasta'd from the edit page
- let message = errorMessage(error);
if (error.message === 'Control Group encountered') {
this.controlGroup.saveTokenFromError(error);
- const err = this.controlGroup.logFromError(error);
- message = err.content;
+ this.controlGroupError = this.controlGroup.logFromError(error);
+ return;
}
- this.errorMessage = message;
+ this.errorMessage = errorMessage(error);
this.invalidFormAlert = 'There was an error submitting this form.';
}
}
diff --git a/ui/lib/kv/addon/routes/secret.js b/ui/lib/kv/addon/routes/secret.js
index 16dd9410da..f06a9960c0 100644
--- a/ui/lib/kv/addon/routes/secret.js
+++ b/ui/lib/kv/addon/routes/secret.js
@@ -35,33 +35,38 @@ export default class KvSecretRoute extends Route {
return null;
}
- isPatchAllowed(backend, path) {
+ isPatchAllowed({ subkeys, data }) {
if (!this.version.isEnterprise) return false;
- const capabilities = {
- canPatch: this.capabilities.canPatch(`${backend}/data/${path}`),
- canReadSubkeys: this.capabilities.canRead(`${backend}/subkeys/${path}`),
- };
- return hash(capabilities).then(
- ({ canPatch, canReadSubkeys }) => canPatch && canReadSubkeys,
- // this callback fires if either promise is rejected
- // since this feature is only client-side gated we return false (instead of default to true)
- // for debugging you can pass an arg to log the failure reason
- () => false
- );
+ return subkeys.canRead && data.canPatch;
}
- model() {
+ async fetchCapabilities(backend, path) {
+ const metadataPath = `${backend}/metadata/${path}`;
+ const dataPath = `${backend}/data/${path}`;
+ const subkeysPath = `${backend}/subkeys/${path}`;
+ const perms = await this.capabilities.fetchMultiplePaths([metadataPath, dataPath, subkeysPath]);
+ return {
+ metadata: perms[metadataPath],
+ data: perms[dataPath],
+ subkeys: perms[subkeysPath],
+ };
+ }
+
+ async model() {
const backend = this.secretMountPath.currentPath;
const { name: path } = this.paramsFor('secret');
-
+ const capabilities = await this.fetchCapabilities(backend, path);
return hash({
path,
backend,
subkeys: this.fetchSubkeys(backend, path),
metadata: this.fetchSecretMetadata(backend, path),
- isPatchAllowed: this.isPatchAllowed(backend, path),
- // for creating a new secret version
- canUpdateSecret: this.capabilities.canUpdate(`${backend}/data/${path}`),
+ isPatchAllowed: this.isPatchAllowed(capabilities),
+ canUpdateData: capabilities.data.canUpdate,
+ canReadData: capabilities.data.canRead,
+ canReadMetadata: capabilities.metadata.canRead,
+ canDeleteMetadata: capabilities.metadata.canDelete,
+ canUpdateMetadata: capabilities.metadata.canUpdate,
});
}
diff --git a/ui/lib/kv/addon/routes/secret/metadata.js b/ui/lib/kv/addon/routes/secret/metadata.js
index 1eb77bc269..0f15807bf2 100644
--- a/ui/lib/kv/addon/routes/secret/metadata.js
+++ b/ui/lib/kv/addon/routes/secret/metadata.js
@@ -20,44 +20,22 @@ export default class KvSecretMetadataRoute extends Route {
});
}
- async fetchCapabilities(backend, path) {
- const metadataPath = `${backend}/metadata/${path}`;
- const dataPath = `${backend}/data/${path}`;
- const capabilities = await this.capabilities.fetchMultiplePaths([metadataPath, dataPath]);
- return {
- metadata: capabilities[metadataPath],
- data: capabilities[dataPath],
- };
- }
-
async model() {
const parentModel = this.modelFor('secret');
const { backend, path } = parentModel;
- const permissions = await this.fetchCapabilities(backend, path);
- const model = {
- ...parentModel,
- permissions,
- };
if (!parentModel.metadata) {
// metadata read on the secret root fails silently
// if there's no metadata, try again in case it's a control group
const metadata = await this.fetchMetadata(backend, path);
if (metadata) {
return {
- ...model,
+ ...parentModel,
metadata,
};
}
- // only fetch secret data if metadata is unavailable and user can read endpoint
- if (permissions.data.canRead) {
- // fail silently because this request is just for custom_metadata
- const secret = await this.store.queryRecord('kv/data', { backend, path }).catch(() => {});
- return {
- ...model,
- secret,
- };
- }
}
- return model;
+ // if users can read secret data they can make an explicit request
+ // to retrieve secret data in the component
+ return parentModel;
}
}
diff --git a/ui/lib/kv/addon/templates/secret/details/index.hbs b/ui/lib/kv/addon/templates/secret/details/index.hbs
index 0529c91697..46d53b2421 100644
--- a/ui/lib/kv/addon/templates/secret/details/index.hbs
+++ b/ui/lib/kv/addon/templates/secret/details/index.hbs
@@ -4,9 +4,13 @@
~}}
\ No newline at end of file
diff --git a/ui/lib/kv/addon/templates/secret/index.hbs b/ui/lib/kv/addon/templates/secret/index.hbs
index e915cda8fb..f9cea69072 100644
--- a/ui/lib/kv/addon/templates/secret/index.hbs
+++ b/ui/lib/kv/addon/templates/secret/index.hbs
@@ -7,8 +7,8 @@
@backend={{this.model.backend}}
@breadcrumbs={{this.breadcrumbs}}
@canReadMetadata={{this.model.metadata.canReadMetadata}}
+ @canUpdateData={{this.model.canUpdateData}}
@isPatchAllowed={{this.model.isPatchAllowed}}
- @canUpdateSecret={{this.model.canUpdateSecret}}
@metadata={{this.model.metadata}}
@path={{this.model.path}}
@subkeys={{this.model.subkeys}}
diff --git a/ui/lib/kv/addon/templates/secret/metadata/index.hbs b/ui/lib/kv/addon/templates/secret/metadata/index.hbs
index 90a626c2bf..02f51f4ec7 100644
--- a/ui/lib/kv/addon/templates/secret/metadata/index.hbs
+++ b/ui/lib/kv/addon/templates/secret/metadata/index.hbs
@@ -6,10 +6,10 @@
\ No newline at end of file
diff --git a/ui/mirage/factories/kv-metadatum.js b/ui/mirage/factories/kv-metadatum.js
index b4ea2d56d7..2ddcdc5ac6 100644
--- a/ui/mirage/factories/kv-metadatum.js
+++ b/ui/mirage/factories/kv-metadatum.js
@@ -17,6 +17,9 @@ const data = {
max_versions: 15,
oldest_version: 0,
updated_time: '2023-07-21T03:11:58.095971Z',
+ // the API returns custom_metadata: null if empty but because the attr is an 'object' ember data transforms it to an empty object.
+ // this is important because we rely on the empty object as a truthy value in template conditionals
+ custom_metadata: null,
versions: {
1: {
created_time: '2018-03-22T02:24:06.945319214Z',
@@ -45,10 +48,13 @@ export default Factory.extend({
data,
withCustomMetadata: trait({
- custom_metadata: {
- foo: 'abc',
- bar: '123',
- baz: '5c07d823-3810-48f6-a147-4c06b5219e84',
+ data: {
+ ...data,
+ custom_metadata: {
+ foo: 'abc',
+ bar: '123',
+ baz: '5c07d823-3810-48f6-a147-4c06b5219e84',
+ },
},
}),
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 0bc16b1016..606267b14f 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
@@ -403,6 +403,10 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Metadata page
await click(PAGE.secretTab('Metadata'));
+ assert
+ .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
+ .hasText('Request custom metadata?');
+ await click(PAGE.metadata.requestData);
assert
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
.hasText('No custom metadata', 'No custom metadata empty state');
@@ -548,6 +552,10 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Metadata page
await click(PAGE.secretTab('Metadata'));
+ assert
+ .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
+ .hasText('Request custom metadata?');
+ await click(PAGE.metadata.requestData);
assert
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
.hasText('No custom metadata', 'No custom metadata empty state');
diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js
index c56551b1c8..325ec9fc43 100644
--- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js
+++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js
@@ -122,7 +122,7 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) {
await click(PAGE.list.item(secret));
assert
.dom(GENERAL.overviewCard.container('Current version'))
- .hasText(`Current version Create new The current version of this secret. 1`);
+ .hasText(`Current version The current version of this secret. 1`);
// Secret details visible
await click(PAGE.secretTab('Secret'));
assert.dom(PAGE.title).hasText(this.fullSecretPath);
diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js
index edf9986a64..dfd2e1eb3c 100644
--- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js
+++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js
@@ -5,7 +5,17 @@
import { module, test } from 'qunit';
import { v4 as uuidv4 } from 'uuid';
-import { click, currentRouteName, currentURL, findAll, typeIn, visit, waitUntil } from '@ember/test-helpers';
+import {
+ click,
+ currentRouteName,
+ currentURL,
+ find,
+ findAll,
+ fillIn,
+ typeIn,
+ visit,
+ waitUntil,
+} from '@ember/test-helpers';
import { setupApplicationTest } from 'vault/tests/helpers';
import authPage from 'vault/tests/pages/auth';
import {
@@ -571,7 +581,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
assert.true(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root');
});
test('versioned secret nav, tabs, breadcrumbs (dr)', async function (assert) {
- assert.expect(31);
+ assert.expect(32);
const backend = this.backend;
await navToBackend(backend);
@@ -614,6 +624,10 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
assertCorrectBreadcrumbs(assert, ['Secrets', backend, secretPath, 'Metadata']);
assert.dom(PAGE.title).hasText(secretPath);
assert.dom(PAGE.toolbarAction).doesNotExist('no toolbar actions available on metadata');
+ assert
+ .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
+ .hasText('Request custom metadata?');
+ await click(PAGE.metadata.requestData);
assert
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
.hasText('No custom metadata');
@@ -764,7 +778,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
assert.true(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root');
});
test('versioned secret nav, tabs, breadcrumbs (dlr)', async function (assert) {
- assert.expect(31);
+ assert.expect(32);
const backend = this.backend;
await navToBackend(backend);
await click(PAGE.list.item(secretPath));
@@ -804,6 +818,10 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
);
assertCorrectBreadcrumbs(assert, ['Secrets', backend, secretPath, 'Metadata']);
assert.dom(PAGE.title).hasText(secretPath);
+ assert
+ .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
+ .hasText('Request custom metadata?');
+ await click(PAGE.metadata.requestData);
assert
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
.hasText('No custom metadata');
@@ -1291,11 +1309,11 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) {
// Set up control group scenario
const userPolicy = `
path "${this.backend}/data/*" {
- capabilities = ["create", "read", "update", "delete", "list"]
+ capabilities = ["create", "read", "update", "delete", "list", "patch"]
control_group = {
max_ttl = "24h"
factor "ops_manager" {
- controlled_capabilities = ["read"]
+ controlled_capabilities = ["read", "patch"]
identity {
group_names = ["managers"]
approvals = 1
@@ -1307,6 +1325,10 @@ path "${this.backend}/data/*" {
path "${this.backend}/*" {
capabilities = ["list"]
}
+
+path "${this.backend}/subkeys/*" {
+ capabilities = ["read"]
+}
`;
const { userToken } = await setupControlGroup({ userPolicy, backend: this.backend });
this.userToken = userToken;
@@ -1315,7 +1337,7 @@ path "${this.backend}/*" {
return;
});
test('can access nested secret (cg)', async function (assert) {
- assert.expect(43);
+ assert.expect(44);
const backend = this.backend;
await navToBackend(backend);
assert.dom(PAGE.title).hasText(`${backend} version 2`, 'title text correct');
@@ -1379,7 +1401,7 @@ path "${this.backend}/*" {
);
assertCorrectBreadcrumbs(assert, ['Secrets', backend, 'app', 'nested', 'secret']);
assert.dom(PAGE.title).hasText('app/nested/secret', 'title is full secret path');
- assertDetailsToolbar(assert, ['delete', 'copy', 'createNewVersion']);
+ assertDetailsToolbar(assert, ['delete', 'copy', 'createNewVersion', 'patchLatest']);
await click(PAGE.breadcrumbAtIdx(3));
assert.true(
@@ -1397,7 +1419,7 @@ path "${this.backend}/*" {
assert.true(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root');
});
test('breadcrumbs & page titles are correct (cg)', async function (assert) {
- assert.expect(43);
+ assert.expect(42);
const backend = this.backend;
await navToBackend(backend);
await click(PAGE.secretTab('Configuration'));
@@ -1446,17 +1468,6 @@ path "${this.backend}/*" {
assert.dom(PAGE.secretTab('Version History')).doesNotExist('Version History tab not shown');
- await click(PAGE.secretTab('Secret'));
- assert.true(
- await waitUntil(() => currentRouteName() === 'vault.cluster.access.control-group-accessor'),
- 'redirects to access control group route'
- );
- await grantAccess({
- apiPath: `${backend}/data/${encodeURIComponent(secretPath)}`,
- originUrl: `/vault/secrets/${backend}/kv/${secretPathUrlEncoded}/paths`,
- userToken: this.userToken,
- backend: this.backend,
- });
await click(PAGE.secretTab('Secret'));
assertCorrectBreadcrumbs(assert, ['Secrets', backend, secretPath]);
assert.dom(PAGE.title).hasText(secretPath, 'correct page title for secret details');
@@ -1464,6 +1475,102 @@ path "${this.backend}/*" {
assertCorrectBreadcrumbs(assert, ['Secrets', backend, secretPath, 'Edit']);
assert.dom(PAGE.title).hasText('Create New Version', 'correct page title for secret edit');
});
+ test('can request custom_metadata from data endpoint (cg)', async function (assert) {
+ // custom metadata is empty
+ assert.expect(3);
+ const backend = this.backend;
+ await visit(`/vault/secrets/${backend}/kv/${secretPathUrlEncoded}`);
+ await click(PAGE.secretTab('Metadata'));
+ assert
+ .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
+ .hasText('Request custom metadata?');
+ await click(PAGE.metadata.requestData);
+ assert
+ .dom(GENERAL.messageError)
+ .hasTextContaining(
+ `Control Group Error A Control Group was encountered at ${backend}/data/${secretPath}.`
+ );
+ const url = find('[data-test-control-error="href"]').innerText;
+ await visit(url);
+ await grantAccess({
+ apiPath: `${backend}/data/${encodeURIComponent(secretPath)}`,
+ originUrl: `/vault/secrets/${backend}/kv/${secretPathUrlEncoded}/metadata`,
+ userToken: this.userToken,
+ backend: this.backend,
+ });
+ await click(PAGE.metadata.requestData);
+ assert
+ .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
+ .hasText('No custom metadata', 'empty state updates when access is granted');
+ });
+ test('can patch a secret (cg)', async function (assert) {
+ assert.expect(3);
+ const backend = this.backend;
+ await visit(`/vault/secrets/${backend}/kv/${secretPathUrlEncoded}`);
+ await click(GENERAL.overviewCard.actionText('Patch secret'));
+ await fillIn(FORM.keyInput('new'), 'newkey');
+ await fillIn(FORM.valueInput('new'), 'newvalue');
+ await click(FORM.saveBtn);
+ assert
+ .dom(GENERAL.messageError)
+ .hasTextContaining(
+ `Control Group Error A Control Group was encountered at ${backend}/data/${secretPath}.`
+ );
+ assert
+ .dom(GENERAL.messageError)
+ .hasTextContaining(
+ 'You can re-submit the form once access is granted. Ask your authorizer when to attempt saving again.'
+ );
+ const url = find('[data-test-control-error="href"]').innerText;
+ await visit(url);
+ await grantAccess({
+ apiPath: `${backend}/data/${encodeURIComponent(secretPath)}`,
+ originUrl: `/vault/secrets/${backend}/kv/${secretPathUrlEncoded}/patch`,
+ userToken: this.userToken,
+ backend: this.backend,
+ });
+ // we have to refill the data because granting access reloads the form
+ // however in the real world it's likely access is authorized in a separate browser
+ // once granted, the user can click "submit" the form will save successfully.
+ await fillIn(FORM.keyInput('new'), 'newkey');
+ await fillIn(FORM.valueInput('new'), 'newvalue');
+ await click(FORM.saveBtn);
+ assert.dom(GENERAL.overviewCard.container('Subkeys')).hasTextContaining('Keys foo newkey');
+ });
+ test('can read custom_metadata from data endpoint (cg)', async function (assert) {
+ assert.expect(3);
+ // login is root user and make custom metadata since console can't be used to pass an object
+ await authPage.login();
+ await visit(`/vault/secrets/${this.backend}/kv/${secretPathUrlEncoded}/metadata/edit`);
+ await fillIn(FORM.keyInput(), 'special');
+ await fillIn(FORM.valueInput(), 'secret');
+ await click(FORM.saveBtn);
+ await authPage.login(this.userToken);
+
+ const backend = this.backend;
+ await visit(`/vault/secrets/${backend}/kv/${secretPathUrlEncoded}`);
+
+ await click(PAGE.secretTab('Metadata'));
+ assert
+ .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
+ .hasText('Request custom metadata?');
+ await click(PAGE.metadata.requestData);
+ assert
+ .dom(GENERAL.messageError)
+ .hasTextContaining(
+ `Control Group Error A Control Group was encountered at ${backend}/data/${secretPath}.`
+ );
+ const url = find('[data-test-control-error="href"]').innerText;
+ await visit(url);
+ await grantAccess({
+ apiPath: `${backend}/data/${encodeURIComponent(secretPath)}`,
+ originUrl: `/vault/secrets/${backend}/kv/${secretPathUrlEncoded}/metadata`,
+ userToken: this.userToken,
+ backend: this.backend,
+ });
+ await click(PAGE.metadata.requestData);
+ assert.dom(PAGE.infoRowValue('special')).hasText('secret', 'it renders custom metadata');
+ });
});
// patch is technically enterprise only but stubbing the version so they can run on both CE and enterprise
diff --git a/ui/tests/helpers/kv/kv-selectors.js b/ui/tests/helpers/kv/kv-selectors.js
index 1e0fddbf4e..d919a69c92 100644
--- a/ui/tests/helpers/kv/kv-selectors.js
+++ b/ui/tests/helpers/kv/kv-selectors.js
@@ -29,6 +29,7 @@ export const PAGE = {
link: (backend) => `[data-test-secrets-backend-link="${backend}"]`,
},
metadata: {
+ requestData: '[data-test-request-data]',
editBtn: '[data-test-edit-metadata]',
addCustomMetadataBtn: '[data-test-add-custom-metadata]',
customMetadataSection: '[data-test-kv-custom-metadata-section]',
diff --git a/ui/tests/integration/components/edit-form-kmip-role-test.js b/ui/tests/integration/components/edit-form-kmip-role-test.js
index dac56709a4..8790571f15 100644
--- a/ui/tests/integration/components/edit-form-kmip-role-test.js
+++ b/ui/tests/integration/components/edit-form-kmip-role-test.js
@@ -221,7 +221,7 @@ module('Integration | Component | edit form kmip role', function (hooks) {
);
}
- click('[data-test-edit-form-submit]');
+ await click('[data-test-edit-form-submit]');
later(() => cancelTimers(), 50);
await settled();
diff --git a/ui/tests/integration/components/kv/page/kv-page-metadata-details-test.js b/ui/tests/integration/components/kv/page/kv-page-metadata-details-test.js
index 9a946c283f..0bb92307b8 100644
--- a/ui/tests/integration/components/kv/page/kv-page-metadata-details-test.js
+++ b/ui/tests/integration/components/kv/page/kv-page-metadata-details-test.js
@@ -9,7 +9,6 @@ import { setupEngine } from 'ember-engines/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
-import { kvDataPath } from 'vault/utils/kv-path';
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
import { baseSetup, metadataModel } from 'vault/tests/helpers/kv/kv-run-commands';
import { dateFormat } from 'core/helpers/date-format';
@@ -20,15 +19,8 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
setupMirage(hooks);
hooks.beforeEach(async function () {
+ // this.metadata is setup by baseSetup
baseSetup(this);
- this.dataId = kvDataPath(this.backend, this.path);
- // empty secret model always exists for permissions
- this.store.pushPayload('kv/data', {
- modelName: 'kv/data',
- id: this.dataId,
- custom_metadata: null,
- });
- this.secret = this.store.peekRecord('kv/data', this.dataId);
// this is the route model, not an ember data model
this.model = {
@@ -36,26 +28,28 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
path: this.path,
secret: this.secret,
metadata: this.metadata,
+ canDeleteMetadata: true,
+ canReadData: true,
+ canReadCustomMetadata: true,
+ canReadMetadata: true,
+ canUpdateMetadata: true,
};
this.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: this.model.backend, route: 'list' },
{ label: this.model.path },
];
- this.canDeleteMetadata = true;
- this.canReadCustomMetadata = true;
- this.canReadMetadata = true;
- this.canUpdateMetadata = true;
this.renderComponent = () => {
return render(
hbs`
@@ -86,13 +80,12 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
.hasText('3 hours 25 minutes 19 seconds', 'correctly shows and formats the timestamp.');
});
- test('it renders custom metadata from secret model', async function (assert) {
- assert.expect(2);
- this.secret.customMetadata = { hi: 'there' };
+ test('it renders empty state if cannot read metadata but can read data', async function (assert) {
+ this.model.metadata = null;
await this.renderComponent();
-
- assert.dom(PAGE.emptyStateTitle).doesNotExist();
- assert.dom(PAGE.infoRowValue('hi')).hasText('there', 'renders custom metadata from secret');
+ assert
+ .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
+ .hasText('Request custom metadata?');
});
test('it renders custom metadata from metadata model', async function (assert) {
@@ -107,26 +100,13 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
assert.dom(PAGE.infoRowValue('baz')).hasText('5c07d823-3810-48f6-a147-4c06b5219e84');
});
- test('it renders custom metadata from metadata if secret data exists', async function (assert) {
- assert.expect(4);
- this.secret.customMetadata = { hi: 'there' };
- this.model.metadata = metadataModel(this, { withCustom: true });
- await this.renderComponent();
-
- assert.dom(PAGE.emptyStateTitle).doesNotExist();
- // Metadata details
- assert.dom(PAGE.infoRowValue('foo')).hasText('abc');
- assert.dom(PAGE.infoRowValue('bar')).hasText('123');
- assert.dom(PAGE.infoRowValue('baz')).hasText('5c07d823-3810-48f6-a147-4c06b5219e84');
- });
-
test('it hides delete modal when no permissions', async function (assert) {
- this.canDeleteMetadata = false;
+ this.model.canDeleteMetadata = false;
assert.dom(PAGE.metadata.deleteMetadata).doesNotExist();
});
test('it hides edit action when no permissions', async function (assert) {
- this.canUpdateMetadata = false;
+ this.model.canUpdateMetadata = false;
assert.dom(PAGE.metadata.editBtn).doesNotExist();
});
});
diff --git a/ui/tests/integration/components/kv/page/kv-page-overview-test.js b/ui/tests/integration/components/kv/page/kv-page-overview-test.js
index 9342d621fe..57f087dab3 100644
--- a/ui/tests/integration/components/kv/page/kv-page-overview-test.js
+++ b/ui/tests/integration/components/kv/page/kv-page-overview-test.js
@@ -47,7 +47,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hoo
},
};
this.canReadMetadata = true;
- this.canUpdateSecret = true;
+ this.canUpdateData = true;
this.format = (time) => dateFormat([time, 'MMM d yyyy, h:mm:ss aa'], {});
this.renderComponent = async () => {
@@ -57,7 +57,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hoo
@backend={{this.backend}}
@breadcrumbs={{this.breadcrumbs}}
@canReadMetadata={{this.canReadMetadata}}
- @canUpdateSecret={{this.canUpdateSecret}}
+ @canUpdateData={{this.canUpdateData}}
@metadata={{this.metadata}}
@path={{this.path}}
@subkeys={{this.subkeys}}
@@ -116,7 +116,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hoo
// creating a new version of a secret is updating a secret
// the overview only exists after an initial version is created
// which is why we just check for update and not also create
- this.canUpdateSecret = false;
+ this.canUpdateData = false;
await this.renderComponent();
assert
.dom(`${overviewCard.container('Current version')} a`)
diff --git a/ui/tests/integration/components/kv/page/kv-page-patch-test.js b/ui/tests/integration/components/kv/page/kv-page-patch-test.js
index 06da592043..fc473d2f4f 100644
--- a/ui/tests/integration/components/kv/page/kv-page-patch-test.js
+++ b/ui/tests/integration/components/kv/page/kv-page-patch-test.js
@@ -38,7 +38,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Patch', function (hooks)
},
quux: null,
};
- this.subkeyMeta = {
+ this.subkeysMeta = {
created_time: '2021-12-14T20:28:00.773477Z',
custom_metadata: null,
deletion_time: '',
@@ -244,6 +244,38 @@ module('Integration | Component | kv-v2 | Page::Secret::Patch', function (hooks)
await codemirror().setValue('{ "foo": "" }');
await click(FORM.saveBtn);
});
+
+ test('patch data without metadata permissions', async function (assert) {
+ assert.expect(3);
+ this.metadata = null;
+ this.server.patch(this.endpoint, (schema, req) => {
+ const payload = JSON.parse(req.requestBody);
+ const expected = {
+ data: { aKey: '1' },
+ options: {
+ cas: this.subkeysMeta.version,
+ },
+ };
+ assert.true(true, `PATCH request made to ${this.endpoint}`);
+ assert.propEqual(
+ payload,
+ expected,
+ `payload: ${JSON.stringify(payload)} matches expected: ${JSON.stringify(payload)}`
+ );
+ return EXAMPLE_KV_DATA_CREATE_RESPONSE;
+ });
+
+ await this.renderComponent();
+ await fillIn(FORM.keyInput('new'), 'aKey');
+ await fillIn(FORM.valueInput('new'), '1');
+ await click(FORM.saveBtn);
+ const [route] = this.transitionStub.lastCall.args;
+ assert.strictEqual(
+ route,
+ 'vault.cluster.secrets.backend.kv.secret.index',
+ `it transitions on save to: ${route}`
+ );
+ });
});
module('it does not submit', function (hooks) {
diff --git a/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js b/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js
index 3046ab8f2f..91f00ce749 100644
--- a/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js
+++ b/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js
@@ -26,7 +26,6 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
this.version = 2;
this.dataId = kvDataPath(this.backend, this.path);
this.dataIdComplex = kvDataPath(this.backend, this.pathComplex);
-
this.secretData = { foo: 'bar' };
this.store.pushPayload('kv/data', {
modelName: 'kv/data',
@@ -58,13 +57,17 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
});
this.secret = this.store.peekRecord('kv/data', this.dataId);
this.secretComplex = this.store.peekRecord('kv/data', this.dataIdComplex);
-
// this is the route model, not an ember data model
this.model = {
backend: this.backend,
+ // permissions are tested in navigation acceptance test, so just stub as all true here
+ canReadData: true,
+ canReadMetadata: true,
+ canUpdateData: true,
+ isPatchAllowed: true,
+ metadata: this.metadata,
path: this.path,
secret: this.secret,
- metadata: this.metadata,
};
this.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
@@ -77,6 +80,25 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
secret: this.secretComplex,
metadata: this.metadata,
};
+ this.renderComponent = (model) => {
+ this.model = model ? { ...this.model, ...model } : this.model;
+ return render(
+ hbs`
+
+ `,
+ { owner: this.engine }
+ );
+ };
});
test('it renders secret details and toggles json view', async function (assert) {
@@ -94,19 +116,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
// no records so response returns 404
return syncStatusResponse(schema, req);
});
-
- await render(
- hbs`
-
- `,
- { owner: this.engine }
- );
-
+ await this.renderComponent();
assert
.dom(PAGE.detail.syncAlert())
.doesNotExist('sync page alert banner does not render when sync status errors');
@@ -125,16 +135,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
test('it renders json view when secret is complex', async function (assert) {
assert.expect(4);
- await render(
- hbs`
-
- `,
- { owner: this.engine }
- );
+ await this.renderComponent(this.modelComplex);
assert.dom(PAGE.infoRowValue('foo')).doesNotExist('does not render rows of secret data');
assert.dom(FORM.toggleJson).isChecked();
assert.dom(FORM.toggleJson).isNotDisabled();
@@ -144,17 +145,8 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
test('it renders deleted empty state', async function (assert) {
assert.expect(3);
this.secret.deletionTime = '2023-07-23T02:12:17.379762Z';
- await render(
- hbs`
-
- `,
- { owner: this.engine }
- );
+ await this.renderComponent();
+
assert.dom(PAGE.emptyStateTitle).hasText('Version 2 of this secret has been deleted');
assert
.dom(PAGE.emptyStateMessage)
@@ -169,17 +161,8 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
test('it renders destroyed empty state', async function (assert) {
assert.expect(2);
this.secret.destroyed = true;
- await render(
- hbs`
-
- `,
- { owner: this.engine }
- );
+ await this.renderComponent();
+
assert.dom(PAGE.emptyStateTitle).hasText('Version 2 of this secret has been permanently destroyed');
assert
.dom(PAGE.emptyStateMessage)
@@ -190,18 +173,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
test('it renders secret version dropdown', async function (assert) {
assert.expect(9);
-
- await render(
- hbs`
-
- `,
- { owner: this.engine }
- );
+ await this.renderComponent();
assert.dom(PAGE.detail.versionTimestamp).includesText(this.version, 'renders version');
assert.dom(PAGE.detail.versionDropdown).hasText(`Version ${this.secret.version}`);
@@ -246,18 +218,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
return syncStatusResponse(schema, req);
});
- await render(
- hbs`
-
- `,
- { owner: this.engine }
- );
-
+ await this.renderComponent();
assert
.dom(PAGE.detail.syncAlert(destinationName))
.hasTextContaining(
@@ -290,18 +251,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
creation_path: `${this.backend}/data/${this.path}}`,
};
});
-
- await render(
- hbs`
-
- `,
- { owner: this.engine }
- );
+ await this.renderComponent();
await click(PAGE.detail.copy);
await click(PAGE.detail.wrap);
@@ -325,17 +275,8 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
return syncStatusResponse(schema, req);
});
- await render(
- hbs`
-
- `,
- { owner: this.engine }
- );
+ await this.renderComponent();
+
assert
.dom(PAGE.detail.syncAlert('aws-dest'))
.hasTextContaining('Synced aws-dest - last updated September', 'renders status for aws destination');