Add GCP secret engine configuration Create/Edit views (#29423)

* gcp initial changes

* acceptance test coverage for gcp

* update config-wif component test so tests are passing

* specific gcp test coverage

* changelog

* comment clean up

* one more test

* comment things

* address pr comments
This commit is contained in:
Angel Garbarino
2025-01-30 13:37:20 -07:00
committed by GitHub
parent 9c0f2fbfe5
commit 14082d08f1
14 changed files with 469 additions and 143 deletions

6
changelog/29423.txt Normal file
View File

@@ -0,0 +1,6 @@
```release-note:improvement
ui: Adds ability to edit, create, and view the GCP secrets engine configuration.
```
```release-note:improvement
ui (enterprise): Allow WIF configuration on the GCP secrets engine.
```

View File

@@ -23,4 +23,24 @@ export default class GcpConfig extends ApplicationAdapter {
};
});
}
createOrUpdate(store, type, snapshot) {
const serializer = store.serializerFor(type.modelName);
const data = serializer.serialize(snapshot);
const backend = snapshot.record.backend;
return this.ajax(this._url(backend), 'POST', { data }).then((resp) => {
return {
...resp,
id: backend,
};
});
}
createRecord() {
return this.createOrUpdate(...arguments);
}
updateRecord() {
return this.createOrUpdate(...arguments);
}
}

View File

