mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
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:
parent
6f653692ea
commit
2c3c585d70
6 changed files with 123 additions and 110 deletions
3
changelog/28822.txt
Normal file
3
changelog/28822.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: Add identity_token_key to mount view for the GCP and Azure Secret engines.
|
||||
```
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 `
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue