Files
vault/ui/tests/acceptance/settings/mount-secret-backend-test.js
Angel Garbarino 3abca46464 WIF sidebranch (#28148)
* manual cherry pick to deal with all the merge things

* changelog

* test fixes

* Update 28148.txt

* fix tests failures after main merge

* fix test failures after main merge

* Add Access Type and conditionally render WIF fields (#28149)

* initial work.

* remove access_type

* better no model logic well kind of

* rollback attrs

* remove defaults

* stopping point

* wip changing back to sidebranch

* hustling shuffling and serializing

* some of the component test coverage

* disable acces type if editing

* test coverage

* hide max retries that sneaky bugger

* cleanup

* cleanup

* Update root-config.js

* remove flash message check, locally passes great but on ci flaky

* clean up

* thank you chelsea

* test clean up per enterprise vs community

* address pr comments

* welp a miss add

* UI (sidebranch) WIF Issuer field (#28187)

* Add type declaration files for aws config models

* use updated task syntax for save method on configure-aws

* fix types on edit route

* fetch issuer on configure edit page if aws + enterprise

* track issuer within configure-aws component

* add placeholder support on form-field

* Add warning if issuer changed from previous value or could not be read

* cleanup

* preliminary tests

* dont use while loop so we can test the modal

* tests

* cleanup

* fix tests

* remove extra tracked value and duplicate changed attrs check

* modal footer

---------

Co-authored-by: Angel Garbarino <argarbarino@gmail.com>

* Display issuer on Configuration details (#28209)

* display issuer on configuration details

* workflow complete, now on to testing

* handle issuer things

* fix all the broken tests things

* add test coveragE:

* cleanup

* rename model/adapter

* Update configure-aws.ts

* Update aws-configuration-test.js

* 90 percent there for pr comments

* last one for tonight

* a few more because why not

* hasDirtyAttributes fixes

* revert back to previous noRead->queryIssuerError

---------

Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
2024-08-29 12:17:51 -06:00

421 lines
16 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import {
currentRouteName,
currentURL,
settled,
click,
findAll,
fillIn,
visit,
typeIn,
} 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 authPage from 'vault/tests/pages/auth';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
import logout from 'vault/tests/pages/logout';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import { mountableEngines } from 'vault/helpers/mountable-secret-engines'; // allEngines() includes enterprise engines, those are tested elsewhere
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 { SELECTORS as OIDC } from 'vault/tests/helpers/oidc-config';
import { adminOidcCreateRead, adminOidcCreate } from 'vault/tests/helpers/secret-engine/policy-generator';
import { WIF_ENGINES } from 'vault/helpers/mountable-secret-engines';
const consoleComponent = create(consoleClass);
// enterprise backends are tested separately
const BACKENDS_WITH_ENGINES = ['kv', 'pki', 'ldap', 'kubernetes'];
module('Acceptance | settings/mount-secret-backend', 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 authPage.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.settings.mount-secret-backend');
await page.selectType('kv');
await page
.path(path)
.toggleOptions()
.enableDefaultTtl()
.defaultTTLUnit('h')
.defaultTTLVal(defaultTTLHours)
.enableMaxTtl()
.maxTTLUnit('h')
.maxTTLVal(maxTTLHours)
.submit();
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.settings.mount-secret-backend');
await page.selectType('kv');
await page
.path(path)
.toggleOptions()
.enableDefaultTtl()
.enableMaxTtl()
.maxTTLUnit('h')
.maxTTLVal(maxTTLHours)
.submit();
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.settings.mount-secret-backend');
await page.selectType('pki');
assert.dom('[data-test-input="maxLeaseTtl"]').exists();
assert
.dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-toggle]')
.isChecked('Toggle is checked by default');
assert.dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-value]').hasValue('3650');
assert.dom('[data-test-input="maxLeaseTtl"] [data-test-select="ttl-unit"]').hasValue('d');
// Go back and choose a different type
await page.back();
await page.selectType('database');
assert.dom('[data-test-input="maxLeaseTtl"]').exists('3650');
assert
.dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-toggle]')
.isNotChecked('Toggle is unchecked by default');
await page.enableMaxTtl();
assert.dom('[data-test-input="maxLeaseTtl"] [data-test-ttl-value]').hasValue('');
assert.dom('[data-test-input="maxLeaseTtl"] [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.settings.mount-secret-backend');
await page.selectType('kv');
await page.path(path).submit();
await page.secretList();
await settled();
await page.enableEngine();
await page.selectType('kv');
await page.path(path).submit();
assert.dom('[data-test-message-error-description]').containsText(`path is already in use at ${path}`);
assert.strictEqual(currentRouteName(), 'vault.cluster.settings.mount-secret-backend');
await page.secretList();
await settled();
assert
.dom(`[data-test-secrets-backend-link=${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 logout.visit();
await authPage.login(userToken);
// create the engine
await mountSecrets.visit();
await mountSecrets.selectType('kv');
await mountSecrets.path(enginePath).setMaxVersion(101).submit();
await settled();
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 = mountableEngines().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 mountSecrets.selectType(engine.type);
await mountSecrets.path(engine.type).submit();
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 = mountableEngines().filter((e) => nonEngineBackends.includes(e.type) || e.type === 'kv');
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 mountSecrets.selectType(engine.type);
await mountSecrets.path(engine.type);
if (engine.type === 'kv') {
await mountSecrets.toggleOptions().version(1);
}
await mountSecrets.submit();
assert.strictEqual(
currentRouteName(),
`vault.cluster.secrets.backend.list-root`,
`${engine.type} navigates to list view`
);
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 = mountableEngines().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 mountSecrets.selectType(engine.type);
await mountSecrets.path(engine.type).submit();
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 mountSecrets.selectType('kv');
await mountSecrets.path(v2).submit();
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 mountSecrets.selectType('kv');
await mountSecrets.path(v1).toggleOptions().version(1).submit();
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="*"`);
for (const engine of WIF_ENGINES) {
await page.visit();
await page.selectType(engine);
await click(GENERAL.toggleGroup('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');
}
// Go back and choose a non-wif engine type
await page.back();
await page.selectType('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) {
for (const engine of WIF_ENGINES) {
const path = `secrets-adminPolicy-${engine}`;
const newKey = `key-${uuidv4()}`;
const secrets_admin_policy = adminOidcCreateRead(path);
const secretsAdminToken = await runCmd(
tokenWithPolicyCmd(`secrets-admin-${path}`, secrets_admin_policy)
);
await logout.visit();
await authPage.login(secretsAdminToken);
await page.visit();
await page.selectType(engine);
await page.path(path);
await click(GENERAL.toggleGroup('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');
assert.dom('[data-test-modal-title]').hasText('Create new key', 'Create key modal renders');
await click(OIDC.keySaveButton);
assert.dom('#search-select-modal').doesNotExist('modal disappears onSave');
assert.dom(GENERAL.searchSelect.selectedOption()).hasText(newKey, `${newKey} is now selected`);
await page.submit();
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');
// 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) {
for (const engine of WIF_ENGINES) {
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 logout.visit();
await authPage.login(secretsNoOidcAdminToken);
await page.visit();
await page.selectType(engine);
await page.path(path);
await click(GENERAL.toggleGroup('Method Options'));
// type-in fallback component to create new key
await typeIn(GENERAL.inputSearch('key'), 'general-key');
await page.submit();
assert
.dom(GENERAL.latestFlashContent)
.hasText(`Successfully mounted the aws 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');
// cleanup
await runCmd(`delete sys/mounts/${path}`);
}
});
});
});