Prep work for creating one WIF configuration component (#29345)

* initial things without helper changes

* adjust test for clean up of secret-engine-helper

* remove added line thats better in next pr

* remove extra check

* 🧹

* replace return with continue within loops
This commit is contained in:
Angel Garbarino
2025-01-10 15:06:42 -07:00
committed by GitHub
parent 8cee664204
commit a73a6983c4
11 changed files with 185 additions and 149 deletions

View File

@@ -32,8 +32,15 @@ export default class AwsLeaseConfig extends Model {
})
lease;
configurableParams = ['lease', 'leaseMax'];
get displayAttrs() {
const keys = ['lease', 'leaseMax'];
return expandAttributeMeta(this, keys);
// while identical to formFields, keeping the same pattern as other configurable secret engines for consistency
// and to easily filter out displayAttributes in the future if needed
return this.formFields;
}
get formFields() {
return expandAttributeMeta(this, this.configurableParams);
}
}

View File

@@ -45,23 +45,33 @@ export default class AwsRootConfig extends Model {
iamEndpoint;
@attr('string', { label: 'STS endpoint' }) stsEndpoint;
@attr('number', {
label: 'Maximum retries',
subText: 'Number of max retries the client should use for recoverable errors. Default is -1.',
})
maxRetries;
configurableParams = [
'roleArn',
'identityTokenAudience',
'identityTokenTtl',
'accessKey',
'secretKey',
'region',
'iamEndpoint',
'stsEndpoint',
'maxRetries',
];
get isWifPluginConfigured() {
return !!this.identityTokenAudience || !!this.identityTokenTtl || !!this.roleArn;
}
get isAccountPluginConfigured() {
return !!this.accessKey;
}
get displayAttrs() {
const keys = [
'roleArn',
'identityTokenAudience',
'identityTokenTtl',
'accessKey',
'region',
'iamEndpoint',
'stsEndpoint',
'maxRetries',
];
return expandAttributeMeta(this, keys);
const formFields = expandAttributeMeta(this, this.configurableParams);
return formFields.filter((attr) => attr.name !== 'secretKey');
}
// "filedGroupsWif" and "fieldGroupsIam" are passed to the FormFieldGroups component to determine which group to show in the form (ex: @groupName="fieldGroupsWif")

View File

@@ -25,6 +25,7 @@ export default class GcpConfig extends Model {
})
maxTtl;
/* GCP credential config field */
@attr('string', {
label: 'JSON credentials',
subText:

View File

@@ -38,16 +38,15 @@ export default class SshCaConfig extends Model {
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
@attr('string', { sensitive: true }) privateKey; // obfuscated, never returned by API
@attr('string') publicKey;
@attr('boolean', { defaultValue: true })
generateSigningKey;
@attr('boolean', { defaultValue: true }) generateSigningKey;
configurableParams = ['privateKey', 'publicKey', 'generateSigningKey'];
// do not return private key for configuration.index view
get displayAttrs() {
return this.formFields.filter((attr) => attr.name !== 'privateKey');
}
// return private key for edit/create view
get formFields() {
const keys = ['privateKey', 'publicKey', 'generateSigningKey'];
return expandAttributeMeta(this, keys);
return expandAttributeMeta(this, this.configurableParams);
}
}

View File

@@ -185,7 +185,7 @@ module('Acceptance | aws | configuration', function (hooks) {
assert
.dom(GENERAL.infoRowValue('Identity token TTL'))
.doesNotExist('Identity token TTL does not show.');
assert.dom(GENERAL.infoRowValue('Maximum retries')).doesNotExist('Maximum retries does not show.');
assert.dom(GENERAL.infoRowValue('Max retries')).doesNotExist('Max retries does not show.');
// cleanup
await runCmd(`delete sys/mounts/${path}`);
});
@@ -219,7 +219,6 @@ module('Acceptance | aws | configuration', function (hooks) {
});
test('it shows AWS mount configuration details', async function (assert) {
assert.expect(12);
const path = `aws-${this.uid}`;
const type = 'aws';
this.server.get(`${path}/config/root`, (schema, req) => {
@@ -231,6 +230,7 @@ module('Acceptance | aws | configuration', function (hooks) {
createConfig(this.store, path, type); // create the aws root config in the store
await click(SES.configTab);
for (const key of expectedConfigKeys(type)) {
if (key === 'Secret key') continue; // secret-key is not returned by the API
assert.dom(GENERAL.infoRowLabel(key)).exists(`${key} on the ${type} config details exists.`);
const responseKeyAndValue = expectedValueOfConfigKeys(type, key);
assert
@@ -368,7 +368,7 @@ module('Acceptance | aws | configuration', function (hooks) {
.doesNotExist('Access type section does not render for a community user');
// check all the form fields are present
await click(GENERAL.toggleGroup('Root config options'));
for (const key of expectedConfigKeys('aws-root-create')) {
for (const key of expectedConfigKeys('aws', true)) {
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section.`);
}
for (const key of expectedConfigKeys('aws-lease')) {

View File

@@ -94,6 +94,7 @@ module('Acceptance | Azure | configuration', function (hooks) {
});
await enablePage.enable(this.type, path);
for (const key of expectedConfigKeys('azure')) {
if (key === 'Client secret') continue; // client-secret is not returned by the API
assert.dom(GENERAL.infoRowLabel(key)).exists(`${key} on the ${this.type} config details exists.`);
const responseKeyAndValue = expectedValueOfConfigKeys(this.type, key);
assert

View File

@@ -68,6 +68,8 @@ module('Acceptance | GCP | configuration', function (hooks) {
service_account_email: 'service-email',
identity_token_audience: 'audience',
identity_token_ttl: 720000,
max_ttl: 14400,
ttl: 3600,
};
this.server.get(`${path}/config`, () => {
assert.ok(true, 'request made to config when navigating to the configuration page.');
@@ -102,6 +104,7 @@ module('Acceptance | GCP | configuration', function (hooks) {
});
await enablePage.enable(this.type, path);
for (const key of expectedConfigKeys(this.type)) {
if (key === 'Credentials') continue; // not returned by the API
const responseKeyAndValue = expectedValueOfConfigKeys(this.type, key);
assert
.dom(GENERAL.infoRowValue(key))

View File

@@ -6,6 +6,7 @@
import { click, fillIn } from '@ember/test-helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
import { stringArrayToCamelCase } from 'vault/helpers/string-array-to-camel';
import { v4 as uuidv4 } from 'uuid';
export const createSecretsEngine = (store, type, path) => {
@@ -20,6 +21,32 @@ export const createSecretsEngine = (store, type, path) => {
});
return store.peekRecord('secret-engine', path);
};
/* Create configurations methods
* for each configuration we create the record and then push it to the store.
*/
export function configUrl(type, backend) {
switch (type) {
case 'aws':
return `/${backend}/config/root`;
case 'aws-lease':
return `/${backend}/config/lease`;
case 'ssh':
return `/${backend}/config/ca`;
default:
return `/${backend}/config`;
}
}
const createIssuerConfig = (store) => {
store.pushPayload('identity/oidc/config', {
id: 'identity-oidc-config',
modelName: 'identity/oidc/config',
data: {
issuer: ``,
},
});
return store.peekRecord('identity/oidc/config', 'identity-oidc-config');
};
const createAwsRootConfig = (store, backend, accessType = 'iam') => {
// clear any records first
@@ -62,17 +89,6 @@ const createAwsRootConfig = (store, backend, accessType = 'iam') => {
return store.peekRecord('aws/root-config', backend);
};
const createIssuerConfig = (store) => {
store.pushPayload('identity/oidc/config', {
id: 'identity-oidc-config',
modelName: 'identity/oidc/config',
data: {
issuer: ``,
},
});
return store.peekRecord('identity/oidc/config', 'identity-oidc-config');
};
const createAwsLeaseConfig = (store, backend) => {
store.pushPayload('aws/lease-config', {
id: backend,
@@ -177,22 +193,10 @@ const createGcpConfig = (store, backend, accessType = 'gcp') => {
return store.peekRecord('gcp/config', backend);
};
export function configUrl(type, backend) {
switch (type) {
case 'aws':
return `/${backend}/config/root`;
case 'aws-lease':
return `/${backend}/config/lease`;
case 'ssh':
return `/${backend}/config/ca`;
default:
return `/${backend}/config`;
}
}
// send the type of config you want and the name of the backend path to push the config to the store.
export const createConfig = (store, backend, type) => {
switch (type) {
case 'aws':
case 'aws-generic':
return createAwsRootConfig(store, backend);
case 'aws-wif':
return createAwsRootConfig(store, backend, 'wif');
@@ -214,49 +218,93 @@ export const createConfig = (store, backend, type) => {
return createGcpConfig(store, backend);
}
};
// Used in tests to assert the expected keys in the config details of configurable secret engines
export const expectedConfigKeys = (type) => {
/* Manually create the configuration by filling in the configuration form */
export const fillInAwsConfig = async (situation = 'withAccess') => {
if (situation === 'withAccess') {
await fillIn(GENERAL.inputByAttr('accessKey'), 'foo');
await fillIn(GENERAL.inputByAttr('secretKey'), 'bar');
}
if (situation === 'withAccessOptions') {
await click(GENERAL.toggleGroup('Root config options'));
await fillIn(GENERAL.inputByAttr('region'), 'ca-central-1');
await fillIn(GENERAL.inputByAttr('iamEndpoint'), 'iam-endpoint');
await fillIn(GENERAL.inputByAttr('stsEndpoint'), 'sts-endpoint');
await fillIn(GENERAL.inputByAttr('maxRetries'), '3');
}
if (situation === 'withLease') {
await click(GENERAL.ttl.toggle('Default Lease TTL'));
await fillIn(GENERAL.ttl.input('Default Lease TTL'), '33');
await click(GENERAL.ttl.toggle('Max Lease TTL'));
await fillIn(GENERAL.ttl.input('Max Lease TTL'), '44');
}
if (situation === 'withWif') {
await click(SES.wif.accessType('wif')); // toggle to wif
await fillIn(GENERAL.inputByAttr('issuer'), `http://bar.${uuidv4()}`); // make random because global setting
await fillIn(GENERAL.inputByAttr('roleArn'), 'foo-role');
await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'foo-audience');
await click(GENERAL.ttl.toggle('Identity token TTL'));
await fillIn(GENERAL.ttl.input('Identity token TTL'), '7200');
}
};
export const fillInAzureConfig = async (situation = 'azure') => {
await fillIn(GENERAL.inputByAttr('subscriptionId'), 'subscription-id');
await fillIn(GENERAL.inputByAttr('tenantId'), 'tenant-id');
await fillIn(GENERAL.inputByAttr('clientId'), 'client-id');
await fillIn(GENERAL.inputByAttr('environment'), 'AZUREPUBLICCLOUD');
if (situation === 'azure') {
await fillIn(GENERAL.inputByAttr('clientSecret'), 'client-secret');
await click(GENERAL.ttl.toggle('Root password TTL'));
await fillIn(GENERAL.ttl.input('Root password TTL'), '5200');
}
if (situation === 'withWif') {
await click(SES.wif.accessType('wif')); // toggle to wif
await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'azure-audience');
await click(GENERAL.ttl.toggle('Identity token TTL'));
await fillIn(GENERAL.ttl.input('Identity token TTL'), '7200');
}
};
/* Generate arrays of keys to iterate over.
* used to check details of the secret engine configuration
* and used to check the form to configure the secret engine
*/
// WIF specific keys
const genericWifKeys = ['Identity token audience', 'Identity token TTL'];
// AWS specific keys
const awsLeaseKeys = ['Default Lease TTL', 'Max Lease TTL'];
const awsKeys = ['Access key', 'Secret key', 'Region', 'IAM endpoint', 'STS endpoint', 'Max retries'];
const awsWifKeys = ['Issuer', 'Role ARN', ...genericWifKeys];
// Azure specific keys
const genericAzureKeys = ['Subscription ID', 'Tenant ID', 'Client ID', 'Environment'];
const azureKeys = [...genericAzureKeys, 'Client secret', 'Root password TTL'];
const azureWifKeys = [...genericAzureKeys, ...genericWifKeys];
// GCP specific keys
const genericGcpKeys = ['Config TTL', 'Max TTL'];
const gcpKeys = [...genericGcpKeys, 'Credentials'];
const gcpWifKeys = [...genericGcpKeys, ...genericWifKeys, 'Service account email'];
// SSH specific keys
const sshKeys = ['Private key', 'Public key', 'Generate signing key'];
export const expectedConfigKeys = (type, camelCase = false) => {
switch (type) {
case 'aws':
return ['Access key', 'Region', 'IAM endpoint', 'STS endpoint', 'Maximum retries'];
return camelCase ? stringArrayToCamelCase(awsKeys) : awsKeys;
case 'aws-wif':
return camelCase ? stringArrayToCamelCase(awsWifKeys) : awsWifKeys;
case 'aws-lease':
return ['Default Lease TTL', 'Max Lease TTL'];
case 'aws-root-create':
return ['accessKey', 'secretKey', 'region', 'iamEndpoint', 'stsEndpoint', 'maxRetries'];
case 'aws-root-create-wif':
return ['issuer', 'roleArn', 'identityTokenAudience', 'Identity token TTL'];
case 'aws-root-create-iam':
return ['accessKey', 'secretKey'];
case 'ssh':
return ['Public key', 'Generate signing key'];
return camelCase ? stringArrayToCamelCase(awsLeaseKeys) : awsLeaseKeys;
case 'azure':
return ['Subscription ID', 'Tenant ID', 'Client ID', 'Root password TTL', 'Environment'];
case 'azure-camelCase':
return ['subscriptionId', 'tenantId', 'clientId', 'rootPasswordTtl', 'environment'];
return camelCase ? stringArrayToCamelCase(azureKeys) : azureKeys;
case 'azure-wif':
return [
'Subscription ID',
'Tenant ID',
'Client ID',
'Environment',
'Identity token audience',
'Identity token TTL',
];
case 'azure-wif-camelCase':
return [
'subscriptionId',
'tenantId',
'clientId',
'environment',
'identityTokenAudience',
'Identity token TTL',
];
return camelCase ? stringArrayToCamelCase(azureWifKeys) : azureWifKeys;
case 'gcp':
return ['Config TTL', 'Max TTL'];
return camelCase ? stringArrayToCamelCase(gcpKeys) : gcpKeys;
case 'gcp-wif':
return ['Service account email', 'Identity token audience', 'Identity token TTL'];
case 'gcp-wif-camelCase':
return ['serviceAccountEmail', 'identityTokenAudience', 'Identity token TTL'];
return camelCase ? stringArrayToCamelCase(gcpWifKeys) : gcpWifKeys;
case 'ssh':
return camelCase ? stringArrayToCamelCase(sshKeys) : sshKeys;
}
};
@@ -270,7 +318,7 @@ const valueOfAwsKeys = (string) => {
return 'iam-endpoint';
case 'STS endpoint':
return 'sts-endpoint';
case 'Maximum retries':
case 'Max retries':
return '1';
}
};
@@ -333,53 +381,6 @@ export const expectedValueOfConfigKeys = (type, string) => {
}
};
export const fillInAwsConfig = async (situation = 'withAccess') => {
if (situation === 'withAccess') {
await fillIn(GENERAL.inputByAttr('accessKey'), 'foo');
await fillIn(GENERAL.inputByAttr('secretKey'), 'bar');
}
if (situation === 'withAccessOptions') {
await click(GENERAL.toggleGroup('Root config options'));
await fillIn(GENERAL.inputByAttr('region'), 'ca-central-1');
await fillIn(GENERAL.inputByAttr('iamEndpoint'), 'iam-endpoint');
await fillIn(GENERAL.inputByAttr('stsEndpoint'), 'sts-endpoint');
await fillIn(GENERAL.inputByAttr('maxRetries'), '3');
}
if (situation === 'withLease') {
await click(GENERAL.ttl.toggle('Default Lease TTL'));
await fillIn(GENERAL.ttl.input('Default Lease TTL'), '33');
await click(GENERAL.ttl.toggle('Max Lease TTL'));
await fillIn(GENERAL.ttl.input('Max Lease TTL'), '44');
}
if (situation === 'withWif') {
await click(SES.wif.accessType('wif')); // toggle to wif
await fillIn(GENERAL.inputByAttr('issuer'), `http://bar.${uuidv4()}`); // make random because global setting
await fillIn(GENERAL.inputByAttr('roleArn'), 'foo-role');
await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'foo-audience');
await click(GENERAL.ttl.toggle('Identity token TTL'));
await fillIn(GENERAL.ttl.input('Identity token TTL'), '7200');
}
};
export const fillInAzureConfig = async (situation = 'azure') => {
await fillIn(GENERAL.inputByAttr('subscriptionId'), 'subscription-id');
await fillIn(GENERAL.inputByAttr('tenantId'), 'tenant-id');
await fillIn(GENERAL.inputByAttr('clientId'), 'client-id');
await fillIn(GENERAL.inputByAttr('environment'), 'AZUREPUBLICCLOUD');
if (situation === 'azure') {
await fillIn(GENERAL.inputByAttr('clientSecret'), 'client-secret');
await click(GENERAL.ttl.toggle('Root password TTL'));
await fillIn(GENERAL.ttl.input('Root password TTL'), '5200');
}
if (situation === 'withWif') {
await click(SES.wif.accessType('wif')); // toggle to wif
await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'azure-audience');
await click(GENERAL.ttl.toggle('Identity token TTL'));
await fillIn(GENERAL.ttl.input('Identity token TTL'), '7200');
}
};
// Example usage
// createLongJson (2, 3) will create a json object with 2 original keys, each with 3 nested keys
// {

View File

@@ -38,7 +38,7 @@ module('Integration | Component | SecretEngine/ConfigurationDetails', function (
});
for (const type of CONFIGURABLE_SECRET_ENGINES) {
test(`it shows config details if configModel(s) are passed in for type: ${type}`, async function (assert) {
test(`${type}: it shows config details if configModel(s) are passed in`, async function (assert) {
const backend = `test-${type}`;
this.configModels = createConfig(this.store, backend, type);
this.typeDisplay = allEnginesArray.find((engine) => engine.type === type).displayName;
@@ -48,16 +48,30 @@ module('Integration | Component | SecretEngine/ConfigurationDetails', function (
);
for (const key of expectedConfigKeys(type)) {
assert.dom(GENERAL.infoRowLabel(key)).exists(`${key} on the ${type} config details exists.`);
const responseKeyAndValue = expectedValueOfConfigKeys(type, key);
assert
.dom(GENERAL.infoRowValue(key))
.hasText(responseKeyAndValue, `${key} value for the ${type} config details exists.`);
// make sure the ones that should be masked are masked, and others are not.
if (key === 'private_key' || key === 'public_key') {
assert.dom(GENERAL.infoRowValue(key)).hasClass('masked-input', `${key} is masked`);
if (
key === 'Secret key' ||
key === 'Client secret' ||
key === 'Private key' ||
key === 'Credentials'
) {
// these keys are not returned by the API and should not show on the details page
assert
.dom(GENERAL.infoRowLabel(key))
.doesNotExist(`${key} on the ${type} config details does NOT exists.`);
} else {
assert.dom(GENERAL.infoRowValue(key)).doesNotHaveClass('masked-input', `${key} is not masked`);
// check the label appears
assert.dom(GENERAL.infoRowLabel(key)).exists(`${key} on the ${type} config details exists.`);
const responseKeyAndValue = expectedValueOfConfigKeys(type, key);
// check the value appears
assert
.dom(GENERAL.infoRowValue(key))
.hasText(responseKeyAndValue, `${key} value for the ${type} config details exists.`);
// make sure the values that should be masked are masked, and others are not.
if (key === 'Public Key') {
assert.dom(GENERAL.infoRowValue(key)).hasClass('masked-input', `${key} is masked`);
} else {
assert.dom(GENERAL.infoRowValue(key)).doesNotHaveClass('masked-input', `${key} is not masked`);
}
}
}
});

View File

@@ -68,7 +68,7 @@ module('Integration | Component | SecretEngine/ConfigureAws', function (hooks) {
assert.dom(SES.wif.accessType('wif')).isNotChecked('wif access type is not checked');
// check all the form fields are present
await click(GENERAL.toggleGroup('Root config options'));
for (const key of expectedConfigKeys('aws-root-create')) {
for (const key of expectedConfigKeys('aws', true)) {
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section.`);
}
for (const key of expectedConfigKeys('aws-lease')) {
@@ -81,7 +81,7 @@ module('Integration | Component | SecretEngine/ConfigureAws', function (hooks) {
await this.renderComponent();
await click(SES.wif.accessType('wif'));
// check for the wif fields only
for (const key of expectedConfigKeys('aws-root-create-wif')) {
for (const key of expectedConfigKeys('aws-wif', true)) {
if (key === 'Identity token TTL') {
assert.dom(GENERAL.ttl.toggle(key)).exists(`${key} shows for wif section.`);
} else {
@@ -89,7 +89,7 @@ module('Integration | Component | SecretEngine/ConfigureAws', function (hooks) {
}
}
// check iam fields do not show
for (const key of expectedConfigKeys('aws-root-create-iam')) {
for (const key of expectedConfigKeys('aws', true)) {
assert.dom(GENERAL.inputByAttr(key)).doesNotExist(`${key} does not show when wif is selected.`);
}
});
@@ -426,7 +426,7 @@ module('Integration | Component | SecretEngine/ConfigureAws', function (hooks) {
.doesNotExist('Access type section does not render for a community user');
// check all the form fields are present
await click(GENERAL.toggleGroup('Root config options'));
for (const key of expectedConfigKeys('aws-root-create')) {
for (const key of expectedConfigKeys('aws', true)) {
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section.`);
}
for (const key of expectedConfigKeys('aws-lease')) {

View File

@@ -60,7 +60,7 @@ module('Integration | Component | SecretEngine/ConfigureAzure', function (hooks)
assert.dom(SES.wif.accessType('azure')).isChecked('defaults to showing Azure access type checked');
assert.dom(SES.wif.accessType('wif')).isNotChecked('wif access type is not checked');
// check all the form fields are present
for (const key of expectedConfigKeys('azure-camelCase')) {
for (const key of expectedConfigKeys('azure', true)) {
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section`);
}
assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist();
@@ -70,7 +70,7 @@ module('Integration | Component | SecretEngine/ConfigureAzure', function (hooks)
await this.renderComponent();
await click(SES.wif.accessType('wif'));
// check for the wif fields only
for (const key of expectedConfigKeys('azure-wif-camelCase')) {
for (const key of expectedConfigKeys('azure-wif', true)) {
if (key === 'Identity token TTL') {
assert.dom(GENERAL.ttl.toggle(key)).exists(`${key} shows for wif section.`);
} else {
@@ -285,14 +285,14 @@ module('Integration | Component | SecretEngine/ConfigureAzure', function (hooks)
});
test('it renders fields', async function (assert) {
assert.expect(8);
assert.expect(9);
await this.renderComponent();
assert.dom(SES.configureForm).exists('t lands on the Azure configuration form');
assert
.dom(SES.wif.accessTypeSection)
.doesNotExist('Access type section does not render for a community user');
// check all the form fields are present
for (const key of expectedConfigKeys('azure-camelCase')) {
for (const key of expectedConfigKeys('azure', true)) {
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for azure account creds section.`);
}
assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist();