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
This commit is contained in:
Angel Garbarino 2024-11-08 09:32:05 -07:00 committed by GitHub
parent 6f653692ea
commit 2c3c585d70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 123 additions and 110 deletions

3
changelog/28822.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:improvement
ui: Add identity_token_key to mount view for the GCP and Azure Secret engines.
```

View file

@ -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();

View file

@ -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,

View file

@ -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();
});
});
});

View file

@ -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 `

View file

@ -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`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
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`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
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);
}
});
});
});