From 2c3c585d7009451dd660fb5bb3dcc94d340d7686 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Fri, 8 Nov 2024 09:32:05 -0700 Subject: [PATCH] Add identity_token_key to Azure and GCP secret engines (#28822) * changes then onto tests * fix wif test failures * changelog * clean up * address pr comments * only test one wif engine for relevant tests * add back engine loop for tests that depend on type --- changelog/28822.txt | 3 + ui/app/helpers/mountable-secret-engines.js | 4 +- ui/app/models/secret-engine.js | 8 +- .../settings/mount-secret-backend-test.js | 160 +++++++++--------- .../helpers/secret-engine/policy-generator.ts | 2 +- .../components/mount-backend-form-test.js | 56 +++--- 6 files changed, 123 insertions(+), 110 deletions(-) create mode 100644 changelog/28822.txt diff --git a/changelog/28822.txt b/changelog/28822.txt new file mode 100644 index 0000000000..7f8f48fe72 --- /dev/null +++ b/changelog/28822.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Add identity_token_key to mount view for the GCP and Azure Secret engines. +``` diff --git a/ui/app/helpers/mountable-secret-engines.js b/ui/app/helpers/mountable-secret-engines.js index 216eb960fa..ca9341a68c 100644 --- a/ui/app/helpers/mountable-secret-engines.js +++ b/ui/app/helpers/mountable-secret-engines.js @@ -134,8 +134,8 @@ const MOUNTABLE_SECRET_ENGINES = [ }, ]; -// A list of Workload Identity Federation engines. Will eventually include Azure and GCP. -export const WIF_ENGINES = ['aws']; +// A list of Workflow Identity Federation engines. +export const WIF_ENGINES = ['aws', 'azure', 'gcp']; export function wifEngines() { return WIF_ENGINES.slice(); diff --git a/ui/app/models/secret-engine.js b/ui/app/models/secret-engine.js index 86d497281e..b64cebeed3 100644 --- a/ui/app/models/secret-engine.js +++ b/ui/app/models/secret-engine.js @@ -9,7 +9,7 @@ import { equal } from '@ember/object/computed'; // eslint-disable-line import { withModelValidations } from 'vault/decorators/model-validations'; import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; -import { isAddonEngine, allEngines } from 'vault/helpers/mountable-secret-engines'; +import { isAddonEngine, allEngines, WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; import { WHITESPACE_WARNING } from 'vault/utils/model-helpers/validators'; const LINKED_BACKENDS = supportedSecretBackends(); @@ -179,8 +179,8 @@ export default class SecretEngineModel extends Model { if (type === 'kv' && parseInt(this.version, 10) === 2) { fields.push('casRequired', 'deleteVersionAfter', 'maxVersions'); } - // WIF secret engines - if (type === 'aws') { + // For WIF Secret engines, allow users to set the identity token key when mounting the engine. + if (WIF_ENGINES.includes(type)) { fields.push('config.identityTokenKey'); } return fields; @@ -232,7 +232,7 @@ export default class SecretEngineModel extends Model { // no ttl options for keymgmt optionFields = [...CORE_OPTIONS, 'config.allowedManagedKeys', ...STANDARD_CONFIG]; break; - case 'aws': + case WIF_ENGINES.find((type) => type === this.engineType): defaultFields = ['path']; optionFields = [ ...CORE_OPTIONS, diff --git a/ui/tests/acceptance/settings/mount-secret-backend-test.js b/ui/tests/acceptance/settings/mount-secret-backend-test.js index 6a648b4514..8894067f16 100644 --- a/ui/tests/acceptance/settings/mount-secret-backend-test.js +++ b/ui/tests/acceptance/settings/mount-secret-backend-test.js @@ -34,7 +34,6 @@ import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend 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 { WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; const consoleComponent = create(consoleClass); @@ -324,23 +323,19 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { // 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 click(MOUNT_BACKEND_FORM.mountType(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.visit(); + await click(MOUNT_BACKEND_FORM.mountType('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.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'); await click(GENERAL.backButton); + // Choose a non-wif engine await click(MOUNT_BACKEND_FORM.mountType('ssh')); assert .dom('[data-test-search-select-with-modal]') @@ -350,75 +345,84 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { }); 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) - ); + logout.visit(); + 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 authPage.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 logout.visit(); - await authPage.login(secretsAdminToken); - await page.visit(); - await click(MOUNT_BACKEND_FORM.mountType(engine)); - await fillIn(GENERAL.inputByAttr('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 logout.visit(); + await authPage.login(secretsAdminToken); + await visit('/vault/settings/mount-secret-backend'); + await click(MOUNT_BACKEND_FORM.mountType(engine)); + await fillIn(GENERAL.inputByAttr('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 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'); - assert.dom(GENERAL.searchSelect.selectedOption()).hasText(newKey, `${newKey} is now selected`); + 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.saveButton); - 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}`); - } + await click(GENERAL.saveButton); + 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}`); + await logout.visit(); }); 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(); + 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 authPage.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 logout.visit(); - await authPage.login(secretsNoOidcAdminToken); - await page.visit(); - await click(MOUNT_BACKEND_FORM.mountType(engine)); - await fillIn(GENERAL.inputByAttr('path'), path); - await click(GENERAL.toggleGroup('Method Options')); - // type-in fallback component to create new key - await typeIn(GENERAL.inputSearch('key'), 'general-key'); - await click(GENERAL.saveButton); - assert - .dom(GENERAL.latestFlashContent) - .hasText(`Successfully mounted the aws secrets engine at ${path}.`); + await logout.visit(); + await authPage.login(secretsNoOidcAdminToken); + await page.visit(); + await click(MOUNT_BACKEND_FORM.mountType(engine)); + await fillIn(GENERAL.inputByAttr('path'), path); + await click(GENERAL.toggleGroup('Method Options')); + // type-in fallback component to create new key + await typeIn(GENERAL.inputSearch('key'), 'general-key'); + await click(GENERAL.saveButton); + 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'); - // cleanup - await runCmd(`delete sys/mounts/${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}`); + await logout.visit(); }); }); }); diff --git a/ui/tests/helpers/secret-engine/policy-generator.ts b/ui/tests/helpers/secret-engine/policy-generator.ts index 5f758bbb88..d8a08e55d3 100644 --- a/ui/tests/helpers/secret-engine/policy-generator.ts +++ b/ui/tests/helpers/secret-engine/policy-generator.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -// This is policy can mount a secret engine +// This policy can mount a secret engine // and list and create oidc keys, relevant for setting identity_key_token for WIF export const adminOidcCreateRead = (mountPath: string) => { return ` diff --git a/ui/tests/integration/components/mount-backend-form-test.js b/ui/tests/integration/components/mount-backend-form-test.js index adcc1079f4..5579d36ea5 100644 --- a/ui/tests/integration/components/mount-backend-form-test.js +++ b/ui/tests/integration/components/mount-backend-form-test.js @@ -13,9 +13,8 @@ import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend-form-selectors'; import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers'; import { methods } from 'vault/helpers/mountable-auth-methods'; -import { mountableEngines } from 'vault/helpers/mountable-secret-engines'; +import { mountableEngines, WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; import hbs from 'htmlbars-inline-precompile'; - import sinon from 'sinon'; module('Integration | Component | mount backend form', function (hooks) { @@ -202,43 +201,50 @@ module('Integration | Component | mount backend form', function (hooks) { }); module('WIF secret engines', function () { - test('it shows identityTokenKey when type is aws and hides when its not', async function (assert) { + test('it shows identityTokenKey when type is a WIF engine and hides when its not', async function (assert) { await render( hbs`` ); - await click(MOUNT_BACKEND_FORM.mountType('ldap')); - - await click(GENERAL.toggleGroup('Method Options')); - assert - .dom(GENERAL.fieldByAttr('identityTokenKey')) - .doesNotExist(`Identity token key field hidden when type=${this.model.type}`); - - await click(GENERAL.backButton); - await click(MOUNT_BACKEND_FORM.mountType('aws')); - await click(GENERAL.toggleGroup('Method Options')); - assert - .dom(GENERAL.fieldByAttr('identityTokenKey')) - .exists(`Identity token key field shows when type=${this.model.type}`); + for (const engine of WIF_ENGINES) { + await click(MOUNT_BACKEND_FORM.mountType(engine)); + await click(GENERAL.toggleGroup('Method Options')); + assert + .dom(GENERAL.fieldByAttr('identityTokenKey')) + .exists(`Identity token key field shows when type=${this.model.type}`); + await click(GENERAL.backButton); + } + for (const engine of mountableEngines().filter((e) => !WIF_ENGINES.includes(e.type))) { + // check non-wif engine + await click(MOUNT_BACKEND_FORM.mountType(engine.type)); + await click(GENERAL.toggleGroup('Method Options')); + assert + .dom(GENERAL.fieldByAttr('identityTokenKey')) + .doesNotExist(`Identity token key field hidden when type=${this.model.type}`); + await click(GENERAL.backButton); + } }); test('it updates identityTokeKey if user has changed it', async function (assert) { await render( hbs`` ); - await click(MOUNT_BACKEND_FORM.mountType('aws')); assert.strictEqual( this.model.config.identityTokenKey, undefined, - 'On init identityTokenKey is not set on the model' + `On init identityTokenKey is not set on the model` ); + for (const engine of WIF_ENGINES) { + await click(MOUNT_BACKEND_FORM.mountType(engine)); + await click(GENERAL.toggleGroup('Method Options')); + await typeIn(GENERAL.inputSearch('key'), `${engine}+specialKey`); // set to something else - await click(GENERAL.toggleGroup('Method Options')); - await typeIn(GENERAL.inputSearch('key'), 'default'); - assert.strictEqual( - this.model.config.identityTokenKey, - 'default', - 'updates model with default identityTokenKey' - ); + assert.strictEqual( + this.model.config.identityTokenKey, + `${engine}+specialKey`, + `updates ${engine} model with custom identityTokenKey` + ); + await click(GENERAL.backButton); + } }); }); });