@@ -30,15 +30,12 @@
@title="{{@typeDisplay}} not configured"
@message="Get started by configuring your {{@typeDisplay}} secrets engine."
>
{{! TODO: short-term conditional to be removed once configuration for gcp is merged. }}
{{#unless (eq @typeDisplay "Google Cloud")}}
<Hds::Link::Standalone
@icon="chevron-right"
@iconPosition="trailing"
@text="Configure {{@typeDisplay}}"
@route="vault.cluster.secrets.backend.configuration.edit"
@model={{@id}}
/>
{{/unless}}
<Hds::Link::Standalone
@icon="chevron-right"
@iconPosition="trailing"
@text="Configure {{@typeDisplay}}"
@route="vault.cluster.secrets.backend.configuration.edit"
@model={{@id}}
/>
</EmptyState>
{{/each}}

View File

@@ -135,7 +135,7 @@ const MOUNTABLE_SECRET_ENGINES = [
];
// A list of Workload Identity Federation engines.
export const WIF_ENGINES = ['aws', 'azure'];
export const WIF_ENGINES = ['aws', 'azure', 'gcp'];
export function wifEngines() {
return WIF_ENGINES.slice();

View File

@@ -4,28 +4,12 @@
*/
import Model, { attr } from '@ember-data/model';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
export default class GcpConfig extends Model {
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
/* GCP config fields */
@attr({
label: 'Config TTL',
editType: 'ttl',
helperTextDisabled: 'The TTL (time-to-live) of generated tokens.',
})
ttl;
@attr({
label: 'Max TTL',
editType: 'ttl',
helperTextDisabled:
'Specifies the maximum config TTL (time-to-live) for long-lived credentials (i.e. service account keys).',
})
maxTtl;
/* GCP credential config field */
// GCP only field
@attr('string', {
label: 'JSON credentials',
subText:
@@ -35,7 +19,7 @@ export default class GcpConfig extends Model {
})
credentials; // obfuscated, never returned by API.
/* WIF config fields */
// WIF only fields
@attr('string', {
subText:
'The audience claim value for plugin identity tokens. Must match an allowed audience configured for the target IAM OIDC identity provider.',
@@ -45,7 +29,7 @@ export default class GcpConfig extends Model {
@attr({
label: 'Identity token TTL',
helperTextDisabled:
'The TTL of generated tokens. Defaults to 1 hour, toggle on to specify a different value.',
'The TTL of generated tokens. Defaults to 1 hour, turn on the toggle to specify a different value.',
helperTextEnabled: 'The TTL of generated tokens.',
editType: 'ttl',
})
@@ -56,6 +40,26 @@ export default class GcpConfig extends Model {
})
serviceAccountEmail;
// Fields that show regardless of access type
@attr({
label: 'Config TTL',
editType: 'ttl',
helperTextDisabled: 'Vault will use the default config TTL (time-to-live) for long-lived credentials.',
helperTextEnabled:
'The default config TTL (time-to-live) for long-lived credentials (i.e. service account keys).',
})
ttl;
@attr({
label: 'Max TTL',
editType: 'ttl',
helperTextDisabled:
'Vault will use the default maximum config TTL (time-to-live) for long-lived credentials.',
helperTextEnabled:
'The maximum config TTL (time-to-live) for long-lived credentials (i.e. service account keys).',
})
maxTtl;
configurableParams = [
'credentials',
'serviceAccountEmail',
@@ -65,8 +69,43 @@ export default class GcpConfig extends Model {
'identityTokenTtl',
];
get isWifPluginConfigured() {
return !!this.identityTokenAudience || !!this.identityTokenTtl || !!this.serviceAccountEmail;
}
isAccountPluginConfigured = false;
// the "credentials" param is not checked for "isAccountPluginConfigured" because it's never return by the API
// additionally credentials can be set via GOOGLE_APPLICATION_CREDENTIALS env var so we cannot call it a required field in the ui.
// thus we can never say for sure if the account accessType has been configured so we always return false
get displayAttrs() {
const formFields = expandAttributeMeta(this, this.configurableParams);
return formFields.filter((attr) => attr.name !== 'credentials');
}
get fieldGroupsWif() {
return fieldToAttrs(this, this.formFieldGroups('wif'));
}
get fieldGroupsAccount() {
return fieldToAttrs(this, this.formFieldGroups('account'));
}
formFieldGroups(accessType = 'account') {
const formFieldGroups = [];
if (accessType === 'wif') {
formFieldGroups.push({
default: ['identityTokenAudience', 'serviceAccountEmail', 'identityTokenTtl'],
});
}
if (accessType === 'account') {
formFieldGroups.push({
default: ['credentials'],
});
}
formFieldGroups.push({
'More options': ['ttl', 'maxTtl'],
});
return formFieldGroups;
}
}

View File

@@ -22,6 +22,7 @@ import type VersionService from 'vault/services/version';
const MOUNT_CONFIG_MODEL_NAMES: Record<string, string[]> = {
aws: ['aws/root-config', 'aws/lease-config'],
azure: ['azure/config'],
gcp: ['gcp/config'],
ssh: ['ssh/ca-config'],
};

View File

@@ -6,20 +6,17 @@
<SecretListHeader @model={{this.model.secretEngineModel}} @isConfigure={{true}} />
{{#if this.isConfigurable}}
{{! TODO: short-term conditional to be removed once configuration for gcp is merged. }}
{{#unless (eq this.typeDisplay "Google Cloud")}}
<Toolbar>
<ToolbarActions>
<ToolbarLink
@route="vault.cluster.secrets.backend.configuration.edit"
@model={{this.model.secretEngineModel.id}}
data-test-secret-backend-configure
>
Configure
</ToolbarLink>
</ToolbarActions>
</Toolbar>
{{/unless}}
<Toolbar>
<ToolbarActions>
<ToolbarLink
@route="vault.cluster.secrets.backend.configuration.edit"
@model={{this.model.secretEngineModel.id}}
data-test-secret-backend-configure
>
Configure
</ToolbarLink>
</ToolbarActions>
</Toolbar>
<SecretEngine::ConfigurationDetails
@configModels={{this.model.configModels}}

View File

@@ -40,7 +40,7 @@ module('Acceptance | aws | configuration', function (hooks) {
return authPage.login();
});
module('isEnterprise', function (hooks) {
module('Enterprise', function (hooks) {
hooks.beforeEach(function () {
this.version.type = 'enterprise';
});
@@ -338,7 +338,7 @@ module('Acceptance | aws | configuration', function (hooks) {
});
});
module('isCommunity', function (hooks) {
module('Community', function (hooks) {
hooks.beforeEach(function () {
this.version.type = 'community';
});

View File

@@ -72,7 +72,7 @@ module('Acceptance | Azure | configuration', function (hooks) {
await runCmd(`delete sys/mounts/${path}`);
});
module('isCommunity', function (hooks) {
module('Community', function (hooks) {
hooks.beforeEach(function () {
this.version.type = 'community';
});
@@ -252,7 +252,7 @@ module('Acceptance | Azure | configuration', function (hooks) {
});
});
module('isEnterprise', function (hooks) {
module('Enterprise', function (hooks) {
hooks.beforeEach(function () {
this.version.type = 'enterprise';
});

View File

@@ -3,10 +3,11 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { click, visit, currentURL } from '@ember/test-helpers';
import { click, visit, currentURL, fillIn } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { v4 as uuidv4 } from 'uuid';
import { spy } from 'sinon';
import authPage from 'vault/tests/pages/auth';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
@@ -20,6 +21,7 @@ import {
expectedConfigKeys,
expectedValueOfConfigKeys,
configUrl,
fillInGcpConfig,
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
module('Acceptance | GCP | configuration', function (hooks) {
@@ -27,55 +29,226 @@ module('Acceptance | GCP | configuration', function (hooks) {
setupMirage(hooks);
hooks.beforeEach(function () {
const flash = this.owner.lookup('service:flash-messages');
this.store = this.owner.lookup('service:store');
this.flashSuccessSpy = spy(flash, 'success');
this.flashDangerSpy = spy(flash, 'danger');
this.flashInfoSpy = spy(flash, 'info');
this.version = this.owner.lookup('service:version');
this.uid = uuidv4();
this.type = 'gcp';
this.path = `GCP-${this.uid}`;
return authPage.login();
});
module('isEnterprise', function (hooks) {
test('it should prompt configuration after mounting the GCP engine', async function (assert) {
await visit('/vault/settings/mount-secret-backend');
await mountBackend(this.type, this.path);
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.path}/configuration`,
'navigated to configuration view'
);
assert.dom(GENERAL.emptyStateTitle).hasText('Google Cloud not configured');
assert.dom(GENERAL.emptyStateActions).hasText('Configure Google Cloud');
// cleanup
await runCmd(`delete sys/mounts/${this.path}`);
});
test('it should transition to configure page on click "Configure" from toolbar', async function (assert) {
await enablePage.enable(this.type, this.path);
await click(SES.configure);
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.path}/configuration/edit`,
'navigated to configuration edit view'
);
// cleanup
await runCmd(`delete sys/mounts/${this.path}`);
});
module('Community', function (hooks) {
hooks.beforeEach(function () {
this.version.type = 'community';
});
module('details', function () {
test('it should show configuration details with GCP account options configured', async function (assert) {
const gcpAccountAttrs = {
credentials: '{"some-key":"some-value"}',
ttl: '1 minute 40 seconds',
max_ttl: '1 minute 41 seconds',
};
this.server.get(`${this.path}/config`, () => {
assert.true(true, 'request made to config when navigating to the configuration page.');
return { data: { id: this.path, type: this.type, ...gcpAccountAttrs } };
});
await enablePage.enable(this.type, this.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))
.hasText(responseKeyAndValue, `value for ${key} on the ${this.type} config details exists.`);
}
// check mount configuration details are present and accurate.
await click(SES.configurationToggle);
assert
.dom(GENERAL.infoRowValue('Path'))
.hasText(`${this.path}/`, 'mount path is displayed in the configuration details');
// cleanup
await runCmd(`delete sys/mounts/${this.path}`);
});
});
module('create', function () {
test('it should save gcp account accessType options', async function (assert) {
await enablePage.enable(this.type, this.path);
await click(SES.configTab);
await click(SES.configure);
await fillInGcpConfig();
await click(GENERAL.saveButton);
assert.true(
this.flashSuccessSpy.calledWith(`Successfully saved ${this.path}'s configuration.`),
'Success flash message is rendered showing the GCP model configuration was saved.'
);
assert
.dom(GENERAL.infoRowValue('Config TTL'))
.hasText('2 hours', 'Config TTL, a generic account specific field, has been set.');
assert
.dom(GENERAL.infoRowValue('Max TTL'))
.hasText('2 hours 16 minutes 40 seconds', 'Max TTL, a generic field, has been set.');
assert
.dom(GENERAL.infoRowValue('Credentials'))
.doesNotExist('credentials are not shown in the configuration details');
// cleanup
await runCmd(`delete sys/mounts/${this.path}`);
});
});
module('edit', function (hooks) {
hooks.beforeEach(async function () {
const genericAttrs = {
configTtl: '2h',
maxTtl: '4h',
};
this.server.get(`${this.path}/config`, () => {
return { data: { id: this.path, type: this.type, ...genericAttrs } };
});
await enablePage.enable(this.type, this.path);
});
test('it should save credentials', async function (assert) {
assert.expect(3);
const credentials = '{"some-key":"some-value"}';
await click(SES.configure);
this.server.post(configUrl('gcp', this.path), (schema, req) => {
const payload = JSON.parse(req.requestBody);
assert.strictEqual(credentials, payload.credentials, 'credentials are sent in post request');
assert.strictEqual(
undefined,
payload.configTtl,
'config_ttl is not included in payload if value has not been updated'
);
assert.strictEqual(
undefined,
payload.maxTtl,
'max_ttl is not included in payload if value has not been updated'
);
});
await click(GENERAL.textToggle);
await fillIn(GENERAL.textToggleTextarea, credentials);
await click(GENERAL.saveButton);
// cleanup
await runCmd(`delete sys/mounts/${this.path}`);
});
test('it should not save credentials if it has NOT been changed', async function (assert) {
assert.expect(3);
await click(SES.configure);
this.server.post(configUrl('gcp', this.path), (schema, req) => {
const payload = JSON.parse(req.requestBody);
assert.strictEqual(payload.credentials, undefined, 'credentials are not sent in post request');
assert.strictEqual(
payload.ttl,
'10800s',
'config_ttl is included in payload because the value was updated'
);
assert.strictEqual(
payload.maxTtl,
undefined,
'max_ttl is not included in payload if value has not been updated'
);
});
await click(GENERAL.toggleGroup('More options'));
await click(GENERAL.ttl.toggle('Config TTL'));
await fillIn(GENERAL.ttl.input('Config TTL'), '10800');
await click(GENERAL.saveButton);
// cleanup
await runCmd(`delete sys/mounts/${this.path}`);
});
});
module('Error handling', function () {
test('it prevents transition and shows api error if config errored on save', async function (assert) {
await enablePage.enable(this.type, this.path);
this.server.post(configUrl(this.type, this.path), () => {
return overrideResponse(400, { errors: ['my goodness, that did not work!'] });
});
await click(SES.configTab);
await click(SES.configure);
await fillInGcpConfig();
await click(GENERAL.saveButton);
assert
.dom(GENERAL.messageError)
.hasText('Error my goodness, that did not work!', 'API error shows on form');
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.path}/configuration/edit`,
'the form did not transition because the save failed.'
);
// cleanup
await runCmd(`delete sys/mounts/${this.path}`);
});
test('it should show API error when configuration read fails', async function (assert) {
this.server.get(configUrl(this.type, this.path), () => {
return overrideResponse(400, { errors: ['bad request'] });
});
await enablePage.enable(this.type, this.path);
assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route');
});
});
});
module('Enterprise', function (hooks) {
hooks.beforeEach(function () {
this.version.type = 'enterprise';
});
test('it should show empty state and navigate to configuration view after mounting the GCP engine', async function (assert) {
const path = `GCP-${this.uid}`;
await visit('/vault/settings/mount-secret-backend');
await mountBackend(this.type, path);
assert.strictEqual(
currentURL(),
`/vault/secrets/${path}/configuration`,
'navigated to configuration view'
);
assert.dom(GENERAL.emptyStateTitle).hasText('Google Cloud not configured');
assert.dom(GENERAL.emptyStateActions).doesNotContainText('Configure GCP');
// cleanup
await runCmd(`delete sys/mounts/${path}`);
});
test('it should not show "Configure" from toolbar', async function (assert) {
const path = `GCP-${this.uid}`;
await enablePage.enable(this.type, path);
assert.dom(SES.configure).doesNotExist('Configure button does not exist.');
// cleanup
await runCmd(`delete sys/mounts/${path}`);
});
test('it should show configuration details with WIF options configured', async function (assert) {
const path = `GCP-${this.uid}`;
const wifAttrs = {
this.wifAttrs = {
service_account_email: 'service-email',
identity_token_audience: 'audience',
identity_token_ttl: 720000,
max_ttl: 14400,
ttl: 3600,
};
this.server.get(`${path}/config`, () => {
});
test('it should show configuration details with WIF options configured', async function (assert) {
this.server.get(`${this.path}/config`, () => {
assert.true(true, 'request made to config when navigating to the configuration page.');
return { data: { id: path, type: this.type, ...wifAttrs } };
return { data: { id: this.path, type: this.type, ...this.wifAttrs } };
});
await enablePage.enable(this.type, path);
await enablePage.enable(this.type, this.path);
for (const key of expectedConfigKeys('gcp-wif')) {
const responseKeyAndValue = expectedValueOfConfigKeys(this.type, key);
assert
@@ -86,48 +259,32 @@ module('Acceptance | GCP | configuration', function (hooks) {
await click(SES.configurationToggle);
assert
.dom(GENERAL.infoRowValue('Path'))
.hasText(`${path}/`, 'mount path is displayed in the configuration details');
.hasText(`${this.path}/`, 'mount path is displayed in the configuration details');
// cleanup
await runCmd(`delete sys/mounts/${path}`);
await runCmd(`delete sys/mounts/${this.path}`);
});
test('it should show configuration details with GCP account options configured', async function (assert) {
const path = `GCP-${this.uid}`;
const GCPAccountAttrs = {
credentials: '{"some-key":"some-value"}',
ttl: '1 hour',
max_ttl: '4 hours',
};
this.server.get(`${path}/config`, () => {
assert.true(true, 'request made to config when navigating to the configuration page.');
return { data: { id: path, type: this.type, ...GCPAccountAttrs } };
});
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);
module('Error handling', function () {
test('it shows API error if user previously set credentials but tries to edit the configuration with wif fields', async function (assert) {
await enablePage.enable(this.type, this.path);
await click(SES.configTab);
await click(SES.configure);
await fillInGcpConfig();
await click(GENERAL.saveButton); // save GCP credentials
await click(SES.configure); // navigate so you can edit that configuration
await fillInGcpConfig(true);
await click(GENERAL.saveButton); // try and save wif fields
assert
.dom(GENERAL.infoRowValue(key))
.hasText(responseKeyAndValue, `value for ${key} on the ${this.type} config details exists.`);
}
// check mount configuration details are present and accurate.
await click(SES.configurationToggle);
assert
.dom(GENERAL.infoRowValue('Path'))
.hasText(`${path}/`, 'mount path is displayed in the configuration details');
// cleanup
await runCmd(`delete sys/mounts/${path}`);
});
test('it should show API error when configuration read fails', async function (assert) {
assert.expect(1);
const path = `GCP-${this.uid}`;
// interrupt get and return API error
this.server.get(configUrl(this.type, path), () => {
return overrideResponse(400, { errors: ['bad request'] });
.dom(GENERAL.messageError)
.hasText(
`Error only one of 'credentials' or 'identity_token_audience' can be set`,
'api error about conflicting fields is shown'
);
assert.dom(GENERAL.inlineError).hasText('There was an error submitting this form.');
// cleanup
await runCmd(`delete sys/mounts/${this.path}`);
});
await enablePage.enable(this.type, path);
assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route');
});
});
});

View File

@@ -43,6 +43,8 @@ export const GENERAL = {
infoRowValue: (label: string) => `[data-test-value-div="${label}"]`,
inputByAttr: (attr: string) => `[data-test-input="${attr}"]`,
selectByAttr: (attr: string) => `[data-test-select="${attr}"]`,
textToggle: '[data-test-text-toggle]',
textToggleTextarea: '[data-test-text-file-textarea]',
toggleInput: (attr: string) => `[data-test-toggle-input="${attr}"]`,
toggleGroup: (attr: string) => `[data-test-toggle-group="${attr}"]`,
ttl: {

View File

@@ -185,8 +185,8 @@ const createGcpConfig = (store, backend, accessType = 'gcp') => {
data: {
backend,
credentials: '{"some-key":"some-value"}',
ttl: '1 hour',
max_ttl: '4 hours',
ttl: '100s',
max_ttl: '101s',
},
});
}
@@ -215,7 +215,10 @@ export const createConfig = (store, backend, type) => {
case 'azure-generic':
return createAzureConfig(store, backend, 'generic');
case 'gcp':
case 'gcp-generic':
return createGcpConfig(store, backend);
case 'gcp-wif':
return createGcpConfig(store, backend, 'wif');
}
};
/* Manually create the configuration by filling in the configuration form */
@@ -266,6 +269,24 @@ export const fillInAzureConfig = async (situation = 'azure') => {
}
};
export const fillInGcpConfig = async (withWif = false) => {
if (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');
await fillIn(GENERAL.inputByAttr('serviceAccountEmail'), 'some@email.com');
} else {
await click(GENERAL.toggleGroup('More options'));
await click(GENERAL.ttl.toggle('Config TTL'));
await fillIn(GENERAL.ttl.input('Config TTL'), '7200');
await click(GENERAL.ttl.toggle('Max TTL'));
await fillIn(GENERAL.ttl.input('Max TTL'), '8200');
await click(GENERAL.textToggle);
await fillIn(GENERAL.textToggleTextarea, '{"some-key":"some-value"}');
}
};
/* 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
@@ -283,7 +304,7 @@ const azureWifKeys = [...genericAzureKeys, ...genericWifKeys];
// GCP specific keys
const genericGcpKeys = ['Config TTL', 'Max TTL'];
const gcpKeys = [...genericGcpKeys, 'Credentials'];
const gcpWifKeys = [...genericGcpKeys, ...genericWifKeys, 'Service account email'];
const gcpWifKeys = [...genericWifKeys, 'Service account email'];
// SSH specific keys
const sshKeys = ['Private key', 'Public key', 'Generate signing key'];
@@ -349,9 +370,9 @@ const valueOfGcpKeys = (string) => {
case 'Service account email':
return 'service-email';
case 'Config TTL':
return '1 hour';
return '1 minute 40 seconds';
case 'Max TTL':
return '4 hours';
return '1 minute 41 seconds';
case 'Identity token audience':
return 'audience';
case 'Identity token TTL':

View File

@@ -18,7 +18,7 @@ import {
const allEnginesArray = allEngines(); // saving as const so we don't invoke the method multiple times via the for loop
module('Integration | Component | SecretEngine/ConfigurationDetails', function (hooks) {
module('Integration | Component | SecretEngine::ConfigurationDetails', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {

View File

@@ -44,7 +44,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
});
module('Create view', function () {
module('isEnterprise', function (hooks) {
module('Enterprise', function (hooks) {
hooks.beforeEach(function () {
this.version.type = 'enterprise';
});
@@ -74,11 +74,20 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
toggleGroup ? await click(toggleGroup) : null;
for (const key of expectedConfigKeys(type, true)) {
assert
.dom(GENERAL.inputByAttr(key))
.exists(
`${key} shows for ${type} configuration create section when wif is not the access type`
);
if (key === 'configTtl' || key === 'maxTtl') {
// because toggle.hbs passes in the name rather than the camelized attr, we have a difference of data-test=attrName vs data-test="Item name" being passed into the data-test selectors. Long-term solution we should match toggle.hbs selectors to formField.hbs selectors syntax
assert
.dom(GENERAL.ttl.toggle(key === 'configTtl' ? 'Config TTL' : 'Max TTL'))
.exists(
`${key} shows for ${type} configuration create section when wif is not the access type.`
);
} else {
assert
.dom(GENERAL.inputByAttr(key))
.exists(
`${key} shows for ${type} configuration create section when wif is not the access type`
);
}
}
assert
.dom(GENERAL.inputByAttr('issuer'))
@@ -604,7 +613,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
});
});
module('isCommunity', function (hooks) {
module('Community', function (hooks) {
hooks.beforeEach(function () {
this.version.type = 'community';
});
@@ -637,7 +646,13 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
toggleGroup ? await click(toggleGroup) : null;
// check all the form fields are present
for (const key of expectedConfigKeys(type, true)) {
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for ${type} account access section.`);
if (key === 'configTtl' || key === 'maxTtl') {
assert
.dom(GENERAL.ttl.toggle(key === 'configTtl' ? 'Config TTL' : 'Max TTL'))
.exists(`${key} shows for ${type} account access section.`);
} else {
assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for ${type} account access section.`);
}
}
assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist();
});
@@ -646,7 +661,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
});
module('Edit view', function () {
module('isEnterprise', function (hooks) {
module('Enterprise', function (hooks) {
hooks.beforeEach(function () {
this.version.type = 'enterprise';
});
@@ -830,9 +845,66 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
await click(GENERAL.saveButton);
});
});
module('GCP specific', function (hooks) {
// GCP is unique in that "credentials" is the only mutually exclusive GCP account attr and it's never returned from the API. Thus, we can only check for the presence of configured wif fields to determine if the accessType should be preselected to wif and disabled.
// If the user has configured the credentials field, the ui will not know until the user tries to save WIF fields. This is a limitation of the API and surfaced to the user in a descriptive API error.
// We cover some of this workflow here and error testing in the gcp-configuration acceptance test.
hooks.beforeEach(function () {
this.id = `gcp-${this.uid}`;
this.mountConfigModel = createConfig(this.store, this.id, 'gcp');
this.type = 'gcp';
this.displayName = 'Google Cloud';
});
test('it allows you to change access type if no wif fields are set', async function (assert) {
await render(hbs`
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
`);
assert.dom(SES.wif.accessType('gcp')).isChecked('GCP accessType is checked');
assert
.dom(SES.wif.accessType('gcp'))
.isNotDisabled(
'GCP accessType is not disabled because we cannot determine if credentials was set as it is not returned by the api.'
);
assert.dom(SES.wif.accessType('wif')).isNotChecked('WIF accessType is not checked');
assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is not disabled');
assert
.dom(SES.wif.accessTypeSubtext)
.hasText(
'Choose the way to configure access to Google Cloud. Access can be configured either using Google Cloud account credentials or with the Plugin Workload Identity Federation (WIF).'
);
});
test('it sets access type to wif if wif fields are set', async function (assert) {
this.mountConfigModel = createConfig(this.store, this.id, 'gcp-wif');
await render(hbs`
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
`);
assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked');
assert
.dom(SES.wif.accessType('gcp'))
.isDisabled('GCP accessType IS disabled because wif attributes are set.');
assert
.dom(SES.wif.accessTypeSubtext)
.hasText('You cannot edit Access Type if you have already saved access credentials.');
});
test('it shows previously saved config information', async function (assert) {
this.mountConfigModel = createConfig(this.store, this.id, 'gcp-generic');
await render(hbs`
<SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/>
`);
await click(GENERAL.toggleGroup('More options'));
assert.dom(GENERAL.ttl.input('Config TTL')).hasValue('100');
assert.dom(GENERAL.ttl.input('Max TTL')).hasValue('101');
});
});
});
module('isCommunity', function (hooks) {
module('Community', function (hooks) {
hooks.beforeEach(function () {
this.version.type = 'community';
});
@@ -851,13 +923,27 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
toggleGroup ? await click(toggleGroup) : null;
for (const key of expectedConfigKeys(type, true)) {
if (key === 'secretKey' || key === 'clientSecret') return; // these keys are not returned by the API
assert
.dom(GENERAL.inputByAttr(key))
.hasValue(
this.mountConfigModel[key],
`${key} for ${type}: has the expected value set on the config`
);
if (key === 'secretKey' || key === 'clientSecret' || key === 'credentials') return; // these keys are not returned by the API
if (type === 'gcp') {
// same issues noted in wif enterprise tests with how toggle.hbs passes in name vs how formField input passes in attr to data test selector
if (key === 'configTtl') {
assert
.dom(GENERAL.ttl.input('Config TTL'))
.hasValue('100', `${key} for ${type}: has the expected value set on the config`);
}
if (key === 'maxTtl') {
assert
.dom(GENERAL.ttl.input('Max TTL'))
.hasValue('101', `${key} for ${type}: has the expected value set on the config`);
}
} else {
assert
.dom(GENERAL.inputByAttr(key))
.hasValue(
this.mountConfigModel[key],
`${key} for ${type}: has the expected value set on the config`
);
}
}
});
}