mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
* UI: Implement overview page for KV v2 (#28162) * build json editor patch form * finish patch component and tests * add tab to each route * and path route * add overview tab to tests * update overview to use updated_time instead of created_time * redirect relevant secret.details to secret.index * compute secretState in component instead of pass as arg * add capabilities service * add error handling to fetchSubkeys adapter request * add overview tabs to test * add subtext to overview card * remaining redirects in secret edit * remove create new version from popup menu * fix breadcrumbs for overview * separate adding capabilities service * add service to kv engine * Revert "separate adding capabilities service" This reverts commit bb70b12ab7dbcde0fbd2d4d81768e5c8b1c420cc. * Revert "add service to kv engine" This reverts commit bfa880535ef7d529d7610936b2c1aae55673d23f. * update navigation test * consistently navigate to secret.index route to be explicit * finish overview navigation tests * add copyright header * update delete tests * fix nav testrs * cleanup secret edit redirects * remove redundant async/awaits * fix create test * edge case tests * secret acceptance tests * final component tests * rename kvSecretDetails external route to kvSecretOverview * add comment * UI: Add patch route and implement Page::Secret::Patch page component (sidebranch) (#28192) * add tab to each route * and path route * add overview tab to tests * update overview to use updated_time instead of created_time * redirect relevant secret.details to secret.index * compute secretState in component instead of pass as arg * add capabilities service * add error handling to fetchSubkeys adapter request * add patch route and put in page component * add patch secret action to subkeys card * fix component name * add patch capability * alphabetize computed capabilities * update links, cleanup selectors * fix more merge conflict stuff * add capabilities test * add models to patch link * add test for patch route * rename external route * add error templates * make notes about enterprise tests, filter one * remove errors, transition (redirect) instead * redirect patch routes * UI: Move fetching secret data to child route (#28198) * remove @secret from metadata details * use metadata model instead of secret in paths page * put delete back into kv/data adapter * grant access in control group test * update metadata route and permissions * remove secret from parent route, only fetch in details route * change more permissions to route perms, add tests * revert overview redirect from list view * wrap model in conditional for perms * remove redundant canReadCustomMetadata check * rename adapter method * handle overview 404 * remove comment * add customMetadata as an arg * update grantAccess in test * make version param easier to follow * VAULT-30494 handle 404 jira * refactor capabilities to return an object * update create tests * add test for default truthy capabilities * remove destroy-all-versions from kv/data adapter * UI: Add enterprise checks (#28215) * add enterprise check for subkey card * add max height and scroll to subkey card * only fetch subkeys if enterprise * remove check in overview * add test * Update ui/tests/integration/components/kv/page/kv-page-overview-test.js * fix test failures (#28222) * add assertion * add optional chaining * create/delete versioned secret in each module * wait for transition * add another waitUntil * UI: Add patch latest version to toolbar (#28223) * add patch latest version action to toolbar * make isPatchAllowed arg all encompassing * no longer need model check * use hash so both promises fire at the same time * add subkeys to policy * Update ui/lib/kv/addon/routes/secret.js * add changelog * small cleanup items! (#28229) * add conditional for enterprise checking tabs * cleanup fetchMultiplePaths method * add test * remove todo comment, ticket created and design wants to hold off * keep transition, update comments * cleanup tests, add index to breadcrumbs * add some test coverage * toggle so value is readable
655 lines
27 KiB
JavaScript
655 lines
27 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
/* eslint-disable no-useless-escape */
|
|
import { module, test } from 'qunit';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import {
|
|
click,
|
|
currentURL,
|
|
fillIn,
|
|
findAll,
|
|
setupOnerror,
|
|
typeIn,
|
|
visit,
|
|
triggerKeyEvent,
|
|
} from '@ember/test-helpers';
|
|
import { setupApplicationTest } from 'vault/tests/helpers';
|
|
import authPage from 'vault/tests/pages/auth';
|
|
import {
|
|
createPolicyCmd,
|
|
deleteEngineCmd,
|
|
mountEngineCmd,
|
|
runCmd,
|
|
createTokenCmd,
|
|
} from 'vault/tests/helpers/commands';
|
|
import {
|
|
dataPolicy,
|
|
deleteVersionsPolicy,
|
|
destroyVersionsPolicy,
|
|
metadataListPolicy,
|
|
metadataPolicy,
|
|
} from 'vault/tests/helpers/kv/policy-generator';
|
|
import { clearRecords, writeSecret, writeVersionedSecret } from 'vault/tests/helpers/kv/kv-run-commands';
|
|
import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
|
import codemirror from 'vault/tests/helpers/codemirror';
|
|
import { personas } from 'vault/tests/helpers/kv/policy-generator';
|
|
|
|
/**
|
|
* This test set is for testing edge cases, such as specific bug fixes or reported user workflows
|
|
*/
|
|
module('Acceptance | kv-v2 workflow | edge cases', function (hooks) {
|
|
setupApplicationTest(hooks);
|
|
|
|
hooks.beforeEach(async function () {
|
|
const uid = uuidv4();
|
|
this.backend = `kv-edge-${uid}`;
|
|
this.rootSecret = 'root-directory';
|
|
this.fullSecretPath = `${this.rootSecret}/nested/child-secret`;
|
|
await authPage.login();
|
|
await runCmd(mountEngineCmd('kv-v2', this.backend), false);
|
|
await writeSecret(this.backend, this.fullSecretPath, 'foo', 'bar');
|
|
await writeSecret(this.backend, 'edge/one', 'foo', 'bar');
|
|
await writeSecret(this.backend, 'edge/two', 'foo', 'bar');
|
|
return;
|
|
});
|
|
|
|
hooks.afterEach(async function () {
|
|
await authPage.login();
|
|
await runCmd(deleteEngineCmd(this.backend));
|
|
return;
|
|
});
|
|
|
|
module('persona with read and list access on the secret level', function (hooks) {
|
|
// see github issue for more details https://github.com/hashicorp/vault/issues/5362
|
|
hooks.beforeEach(async function () {
|
|
const secretPath = `${this.rootSecret}/*`; // user has LIST and READ access within this root secret directory
|
|
const capabilities = ['list', 'read'];
|
|
const backend = this.backend;
|
|
const token = await runCmd([
|
|
createPolicyCmd(
|
|
`nested-secret-list-reader-${this.backend}`,
|
|
metadataPolicy({ backend, secretPath, capabilities }) +
|
|
dataPolicy({ backend, secretPath, capabilities })
|
|
),
|
|
createTokenCmd(`nested-secret-list-reader-${this.backend}`),
|
|
]);
|
|
await authPage.login(token);
|
|
});
|
|
|
|
test('it can navigate to secrets within a secret directory', async function (assert) {
|
|
assert.expect(23);
|
|
const backend = this.backend;
|
|
const [root, subdirectory, secret] = this.fullSecretPath.split('/');
|
|
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/kv/list`, 'lands on secrets list page');
|
|
|
|
await typeIn(PAGE.list.overviewInput, `${root}/no-access/`);
|
|
assert
|
|
.dom(PAGE.list.overviewButton)
|
|
.hasText('View list', 'shows list and not secret because search is a directory');
|
|
await click(PAGE.list.overviewButton);
|
|
assert.dom(PAGE.emptyStateTitle).hasText(`There are no secrets matching "${root}/no-access/".`);
|
|
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await typeIn(PAGE.list.overviewInput, `${root}/`); // add slash because this is a directory
|
|
await click(PAGE.list.overviewButton);
|
|
|
|
// URL correct
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/list/${root}/`,
|
|
'visits list-directory of root'
|
|
);
|
|
|
|
// Title correct
|
|
assert.dom(PAGE.title).hasText(`${backend} version 2`);
|
|
// Tabs correct
|
|
assert.dom(PAGE.secretTab('Secrets')).hasText('Secrets');
|
|
assert.dom(PAGE.secretTab('Secrets')).hasClass('active');
|
|
assert.dom(PAGE.secretTab('Configuration')).hasText('Configuration');
|
|
assert.dom(PAGE.secretTab('Configuration')).doesNotHaveClass('active');
|
|
// Toolbar correct
|
|
assert.dom(PAGE.toolbarAction).exists({ count: 1 }, 'toolbar only renders create secret action');
|
|
assert.dom(PAGE.list.filter).hasValue(`${root}/`);
|
|
// List content correct
|
|
assert.dom(PAGE.list.item(`${subdirectory}/`)).exists('renders linked block for subdirectory');
|
|
await click(PAGE.list.item(`${subdirectory}/`));
|
|
assert.dom(PAGE.list.item(secret)).exists('renders linked block for child secret');
|
|
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`);
|
|
// Secret details visible
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.dom(PAGE.title).hasText(this.fullSecretPath);
|
|
assert.dom(PAGE.secretTab('Secret')).hasText('Secret');
|
|
assert.dom(PAGE.secretTab('Secret')).hasClass('active');
|
|
assert.dom(PAGE.secretTab('Metadata')).hasText('Metadata');
|
|
assert.dom(PAGE.secretTab('Metadata')).doesNotHaveClass('active');
|
|
assert.dom(PAGE.secretTab('Version History')).hasText('Version History');
|
|
assert.dom(PAGE.secretTab('Version History')).doesNotHaveClass('active');
|
|
assert.dom(PAGE.detail.copy).exists();
|
|
assert.dom(PAGE.detail.versionDropdown).exists();
|
|
});
|
|
|
|
test('it navigates back to engine index route via breadcrumbs from secret details', async function (assert) {
|
|
assert.expect(6);
|
|
const backend = this.backend;
|
|
const [root, subdirectory, secret] = this.fullSecretPath.split('/');
|
|
|
|
await visit(`vault/secrets/${backend}/kv/${encodeURIComponent(this.fullSecretPath)}/details?version=1`);
|
|
// navigate back through crumbs
|
|
let previousCrumb = findAll('[data-test-breadcrumbs] li').length - 2;
|
|
await click(PAGE.breadcrumbAtIdx(previousCrumb));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/list/${root}/${subdirectory}/`,
|
|
'goes back to subdirectory list'
|
|
);
|
|
assert.dom(PAGE.list.filter).hasValue(`${root}/${subdirectory}/`);
|
|
assert.dom(PAGE.list.item(secret)).exists('renders linked block for child secret');
|
|
|
|
// back again
|
|
previousCrumb = findAll('[data-test-breadcrumbs] li').length - 2;
|
|
await click(PAGE.breadcrumbAtIdx(previousCrumb));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/list/${root}/`,
|
|
'goes back to root directory'
|
|
);
|
|
assert.dom(PAGE.list.item(`${subdirectory}/`)).exists('renders linked block for subdirectory');
|
|
|
|
// and back to the engine list view
|
|
previousCrumb = findAll('[data-test-breadcrumbs] li').length - 2;
|
|
await click(PAGE.breadcrumbAtIdx(previousCrumb));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/list`,
|
|
'navigates back to engine list from crumbs'
|
|
);
|
|
});
|
|
|
|
test('it handles errors when attempting to view details of a secret that is a directory', async function (assert) {
|
|
assert.expect(7);
|
|
const backend = this.backend;
|
|
const [root, subdirectory] = this.fullSecretPath.split('/');
|
|
setupOnerror((error) => assert.strictEqual(error.httpStatus, 404), '404 error is thrown'); // catches error so qunit test doesn't fail
|
|
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await typeIn(PAGE.list.overviewInput, `${root}/${subdirectory}`); // intentionally leave out trailing slash
|
|
await click(PAGE.list.overviewButton);
|
|
assert.dom(PAGE.error.title).hasText('404 Not Found');
|
|
assert
|
|
.dom(PAGE.error.message)
|
|
.hasText(
|
|
`Sorry, we were unable to find any content at /v1/${backend}/metadata/${root}/${subdirectory}.`
|
|
);
|
|
|
|
assert.dom(PAGE.breadcrumbAtIdx(0)).hasText('Secrets');
|
|
assert.dom(PAGE.breadcrumbAtIdx(1)).hasText(backend);
|
|
assert.dom(PAGE.secretTab('Secrets')).doesNotHaveClass('is-active');
|
|
assert.dom(PAGE.secretTab('Configuration')).doesNotHaveClass('is-active');
|
|
});
|
|
});
|
|
|
|
module('destruction without read', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
const backend = this.backend;
|
|
const testSecrets = [
|
|
'data-delete-only',
|
|
'delete-version-only',
|
|
'destroy-version-only',
|
|
'destroy-metadata-only',
|
|
];
|
|
|
|
// user has different permissions for each secret path
|
|
const token = await runCmd([
|
|
createPolicyCmd(
|
|
`destruction-no-read-${this.backend}`,
|
|
dataPolicy({ backend, secretPath: 'data-delete-only', capabilities: ['delete'] }) +
|
|
deleteVersionsPolicy({ backend, secretPath: 'delete-version-only' }) +
|
|
destroyVersionsPolicy({ backend, secretPath: 'destroy-version-only' }) +
|
|
metadataPolicy({ backend, secretPath: 'destroy-metadata-only', capabilities: ['delete'] }) +
|
|
metadataListPolicy(backend)
|
|
),
|
|
createTokenCmd(`destruction-no-read-${this.backend}`),
|
|
]);
|
|
for (const secret of testSecrets) {
|
|
await writeVersionedSecret(backend, secret, 'foo', 'bar', 2);
|
|
}
|
|
await authPage.login(token);
|
|
});
|
|
|
|
test('it renders the delete action and disables delete this version option', async function (assert) {
|
|
assert.expect(4);
|
|
const testSecret = 'data-delete-only';
|
|
await visit(`/vault/secrets/${this.backend}/kv/${testSecret}/details`);
|
|
|
|
assert.dom(PAGE.detail.delete).exists('renders delete button');
|
|
await click(PAGE.detail.delete);
|
|
assert
|
|
.dom(PAGE.detail.deleteModal)
|
|
.hasTextContaining('Delete this version This deletes a specific version of the secret');
|
|
assert.dom(PAGE.detail.deleteOption).isDisabled('disables version specific option');
|
|
assert.dom(PAGE.detail.deleteOptionLatest).isEnabled('enables version specific option');
|
|
});
|
|
|
|
test('it renders the delete action and disables delete latest version option', async function (assert) {
|
|
assert.expect(4);
|
|
const testSecret = 'delete-version-only';
|
|
await visit(`/vault/secrets/${this.backend}/kv/${testSecret}/details`);
|
|
|
|
assert.dom(PAGE.detail.delete).exists('renders delete button');
|
|
await click(PAGE.detail.delete);
|
|
assert
|
|
.dom(PAGE.detail.deleteModal)
|
|
.hasTextContaining('Delete this version This deletes a specific version of the secret');
|
|
|
|
assert.dom(PAGE.detail.deleteOption).isEnabled('enables version specific option');
|
|
assert.dom(PAGE.detail.deleteOptionLatest).isDisabled('disables version specific option');
|
|
});
|
|
|
|
test('it hides destroy option without version number', async function (assert) {
|
|
assert.expect(1);
|
|
const testSecret = 'destroy-version-only';
|
|
await visit(`/vault/secrets/${this.backend}/kv/${testSecret}/details`);
|
|
|
|
assert.dom(PAGE.detail.destroy).doesNotExist();
|
|
});
|
|
|
|
test('it renders the destroy metadata action and expected modal copy', async function (assert) {
|
|
assert.expect(2);
|
|
|
|
const testSecret = 'destroy-metadata-only';
|
|
await visit(`/vault/secrets/${this.backend}/kv/${testSecret}/metadata`);
|
|
assert.dom(PAGE.metadata.deleteMetadata).exists('renders delete metadata button');
|
|
await click(PAGE.metadata.deleteMetadata);
|
|
assert
|
|
.dom(PAGE.detail.deleteModal)
|
|
.hasText(
|
|
'Delete metadata and secret data? This will permanently delete the metadata and versions of the secret. All version history will be removed. This cannot be undone. Confirm Cancel'
|
|
);
|
|
});
|
|
});
|
|
|
|
test('no ghost item after editing metadata', async function (assert) {
|
|
await visit(`/vault/secrets/${this.backend}/kv/list/edge/`);
|
|
assert.dom(PAGE.list.item()).exists({ count: 2 }, 'two secrets are listed');
|
|
await click(PAGE.list.item('two'));
|
|
await click(PAGE.secretTab('Metadata'));
|
|
await click(PAGE.metadata.editBtn);
|
|
await fillIn(FORM.keyInput(), 'foo');
|
|
await fillIn(FORM.valueInput(), 'bar');
|
|
await click(FORM.saveBtn);
|
|
await click(PAGE.breadcrumbAtIdx(2));
|
|
assert.dom(PAGE.list.item()).exists({ count: 2 }, 'two secrets are listed');
|
|
});
|
|
|
|
test('advanced secret values default to JSON display', async function (assert) {
|
|
const obscuredData = `{
|
|
"foo3": {
|
|
"name": "********"
|
|
}
|
|
}`;
|
|
await visit(`/vault/secrets/${this.backend}/kv/create`);
|
|
await fillIn(FORM.inputByAttr('path'), 'complex');
|
|
|
|
await click(FORM.toggleJson);
|
|
|
|
assert.strictEqual(
|
|
codemirror().getValue(),
|
|
`{
|
|
\"\": \"\"
|
|
}`
|
|
);
|
|
codemirror().setValue('{ "foo3": { "name": "bar3" } }');
|
|
await click(FORM.saveBtn);
|
|
|
|
// Details view
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.dom(FORM.toggleJson).isNotDisabled();
|
|
assert.dom(FORM.toggleJson).isChecked();
|
|
assert.strictEqual(
|
|
codemirror().getValue(),
|
|
obscuredData,
|
|
'Value is obscured by default on details view when advanced'
|
|
);
|
|
await click('[data-test-toggle-input="revealValues"]');
|
|
assert.false(codemirror().getValue().includes('*'), 'Value unobscured after toggle');
|
|
|
|
// New version view
|
|
await click(PAGE.detail.createNewVersion);
|
|
assert.dom(FORM.toggleJson).isNotDisabled();
|
|
assert.dom(FORM.toggleJson).isChecked();
|
|
assert.false(codemirror().getValue().includes('*'), 'Values are not obscured on edit view');
|
|
});
|
|
|
|
test('on enter the JSON editor cursor goes to the next line', async function (assert) {
|
|
// see issue here: https://github.com/hashicorp/vault/issues/27524
|
|
const predictedCursorPosition = JSON.stringify({ line: 3, ch: 0, sticky: null });
|
|
await visit(`/vault/secrets/${this.backend}/kv/create`);
|
|
await fillIn(FORM.inputByAttr('path'), 'json jump');
|
|
|
|
await click(FORM.toggleJson);
|
|
codemirror().setCursor({ line: 2, ch: 1 });
|
|
await triggerKeyEvent(GENERAL.codemirrorTextarea, 'keydown', 'Enter');
|
|
const actualCursorPosition = JSON.stringify(codemirror().getCursor());
|
|
assert.strictEqual(actualCursorPosition, predictedCursorPosition, 'the cursor stayed on the next line');
|
|
});
|
|
|
|
test('viewing advanced secret data versions displays the correct version data', async function (assert) {
|
|
assert.expect(2);
|
|
const obscuredDataV1 = `{
|
|
"foo1": {
|
|
"name": "********"
|
|
}
|
|
}`;
|
|
const obscuredDataV2 = `{
|
|
"foo2": {
|
|
"name": "********"
|
|
}
|
|
}`;
|
|
|
|
await visit(`/vault/secrets/${this.backend}/kv/create`);
|
|
await fillIn(FORM.inputByAttr('path'), 'complex_version_test');
|
|
|
|
await click(FORM.toggleJson);
|
|
codemirror().setValue('{ "foo1": { "name": "bar1" } }');
|
|
await click(FORM.saveBtn);
|
|
|
|
// Create another version
|
|
await click(GENERAL.overviewCard.actionText('Create new'));
|
|
codemirror().setValue('{ "foo2": { "name": "bar2" } }');
|
|
await click(FORM.saveBtn);
|
|
|
|
// View the first version and make sure the secret data is correct
|
|
await click(PAGE.secretTab('Secret'));
|
|
await click(PAGE.detail.versionDropdown);
|
|
await click(`${PAGE.detail.version(1)} a`);
|
|
assert.strictEqual(codemirror().getValue(), obscuredDataV1, 'Version one data is displayed');
|
|
|
|
// Navigate back the second version and make sure the secret data is correct
|
|
await click(PAGE.detail.versionDropdown);
|
|
await click(`${PAGE.detail.version(2)} a`);
|
|
assert.strictEqual(codemirror().getValue(), obscuredDataV2, 'Version two data is displayed');
|
|
});
|
|
|
|
test('does not register as advanced when value includes {', async function (assert) {
|
|
await visit(`/vault/secrets/${this.backend}/kv/create`);
|
|
await fillIn(FORM.inputByAttr('path'), 'not-advanced');
|
|
|
|
await fillIn(FORM.keyInput(), 'foo');
|
|
await fillIn(FORM.maskedValueInput(), '{bar}');
|
|
await click(FORM.saveBtn);
|
|
await click(GENERAL.overviewCard.actionText('Create new'));
|
|
assert.dom(FORM.toggleJson).isNotDisabled();
|
|
assert.dom(FORM.toggleJson).isNotChecked();
|
|
});
|
|
|
|
// patch is technically enterprise only but stubbing the version so these run on both CE and enterprise
|
|
module('patch-persona', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
this.patchSecret = 'patch-secret';
|
|
this.version = this.owner.lookup('service:version');
|
|
this.version.type = 'enterprise';
|
|
this.store = this.owner.lookup('service:store');
|
|
await writeSecret(this.backend, this.patchSecret, 'foo', 'bar');
|
|
const token = await runCmd([
|
|
createPolicyCmd(
|
|
`secret-patcher-${this.backend}`,
|
|
personas.secretPatcher(this.backend) + personas.secretPatcher(this.emptyBackend)
|
|
),
|
|
createTokenCmd(`secret-patcher-${this.backend}`),
|
|
]);
|
|
await authPage.login(token);
|
|
clearRecords(this.store);
|
|
return;
|
|
});
|
|
|
|
test('it patches a secret from the overview page', async function (assert) {
|
|
await visit(`/vault/secrets/${this.backend}/kv/${this.patchSecret}`);
|
|
assert.dom(GENERAL.overviewCard.content('Subkeys')).hasText('Keys foo');
|
|
|
|
await click(GENERAL.overviewCard.actionText('Patch secret'));
|
|
await click(FORM.patchEdit(0));
|
|
await fillIn(FORM.valueInput(0), 'newvalue');
|
|
await fillIn(FORM.keyInput('new'), 'newkey');
|
|
await fillIn(FORM.valueInput('new'), 'newvalue');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(GENERAL.overviewCard.content('Subkeys')).hasText('Keys foo newkey');
|
|
});
|
|
|
|
test('it patches a secret from the secret details', async function (assert) {
|
|
await visit(`/vault/secrets/${this.backend}/kv/${this.patchSecret}`);
|
|
assert.dom(GENERAL.overviewCard.content('Subkeys')).hasText('Keys foo');
|
|
await click(PAGE.secretTab('Secret'));
|
|
await click(PAGE.detail.patchLatest);
|
|
await click(FORM.patchEdit(0));
|
|
await fillIn(FORM.valueInput(0), 'newvalue');
|
|
await fillIn(FORM.keyInput('new'), 'newkey');
|
|
await fillIn(FORM.valueInput('new'), 'newvalue');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(GENERAL.overviewCard.content('Subkeys')).hasText('Keys foo newkey');
|
|
});
|
|
|
|
// in the same test because the writeSecret helper only creates a single key/value pair
|
|
test('it adds and deletes a key', async function (assert) {
|
|
await visit(`/vault/secrets/${this.backend}/kv/${this.patchSecret}`);
|
|
// add a new key
|
|
assert.dom(GENERAL.overviewCard.content('Subkeys')).hasText('Keys foo');
|
|
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.overviewCard.content('Subkeys')).hasText('Keys foo newkey');
|
|
|
|
// deletes a key
|
|
await click(GENERAL.overviewCard.actionText('Patch secret'));
|
|
await click(FORM.patchDelete());
|
|
await click(FORM.saveBtn);
|
|
assert.dom(GENERAL.overviewCard.content('Subkeys')).hasText('Keys newkey');
|
|
});
|
|
});
|
|
});
|
|
|
|
// NAMESPACE TESTS
|
|
module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks) {
|
|
setupApplicationTest(hooks);
|
|
|
|
const navToEngine = async (backend) => {
|
|
await click('[data-test-sidebar-nav-link="Secrets Engines"]');
|
|
return await click(PAGE.backends.link(backend));
|
|
};
|
|
|
|
const assertDeleteActions = (assert, expected = ['delete', 'destroy']) => {
|
|
['delete', 'destroy', 'undelete'].forEach((toolbar) => {
|
|
if (expected.includes(toolbar)) {
|
|
assert.dom(PAGE.detail[toolbar]).exists(`${toolbar} toolbar action exists`);
|
|
} else {
|
|
assert.dom(PAGE.detail[toolbar]).doesNotExist(`${toolbar} toolbar action not rendered`);
|
|
}
|
|
});
|
|
};
|
|
|
|
const assertVersionDropdown = async (assert, deleted = [], versions = [2, 1]) => {
|
|
assert.dom(PAGE.detail.versionDropdown).hasText(`Version ${versions[0]}`);
|
|
await click(PAGE.detail.versionDropdown);
|
|
versions.forEach((num) => {
|
|
assert.dom(PAGE.detail.version(num)).exists(`renders version ${num} link in dropdown`);
|
|
});
|
|
// also asserts destroyed icon
|
|
deleted.forEach((num) => {
|
|
assert.dom(`${PAGE.detail.version(num)} [data-test-icon="x-square"]`);
|
|
});
|
|
};
|
|
|
|
// each test uses a different secret path
|
|
hooks.beforeEach(async function () {
|
|
const uid = uuidv4();
|
|
this.store = this.owner.lookup('service:store');
|
|
this.backend = `kv-enterprise-edge-${uid}`;
|
|
this.namespace = `ns-${uid}`;
|
|
await authPage.login();
|
|
await runCmd([`write sys/namespaces/${this.namespace} -force`]);
|
|
return;
|
|
});
|
|
|
|
hooks.afterEach(async function () {
|
|
await authPage.login();
|
|
await runCmd([`delete /sys/auth/${this.namespace}`]);
|
|
await runCmd(deleteEngineCmd(this.backend));
|
|
return;
|
|
});
|
|
|
|
module('admin persona', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
await authPage.loginNs(this.namespace);
|
|
// mount engine within namespace
|
|
await runCmd(mountEngineCmd('kv-v2', this.backend), false);
|
|
clearRecords(this.store);
|
|
return;
|
|
});
|
|
hooks.afterEach(async function () {
|
|
// visit logout with namespace query param because we're transitioning from within an engine
|
|
// and navigating directly to /vault/auth caused test context routing problems :(
|
|
await visit(`/vault/logout?namespace=${this.namespace}`);
|
|
await authPage.namespaceInput(''); // clear login form namespace input
|
|
});
|
|
|
|
test('namespace: it can create a secret and new secret version', async function (assert) {
|
|
assert.expect(16);
|
|
const backend = this.backend;
|
|
const ns = this.namespace;
|
|
const secret = 'my-create-secret';
|
|
await navToEngine(backend);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/list?namespace=${ns}`,
|
|
'navigates to list'
|
|
);
|
|
// Create first version of secret
|
|
await click(PAGE.list.createSecret);
|
|
await fillIn(FORM.inputByAttr('path'), secret);
|
|
assert.dom(FORM.toggleMetadata).exists('Shows metadata toggle when creating new secret');
|
|
await fillIn(FORM.keyInput(), 'foo');
|
|
await fillIn(FORM.maskedValueInput(), 'woahsecret');
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(GENERAL.overviewCard.container('Current version'))
|
|
.hasText(`Current version Create new The current version of this secret. 1`);
|
|
|
|
// Create a new version
|
|
await click(GENERAL.overviewCard.actionText('Create new'));
|
|
assert.dom(FORM.inputByAttr('path')).isDisabled('path input is disabled');
|
|
assert.dom(FORM.inputByAttr('path')).hasValue(secret);
|
|
assert.dom(FORM.toggleMetadata).doesNotExist('Does not show metadata toggle when creating new version');
|
|
assert.dom(FORM.keyInput()).hasValue('foo');
|
|
assert.dom(FORM.maskedValueInput()).hasValue('woahsecret');
|
|
await fillIn(FORM.keyInput(1), 'foo-two');
|
|
await fillIn(FORM.maskedValueInput(1), 'supersecret');
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(GENERAL.overviewCard.container('Current version'))
|
|
.hasText(`Current version Create new The current version of this secret. 2`);
|
|
|
|
// Check details
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${secret}/details?namespace=${ns}&version=2`,
|
|
'navigates to details'
|
|
);
|
|
await assertVersionDropdown(assert);
|
|
assert
|
|
.dom(`${PAGE.detail.version(2)} [data-test-icon="check-circle"]`)
|
|
.exists('renders current version icon');
|
|
assert.dom(PAGE.infoRowValue('foo-two')).hasText('***********');
|
|
await click(PAGE.infoRowToggleMasked('foo-two'));
|
|
assert.dom(PAGE.infoRowValue('foo-two')).hasText('supersecret', 'secret value shows after toggle');
|
|
});
|
|
|
|
test('namespace: it manages state throughout delete, destroy and undelete operations', async function (assert) {
|
|
assert.expect(36);
|
|
const backend = this.backend;
|
|
const ns = this.namespace;
|
|
const secret = 'my-delete-secret';
|
|
await writeVersionedSecret(backend, secret, 'foo', 'bar', 2, ns);
|
|
await navToEngine(backend);
|
|
|
|
await click(PAGE.list.item(secret));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${secret}?namespace=${ns}`,
|
|
'navigates to overview'
|
|
);
|
|
|
|
// correct toolbar options & details show
|
|
await click(PAGE.secretTab('Secret'));
|
|
assertDeleteActions(assert);
|
|
await assertVersionDropdown(assert);
|
|
// delete flow
|
|
await click(PAGE.detail.delete);
|
|
await click(PAGE.detail.deleteOption);
|
|
await click(PAGE.detail.deleteConfirm);
|
|
assert
|
|
.dom(GENERAL.overviewCard.container('Current version'))
|
|
.hasTextContaining(
|
|
'Current version Deleted Create new The current version of this secret was deleted'
|
|
);
|
|
|
|
await click(PAGE.secretTab('Secret'));
|
|
// check empty state and toolbar
|
|
assertDeleteActions(assert, ['undelete', 'destroy']);
|
|
assert
|
|
.dom(PAGE.emptyStateTitle)
|
|
.hasText('Version 2 of this secret has been deleted', 'Shows deleted message');
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 deleted');
|
|
await assertVersionDropdown(assert, [2]); // important to test dropdown versions are accurate
|
|
|
|
// navigate to sibling route to make sure empty state remains for details tab
|
|
await click(PAGE.secretTab('Version History'));
|
|
assert.dom(PAGE.versions.linkedBlock()).exists({ count: 2 });
|
|
|
|
// back to secret tab to confirm deleted state
|
|
await click(PAGE.secretTab('Secret'));
|
|
// if this assertion fails, the view is rendering a stale model
|
|
assert.dom(PAGE.emptyStateTitle).exists('still renders empty state!!');
|
|
await assertVersionDropdown(assert, [2]);
|
|
|
|
// undelete flow
|
|
await click(PAGE.detail.undelete);
|
|
assert
|
|
.dom(GENERAL.overviewCard.container('Current version'))
|
|
.hasTextContaining('Current version Create new The current version of this secret.');
|
|
// details update accordingly
|
|
await click(PAGE.secretTab('Secret'));
|
|
assertDeleteActions(assert, ['delete', 'destroy']);
|
|
assert.dom(PAGE.infoRow).exists('shows secret data');
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 created');
|
|
|
|
// destroy flow
|
|
await click(PAGE.detail.destroy);
|
|
await click(PAGE.detail.deleteConfirm);
|
|
await click(PAGE.secretTab('Secret'));
|
|
assertDeleteActions(assert, []);
|
|
assert
|
|
.dom(PAGE.emptyStateTitle)
|
|
.hasText('Version 2 of this secret has been permanently destroyed', 'Shows destroyed message');
|
|
|
|
// navigate to sibling route to make sure empty state remains for details tab
|
|
await click(PAGE.secretTab('Version History'));
|
|
assert.dom(PAGE.versions.linkedBlock()).exists({ count: 2 });
|
|
|
|
// back to secret tab to confirm destroyed state
|
|
await click(PAGE.secretTab('Secret'));
|
|
// if this assertion fails, the view is rendering a stale model
|
|
assert.dom(PAGE.emptyStateTitle).exists('still renders empty state!!');
|
|
await assertVersionDropdown(assert, [2]);
|
|
});
|
|
});
|
|
});
|