mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 02:02:43 +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
1227 lines
57 KiB
JavaScript
1227 lines
57 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import { module, test } from 'qunit';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { click, currentURL, fillIn, typeIn, visit } from '@ember/test-helpers';
|
|
|
|
import { setupApplicationTest } from 'vault/tests/helpers';
|
|
import authPage from 'vault/tests/pages/auth';
|
|
import { deleteEngineCmd, mountEngineCmd, runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
|
|
import { personas } 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 { grantAccessForWrite, setupControlGroup } from 'vault/tests/helpers/control-groups';
|
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
|
|
|
/**
|
|
* This test set is for testing the flow for creating new secrets and versions.
|
|
* Letter(s) in parenthesis at the end are shorthand for the persona,
|
|
* for ease of tracking down specific tests failures from CI
|
|
*/
|
|
module('Acceptance | kv-v2 workflow | secret and version create', function (hooks) {
|
|
setupApplicationTest(hooks);
|
|
|
|
hooks.beforeEach(async function () {
|
|
this.backend = `kv-create-${uuidv4()}`;
|
|
this.store = this.owner.lookup('service:store');
|
|
await authPage.login();
|
|
await runCmd(mountEngineCmd('kv-v2', this.backend), false);
|
|
await writeVersionedSecret(this.backend, 'app/first', 'foo', 'bar', 2);
|
|
});
|
|
|
|
hooks.afterEach(async function () {
|
|
await authPage.login();
|
|
return runCmd(deleteEngineCmd(this.backend));
|
|
});
|
|
|
|
module('admin persona', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
const token = await runCmd(tokenWithPolicyCmd(`admin-${this.backend}`, personas.admin(this.backend)));
|
|
await authPage.login(token);
|
|
clearRecords(this.store);
|
|
return;
|
|
});
|
|
test('cancel on create clears model (a)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
assert.dom(PAGE.list.item()).exists({ count: 1 }, 'single secret exists on list');
|
|
assert.dom(PAGE.list.item('app/')).hasText('app/', 'expected list item');
|
|
await click(PAGE.list.createSecret);
|
|
await fillIn(FORM.inputByAttr('path'), 'jk');
|
|
await click(FORM.cancelBtn);
|
|
assert.dom(PAGE.list.item()).exists({ count: 1 }, 'same amount of secrets');
|
|
assert.dom(PAGE.list.item('app/')).hasText('app/', 'expected list item');
|
|
await click(PAGE.list.createSecret);
|
|
await fillIn(FORM.inputByAttr('path'), 'psych');
|
|
await click(PAGE.breadcrumbAtIdx(1));
|
|
assert.dom(PAGE.list.item()).exists({ count: 1 }, 'same amount of secrets');
|
|
assert.dom(PAGE.list.item('app/')).hasText('app/', 'expected list item');
|
|
});
|
|
test('cancel on new version rolls back model (a)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/${encodeURIComponent('app/first')}/details`);
|
|
assert.dom(PAGE.infoRowValue('foo')).exists('key has expected value');
|
|
await click(PAGE.detail.createNewVersion);
|
|
await fillIn(FORM.keyInput(), 'bar');
|
|
await click(FORM.cancelBtn);
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.dom(PAGE.infoRowValue('foo')).exists('secret is previous value');
|
|
await click(PAGE.detail.createNewVersion);
|
|
await fillIn(FORM.keyInput(), 'bar');
|
|
await click(PAGE.breadcrumbAtIdx(3));
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.dom(PAGE.infoRowValue('foo')).exists('secret is previous value');
|
|
});
|
|
test('create & update root secret with default metadata (a)', async function (assert) {
|
|
const backend = this.backend;
|
|
const secretPath = 'some secret';
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await click(PAGE.list.createSecret);
|
|
|
|
// Create secret form -- validations
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
|
|
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
|
|
await typeIn(FORM.inputByAttr('path'), secretPath);
|
|
assert
|
|
.dom(FORM.validationWarning)
|
|
.hasText(
|
|
"Path contains whitespace. If this is desired, you'll need to encode it with %20 in API requests."
|
|
);
|
|
assert.dom(PAGE.create.metadataSection).doesNotExist('Hides metadata section by default');
|
|
|
|
// Submit with API errors
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.messageError).hasText('Error no data provided', 'API error shows on form');
|
|
|
|
await fillIn(FORM.keyInput(), 'api_key');
|
|
await fillIn(FORM.maskedValueInput(), 'partyparty');
|
|
await click(FORM.saveBtn);
|
|
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent(secretPath)}`,
|
|
'Goes to overview after save'
|
|
);
|
|
// Details page
|
|
await click(PAGE.secretTab('Secret'));
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent(secretPath)}/details?version=1`,
|
|
'details has version 1 param';
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 1 created');
|
|
assert.dom(PAGE.infoRow).exists({ count: 1 }, '1 row of data shows');
|
|
assert.dom(PAGE.infoRowValue('api_key')).hasText('***********');
|
|
await click(PAGE.infoRowToggleMasked('api_key'));
|
|
assert.dom(PAGE.infoRowValue('api_key')).hasText('partyparty', 'secret value shows after toggle');
|
|
|
|
// Metadata page
|
|
await click(PAGE.secretTab('Metadata'));
|
|
assert
|
|
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
|
.hasText('No custom metadata', 'No custom metadata empty state');
|
|
assert
|
|
.dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.infoRow}`)
|
|
.exists({ count: 4 }, '4 metadata rows show');
|
|
assert.dom(PAGE.infoRowValue('Maximum versions')).hasText('0', 'max versions shows 0');
|
|
assert.dom(PAGE.infoRowValue('Check-and-Set required')).hasText('No', 'cas not enforced');
|
|
assert
|
|
.dom(PAGE.infoRowValue('Delete version after'))
|
|
.hasText('Never delete', 'Delete version after has default 0s');
|
|
|
|
// Add new version
|
|
await click(PAGE.secretTab('Secret'));
|
|
await click(PAGE.detail.createNewVersion);
|
|
assert.dom(FORM.inputByAttr('path')).isDisabled('path input is disabled');
|
|
assert.dom(FORM.inputByAttr('path')).hasValue(secretPath);
|
|
assert.dom(FORM.toggleMetadata).doesNotExist('Does not show metadata toggle when creating new version');
|
|
assert.dom(FORM.keyInput()).hasValue('api_key');
|
|
assert.dom(FORM.maskedValueInput()).hasValue('partyparty');
|
|
await fillIn(FORM.keyInput(1), 'api_url');
|
|
await fillIn(FORM.maskedValueInput(1), 'hashicorp.com');
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(GENERAL.overviewCard.container('Current version'))
|
|
.hasTextContaining('2', 'Overview shows updated version');
|
|
|
|
// Back to details page
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent(secretPath)}/details?version=2`
|
|
);
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 created');
|
|
assert.dom(PAGE.infoRow).exists({ count: 2 }, '2 rows of data shows');
|
|
assert.dom(PAGE.infoRowValue('api_key')).hasText('***********');
|
|
assert.dom(PAGE.infoRowValue('api_url')).hasText('***********');
|
|
await click(PAGE.infoRowToggleMasked('api_key'));
|
|
await click(PAGE.infoRowToggleMasked('api_url'));
|
|
assert.dom(PAGE.infoRowValue('api_key')).hasText('partyparty', 'secret value shows after toggle');
|
|
assert.dom(PAGE.infoRowValue('api_url')).hasText('hashicorp.com', 'secret value shows after toggle');
|
|
});
|
|
test('create nested secret with metadata (a)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await click(PAGE.list.createSecret);
|
|
|
|
// Create secret
|
|
await typeIn(FORM.inputByAttr('path'), 'my/');
|
|
assert.dom(FORM.validation('path')).hasText("Path can't end in forward slash '/'.");
|
|
await typeIn(FORM.inputByAttr('path'), 'secret');
|
|
assert.dom(FORM.validation('path')).doesNotExist('form validation goes away');
|
|
await fillIn(FORM.keyInput(), 'password');
|
|
await fillIn(FORM.maskedValueInput(), 'kittens1234');
|
|
|
|
await click(FORM.toggleMetadata);
|
|
assert.dom(PAGE.create.metadataSection).exists('Shows metadata section after toggled');
|
|
// Check initial values
|
|
assert.dom(FORM.inputByAttr('maxVersions')).hasValue('0');
|
|
assert.dom(FORM.inputByAttr('casRequired')).isNotChecked();
|
|
assert.dom(FORM.toggleByLabel('Automate secret deletion')).isNotChecked();
|
|
// MaxVersions validation
|
|
await fillIn(FORM.inputByAttr('maxVersions'), 'seven');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.validation('maxVersions')).hasText('Maximum versions must be a number.');
|
|
await fillIn(FORM.inputByAttr('maxVersions'), '99999999999999999');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.validation('maxVersions')).hasText('You cannot go over 16 characters.');
|
|
await fillIn(FORM.inputByAttr('maxVersions'), '7');
|
|
|
|
// Fill in other metadata
|
|
await click(FORM.inputByAttr('casRequired'));
|
|
await click(FORM.toggleByLabel('Automate secret deletion'));
|
|
await fillIn(FORM.ttlValue('Automate secret deletion'), '1000');
|
|
|
|
// Fill in custom metadata
|
|
await fillIn(`${PAGE.create.metadataSection} ${FORM.keyInput()}`, 'team');
|
|
await fillIn(`${PAGE.create.metadataSection} ${FORM.valueInput()}`, 'UI');
|
|
// Fill in metadata
|
|
await click(FORM.saveBtn);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent('my/secret')}`,
|
|
'goes to overview after save'
|
|
);
|
|
|
|
// Details
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent('my/secret')}/details?version=1`
|
|
);
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 1 created');
|
|
assert.dom(PAGE.infoRow).exists({ count: 1 }, '1 row of data shows');
|
|
assert.dom(PAGE.infoRowValue('password')).hasText('***********');
|
|
await click(PAGE.infoRowToggleMasked('password'));
|
|
assert.dom(PAGE.infoRowValue('password')).hasText('kittens1234', 'secret value shows after toggle');
|
|
|
|
// Metadata
|
|
await click(PAGE.secretTab('Metadata'));
|
|
assert
|
|
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.infoRow}`)
|
|
.exists({ count: 1 }, 'One custom metadata row shows');
|
|
assert.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.infoRowValue('team')}`).hasText('UI');
|
|
|
|
assert
|
|
.dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.infoRow}`)
|
|
.exists({ count: 4 }, '4 metadata rows show');
|
|
assert.dom(PAGE.infoRowValue('Maximum versions')).hasText('7', 'max versions shows 0');
|
|
assert.dom(PAGE.infoRowValue('Check-and-Set required')).hasText('Yes', 'cas enforced');
|
|
assert
|
|
.dom(PAGE.infoRowValue('Delete version after'))
|
|
.hasText('16 minutes 40 seconds', 'Delete version after has custom value');
|
|
});
|
|
test('creates a secret at a sub-directory (a)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list/app/`);
|
|
assert.dom(PAGE.list.item('first')).exists('Lists first sub-secret');
|
|
assert.dom(PAGE.list.item('new')).doesNotExist('Does not show new secret');
|
|
await click(PAGE.list.createSecret);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/create?initialKey=app%2F`,
|
|
'Goes to create page with initialKey'
|
|
);
|
|
await typeIn(FORM.inputByAttr('path'), 'new');
|
|
await fillIn(FORM.keyInput(), 'api_key');
|
|
await fillIn(FORM.maskedValueInput(), 'partyparty');
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(GENERAL.overviewCard.container('Current version'))
|
|
.hasTextContaining('1', 'Overview shows current version');
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent('app/new')}/details?version=1`,
|
|
'Details url has version param'
|
|
);
|
|
await click(PAGE.breadcrumbAtIdx(2));
|
|
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/kv/list/app/`, 'sub-dir page');
|
|
assert.dom(PAGE.list.item('new')).exists('Lists new secret in sub-dir');
|
|
});
|
|
test('create new version of secret from older version (a)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details`);
|
|
await click(PAGE.detail.versionDropdown);
|
|
await click(`${PAGE.detail.version(1)} a`);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/app%2Ffirst/details?version=1`,
|
|
'goes to version 1'
|
|
);
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 1 created');
|
|
await click(PAGE.detail.createNewVersion);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/app%2Ffirst/details/edit?version=1`,
|
|
'Goes to new version page'
|
|
);
|
|
assert
|
|
.dom(FORM.versionAlert)
|
|
.hasText(
|
|
'Warning You are creating a new version based on data from Version 1. The current version for app/first is Version 2.',
|
|
'Shows version warning'
|
|
);
|
|
assert.dom(FORM.keyInput()).hasValue('key-1', 'Key input has old value');
|
|
assert.dom(FORM.maskedValueInput()).hasValue('val-1', 'Val input has old value');
|
|
|
|
await fillIn(FORM.keyInput(), 'my-key');
|
|
await fillIn(FORM.maskedValueInput(), 'my-value');
|
|
await click(FORM.saveBtn);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/app%2Ffirst`,
|
|
'goes to overview after save'
|
|
);
|
|
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/app%2Ffirst/details?version=3`,
|
|
'goes to latest version 3'
|
|
);
|
|
await click(PAGE.infoRowToggleMasked('my-key'));
|
|
assert.dom(PAGE.infoRowValue('my-key')).hasText('my-value', 'has new value');
|
|
});
|
|
|
|
// patch is technically enterprise only but stubbing the version so these run on both CE and enterprise
|
|
test('it patches a secret', async function (assert) {
|
|
this.owner.lookup('service:version').type = 'enterprise';
|
|
const patchSecret = 'patch-secret';
|
|
await writeSecret(this.backend, patchSecret, 'foo', 'bar');
|
|
assert.dom(GENERAL.overviewCard.content('Subkeys')).hasText('Keys foo');
|
|
|
|
await click(GENERAL.overviewCard.actionText('Patch secret'));
|
|
// edit existing key
|
|
await click(FORM.patchEdit(0));
|
|
await fillIn(FORM.valueInput(0), 'newfoo');
|
|
// add new key
|
|
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');
|
|
|
|
// check patch updated secret
|
|
await click(PAGE.secretTab('Secret'));
|
|
await click(PAGE.infoRowToggleMasked('foo'));
|
|
assert.dom(PAGE.infoRowValue('foo')).hasText('newfoo', 'has updated value');
|
|
await click(PAGE.infoRowToggleMasked('newkey'));
|
|
assert.dom(PAGE.infoRowValue('newkey')).hasText('newvalue', 'has new key/value pair');
|
|
|
|
await click(PAGE.detail.patchLatest);
|
|
await click(FORM.patchDelete());
|
|
await click(FORM.saveBtn);
|
|
assert.dom(GENERAL.overviewCard.content('Subkeys')).hasText('Keys newkey');
|
|
// check patch updated secret
|
|
await click(PAGE.secretTab('Secret'));
|
|
await click(PAGE.infoRowToggleMasked('newkey'));
|
|
assert.dom(PAGE.infoRowValue('foo')).doesNotExist();
|
|
assert.dom(PAGE.infoRowValue('newkey')).hasText('newvalue', 'has new key/value pair');
|
|
});
|
|
});
|
|
|
|
module('data-reader persona', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
const token = await runCmd(
|
|
tokenWithPolicyCmd(`data-reader-${this.backend}`, personas.dataReader(this.backend))
|
|
);
|
|
await authPage.login(token);
|
|
clearRecords(this.store);
|
|
return;
|
|
});
|
|
test('cancel on create clears model (dr)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
assert.dom(PAGE.list.item()).doesNotExist('list view has no items');
|
|
await click(PAGE.list.createSecret);
|
|
await fillIn(FORM.inputByAttr('path'), 'jk');
|
|
await click(FORM.cancelBtn);
|
|
assert.dom(PAGE.list.item()).doesNotExist('list view still has no items');
|
|
await click(PAGE.list.createSecret);
|
|
await fillIn(FORM.inputByAttr('path'), 'psych');
|
|
await click(PAGE.breadcrumbAtIdx(1));
|
|
assert.dom(PAGE.list.item()).doesNotExist('list view still has no items');
|
|
});
|
|
test('cancel on new version rolls back model (dr)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/${encodeURIComponent('app/first')}/details`);
|
|
assert.dom(PAGE.infoRowValue('foo')).exists('key has expected value');
|
|
assert.dom(PAGE.detail.createNewVersion).doesNotExist();
|
|
});
|
|
test('create & update root secret with default metadata (dr)', async function (assert) {
|
|
const backend = this.backend;
|
|
const secretPath = 'some secret';
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await click(PAGE.list.createSecret);
|
|
|
|
// Create secret form -- validations
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
|
|
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
|
|
await typeIn(FORM.inputByAttr('path'), secretPath);
|
|
assert
|
|
.dom(FORM.validationWarning)
|
|
.hasText(
|
|
"Path contains whitespace. If this is desired, you'll need to encode it with %20 in API requests."
|
|
);
|
|
assert.dom(PAGE.create.metadataSection).doesNotExist('Hides metadata section by default');
|
|
|
|
// Submit with API errors
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.hasText('Error 1 error occurred: * permission denied', 'API error shows on form');
|
|
|
|
// Since this persona can't create a new secret, test update with existing:
|
|
await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details`);
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 created');
|
|
assert.dom(PAGE.infoRow).exists({ count: 1 }, '1 row of data shows');
|
|
assert.dom(PAGE.infoRowValue('foo')).hasText('***********');
|
|
await click(PAGE.infoRowToggleMasked('foo'));
|
|
assert.dom(PAGE.infoRowValue('foo')).hasText('bar', 'secret value shows after toggle');
|
|
|
|
// Metadata page
|
|
await click(PAGE.secretTab('Metadata'));
|
|
assert
|
|
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
|
.hasText('No custom metadata', 'No custom metadata empty state');
|
|
assert
|
|
.dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.emptyStateTitle}`)
|
|
.hasText('You do not have access to secret metadata', 'shows no access state on metadata');
|
|
|
|
// Add new version
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.dom(PAGE.detail.createNewVersion).doesNotExist('cannot create new version');
|
|
});
|
|
test('create nested secret with metadata (dr)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await click(PAGE.list.createSecret);
|
|
|
|
// Create secret
|
|
await typeIn(FORM.inputByAttr('path'), 'my/');
|
|
assert.dom(FORM.validation('path')).hasText("Path can't end in forward slash '/'.");
|
|
await typeIn(FORM.inputByAttr('path'), 'secret');
|
|
assert.dom(FORM.validation('path')).doesNotExist('form validation goes away');
|
|
await fillIn(FORM.keyInput(), 'password');
|
|
await fillIn(FORM.maskedValueInput(), 'kittens1234');
|
|
|
|
await click(FORM.toggleMetadata);
|
|
assert.dom(PAGE.create.metadataSection).exists('Shows metadata section after toggled');
|
|
// Check initial values
|
|
assert.dom(FORM.inputByAttr('maxVersions')).hasValue('0');
|
|
assert.dom(FORM.inputByAttr('casRequired')).isNotChecked();
|
|
assert.dom(FORM.toggleByLabel('Automate secret deletion')).isNotChecked();
|
|
// MaxVersions validation
|
|
await fillIn(FORM.inputByAttr('maxVersions'), 'seven');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.validation('maxVersions')).hasText('Maximum versions must be a number.');
|
|
await fillIn(FORM.inputByAttr('maxVersions'), '99999999999999999');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.validation('maxVersions')).hasText('You cannot go over 16 characters.');
|
|
await fillIn(FORM.inputByAttr('maxVersions'), '7');
|
|
|
|
// Fill in other metadata
|
|
await click(FORM.inputByAttr('casRequired'));
|
|
await click(FORM.toggleByLabel('Automate secret deletion'));
|
|
await fillIn(FORM.ttlValue('Automate secret deletion'), '1000');
|
|
|
|
// Fill in custom metadata
|
|
await fillIn(`${PAGE.create.metadataSection} ${FORM.keyInput()}`, 'team');
|
|
await fillIn(`${PAGE.create.metadataSection} ${FORM.valueInput()}`, 'UI');
|
|
// Fill in metadata
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.hasText('Error 1 error occurred: * permission denied', 'API error shows on form');
|
|
});
|
|
test('creates a secret at a sub-directory (dr)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list/app/`);
|
|
assert.dom(PAGE.list.item()).doesNotExist('Does not list any secrets');
|
|
await click(PAGE.list.createSecret);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/create?initialKey=app%2F`,
|
|
'Goes to create page with initialKey'
|
|
);
|
|
await typeIn(FORM.inputByAttr('path'), 'new');
|
|
await fillIn(FORM.keyInput(), 'api_key');
|
|
await fillIn(FORM.maskedValueInput(), 'partyparty');
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.hasText('Error 1 error occurred: * permission denied', 'API error shows on form');
|
|
});
|
|
test('create new version of secret from older version (dr)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details?version=1`);
|
|
assert.dom(PAGE.detail.versionDropdown).doesNotExist('version dropdown does not show');
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 1 created');
|
|
assert.dom(PAGE.detail.createNewVersion).doesNotExist('cannot create new version');
|
|
});
|
|
});
|
|
|
|
module('data-list-reader persona', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
const token = await runCmd(
|
|
tokenWithPolicyCmd(`data-list-reader-${this.backend}`, personas.dataListReader(this.backend))
|
|
);
|
|
await authPage.login(token);
|
|
clearRecords(this.store);
|
|
return;
|
|
});
|
|
test('cancel on create clears model (dlr)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
assert.dom(PAGE.list.item()).exists({ count: 1 }, 'single secret exists on list');
|
|
assert.dom(PAGE.list.item('app/')).hasText('app/', 'expected list item');
|
|
await click(PAGE.list.createSecret);
|
|
await fillIn(FORM.inputByAttr('path'), 'jk');
|
|
await click(FORM.cancelBtn);
|
|
assert.dom(PAGE.list.item()).exists({ count: 1 }, 'same amount of secrets');
|
|
assert.dom(PAGE.list.item('app/')).hasText('app/', 'expected list item');
|
|
await click(PAGE.list.createSecret);
|
|
await fillIn(FORM.inputByAttr('path'), 'psych');
|
|
await click(PAGE.breadcrumbAtIdx(1));
|
|
assert.dom(PAGE.list.item()).exists({ count: 1 }, 'same amount of secrets');
|
|
assert.dom(PAGE.list.item('app/')).hasText('app/', 'expected list item');
|
|
});
|
|
test('cancel on new version rolls back model (dlr)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/${encodeURIComponent('app/first')}/details`);
|
|
assert.dom(PAGE.infoRowValue('foo')).exists('key has expected value');
|
|
assert.dom(PAGE.detail.createNewVersion).doesNotExist('cannot create new version');
|
|
});
|
|
test('create & update root secret with default metadata (dlr)', async function (assert) {
|
|
const backend = this.backend;
|
|
const secretPath = 'some secret';
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await click(PAGE.list.createSecret);
|
|
|
|
// Create secret form -- validations
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
|
|
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
|
|
await typeIn(FORM.inputByAttr('path'), secretPath);
|
|
assert
|
|
.dom(FORM.validationWarning)
|
|
.hasText(
|
|
"Path contains whitespace. If this is desired, you'll need to encode it with %20 in API requests."
|
|
);
|
|
assert.dom(PAGE.create.metadataSection).doesNotExist('Hides metadata section by default');
|
|
|
|
// Submit with API errors
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.hasText('Error 1 error occurred: * permission denied', 'API error shows on form');
|
|
|
|
// Since this persona can't create a new secret, test update with existing:
|
|
await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details`);
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 created');
|
|
assert.dom(PAGE.infoRow).exists({ count: 1 }, '1 row of data shows');
|
|
assert.dom(PAGE.infoRowValue('foo')).hasText('***********');
|
|
await click(PAGE.infoRowToggleMasked('foo'));
|
|
assert.dom(PAGE.infoRowValue('foo')).hasText('bar', 'secret value shows after toggle');
|
|
|
|
// Metadata page
|
|
await click(PAGE.secretTab('Metadata'));
|
|
assert
|
|
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
|
.hasText('No custom metadata', 'No custom metadata empty state');
|
|
assert
|
|
.dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.emptyStateTitle}`)
|
|
.hasText('You do not have access to secret metadata', 'shows no access state on metadata');
|
|
|
|
// Add new version
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.dom(PAGE.detail.createNewVersion).doesNotExist('cannot create new version');
|
|
});
|
|
test('create nested secret with metadata (dlr)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await click(PAGE.list.createSecret);
|
|
|
|
// Create secret
|
|
await typeIn(FORM.inputByAttr('path'), 'my/');
|
|
assert.dom(FORM.validation('path')).hasText("Path can't end in forward slash '/'.");
|
|
await typeIn(FORM.inputByAttr('path'), 'secret');
|
|
assert.dom(FORM.validation('path')).doesNotExist('form validation goes away');
|
|
await fillIn(FORM.keyInput(), 'password');
|
|
await fillIn(FORM.maskedValueInput(), 'kittens1234');
|
|
|
|
await click(FORM.toggleMetadata);
|
|
assert.dom(PAGE.create.metadataSection).exists('Shows metadata section after toggled');
|
|
// Check initial values
|
|
assert.dom(FORM.inputByAttr('maxVersions')).hasValue('0');
|
|
assert.dom(FORM.inputByAttr('casRequired')).isNotChecked();
|
|
assert.dom(FORM.toggleByLabel('Automate secret deletion')).isNotChecked();
|
|
// MaxVersions validation
|
|
await fillIn(FORM.inputByAttr('maxVersions'), 'seven');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.validation('maxVersions')).hasText('Maximum versions must be a number.');
|
|
await fillIn(FORM.inputByAttr('maxVersions'), '99999999999999999');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.validation('maxVersions')).hasText('You cannot go over 16 characters.');
|
|
await fillIn(FORM.inputByAttr('maxVersions'), '7');
|
|
|
|
// Fill in other metadata
|
|
await click(FORM.inputByAttr('casRequired'));
|
|
await click(FORM.toggleByLabel('Automate secret deletion'));
|
|
await fillIn(FORM.ttlValue('Automate secret deletion'), '1000');
|
|
|
|
// Fill in custom metadata
|
|
await fillIn(`${PAGE.create.metadataSection} ${FORM.keyInput()}`, 'team');
|
|
await fillIn(`${PAGE.create.metadataSection} ${FORM.valueInput()}`, 'UI');
|
|
// Fill in metadata
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.hasText('Error 1 error occurred: * permission denied', 'API error shows on form');
|
|
});
|
|
test('creates a secret at a sub-directory (dlr)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list/app/`);
|
|
assert.dom(PAGE.list.item()).doesNotExist('Does not list any secrets');
|
|
await click(PAGE.list.createSecret);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/create?initialKey=app%2F`,
|
|
'Goes to create page with initialKey'
|
|
);
|
|
await typeIn(FORM.inputByAttr('path'), 'new');
|
|
await fillIn(FORM.keyInput(), 'api_key');
|
|
await fillIn(FORM.maskedValueInput(), 'partyparty');
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.hasText('Error 1 error occurred: * permission denied', 'API error shows on form');
|
|
});
|
|
test('create new version of secret from older version (dlr)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details?version=1`);
|
|
assert.dom(PAGE.detail.versionDropdown).doesNotExist('version dropdown does not show');
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 1 created');
|
|
assert.dom(PAGE.detail.createNewVersion).doesNotExist('cannot create new version');
|
|
});
|
|
});
|
|
|
|
module('metadata-maintainer persona', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
const token = await runCmd(
|
|
tokenWithPolicyCmd(`data-list-reader-${this.backend}`, personas.metadataMaintainer(this.backend))
|
|
);
|
|
await authPage.login(token);
|
|
clearRecords(this.store);
|
|
return;
|
|
});
|
|
test('cancel on create clears model (mm)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
assert.dom(PAGE.list.item()).exists({ count: 1 }, 'single secret exists on list');
|
|
assert.dom(PAGE.list.item('app/')).hasText('app/', 'expected list item');
|
|
await click(PAGE.list.createSecret);
|
|
await fillIn(FORM.inputByAttr('path'), 'jk');
|
|
await click(FORM.cancelBtn);
|
|
assert.dom(PAGE.list.item()).exists({ count: 1 }, 'same amount of secrets');
|
|
assert.dom(PAGE.list.item('app/')).hasText('app/', 'expected list item');
|
|
await click(PAGE.list.createSecret);
|
|
await fillIn(FORM.inputByAttr('path'), 'psych');
|
|
await click(PAGE.breadcrumbAtIdx(1));
|
|
assert.dom(PAGE.list.item()).exists({ count: 1 }, 'same amount of secrets');
|
|
assert.dom(PAGE.list.item('app/')).hasText('app/', 'expected list item');
|
|
});
|
|
test('cancel on new version rolls back model (mm)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/${encodeURIComponent('app/first')}/details`);
|
|
assert.dom(PAGE.emptyStateTitle).hasText('You do not have permission to read this secret');
|
|
assert
|
|
.dom(PAGE.detail.createNewVersion)
|
|
.doesNotExist('create new version button now allowed since user cannot read existing');
|
|
});
|
|
test('create & update root secret with default metadata (mm)', async function (assert) {
|
|
const backend = this.backend;
|
|
const secretPath = 'some secret';
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await click(PAGE.list.createSecret);
|
|
|
|
// Create secret form -- validations
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
|
|
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
|
|
await typeIn(FORM.inputByAttr('path'), secretPath);
|
|
assert
|
|
.dom(FORM.validationWarning)
|
|
.hasText(
|
|
"Path contains whitespace. If this is desired, you'll need to encode it with %20 in API requests."
|
|
);
|
|
assert.dom(PAGE.create.metadataSection).doesNotExist('Hides metadata section by default');
|
|
|
|
// Submit with API errors
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.hasText('Error 1 error occurred: * permission denied', 'API error shows on form');
|
|
|
|
// Since this persona can't create a new secret, test update with existing:
|
|
await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details`);
|
|
assert.dom(PAGE.detail.versionTimestamp).doesNotExist('Version created tooltip does not show');
|
|
assert.dom(PAGE.infoRow).doesNotExist('secret data not shown');
|
|
assert.dom(PAGE.emptyStateTitle).hasText('You do not have permission to read this secret');
|
|
|
|
// Metadata page
|
|
await click(PAGE.secretTab('Metadata'));
|
|
assert
|
|
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
|
.hasText('No custom metadata', 'No custom metadata empty state');
|
|
assert
|
|
.dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.infoRow}`)
|
|
.exists({ count: 4 }, '4 metadata rows show');
|
|
assert.dom(PAGE.infoRowValue('Maximum versions')).hasText('0', 'max versions shows 0');
|
|
assert.dom(PAGE.infoRowValue('Check-and-Set required')).hasText('No', 'cas not enforced');
|
|
assert
|
|
.dom(PAGE.infoRowValue('Delete version after'))
|
|
.hasText('Never delete', 'Delete version after has default 0s');
|
|
|
|
// Add new version
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.dom(PAGE.detail.createNewVersion).doesNotExist('create new version button not rendered');
|
|
await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details/edit?version=1`);
|
|
assert
|
|
.dom(FORM.noReadAlert)
|
|
.hasText(
|
|
'Warning You do not have read permissions for this secret data. Saving will overwrite the existing secret.',
|
|
'shows alert for no read permissions'
|
|
);
|
|
|
|
assert.dom(FORM.inputByAttr('path')).isDisabled('path input is disabled');
|
|
assert.dom(FORM.inputByAttr('path')).hasValue('app/first');
|
|
assert.dom(FORM.toggleMetadata).doesNotExist('Does not show metadata toggle when creating new version');
|
|
assert.dom(FORM.keyInput()).hasValue('', 'first row has no key');
|
|
assert.dom(FORM.maskedValueInput()).hasValue('', 'first row has no value');
|
|
await fillIn(FORM.keyInput(), 'api_url');
|
|
await fillIn(FORM.maskedValueInput(), 'hashicorp.com');
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.hasText('Error 1 error occurred: * permission denied', 'API error shows on form');
|
|
});
|
|
test('create nested secret with metadata (mm)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await click(PAGE.list.createSecret);
|
|
|
|
// Create secret
|
|
await typeIn(FORM.inputByAttr('path'), 'my/');
|
|
assert.dom(FORM.validation('path')).hasText("Path can't end in forward slash '/'.");
|
|
await typeIn(FORM.inputByAttr('path'), 'secret');
|
|
assert.dom(FORM.validation('path')).doesNotExist('form validation goes away');
|
|
await fillIn(FORM.keyInput(), 'password');
|
|
await fillIn(FORM.maskedValueInput(), 'kittens1234');
|
|
|
|
await click(FORM.toggleMetadata);
|
|
assert.dom(PAGE.create.metadataSection).exists('Shows metadata section after toggled');
|
|
// Check initial values
|
|
assert.dom(FORM.inputByAttr('maxVersions')).hasValue('0');
|
|
assert.dom(FORM.inputByAttr('casRequired')).isNotChecked();
|
|
assert.dom(FORM.toggleByLabel('Automate secret deletion')).isNotChecked();
|
|
// MaxVersions validation
|
|
await fillIn(FORM.inputByAttr('maxVersions'), 'seven');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.validation('maxVersions')).hasText('Maximum versions must be a number.');
|
|
await fillIn(FORM.inputByAttr('maxVersions'), '99999999999999999');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.validation('maxVersions')).hasText('You cannot go over 16 characters.');
|
|
await fillIn(FORM.inputByAttr('maxVersions'), '7');
|
|
|
|
// Fill in other metadata
|
|
await click(FORM.inputByAttr('casRequired'));
|
|
await click(FORM.toggleByLabel('Automate secret deletion'));
|
|
await fillIn(FORM.ttlValue('Automate secret deletion'), '1000');
|
|
|
|
// Fill in custom metadata
|
|
await fillIn(`${PAGE.create.metadataSection} ${FORM.keyInput()}`, 'team');
|
|
await fillIn(`${PAGE.create.metadataSection} ${FORM.valueInput()}`, 'UI');
|
|
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.hasText('Error 1 error occurred: * permission denied', 'API error shows on form');
|
|
});
|
|
test('creates a secret at a sub-directory (mm)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list/app/`);
|
|
assert.dom(PAGE.list.item('first')).exists('Lists first sub-secret');
|
|
assert.dom(PAGE.list.item('new')).doesNotExist('Does not show new secret');
|
|
await click(PAGE.list.createSecret);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/create?initialKey=app%2F`,
|
|
'Goes to create page with initialKey'
|
|
);
|
|
await typeIn(FORM.inputByAttr('path'), 'new');
|
|
await fillIn(FORM.keyInput(), 'api_key');
|
|
await fillIn(FORM.maskedValueInput(), 'partyparty');
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.hasText('Error 1 error occurred: * permission denied', 'API error shows on form');
|
|
});
|
|
test('create new version of secret from older version (mm)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details`);
|
|
assert.dom(PAGE.detail.versionDropdown).hasText('Version 2');
|
|
await click(PAGE.detail.versionDropdown);
|
|
await click(`${PAGE.detail.version(1)} a`);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/app%2Ffirst/details?version=1`,
|
|
'goes to version 1'
|
|
);
|
|
assert.dom(PAGE.detail.versionDropdown).hasText('Version 1');
|
|
assert.dom(PAGE.detail.versionTimestamp).doesNotExist('version timestamp not shown');
|
|
assert.dom(PAGE.detail.createNewVersion).doesNotExist('create new version button not rendered');
|
|
await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details/edit?version=1`);
|
|
assert
|
|
.dom(FORM.noReadAlert)
|
|
.hasText(
|
|
'Warning You do not have read permissions for this secret data. Saving will overwrite the existing secret.',
|
|
'shows alert for no read permissions'
|
|
);
|
|
|
|
assert.dom(FORM.inputByAttr('path')).isDisabled('path input is disabled');
|
|
assert.dom(FORM.inputByAttr('path')).hasValue('app/first');
|
|
assert.dom(FORM.toggleMetadata).doesNotExist('Does not show metadata toggle when creating new version');
|
|
assert.dom(FORM.keyInput()).hasValue('', 'first row has no key');
|
|
assert.dom(FORM.maskedValueInput()).hasValue('', 'first row has no value');
|
|
await fillIn(FORM.keyInput(), 'api_url');
|
|
await fillIn(FORM.maskedValueInput(), 'hashicorp.com');
|
|
await click(FORM.saveBtn);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.hasText('Error 1 error occurred: * permission denied', 'API error shows on form');
|
|
});
|
|
});
|
|
|
|
module('secret-creator persona', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
const token = await runCmd(
|
|
tokenWithPolicyCmd(`secret-creator-${this.backend}`, personas.secretCreator(this.backend))
|
|
);
|
|
await authPage.login(token);
|
|
clearRecords(this.store);
|
|
return;
|
|
});
|
|
test('cancel on create clears model (sc)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
assert.dom(PAGE.list.item()).doesNotExist('list view has no items');
|
|
await click(PAGE.list.createSecret);
|
|
await fillIn(FORM.inputByAttr('path'), 'jk');
|
|
await click(FORM.cancelBtn);
|
|
assert.dom(PAGE.list.item()).doesNotExist('list view still has no items');
|
|
await click(PAGE.list.createSecret);
|
|
await fillIn(FORM.inputByAttr('path'), 'psych');
|
|
await click(PAGE.breadcrumbAtIdx(1));
|
|
assert.dom(PAGE.list.item()).doesNotExist('list view still has no items');
|
|
});
|
|
test('cancel on new version rolls back model (sc)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/${encodeURIComponent('app/first')}/details`);
|
|
assert
|
|
.dom(PAGE.emptyStateTitle)
|
|
.hasText('You do not have permission to read this secret', 'no permissions state shows');
|
|
await click(PAGE.detail.createNewVersion);
|
|
await fillIn(FORM.keyInput(), 'bar');
|
|
await click(FORM.cancelBtn);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent('app/first')}`,
|
|
'cancel goes to overview'
|
|
);
|
|
await click(PAGE.secretTab('Secret'));
|
|
await click(PAGE.detail.createNewVersion);
|
|
await fillIn(FORM.keyInput(), 'bar');
|
|
await click(PAGE.breadcrumbAtIdx(3));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent('app/first')}`,
|
|
'breadcrumb goes to overview'
|
|
);
|
|
});
|
|
test('create & update root secret with default metadata (sc)', async function (assert) {
|
|
const backend = this.backend;
|
|
const secretPath = 'some secret';
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await click(PAGE.list.createSecret);
|
|
|
|
// Create secret form -- validations
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
|
|
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
|
|
await typeIn(FORM.inputByAttr('path'), secretPath);
|
|
assert
|
|
.dom(FORM.validationWarning)
|
|
.hasText(
|
|
"Path contains whitespace. If this is desired, you'll need to encode it with %20 in API requests."
|
|
);
|
|
assert.dom(PAGE.create.metadataSection).doesNotExist('Hides metadata section by default');
|
|
|
|
// Submit with API errors
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.messageError).hasText('Error no data provided', 'API error shows on form');
|
|
|
|
await fillIn(FORM.keyInput(), 'api_key');
|
|
await fillIn(FORM.maskedValueInput(), 'partyparty');
|
|
await click(FORM.saveBtn);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent(secretPath)}`,
|
|
'Goes to overview page after save'
|
|
);
|
|
// Details page
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.dom(PAGE.detail.versionTimestamp).doesNotExist('Version created not shown');
|
|
assert.dom(PAGE.infoRow).doesNotExist('does not show data contents');
|
|
assert
|
|
.dom(PAGE.emptyStateTitle)
|
|
.hasText('You do not have permission to read this secret', 'shows permissions empty state');
|
|
|
|
// Metadata page
|
|
await click(PAGE.secretTab('Metadata'));
|
|
assert
|
|
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
|
.hasText(
|
|
'You do not have access to read custom metadata',
|
|
'permissions empty state for custom metadata'
|
|
);
|
|
assert
|
|
.dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.emptyStateTitle}`)
|
|
.hasText('You do not have access to secret metadata', 'permissions empty state for secret metadata');
|
|
|
|
// Add new version
|
|
await click(PAGE.secretTab('Secret'));
|
|
await click(PAGE.detail.createNewVersion);
|
|
assert
|
|
.dom(FORM.noReadAlert)
|
|
.hasText(
|
|
'Warning You do not have read permissions for this secret data. Saving will overwrite the existing secret.',
|
|
'shows alert for no read permissions'
|
|
);
|
|
assert.dom(FORM.inputByAttr('path')).isDisabled('path input is disabled');
|
|
assert.dom(FORM.inputByAttr('path')).hasValue(secretPath);
|
|
assert.dom(FORM.toggleMetadata).doesNotExist('Does not show metadata toggle when creating new version');
|
|
assert.dom(FORM.keyInput()).hasValue('', 'Key input has empty value');
|
|
assert.dom(FORM.maskedValueInput()).hasValue('', 'Val input has empty value');
|
|
await fillIn(FORM.keyInput(), 'api_url');
|
|
await fillIn(FORM.maskedValueInput(), 'hashicorp.com');
|
|
await click(FORM.saveBtn);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent(secretPath)}`,
|
|
'goes to overview page'
|
|
);
|
|
// Back to details page
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent(secretPath)}/details`,
|
|
'goes to details page'
|
|
);
|
|
assert.dom(PAGE.detail.versionTimestamp).doesNotExist('Version created does not show');
|
|
assert.dom(PAGE.infoRow).doesNotExist('does not show data contents');
|
|
assert
|
|
.dom(PAGE.emptyStateTitle)
|
|
.hasText('You do not have permission to read this secret', 'shows permissions empty state');
|
|
});
|
|
test('create nested secret with metadata (sc)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await click(PAGE.list.createSecret);
|
|
|
|
// Create secret
|
|
await typeIn(FORM.inputByAttr('path'), 'my/');
|
|
assert.dom(FORM.validation('path')).hasText("Path can't end in forward slash '/'.");
|
|
await typeIn(FORM.inputByAttr('path'), 'secret');
|
|
assert.dom(FORM.validation('path')).doesNotExist('form validation goes away');
|
|
await fillIn(FORM.keyInput(), 'password');
|
|
await fillIn(FORM.maskedValueInput(), 'kittens1234');
|
|
|
|
await click(FORM.toggleMetadata);
|
|
assert.dom(PAGE.create.metadataSection).exists('Shows metadata section after toggled');
|
|
// Check initial values
|
|
assert.dom(FORM.inputByAttr('maxVersions')).hasValue('0');
|
|
assert.dom(FORM.inputByAttr('casRequired')).isNotChecked();
|
|
assert.dom(FORM.toggleByLabel('Automate secret deletion')).isNotChecked();
|
|
// MaxVersions validation
|
|
await fillIn(FORM.inputByAttr('maxVersions'), 'seven');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.validation('maxVersions')).hasText('Maximum versions must be a number.');
|
|
await fillIn(FORM.inputByAttr('maxVersions'), '99999999999999999');
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.validation('maxVersions')).hasText('You cannot go over 16 characters.');
|
|
await fillIn(FORM.inputByAttr('maxVersions'), '7');
|
|
|
|
// Fill in other metadata
|
|
await click(FORM.inputByAttr('casRequired'));
|
|
await click(FORM.toggleByLabel('Automate secret deletion'));
|
|
await fillIn(FORM.ttlValue('Automate secret deletion'), '1000');
|
|
|
|
// Fill in custom metadata
|
|
await fillIn(`${PAGE.create.metadataSection} ${FORM.keyInput()}`, 'team');
|
|
await fillIn(`${PAGE.create.metadataSection} ${FORM.valueInput()}`, 'UI');
|
|
// Fill in metadata
|
|
await click(FORM.saveBtn);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent('my/secret')}`,
|
|
'goes to overview page'
|
|
);
|
|
|
|
// Details
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.dom(PAGE.detail.versionTimestamp).doesNotExist('version created not shown');
|
|
assert.dom(PAGE.infoRow).doesNotExist('does not show data contents');
|
|
assert
|
|
.dom(PAGE.emptyStateTitle)
|
|
.hasText('You do not have permission to read this secret', 'shows permissions empty state');
|
|
|
|
// Metadata
|
|
await click(PAGE.secretTab('Metadata'));
|
|
assert
|
|
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
|
.hasText(
|
|
'You do not have access to read custom metadata',
|
|
'permissions empty state for custom metadata'
|
|
);
|
|
assert
|
|
.dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.emptyStateTitle}`)
|
|
.hasText('You do not have access to secret metadata', 'permissions empty state for secret metadata');
|
|
});
|
|
test('creates a secret at a sub-directory (sc)', async function (assert) {
|
|
const backend = this.backend;
|
|
await visit(`/vault/secrets/${backend}/kv/list/app/`);
|
|
assert.dom(PAGE.list.item()).doesNotExist('Does not list any secrets');
|
|
await click(PAGE.list.createSecret);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/create?initialKey=app%2F`,
|
|
'Goes to create page with initialKey'
|
|
);
|
|
await typeIn(FORM.inputByAttr('path'), 'new');
|
|
await fillIn(FORM.keyInput(), 'api_key');
|
|
await fillIn(FORM.maskedValueInput(), 'partyparty');
|
|
await click(FORM.saveBtn);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent('app/new')}`,
|
|
'Redirects to overview after save'
|
|
);
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent('app/new')}/details`,
|
|
'navigates to details'
|
|
);
|
|
await click(PAGE.breadcrumbAtIdx(2));
|
|
assert.strictEqual(currentURL(), `/vault/secrets/${backend}/kv/list/app/`, 'sub-dir page');
|
|
assert.dom(PAGE.list.item()).doesNotExist('Does not list any secrets');
|
|
});
|
|
});
|
|
|
|
module('enterprise controlled access persona', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
this.controlGroup = this.owner.lookup('service:control-group');
|
|
const userPolicy = `
|
|
path "${this.backend}/data/*" {
|
|
capabilities = ["create", "read", "update"]
|
|
control_group = {
|
|
max_ttl = "24h"
|
|
factor "authorizer" {
|
|
controlled_capabilities = ["create", "update"]
|
|
identity {
|
|
group_names = ["managers"]
|
|
approvals = 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
path "${this.backend}/metadata" {
|
|
capabilities = ["list", "read"]
|
|
}
|
|
path "${this.backend}/metadata/*" {
|
|
capabilities = ["list", "read"]
|
|
}
|
|
`;
|
|
|
|
const { userToken } = await setupControlGroup({
|
|
userPolicy,
|
|
backend: this.backend,
|
|
});
|
|
this.userToken = userToken;
|
|
await authPage.login(userToken);
|
|
clearRecords(this.store);
|
|
return;
|
|
});
|
|
test('create & update root secret with default metadata (cg)', async function (assert) {
|
|
const backend = this.backend;
|
|
// Known issue: control groups do not work correctly in UI when encodable characters in path
|
|
const secretPath = 'some-secret';
|
|
await visit(`/vault/secrets/${backend}/kv/list`);
|
|
await click(PAGE.list.createSecret);
|
|
|
|
// Create secret form -- validations
|
|
await click(FORM.saveBtn);
|
|
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
|
|
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
|
|
await typeIn(FORM.inputByAttr('path'), secretPath);
|
|
assert.dom(PAGE.create.metadataSection).doesNotExist('Hides metadata section by default');
|
|
|
|
await fillIn(FORM.keyInput(), 'api_key');
|
|
await fillIn(FORM.maskedValueInput(), 'partyparty');
|
|
await click(FORM.saveBtn);
|
|
let tokenToUnwrap = this.controlGroup.tokenToUnwrap;
|
|
assert.deepEqual(
|
|
Object.keys(tokenToUnwrap),
|
|
['accessor', 'token', 'creation_path', 'creation_time', 'ttl'],
|
|
'stored tokenToUnwrap includes correct keys'
|
|
);
|
|
assert.strictEqual(
|
|
tokenToUnwrap.creation_path,
|
|
`${backend}/data/${secretPath}`,
|
|
'stored tokenToUnwrap includes correct creation path'
|
|
);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.includesText(
|
|
`Error A Control Group was encountered at ${backend}/data/${secretPath}.`,
|
|
'shows control group error'
|
|
);
|
|
await grantAccessForWrite({
|
|
token: tokenToUnwrap.token,
|
|
accessor: tokenToUnwrap.accessor,
|
|
creation_path: `${backend}/data/${secretPath}`,
|
|
originUrl: `/vault/secrets/${backend}/kv/create`,
|
|
userToken: this.userToken,
|
|
backend: this.backend,
|
|
});
|
|
// In a real scenario the user would stay on page, but in the test
|
|
// we fill in the same info and try again
|
|
await typeIn(FORM.inputByAttr('path'), secretPath);
|
|
await fillIn(FORM.keyInput(), 'this can be anything');
|
|
await fillIn(FORM.maskedValueInput(), 'this too, gonna use the wrapped data');
|
|
await click(FORM.saveBtn);
|
|
assert.strictEqual(this.controlGroup.tokenToUnwrap, null, 'clears tokenToUnwrap after successful save');
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${secretPath}`,
|
|
'Goes to overview page after save'
|
|
);
|
|
// Details page
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 1 created');
|
|
assert.dom(PAGE.infoRow).exists({ count: 1 }, '1 row of data shows');
|
|
assert.dom(PAGE.infoRowValue('api_key')).hasText('***********');
|
|
await click(PAGE.infoRowToggleMasked('api_key'));
|
|
assert.dom(PAGE.infoRowValue('api_key')).hasText('partyparty', 'secret value shows after toggle');
|
|
|
|
// Metadata page
|
|
await click(PAGE.secretTab('Metadata'));
|
|
assert
|
|
.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`)
|
|
.hasText('No custom metadata', 'No custom metadata empty state');
|
|
assert
|
|
.dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.infoRow}`)
|
|
.exists({ count: 4 }, '4 metadata rows show');
|
|
assert.dom(PAGE.infoRowValue('Maximum versions')).hasText('0', 'max versions shows 0');
|
|
assert.dom(PAGE.infoRowValue('Check-and-Set required')).hasText('No', 'cas not enforced');
|
|
assert
|
|
.dom(PAGE.infoRowValue('Delete version after'))
|
|
.hasText('Never delete', 'Delete version after has default 0s');
|
|
|
|
// Add new version
|
|
await click(PAGE.secretTab('Secret'));
|
|
await click(PAGE.detail.createNewVersion);
|
|
assert.dom(FORM.inputByAttr('path')).isDisabled('path input is disabled');
|
|
assert.dom(FORM.inputByAttr('path')).hasValue(secretPath);
|
|
assert.dom(FORM.toggleMetadata).doesNotExist('Does not show metadata toggle when creating new version');
|
|
assert.dom(FORM.keyInput()).hasValue('api_key');
|
|
assert.dom(FORM.maskedValueInput()).hasValue('partyparty');
|
|
await fillIn(FORM.keyInput(1), 'api_url');
|
|
await fillIn(FORM.maskedValueInput(1), 'hashicorp.com');
|
|
await click(FORM.saveBtn);
|
|
tokenToUnwrap = this.controlGroup.tokenToUnwrap;
|
|
assert.strictEqual(
|
|
tokenToUnwrap.creation_path,
|
|
`${backend}/data/${secretPath}`,
|
|
'stored tokenToUnwrap includes correct update path'
|
|
);
|
|
assert
|
|
.dom(FORM.messageError)
|
|
.includesText(
|
|
`Error A Control Group was encountered at ${backend}/data/${secretPath}.`,
|
|
'shows control group error'
|
|
);
|
|
// Normally the user stays on the page and tries again once approval is granted
|
|
// unmark on test so it doesn't use the control group on read at the same path
|
|
// when we return to the page after granting access below
|
|
this.controlGroup.unmarkTokenForUnwrap();
|
|
await grantAccessForWrite({
|
|
accessor: tokenToUnwrap.accessor,
|
|
token: tokenToUnwrap.token,
|
|
creation_path: `${backend}/data/${secretPath}`,
|
|
originUrl: `/vault/secrets/${backend}/kv/${secretPath}/details/edit`,
|
|
userToken: this.userToken,
|
|
backend: this.backend,
|
|
});
|
|
// Remark for unwrap as if we never left the page.
|
|
this.controlGroup.markTokenForUnwrap(tokenToUnwrap.accessor);
|
|
// No need to fill in data because we're using the stored wrapped request
|
|
// and the path already exists
|
|
await click(FORM.saveBtn);
|
|
assert.strictEqual(
|
|
this.controlGroup.tokenToUnwrap,
|
|
null,
|
|
'clears tokenToUnwrap after successful update'
|
|
);
|
|
// Back to details page
|
|
await click(PAGE.secretTab('Secret'));
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${backend}/kv/${encodeURIComponent(secretPath)}/details?version=2`
|
|
);
|
|
assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 created');
|
|
assert.dom(PAGE.infoRow).exists({ count: 2 }, '2 rows of data shows');
|
|
assert.dom(PAGE.infoRowValue('api_key')).hasText('***********');
|
|
assert.dom(PAGE.infoRowValue('api_url')).hasText('***********');
|
|
await click(PAGE.infoRowToggleMasked('api_key'));
|
|
await click(PAGE.infoRowToggleMasked('api_url'));
|
|
assert.dom(PAGE.infoRowValue('api_key')).hasText('partyparty', 'secret value shows after toggle');
|
|
assert.dom(PAGE.infoRowValue('api_url')).hasText('hashicorp.com', 'secret value shows after toggle');
|
|
});
|
|
});
|
|
});
|