mirror of
https://github.com/hashicorp/vault.git
synced 2025-12-07 18:41:30 +01:00
* [VAULT-37521] UI: decouple auth and secret engines * add copyright header * address acceptance test failure Co-authored-by: Shannon Roberts (Beagin) <beagins@users.noreply.github.com>
424 lines
17 KiB
JavaScript
424 lines
17 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import {
|
|
currentRouteName,
|
|
currentURL,
|
|
settled,
|
|
click,
|
|
findAll,
|
|
fillIn,
|
|
visit,
|
|
typeIn,
|
|
waitFor,
|
|
} from '@ember/test-helpers';
|
|
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
|
import { module, test } from 'qunit';
|
|
import { setupApplicationTest } from 'ember-qunit';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
|
|
|
|
import { create } from 'ember-cli-page-object';
|
|
import page from 'vault/tests/pages/settings/mount-secret-backend';
|
|
import configPage from 'vault/tests/pages/secrets/backend/configuration';
|
|
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
|
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
|
|
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
|
|
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
|
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
|
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
|
import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
|
|
import { SELECTORS as OIDC } from 'vault/tests/helpers/oidc-config';
|
|
import { adminOidcCreateRead, adminOidcCreate } from 'vault/tests/helpers/secret-engine/policy-generator';
|
|
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
|
|
import engineDisplayData from 'vault/helpers/engines-display-data';
|
|
|
|
const consoleComponent = create(consoleClass);
|
|
|
|
// enterprise backends are tested separately
|
|
const BACKENDS_WITH_ENGINES = ['kv', 'pki', 'ldap', 'kubernetes'];
|
|
module('Acceptance | secrets/mounts', function (hooks) {
|
|
setupApplicationTest(hooks);
|
|
|
|
hooks.beforeEach(function () {
|
|
this.uid = uuidv4();
|
|
this.calcDays = (hours) => {
|
|
const days = Math.floor(hours / 24);
|
|
const remainder = hours % 24;
|
|
return `${days} days ${remainder} hours`;
|
|
};
|
|
return login();
|
|
});
|
|
|
|
test('it sets the ttl correctly when mounting', async function (assert) {
|
|
// always force the new mount to the top of the list
|
|
const path = `mount-kv-${this.uid}`;
|
|
const defaultTTLHours = 100;
|
|
const maxTTLHours = 300;
|
|
await page.visit();
|
|
|
|
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.mounts.index');
|
|
await click(GENERAL.cardContainer('kv'));
|
|
await fillIn(GENERAL.inputByAttr('path'), path);
|
|
await click(GENERAL.button('Method Options'));
|
|
await click(GENERAL.toggleInput('Default Lease TTL'));
|
|
await page.defaultTTLUnit('h').defaultTTLVal(defaultTTLHours);
|
|
await click(GENERAL.toggleInput('Max Lease TTL'));
|
|
await page.maxTTLUnit('h').maxTTLVal(maxTTLHours);
|
|
await click(GENERAL.submitButton);
|
|
await configPage.visit({ backend: path });
|
|
assert.strictEqual(configPage.defaultTTL, `${this.calcDays(defaultTTLHours)}`, 'shows the proper TTL');
|
|
assert.strictEqual(configPage.maxTTL, `${this.calcDays(maxTTLHours)}`, 'shows the proper max TTL');
|
|
});
|
|
|
|
test('it sets the ttl when enabled then disabled', async function (assert) {
|
|
// always force the new mount to the top of the list
|
|
const path = `mount-kv-${this.uid}`;
|
|
const maxTTLHours = 300;
|
|
|
|
await page.visit();
|
|
|
|
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.mounts.index', 'navigates to mount page');
|
|
await click(GENERAL.cardContainer('kv'));
|
|
await fillIn(GENERAL.inputByAttr('path'), path);
|
|
await click(GENERAL.button('Method Options'));
|
|
await click(GENERAL.toggleInput('Default Lease TTL'));
|
|
await click(GENERAL.toggleInput('Max Lease TTL'));
|
|
await page.maxTTLUnit('h').maxTTLVal(maxTTLHours);
|
|
await click(GENERAL.submitButton);
|
|
await configPage.visit({ backend: path });
|
|
assert.strictEqual(configPage.defaultTTL, '1 month 1 day', 'shows system default TTL');
|
|
assert.strictEqual(configPage.maxTTL, `${this.calcDays(maxTTLHours)}`, 'shows the proper max TTL');
|
|
});
|
|
|
|
test('it sets the max ttl after pki chosen, resets after', async function (assert) {
|
|
await page.visit();
|
|
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.mounts.index');
|
|
await click(GENERAL.cardContainer('pki'));
|
|
assert.dom('[data-test-input="config.max_lease_ttl"]').exists();
|
|
assert
|
|
.dom('[data-test-input="config.max_lease_ttl"] [data-test-ttl-toggle]')
|
|
.isChecked('Toggle is checked by default');
|
|
assert.dom('[data-test-input="config.max_lease_ttl"] [data-test-ttl-value]').hasValue('3650');
|
|
assert.dom('[data-test-input="config.max_lease_ttl"] [data-test-select="ttl-unit"]').hasValue('d');
|
|
|
|
// Go back and choose a different type
|
|
await click(GENERAL.backButton);
|
|
await click(GENERAL.cardContainer('database'));
|
|
assert.dom('[data-test-input="config.max_lease_ttl"]').exists('3650');
|
|
assert
|
|
.dom('[data-test-input="config.max_lease_ttl"] [data-test-ttl-toggle]')
|
|
.isNotChecked('Toggle is unchecked by default');
|
|
await click(GENERAL.toggleInput('Max Lease TTL'));
|
|
assert.dom('[data-test-input="config.max_lease_ttl"] [data-test-ttl-value]').hasValue('');
|
|
assert.dom('[data-test-input="config.max_lease_ttl"] [data-test-select="ttl-unit"]').hasValue('s');
|
|
});
|
|
|
|
test('it throws error if setting duplicate path name', async function (assert) {
|
|
const path = `kv-duplicate`;
|
|
|
|
await consoleComponent.runCommands([
|
|
// delete any kv-duplicate previously written here so that tests can be re-run
|
|
`delete sys/mounts/${path}`,
|
|
]);
|
|
|
|
await page.visit();
|
|
|
|
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.mounts.index');
|
|
await mountBackend('kv', path);
|
|
await page.secretList();
|
|
await settled();
|
|
await page.enableEngine();
|
|
await mountBackend('kv', path);
|
|
await waitFor('[data-test-message-error-description]');
|
|
assert.dom('[data-test-message-error-description]').containsText(`path is already in use at ${path}`);
|
|
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.mounts.create');
|
|
|
|
await page.secretList();
|
|
await settled();
|
|
assert.dom(SES.secretsBackendLink(path)).exists({ count: 1 }, 'renders only one instance of the engine');
|
|
});
|
|
|
|
test('version 2 with no update to config endpoint still allows mount of secret engine', async function (assert) {
|
|
const enginePath = `kv-noUpdate-${this.uid}`;
|
|
const V2_POLICY = `
|
|
path "${enginePath}/*" {
|
|
capabilities = ["list","create","read","sudo","delete"]
|
|
}
|
|
path "sys/mounts/*"
|
|
{
|
|
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
|
|
}
|
|
|
|
# List existing secrets engines.
|
|
path "sys/mounts"
|
|
{
|
|
capabilities = ["read"]
|
|
}
|
|
# Allow page to load after mount
|
|
path "sys/internal/ui/mounts/${enginePath}" {
|
|
capabilities = ["read"]
|
|
}
|
|
`;
|
|
await consoleComponent.toggle();
|
|
await consoleComponent.runCommands(
|
|
[
|
|
// delete any previous mount with same name
|
|
`delete sys/mounts/${enginePath}`,
|
|
`write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`,
|
|
'write -field=client_token auth/token/create policies=kv-v2-degrade',
|
|
],
|
|
false
|
|
);
|
|
await settled();
|
|
const userToken = consoleComponent.lastLogOutput;
|
|
|
|
await login(userToken);
|
|
// create the engine
|
|
await mountSecrets.visit();
|
|
await click(GENERAL.cardContainer('kv'));
|
|
await fillIn(GENERAL.inputByAttr('path'), enginePath);
|
|
await mountSecrets.setMaxVersion(101);
|
|
await click(GENERAL.submitButton);
|
|
|
|
assert
|
|
.dom('[data-test-flash-message]')
|
|
.containsText(
|
|
`You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.`
|
|
);
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
`/vault/secrets/${enginePath}/kv/list`,
|
|
'After mounting, redirects to secrets list page'
|
|
);
|
|
await configPage.visit({ backend: enginePath });
|
|
await settled();
|
|
});
|
|
|
|
test('it should transition to mountable addon engine after mount success', async function (assert) {
|
|
// test supported backends that ARE ember engines (enterprise only engines are tested individually)
|
|
const addons = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: false }).filter(
|
|
(e) => BACKENDS_WITH_ENGINES.includes(e.type)
|
|
);
|
|
assert.expect(addons.length);
|
|
|
|
for (const engine of addons) {
|
|
await consoleComponent.runCommands([
|
|
// delete any previous mount with same name
|
|
`delete sys/mounts/${engine.type}`,
|
|
]);
|
|
await mountSecrets.visit();
|
|
await mountBackend(engine.type, engine.type);
|
|
|
|
assert.strictEqual(
|
|
currentRouteName(),
|
|
`vault.cluster.secrets.backend.${engine.engineRoute}`,
|
|
`Transitions to ${engine.displayName} route on mount success`
|
|
);
|
|
await consoleComponent.runCommands([
|
|
// cleanup after
|
|
`delete sys/mounts/${engine.type}`,
|
|
]);
|
|
}
|
|
});
|
|
|
|
test('it should transition to mountable non-addon engine after mount success', async function (assert) {
|
|
// test supported backends that are not ember engines (enterprise only engines are tested individually)
|
|
const nonEngineBackends = supportedSecretBackends().filter((b) => !BACKENDS_WITH_ENGINES.includes(b));
|
|
// add back kv because we want to test v1
|
|
const engines = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: false }).filter(
|
|
(e) => (nonEngineBackends.includes(e.type) || e.type === 'kv') && e.type !== 'cubbyhole'
|
|
);
|
|
assert.expect(engines.length);
|
|
|
|
for (const engine of engines) {
|
|
await consoleComponent.runCommands([
|
|
// delete any previous mount with same name
|
|
`delete sys/mounts/${engine.type}`,
|
|
]);
|
|
await mountSecrets.visit();
|
|
await click(GENERAL.cardContainer(engine.type));
|
|
await fillIn(GENERAL.inputByAttr('path'), engine.type);
|
|
if (engine.type === 'kv') {
|
|
await click(GENERAL.button('Method Options'));
|
|
await mountSecrets.version(1);
|
|
}
|
|
await click(GENERAL.submitButton);
|
|
|
|
const route = engineDisplayData(engine.type)?.isOnlyMountable ? 'configuration.index' : 'list-root';
|
|
assert.strictEqual(
|
|
currentRouteName(),
|
|
`vault.cluster.secrets.backend.${route}`,
|
|
`${engine.type} navigates to the correct view (either list if not configuration only or configuration if it is).`
|
|
);
|
|
|
|
await consoleComponent.runCommands([
|
|
// cleanup after
|
|
`delete sys/mounts/${engine.type}`,
|
|
]);
|
|
}
|
|
});
|
|
|
|
test('it should transition back to backend list for unsupported backends', async function (assert) {
|
|
const unsupported = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: false }).filter(
|
|
(e) => !supportedSecretBackends().includes(e.type)
|
|
);
|
|
assert.expect(unsupported.length);
|
|
|
|
for (const engine of unsupported) {
|
|
await consoleComponent.runCommands([
|
|
// delete any previous mount with same name
|
|
`delete sys/mounts/${engine.type}`,
|
|
]);
|
|
await mountSecrets.visit();
|
|
await mountBackend(engine.type, engine.type);
|
|
|
|
assert.strictEqual(
|
|
currentRouteName(),
|
|
`vault.cluster.secrets.backends`,
|
|
`${engine.type} returns to backends list`
|
|
);
|
|
}
|
|
});
|
|
|
|
test('it should transition to different locations for kv v1 and v2', async function (assert) {
|
|
assert.expect(4);
|
|
const v2 = 'kv-v2';
|
|
await consoleComponent.runCommands([
|
|
// delete any previous mount with same name
|
|
`delete sys/mounts/${v2}`,
|
|
]);
|
|
await mountSecrets.visit();
|
|
await mountBackend('kv', v2);
|
|
assert.strictEqual(currentURL(), `/vault/secrets/${v2}/kv/list`, `${v2} navigates to list url`);
|
|
assert.strictEqual(
|
|
currentRouteName(),
|
|
`vault.cluster.secrets.backend.kv.list`,
|
|
`${v2} navigates to list url`
|
|
);
|
|
|
|
const v1 = 'kv-v1';
|
|
await consoleComponent.runCommands([
|
|
// delete any previous mount with same name
|
|
`delete sys/mounts/${v1}`,
|
|
]);
|
|
await mountSecrets.visit();
|
|
await click(GENERAL.cardContainer('kv'));
|
|
await fillIn(GENERAL.inputByAttr('path'), v1);
|
|
await click(GENERAL.button('Method Options'));
|
|
await mountSecrets.version(1);
|
|
await click(GENERAL.submitButton);
|
|
|
|
assert.strictEqual(currentURL(), `/vault/secrets/${v1}/list`, `${v1} navigates to list url`);
|
|
assert.strictEqual(
|
|
currentRouteName(),
|
|
`vault.cluster.secrets.backend.list-root`,
|
|
`${v1} navigates to list route`
|
|
);
|
|
});
|
|
|
|
module('WIF secret engines', function () {
|
|
test('it sets identity_token_key on mount config using search select list, resets after', async function (assert) {
|
|
// create an oidc/key
|
|
await runCmd(`write identity/oidc/key/some-key allowed_client_ids="*"`);
|
|
|
|
await page.visit();
|
|
await click(GENERAL.cardContainer('aws')); // only testing aws of the WIF engines as the functionality for all others WIF engines in this form are the same
|
|
await click(GENERAL.button('Method Options'));
|
|
assert.dom('[data-test-search-select-with-modal]').exists('Search select with modal component renders');
|
|
await clickTrigger('#key');
|
|
const dropdownOptions = findAll('[data-option-index]').map((o) => o.innerText);
|
|
assert.ok(dropdownOptions.includes('some-key'), 'search select options show some-key');
|
|
await click(GENERAL.searchSelect.option(GENERAL.searchSelect.optionIndex('some-key')));
|
|
assert
|
|
.dom(GENERAL.searchSelect.selectedOption())
|
|
.hasText('some-key', 'some-key was selected and displays in the search select');
|
|
await click(GENERAL.backButton);
|
|
// Choose a non-wif engine
|
|
await click(GENERAL.cardContainer('ssh'));
|
|
assert
|
|
.dom('[data-test-search-select-with-modal]')
|
|
.doesNotExist('for type ssh, the modal field does not render.');
|
|
// cleanup
|
|
await runCmd(`delete identity/oidc/key/some-key`);
|
|
});
|
|
|
|
test('it allows a user with permissions to oidc/key to create an identity_token_key', async function (assert) {
|
|
const engine = 'aws'; // only testing aws of the WIF engines as the functionality for all others WIF engines in this form are the same
|
|
await login();
|
|
const path = `secrets-adminPolicy-${engine}`;
|
|
const newKey = `key-${engine}-${uuidv4()}`;
|
|
const secrets_admin_policy = adminOidcCreateRead(path);
|
|
const secretsAdminToken = await runCmd(
|
|
tokenWithPolicyCmd(`secrets-admin-${path}`, secrets_admin_policy)
|
|
);
|
|
|
|
await login(secretsAdminToken);
|
|
await visit('/vault/secrets/mounts');
|
|
await click(GENERAL.cardContainer(engine));
|
|
await fillIn(GENERAL.inputByAttr('path'), path);
|
|
await click(GENERAL.button('Method Options'));
|
|
await clickTrigger('#key');
|
|
// create new key
|
|
await fillIn(GENERAL.searchSelect.searchInput, newKey);
|
|
await click(GENERAL.searchSelect.options);
|
|
assert.dom('#search-select-modal').exists(`modal with form opens for engine ${engine}`);
|
|
assert
|
|
.dom('[data-test-modal-title]')
|
|
.hasText('Create new key', `Create key modal renders for engine: ${engine}`);
|
|
|
|
await click(OIDC.keySaveButton);
|
|
assert.dom('#search-select-modal').doesNotExist(`modal disappears onSave for engine ${engine}`);
|
|
assert.dom(GENERAL.searchSelect.selectedOption()).hasText(newKey, `${newKey} is now selected`);
|
|
|
|
await click(GENERAL.submitButton);
|
|
await visit(`/vault/secrets/${path}/configuration`);
|
|
await click(SES.configurationToggle);
|
|
assert
|
|
.dom(GENERAL.infoRowValue('Identity token key'))
|
|
.hasText(newKey, `shows identity token key on configuration page for engine: ${engine}`);
|
|
|
|
// cleanup
|
|
await runCmd(`delete sys/mounts/${path}`);
|
|
await runCmd(`delete identity/oidc/key/some-key`);
|
|
await runCmd(`delete identity/oidc/key/${newKey}`);
|
|
});
|
|
|
|
test('it allows user with NO access to oidc/key to manually input an identity_token_key', async function (assert) {
|
|
const engine = 'aws'; // only testing aws of the WIF engines as the functionality for all others WIF engines in this form are the same
|
|
await login();
|
|
const path = `secrets-noOidcAdmin-${engine}`;
|
|
const secretsNoOidcAdminPolicy = adminOidcCreate(path);
|
|
const secretsNoOidcAdminToken = await runCmd(
|
|
tokenWithPolicyCmd(`secrets-noOidcAdmin-${path}`, secretsNoOidcAdminPolicy)
|
|
);
|
|
// create an oidc/key that they can then use even if they can't read it.
|
|
await runCmd(`write identity/oidc/key/general-key allowed_client_ids="*"`);
|
|
|
|
await login(secretsNoOidcAdminToken);
|
|
await page.visit();
|
|
await click(GENERAL.cardContainer(engine));
|
|
await fillIn(GENERAL.inputByAttr('path'), path);
|
|
await click(GENERAL.button('Method Options'));
|
|
// type-in fallback component to create new key
|
|
await typeIn(GENERAL.inputSearch('key'), 'general-key');
|
|
await click(GENERAL.submitButton);
|
|
assert
|
|
.dom(GENERAL.latestFlashContent)
|
|
.hasText(`Successfully mounted the ${engine} secrets engine at ${path}.`);
|
|
|
|
await visit(`/vault/secrets/${path}/configuration`);
|
|
|
|
await click(SES.configurationToggle);
|
|
assert
|
|
.dom(GENERAL.infoRowValue('Identity token key'))
|
|
.hasText('general-key', `shows identity token key on configuration page for engine: ${engine}`);
|
|
|
|
// cleanup
|
|
await runCmd(`delete sys/mounts/${path}`);
|
|
});
|
|
});
|
|
});
|