From 4228154655afae66a860224bfc3c307606f9e144 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Mon, 13 Jan 2025 16:29:58 -0700 Subject: [PATCH 01/16] make one component and make one test file for that component. remove the two components and associated files the new component replaces --- .../secret-engine/configure-aws.hbs | 136 --- .../components/secret-engine/configure-aws.ts | 232 ----- ...{configure-azure.hbs => configure-wif.hbs} | 58 +- .../{configure-azure.ts => configure-wif.ts} | 120 ++- ui/app/models/aws/lease-config.js | 2 + ui/app/models/aws/root-config.js | 8 +- ui/app/models/azure/config.js | 10 +- .../secrets/backend/configuration/edit.ts | 28 +- .../secrets/backend/configuration/edit.hbs | 23 +- .../backend/aws/aws-configuration-test.js | 40 +- .../secret-engine/secret-engine-selectors.ts | 3 +- .../secret-engine/configure-aws-test.js | 578 ------------ .../secret-engine/configure-azure-test.js | 426 --------- .../secret-engine/configure-wif-test.js | 861 ++++++++++++++++++ ui/types/vault/models/aws/lease-config.d.ts | 22 - ui/types/vault/models/aws/root-config.d.ts | 32 - .../{azure => secret-engine}/config.d.ts | 32 +- 17 files changed, 1091 insertions(+), 1520 deletions(-) delete mode 100644 ui/app/components/secret-engine/configure-aws.hbs delete mode 100644 ui/app/components/secret-engine/configure-aws.ts rename ui/app/components/secret-engine/{configure-azure.hbs => configure-wif.hbs} (59%) rename ui/app/components/secret-engine/{configure-azure.ts => configure-wif.ts} (51%) delete mode 100644 ui/tests/integration/components/secret-engine/configure-aws-test.js delete mode 100644 ui/tests/integration/components/secret-engine/configure-azure-test.js create mode 100644 ui/tests/integration/components/secret-engine/configure-wif-test.js delete mode 100644 ui/types/vault/models/aws/lease-config.d.ts delete mode 100644 ui/types/vault/models/aws/root-config.d.ts rename ui/types/vault/models/{azure => secret-engine}/config.d.ts (57%) diff --git a/ui/app/components/secret-engine/configure-aws.hbs b/ui/app/components/secret-engine/configure-aws.hbs deleted file mode 100644 index bc0109f193ef..000000000000 --- a/ui/app/components/secret-engine/configure-aws.hbs +++ /dev/null @@ -1,136 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -~}} - -
-
- - -

- Note: the client uses the official AWS SDK and will use the specified credentials, environment credentials, shared file - credentials, or IAM role/ECS task credentials in that order. -

-
- {{! Root configuration details }} -

- Access to AWS -

-
- {{! WIF is an enterprise only feature. We default to IAM access type for community users and display only those related form fields. }} - {{#if this.version.isEnterprise}} -
- Access Type -

- {{#if this.disableAccessType}} - You cannot edit Access Type if you have already saved access credentials. - {{else}} - Choose the way to configure access to AWS. Access can be configured either with IAM access keys, or using Plugin - Workload Identity Federation (WIF).{{/if}}

-
- - - - - -
-
- {{/if}} - {{#if (eq this.accessType "wif")}} - {{! WIF Fields }} - {{#each @issuerConfig.displayAttrs as |attr|}} - - {{/each}} - - {{else}} - {{! IAM Fields }} - - {{/if}} -
- - {{! Lease configuration details }} -

- Leases -

-
- {{#each @leaseConfig.displayAttrs as |attr|}} - - {{/each}} -
- -
-
- - -
- {{#if this.invalidFormAlert}} - - {{/if}} -
-
- -{{#if this.saveIssuerWarning}} - - - Are you sure? - - -

- {{this.saveIssuerWarning}} -

-
- - - - - - -
-{{/if}} \ No newline at end of file diff --git a/ui/app/components/secret-engine/configure-aws.ts b/ui/app/components/secret-engine/configure-aws.ts deleted file mode 100644 index 98d3309a94a9..000000000000 --- a/ui/app/components/secret-engine/configure-aws.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Component from '@glimmer/component'; -import { action } from '@ember/object'; -import { task } from 'ember-concurrency'; -import { waitFor } from '@ember/test-waiters'; -import { service } from '@ember/service'; -import { tracked } from '@glimmer/tracking'; -import { ValidationMap } from 'vault/vault/app-types'; -import errorMessage from 'vault/utils/error-message'; - -import type LeaseConfigModel from 'vault/models/aws/lease-config'; -import type RootConfigModel from 'vault/models/aws/root-config'; -import type IdentityOidcConfigModel from 'vault/models/identity/oidc/config'; -import type Router from '@ember/routing/router'; -import type StoreService from 'vault/services/store'; -import type VersionService from 'vault/services/version'; -import type FlashMessageService from 'vault/services/flash-messages'; - -/** - * @module ConfigureAwsComponent is used to configure the AWS secret engine - * A user can configure the endpoint root/config and/or lease/config. - * For enterprise users, they will see an additional option to config WIF attributes in place of IAM attributes. - * The fields for these endpoints are on one form. - * - * @example - * ```js - * - * ``` - * - * @param {object} rootConfig - AWS config/root model - * @param {object} leaseConfig - AWS config/lease model - * @param {string} backendPath - name of the AWS secret engine, ex: 'aws-123' - */ - -interface Args { - leaseConfig: LeaseConfigModel; - rootConfig: RootConfigModel; - issuerConfig: IdentityOidcConfigModel; - backendPath: string; - issuer?: string; -} - -export default class ConfigureAwsComponent extends Component { - @service declare readonly router: Router; - @service declare readonly store: StoreService; - @service declare readonly version: VersionService; - @service declare readonly flashMessages: FlashMessageService; - - @tracked errorMessageRoot: string | null = null; - @tracked errorMessageLease: string | null = null; - @tracked invalidFormAlert: string | null = null; - @tracked modelValidationsLease: ValidationMap | null = null; - @tracked accessType = 'iam'; - @tracked saveIssuerWarning = ''; - - disableAccessType = false; - - constructor(owner: unknown, args: Args) { - super(owner, args); - // the following checks are only relevant to enterprise users and those editing an existing root configuration. - if (this.version.isCommunity || this.args.rootConfig.isNew) return; - const { roleArn, identityTokenAudience, identityTokenTtl, accessKey } = this.args.rootConfig; - // do not include issuer in this check. Issuer is a global endpoint and can be set even if we're not editing wif attributes - const wifAttributesSet = !!roleArn || !!identityTokenAudience || !!identityTokenTtl; - const iamAttributesSet = !!accessKey; - // If any WIF attributes have been set in the rootConfig model, set accessType to 'wif'. - this.accessType = wifAttributesSet ? 'wif' : 'iam'; - // If there are either WIF or IAM attributes set then disable user's ability to change accessType. - this.disableAccessType = wifAttributesSet || iamAttributesSet; - } - - @action continueSubmitForm() { - // called when the user confirms they are okay with the issuer change - this.saveIssuerWarning = ''; - this.save.perform(); - } - - // on form submit - validate inputs and check for issuer changes - submitForm = task( - waitFor(async (event: Event) => { - event?.preventDefault(); - this.resetErrors(); - const { leaseConfig, issuerConfig } = this.args; - // Note: only aws/lease-config model has validations - const isValid = this.validate(leaseConfig); - if (!isValid) return; - if (issuerConfig?.hasDirtyAttributes) { - // if the issuer has changed show modal with warning that the config will change - // if the modal is shown, the user has to click confirm to continue save - this.saveIssuerWarning = `You are updating the global issuer config. This will overwrite Vault's current issuer ${ - issuerConfig.queryIssuerError ? 'if it exists ' : '' - }and may affect other configurations using this value. Continue?`; - // exit task until user confirms - return; - } - await this.save.perform(); - }) - ); - - save = task( - waitFor(async () => { - // when we get here, the models have already been validated so just continue with save - const { leaseConfig, rootConfig, issuerConfig } = this.args; - // Check if any of the models' attributes have changed. - // If no changes to either model, transition and notify user. - // If changes to either model, save the model(s) that changed and notify user. - // Note: "backend" dirties model state so explicity ignore it here. - const leaseAttrChanged = Object.keys(leaseConfig?.changedAttributes()).some( - (item) => item !== 'backend' - ); - const rootAttrChanged = Object.keys(rootConfig?.changedAttributes()).some((item) => item !== 'backend'); - const issuerAttrChanged = issuerConfig?.hasDirtyAttributes; - if (!leaseAttrChanged && !rootAttrChanged && !issuerAttrChanged) { - this.flashMessages.info('No changes detected.'); - this.transition(); - return; - } - // Attempt saves of changed models. If at least one of them succeed, transition - const rootSaved = rootAttrChanged ? await this.saveRoot() : false; - const leaseSaved = leaseAttrChanged ? await this.saveLease() : false; - const issuerSaved = issuerAttrChanged ? await this.updateIssuer() : false; - - if (rootSaved || leaseSaved || issuerSaved) { - this.transition(); - } else { - // otherwise there was a failure and we should not transition and exit the function. - return; - } - }) - ); - - async updateIssuer(): Promise { - try { - await this.args.issuerConfig.save(); - this.flashMessages.success('Issuer saved successfully'); - return true; - } catch (e) { - this.flashMessages.danger(`Issuer was not saved: ${errorMessage(e, 'Check Vault logs for details.')}`); - return false; - } - } - - async saveRoot(): Promise { - const { backendPath, rootConfig } = this.args; - try { - await rootConfig.save(); - this.flashMessages.success(`Successfully saved ${backendPath}'s root configuration.`); - return true; - } catch (error) { - this.errorMessageRoot = errorMessage(error); - this.invalidFormAlert = 'There was an error submitting this form.'; - return false; - } - } - - async saveLease(): Promise { - const { backendPath, leaseConfig } = this.args; - try { - await leaseConfig.save(); - this.flashMessages.success(`Successfully saved ${backendPath}'s lease configuration.`); - return true; - } catch (error) { - // if lease config fails, but there was no error saving rootConfig: notify user of the lease failure with a flash message, save the root config, and transition. - if (!this.errorMessageRoot) { - this.flashMessages.danger(`Lease configuration was not saved: ${errorMessage(error)}`, { - sticky: true, - }); - return true; - } else { - this.errorMessageLease = errorMessage(error); - this.flashMessages.danger( - `Configuration not saved: ${errorMessage(error)}. ${this.errorMessageRoot}` - ); - return false; - } - } - } - - resetErrors() { - this.flashMessages.clearMessages(); - this.errorMessageRoot = null; - this.invalidFormAlert = null; - } - - transition() { - this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath); - } - - validate(model: LeaseConfigModel) { - const { isValid, state, invalidFormMessage } = model.validate(); - this.modelValidationsLease = isValid ? null : state; - this.invalidFormAlert = isValid ? '' : invalidFormMessage; - return isValid; - } - - unloadModels() { - this.args.rootConfig.unloadRecord(); - this.args.leaseConfig.unloadRecord(); - } - - @action - onChangeAccessType(accessType: string) { - this.accessType = accessType; - const { rootConfig } = this.args; - if (accessType === 'iam') { - // reset all WIF attributes - rootConfig.roleArn = rootConfig.identityTokenAudience = rootConfig.identityTokenTtl = undefined; - // for the issuer return to the globally set value (if there is one) on toggle - this.args.issuerConfig.rollbackAttributes(); - } - if (accessType === 'wif') { - // reset all IAM attributes - rootConfig.accessKey = rootConfig.secretKey = undefined; - } - } - - @action - onCancel() { - // clear errors because they're canceling out of the workflow. - this.resetErrors(); - this.unloadModels(); - this.transition(); - } -} diff --git a/ui/app/components/secret-engine/configure-azure.hbs b/ui/app/components/secret-engine/configure-wif.hbs similarity index 59% rename from ui/app/components/secret-engine/configure-azure.hbs rename to ui/app/components/secret-engine/configure-wif.hbs index 64b98758f62c..53bea2bc94b1 100644 --- a/ui/app/components/secret-engine/configure-azure.hbs +++ b/ui/app/components/secret-engine/configure-wif.hbs @@ -3,11 +3,28 @@ SPDX-License-Identifier: BUSL-1.1 ~}} -
+ + {{! AWS specific note and section header }} + {{#if (eq @type "aws")}} +
+

+ Note: the client uses the official AWS SDK and will use the specified credentials, environment credentials, shared + file credentials, or IAM role/ECS task credentials in that order. +

+
+

+ Access to AWS +

+ {{/if}} +
- {{! accessType can be "azure" or "wif" - since WIF is an enterprise only feature we default to "azure" for community users and only display those related form fields. }} + {{! Only enterprise users can select a specific access type: "account" or "wif" }} {{#if this.version.isEnterprise}}
Access Type @@ -15,22 +32,24 @@ {{#if this.disableAccessType}} You cannot edit Access Type if you have already saved access credentials. {{else}} - Choose the way to configure access to Azure. Access can be configured either using an Azure account or with the - Plugin Workload Identity Federation (WIF). + Choose the way to configure access to + {{@displayName}}. Access can be configured either using an + {{@displayName}} + account credentials or with the Plugin Workload Identity Federation (WIF). {{/if}}

- + {{/if}} {{#if (eq this.accessType "wif")}} - {{! WIF Fields }} + {{! if access type is "wif" display Issuer and WIF fields }} {{#each @issuerConfig.displayAttrs as |attr|}} {{/each}} {{else}} - {{! Azure Account Fields }} + {{! otherwise display account credential fields }} {{/if}} + {{! secondModel fields should show regardless of the vault version or what access type is selected }} + {{#if @secondModel}} +

+ {{or @secondModel.modelNameDisplay "Additional"}} + Configuration +

+
+ {{#each @secondModel.formFields as |attr|}} + + {{/each}} +
+ {{/if}}
+ - * - * @param {object} model - Azure config model - * @param {string} backendPath - name of the Azure secret engine, ex: 'azure-123' - * @param {object} issuerConfigModel - the identity/oidc/config model + * + * @param {string} backendPath - name of the secret engine, ex: 'azure-123'. + * @param {string} displayName - Azure vs azure or AWS vs aws. Used for display purposes. + * @param {string} type - The type of the engine, ex: 'azure'. + * @param {object} model - The config model for the engine. + * @param {object} [secondModel] - For engines with two config models. Currently, only used by aws + * @param {object} [issuerConfig] - The identity/oidc/config model. Will be passed in if user has an enterprise license. + * @param {string} [modelNameDisplay] - Specific h2 title to display above the second model's fields. Also used in flash message error if saving the second modal fails */ interface Args { + backendPath: string; + displayName: string; + type: string; model: ConfigModel; + secondModel: ConfigModel; issuerConfig: IdentityOidcConfigModel; - backendPath: string; } -export default class ConfigureAzureComponent extends Component { +export default class ConfigureWif extends Component { @service declare readonly router: Router; @service declare readonly store: StoreService; @service declare readonly version: VersionService; @service declare readonly flashMessages: FlashMessageService; - @tracked accessType = 'azure'; + @tracked accessType = 'account'; // for community users they will not be able to change this. for enterprise users, they will have the option to select "wif". @tracked errorMessage = ''; @tracked invalidFormAlert = ''; @tracked saveIssuerWarning = ''; + @tracked modelValidations: ValidationMap | null = null; disableAccessType = false; @@ -57,10 +72,14 @@ export default class ConfigureAzureComponent extends Component { super(owner, args); // the following checks are only relevant to existing enterprise configurations if (this.version.isCommunity && this.args.model.isNew) return; - const { isWifPluginConfigured, isAzureAccountConfigured } = this.args.model; - this.accessType = isWifPluginConfigured ? 'wif' : 'azure'; - // if there are either WIF or azure attributes, disable user's ability to change accessType - this.disableAccessType = isWifPluginConfigured || isAzureAccountConfigured; + const { isWifPluginConfigured, isAccountPluginConfigured } = this.args.model; + assert( + `'isWifPluginConfigured' is required to be defined on the config model. Must return a boolean.`, + isWifPluginConfigured !== undefined + ); + this.accessType = isWifPluginConfigured ? 'wif' : 'account'; + // if wif or account only attributes are defined, disable the user's ability to change the access type + this.disableAccessType = isWifPluginConfigured || isAccountPluginConfigured; } get modelAttrChanged() { @@ -72,6 +91,13 @@ export default class ConfigureAzureComponent extends Component { return this.args.issuerConfig?.hasDirtyAttributes; } + get secondModelAttrChanged() { + const { secondModel } = this.args; + // required to check for model first otherwise Object.keys will have nothing to iterate over and fails + if (!secondModel) return; + return Object.keys(secondModel.changedAttributes()).some((item) => item !== 'backend'); + } + @action continueSubmitForm() { this.saveIssuerWarning = ''; this.save.perform(); @@ -83,7 +109,10 @@ export default class ConfigureAzureComponent extends Component { waitFor(async (event: Event) => { event?.preventDefault(); this.resetErrors(); - + // AWS lease model has model validations we need to check before saving + if (this.args.secondModel && !this.validate(this.args.secondModel)) { + return; + } if (this.issuerAttrChanged) { // if the issuer has changed show modal with warning that the config will change // if the modal is shown, the user has to click confirm to continue saving @@ -101,9 +130,10 @@ export default class ConfigureAzureComponent extends Component { waitFor(async () => { const modelAttrChanged = this.modelAttrChanged; const issuerAttrChanged = this.issuerAttrChanged; - // check if any of the model or issue attributes have changed + const secondModelAttrChanged = this.secondModelAttrChanged; + // check if any of the mode(s) or issuer attributes have changed // if no changes, transition and notify user - if (!modelAttrChanged && !issuerAttrChanged) { + if (!modelAttrChanged && !issuerAttrChanged && !secondModelAttrChanged) { this.flashMessages.info('No changes detected.'); this.transition(); return; @@ -113,11 +143,13 @@ export default class ConfigureAzureComponent extends Component { const issuerSaved = issuerAttrChanged ? await this.updateIssuer() : false; if (modelSaved || (!modelAttrChanged && issuerSaved)) { - // transition if the model was saved successfully - // we only prevent a transition if the model is edited and fails saving + // if there is a secondModel, attempt to save it. if saving fails, we transition and the failure will surface as a sticky flash message on the configuration details page. + if (secondModelAttrChanged) { + await this.saveSecondModel(); + } + // we only prevent a transition if the first model or issuer are edited and fail when saving this.transition(); } else { - // otherwise there was a failure and we should not transition and exit the function return; } }) @@ -149,6 +181,27 @@ export default class ConfigureAzureComponent extends Component { } } + async saveSecondModel(): Promise { + const { backendPath, secondModel } = this.args; + try { + await secondModel.save(); + this.flashMessages.success( + `Successfully saved ${backendPath}'s ${secondModel.modelNameDisplay?.toLowerCase()} configuration.` + ); + return true; + } catch (error) { + this.errorMessage = errorMessage(error); + // we transition even if the second model fails. surface a sticky flash message so the user can see it on the next view. + this.flashMessages.danger( + `${secondModel.modelNameDisplay} configuration was not saved: ${this.errorMessage}`, + { + sticky: true, + } + ); + return false; + } + } + resetErrors() { this.flashMessages.clearMessages(); this.errorMessage = ''; @@ -159,19 +212,32 @@ export default class ConfigureAzureComponent extends Component { this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath); } + validate(model: ConfigModel) { + const { isValid, state, invalidFormMessage } = model.validate(); + this.modelValidations = isValid ? null : state; + this.invalidFormAlert = isValid ? '' : invalidFormMessage; + return isValid; + } + @action onChangeAccessType(accessType: string) { this.accessType = accessType; - const { model } = this.args; - if (accessType === 'azure') { - // reset all WIF attributes + const { model, type } = this.args; + if (accessType === 'account') { + // reset all "wif" attributes that are mutually exclusive with "account" attributes + // these attributes are the same for each engine model.identityTokenAudience = model.identityTokenTtl = undefined; // return the issuer to the globally set value (if there is one) on toggle this.args.issuerConfig.rollbackAttributes(); } if (accessType === 'wif') { - // reset all Azure attributes - model.clientSecret = model.rootPasswordTtl = undefined; + // reset all "account" attributes that are mutually exclusive with "wif" attributes + // these attributes are different for each engine + type === 'azure' + ? (model.clientSecret = model.rootPasswordTtl = undefined) + : type === 'aws' + ? (model.accessKey = undefined) + : null; } } diff --git a/ui/app/models/aws/lease-config.js b/ui/app/models/aws/lease-config.js index 472d3d172beb..e1ce3f178357 100644 --- a/ui/app/models/aws/lease-config.js +++ b/ui/app/models/aws/lease-config.js @@ -43,4 +43,6 @@ export default class AwsLeaseConfig extends Model { get formFields() { return expandAttributeMeta(this, this.configurableParams); } + + modelNameDisplay = 'Lease'; // shows as Lease Configuration } diff --git a/ui/app/models/aws/root-config.js b/ui/app/models/aws/root-config.js index 961b9a24893a..9f0c37007268 100644 --- a/ui/app/models/aws/root-config.js +++ b/ui/app/models/aws/root-config.js @@ -79,16 +79,16 @@ export default class AwsRootConfig extends Model { return fieldToAttrs(this, this.formFieldGroups('wif')); } - get fieldGroupsIam() { - return fieldToAttrs(this, this.formFieldGroups('iam')); + get fieldGroupsAccount() { + return fieldToAttrs(this, this.formFieldGroups('account')); } - formFieldGroups(accessType = 'iam') { + formFieldGroups(accessType = 'account') { const formFieldGroups = []; if (accessType === 'wif') { formFieldGroups.push({ default: ['roleArn', 'identityTokenAudience', 'identityTokenTtl'] }); } - if (accessType === 'iam') { + if (accessType === 'account') { formFieldGroups.push({ default: ['accessKey', 'secretKey'] }); } formFieldGroups.push({ diff --git a/ui/app/models/azure/config.js b/ui/app/models/azure/config.js index a924ca658f44..73796fd148a3 100644 --- a/ui/app/models/azure/config.js +++ b/ui/app/models/azure/config.js @@ -64,7 +64,7 @@ export default class AzureConfig extends Model { return !!this.identityTokenAudience || !!this.identityTokenTtl; } - get isAzureAccountConfigured() { + get isAccountPluginConfigured() { // clientSecret is not checked here because it's never return by the API // however it is an Azure account field return !!this.rootPasswordTtl; @@ -84,11 +84,11 @@ export default class AzureConfig extends Model { return fieldToAttrs(this, this.formFieldGroups('wif')); } - get fieldGroupsAzure() { - return fieldToAttrs(this, this.formFieldGroups('azure')); + get fieldGroupsAccount() { + return fieldToAttrs(this, this.formFieldGroups('account')); } - formFieldGroups(accessType = 'azure') { + formFieldGroups(accessType = 'account') { const formFieldGroups = []; formFieldGroups.push({ default: ['subscriptionId', 'tenantId', 'clientId', 'environment'], @@ -98,7 +98,7 @@ export default class AzureConfig extends Model { default: ['identityTokenAudience', 'identityTokenTtl'], }); } - if (accessType === 'azure') { + if (accessType === 'account') { formFieldGroups.push({ default: ['clientSecret', 'rootPasswordTtl'], }); diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts index e56efa44e4d2..94aa46c358c4 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts +++ b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts @@ -7,7 +7,7 @@ import AdapterError from '@ember-data/adapter/error'; import { set } from '@ember/object'; import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { CONFIGURABLE_SECRET_ENGINES, WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; +import { CONFIGURABLE_SECRET_ENGINES, WIF_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines'; import errorMessage from 'vault/utils/error-message'; import { action } from '@ember/object'; @@ -20,7 +20,7 @@ import type VersionService from 'vault/services/version'; // Saving and updating of those models are done within the engine specific components. const CONFIG_ADAPTERS_PATHS: Record = { - aws: ['aws/lease-config', 'aws/root-config'], + aws: ['aws/root-config', 'aws/lease-config'], azure: ['azure/config'], ssh: ['ssh/ca-config'], }; @@ -29,10 +29,24 @@ export default class SecretsBackendConfigurationEdit extends Route { @service declare readonly store: Store; @service declare readonly version: VersionService; + standardizedModelName(type: string, adapterPath: string) { + if ( + CONFIG_ADAPTERS_PATHS[type] && + CONFIG_ADAPTERS_PATHS[type].length > 1 && + adapterPath === CONFIG_ADAPTERS_PATHS[type][1] + ) { + return 'second-model'; + } else { + return 'first-model'; + } + } + async model() { const { backend } = this.paramsFor('vault.cluster.secrets.backend'); const secretEngineRecord = this.modelFor('vault.cluster.secrets.backend') as SecretEngineModel; const type = secretEngineRecord.type; + const displayName = allEngines().find((engine) => engine.type === type)?.displayName; + const isWifEngine = WIF_ENGINES.includes(type); // if the engine type is not configurable, return a 404. if (!secretEngineRecord || !CONFIGURABLE_SECRET_ENGINES.includes(type)) { @@ -44,9 +58,9 @@ export default class SecretsBackendConfigurationEdit extends Route { // and pre-set model with type and backend e.g. {type: ssh, id: ssh-123} const model: Record = { type, id: backend }; for (const adapterPath of CONFIG_ADAPTERS_PATHS[type] as string[]) { - // convert the adapterPath with a name that can be passed to the components - // ex: adapterPath = ssh/ca-config, convert to: ssh-ca-config so that you can pass to component @model={{this.model.ssh-ca-config}} - const standardizedKey = adapterPath.replace(/\//g, '-'); + // create a key that corresponds with the configs model order + // ex: adapterPath = ssh/ca-config, convert to: first-model so that you can pass to component @model={{this.model.first-model}} + const standardizedKey = this.standardizedModelName(type, adapterPath); try { const configModel = await this.store.queryRecord(adapterPath, { backend, @@ -82,7 +96,7 @@ export default class SecretsBackendConfigurationEdit extends Route { } // if the type is a WIF engine and it's enterprise, we also fetch the issuer // from a global endpoint which has no associated model/adapter - if (WIF_ENGINES.includes(type) && this.version.isEnterprise) { + if (isWifEngine && this.version.isEnterprise) { try { const response = await this.store.queryRecord('identity/oidc/config', {}); model['identity-oidc-config'] = response; @@ -91,6 +105,8 @@ export default class SecretsBackendConfigurationEdit extends Route { model['identity-oidc-config'] = { queryIssuerError: true }; } } + model['displayName'] = displayName; + model['isWifEngine'] = isWifEngine; return model; } diff --git a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs index 80d0c30628d9..d1cb31eec7d5 100644 --- a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs +++ b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs @@ -15,7 +15,7 @@

Configure - {{get (options-for-backend this.model.type) "displayName"}} + {{this.model.displayName}}

@@ -28,19 +28,16 @@ -{{#if (eq this.model.type "aws")}} - -{{else if (eq this.model.type "azure")}} - + {{! This check is preventive. As of writing all other engines using this route——but ssh——are wif engines }} +{{else if this.model.isWifEngine}} + -{{else if (eq this.model.type "ssh")}} - {{/if}} \ No newline at end of file diff --git a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js index 0d3545655b61..56ec53ca710f 100644 --- a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js +++ b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js @@ -62,7 +62,10 @@ module('Acceptance | aws | configuration', function (hooks) { await click(SES.configure); assert.strictEqual(currentURL(), `/vault/secrets/${path}/configuration/edit`); assert.dom(SES.configureTitle('aws')).hasText('Configure AWS'); - assert.dom(SES.aws.rootForm).exists('it lands on the root configuration form.'); + assert.dom(SES.configureForm).exists('it lands on the configuration form.'); + assert + .dom(SES.secondModelTitle) + .hasText('Lease Configuration', 'it shows the lease configuration section with correct title.'); // cleanup await runCmd(`delete sys/mounts/${path}`); }); @@ -93,13 +96,13 @@ module('Acceptance | aws | configuration', function (hooks) { await click(SES.configure); await fillInAwsConfig('withWif'); await click(GENERAL.saveButton); - assert.dom(SES.wif.issuerWarningModal).exists('issue warning modal exists'); + assert.dom(SES.wif.issuerWarningModal).exists('issuer warning modal exists'); await click(SES.wif.issuerWarningSave); // three flash messages, the first is about mounting the engine, only care about the last two assert.strictEqual( this.flashSuccessSpy.args[1][0], - `Successfully saved ${path}'s root configuration.`, - 'first flash message about the root config.' + `Successfully saved ${path}'s configuration.`, + 'first flash message about the first model config.' ); assert.strictEqual( this.flashSuccessSpy.args[2][0], @@ -156,8 +159,8 @@ module('Acceptance | aws | configuration', function (hooks) { await fillInAwsConfig('withAccess'); await click(GENERAL.saveButton); assert.true( - this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s root configuration.`), - 'Success flash message is rendered showing the root configuration was saved.' + this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s configuration.`), + 'Success flash message is rendered showing the configuration was saved.' ); assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', 'Access Key has been set.'); assert @@ -190,30 +193,27 @@ module('Acceptance | aws | configuration', function (hooks) { await runCmd(`delete sys/mounts/${path}`); }); - test('it should save lease AWS configuration', async function (assert) { - assert.expect(3); + test('it does not save lease AWS configuration if root configuration errored on save', async function (assert) { + assert.expect(1); const path = `aws-${this.uid}`; await enablePage.enable('aws', path); this.server.post(configUrl('aws', path), () => { - assert.false( - true, - 'post request was made to config/root when no data was changed. test should fail.' + assert.ok('post request was made to config/root when no data was changed. test should fail.'); + return overrideResponse(400, { errors: ['bad request!'] }); + }); + this.server.post(configUrl('aws-lease', path), () => { + assert.true( + false, + 'post request was made to config/lease when the first config was not saved. test should fail.' ); + return overrideResponse(400, { errors: ['bad request!'] }); }); await click(SES.configTab); await click(SES.configure); + await fillInAwsConfig('withAccess'); await fillInAwsConfig('withLease'); await click(GENERAL.saveButton); - assert.true( - this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s lease configuration.`), - 'Success flash message is rendered showing the lease configuration was saved.' - ); - - assert - .dom(GENERAL.infoRowValue('Default Lease TTL')) - .hasText('33 seconds', `Default TTL has been set.`); - assert.dom(GENERAL.infoRowValue('Max Lease TTL')).hasText('44 seconds', `Max lease TTL has been set.`); // cleanup await runCmd(`delete sys/mounts/${path}`); }); diff --git a/ui/tests/helpers/secret-engine/secret-engine-selectors.ts b/ui/tests/helpers/secret-engine/secret-engine-selectors.ts index eeb091e95b31..e799cd9eb56a 100644 --- a/ui/tests/helpers/secret-engine/secret-engine-selectors.ts +++ b/ui/tests/helpers/secret-engine/secret-engine-selectors.ts @@ -24,6 +24,7 @@ export const SECRET_ENGINE_SELECTORS = { viewBackend: '[data-test-backend-view-link]', warning: '[data-test-warning]', configureForm: '[data-test-configure-form]', + secondModelTitle: '[data-test-second-model-title]', wif: { accessTypeSection: '[data-test-access-type-section]', accessTitle: '[data-test-access-title]', @@ -35,8 +36,6 @@ export const SECRET_ENGINE_SELECTORS = { issuerWarningSave: '[data-test-issuer-save]', }, aws: { - rootForm: '[data-test-root-form]', - leaseTitle: '[data-test-lease-title]', deleteRole: (role: string) => `[data-test-aws-role-delete="${role}"]`, }, ssh: { diff --git a/ui/tests/integration/components/secret-engine/configure-aws-test.js b/ui/tests/integration/components/secret-engine/configure-aws-test.js deleted file mode 100644 index 13acfe6ff766..000000000000 --- a/ui/tests/integration/components/secret-engine/configure-aws-test.js +++ /dev/null @@ -1,578 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import sinon from 'sinon'; -import { setupRenderingTest } from 'vault/tests/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 { render, click, fillIn } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { hbs } from 'ember-cli-htmlbars'; -import { v4 as uuidv4 } from 'uuid'; -import { overrideResponse } from 'vault/tests/helpers/stubs'; -import { - expectedConfigKeys, - createConfig, - configUrl, - fillInAwsConfig, -} from 'vault/tests/helpers/secret-engine/secret-engine-helpers'; -import { capabilitiesStub } from 'vault/tests/helpers/stubs'; - -module('Integration | Component | SecretEngine/ConfigureAws', function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - this.flashMessages = this.owner.lookup('service:flash-messages'); - this.flashMessages.registerTypes(['success', 'danger']); - this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success'); - this.flashDangerSpy = sinon.spy(this.flashMessages, 'danger'); - this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo'); - - this.uid = uuidv4(); - this.id = `aws-${this.uid}`; - // using createRecord on root and lease configs to simulate a fresh mount - this.rootConfig = this.store.createRecord('aws/root-config'); - this.leaseConfig = this.store.createRecord('aws/lease-config'); - // issuer config is never a createdRecord but the response from the API. - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - // Add backend to the configs because it's not on the testing snapshot (would come from url) - this.rootConfig.backend = this.leaseConfig.backend = this.id; - this.version = this.owner.lookup('service:version'); - // stub capabilities so that by default user can read and update issuer - this.server.post('/sys/capabilities-self', () => capabilitiesStub('identity/oidc/config', ['sudo'])); - - this.renderComponent = () => { - return render(hbs` - - `); - }; - }); - module('Create view', function () { - module('isEnterprise', function (hooks) { - hooks.beforeEach(function () { - this.version.type = 'enterprise'; - }); - - test('it renders fields ', async function (assert) { - await this.renderComponent(); - assert.dom(SES.aws.rootForm).exists('it lands on the aws root configuration form.'); - assert.dom(SES.wif.accessTitle).exists('Access section is rendered'); - assert.dom(SES.aws.leaseTitle).exists('Lease section is rendered'); - assert.dom(SES.wif.accessTypeSection).exists('Access type section is rendered'); - assert.dom(SES.wif.accessType('iam')).isChecked('defaults to showing IAM access type checked'); - 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', true)) { - assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section.`); - } - for (const key of expectedConfigKeys('aws-lease')) { - assert.dom(`[data-test-ttl-form-label="${key}"]`).exists(`${key} shows for Lease section.`); - } - assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist(); - }); - - test('it renders wif fields when selected', async function (assert) { - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - // check for the wif fields only - 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 { - assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for wif section.`); - } - } - // check iam fields do not show - for (const key of expectedConfigKeys('aws', true)) { - assert.dom(GENERAL.inputByAttr(key)).doesNotExist(`${key} does not show when wif is selected.`); - } - }); - - test('it clears wif/iam inputs after toggling accessType', async function (assert) { - await this.renderComponent(); - await fillInAwsConfig('withAccess'); - await fillInAwsConfig('withLease'); - await click(SES.wif.accessType('wif')); // toggle to wif - await fillInAwsConfig('withWif'); - await click(SES.wif.accessType('iam')); // toggle to wif - assert - .dom(GENERAL.inputByAttr('accessKey')) - .hasValue('', 'accessKey is cleared after toggling accessType'); - assert - .dom(GENERAL.inputByAttr('secretKey')) - .hasValue('', 'secretKey is cleared after toggling accessType'); - - await click(SES.wif.accessType('wif')); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasValue('', 'issue shows no value after toggling accessType'); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasAttribute( - 'placeholder', - 'https://vault-test.com', - 'issue shows no value after toggling accessType' - ); - assert - .dom(GENERAL.inputByAttr('roleArn')) - .hasValue('', 'roleArn is cleared after toggling accessType'); - assert - .dom(GENERAL.inputByAttr('identityTokenAudience')) - .hasValue('', 'identityTokenAudience is cleared after toggling accessType'); - assert - .dom(GENERAL.toggleInput('Identity token TTL')) - .isNotChecked('identityTokenTtl is cleared after toggling accessType'); - }); - - test('it does not clear global issuer when toggling accessType', async function (assert) { - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasValue(this.issuerConfig.issuer, 'issuer is what is sent in my the model on first load'); - await fillIn(GENERAL.inputByAttr('issuer'), 'http://ive-changed'); - await click(SES.wif.accessType('iam')); - await click(SES.wif.accessType('wif')); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasValue( - this.issuerConfig.issuer, - 'issuer value is still the same global value after toggling accessType' - ); - }); - - test('it shows validation error if default lease is entered but max lease is not', async function (assert) { - assert.expect(2); - await this.renderComponent(); - this.server.post(configUrl('aws-lease', this.id), () => { - assert.false( - true, - 'post request was made to config/lease when no data was changed. test should fail.' - ); - }); - this.server.post(configUrl('aws', this.id), () => { - assert.false( - true, - 'post request was made to config/root when no data was changed. test should fail.' - ); - }); - await click(GENERAL.ttl.toggle('Default Lease TTL')); - await fillIn(GENERAL.ttl.input('Default Lease TTL'), '33'); - await click(GENERAL.saveButton); - assert - .dom(GENERAL.inlineError) - .hasText('Lease TTL and Max Lease TTL are both required if one of them is set.'); - assert.dom(SES.aws.rootForm).exists('remains on the configuration form'); - }); - - test('it surfaces the API error if one occurs on root/config, preventing user from transitioning', async function (assert) { - assert.expect(3); - await this.renderComponent(); - this.server.post(configUrl('aws', this.id), () => { - return overrideResponse(400, { errors: ['bad request'] }); - }); - this.server.post(configUrl('aws-lease', this.id), () => { - assert.true( - true, - 'post request was made to config/lease when config/root failed. test should pass.' - ); - }); - // fill in both lease and root endpoints to ensure that both payloads are attempted to be sent - await fillInAwsConfig('withAccess'); - await fillInAwsConfig('withLease'); - await click(GENERAL.saveButton); - assert.dom(GENERAL.messageError).exists('API error surfaced to user'); - assert.dom(GENERAL.inlineError).exists('User shown inline error message'); - }); - - test('it allows user to submit root config even if API error occurs on config/lease config', async function (assert) { - assert.expect(3); - await this.renderComponent(); - this.server.post(configUrl('aws', this.id), () => { - assert.true( - true, - 'post request was made to config/root when config/lease failed. test should pass.' - ); - }); - this.server.post(configUrl('aws-lease', this.id), () => { - return overrideResponse(400, { errors: ['bad request'] }); - }); - // fill in both lease and root endpoints to ensure that both payloads are attempted to be sent - await fillInAwsConfig('withAccess'); - await fillInAwsConfig('withLease'); - await click(GENERAL.saveButton); - - assert.true( - this.flashDangerSpy.calledWith('Lease configuration was not saved: bad request'), - 'Flash message shows that lease was not saved.' - ); - assert.ok( - this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), - 'Transitioned to the configuration index route.' - ); - }); - - test('it allows user to submit root config even if API error occurs on issuer config', async function (assert) { - assert.expect(4); - await this.renderComponent(); - this.server.post(configUrl('aws', this.id), () => { - assert.true(true, 'post request was made to config/root when issuer failed. test should pass.'); - }); - this.server.post('/identity/oidc/config', () => { - return overrideResponse(400, { errors: ['bad request'] }); - }); - await fillInAwsConfig('withWif'); - await click(GENERAL.saveButton); - await click(SES.wif.issuerWarningSave); - - assert.true( - this.flashDangerSpy.calledWith('Issuer was not saved: bad request'), - 'Flash message shows that issuer was not saved' - ); - assert.true( - this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s root configuration.`), - 'Flash message shows that root was saved even if issuer was not' - ); - assert.ok( - this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), - 'Transitioned to the configuration index route.' - ); - }); - - test('it transitions without sending a lease, root, or issuer payload on cancel', async function (assert) { - assert.expect(3); - await this.renderComponent(); - this.server.post(configUrl('aws', this.id), () => { - assert.true( - false, - 'post request was made to config/root when user canceled out of flow. test should fail.' - ); - }); - this.server.post(configUrl('aws-lease', this.id), () => { - assert.true( - false, - 'post request was made to config/lease when user canceled out of flow. test should fail.' - ); - }); - this.server.post('/identity/oidc/config', () => { - assert.true( - false, - 'post request was made to save issuer when user canceled out of flow. test should fail.' - ); - }); - // fill in both lease and root endpoints to ensure that both payloads are attempted to be sent - await fillInAwsConfig('withWif'); - await fillInAwsConfig('withLease'); - await click(GENERAL.cancelButton); - - assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); - assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.'); - assert.ok( - this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), - 'Transitioned to the configuration index route.' - ); - }); - - module('issuer field tests', function () { - // the other tests where issuer is not passed do not show modals, so we only need to test when the modal should shows up - test('if issuer API error and user changes issuer value, shows specific warning message', async function (assert) { - this.issuerConfig.queryIssuerError = true; - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - await fillIn(GENERAL.inputByAttr('issuer'), 'http://change.me.no.read'); - await click(GENERAL.saveButton); - assert - .dom(SES.wif.issuerWarningMessage) - .hasText( - `You are updating the global issuer config. This will overwrite Vault's current issuer if it exists and may affect other configurations using this value. Continue?`, - 'modal shows message about overwriting value if it exists' - ); - }); - - test('is shows placeholder issuer, shows modal when saving changes, and does not call APIs on cancel', async function (assert) { - this.server.post('/identity/oidc/config', () => { - assert.notOk(true, 'request should not be made to issuer config endpoint'); - }); - this.server.post(configUrl('aws', this.id), () => { - assert.notOk( - true, - 'post request was made to config/root when user canceled out of flow. test should fail.' - ); - }); - this.server.post(configUrl('aws-lease', this.id), () => { - assert.notOk( - true, - 'post request was made to config/lease when user canceled out of flow. test should fail.' - ); - }); - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasAttribute('placeholder', 'https://vault-test.com', 'shows issuer placeholder'); - assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'shows issuer is empty when not passed'); - await fillIn(GENERAL.inputByAttr('issuer'), 'http://bar.foo'); - await click(GENERAL.saveButton); - assert.dom(SES.wif.issuerWarningModal).exists('issuer modal exists'); - assert - .dom(SES.wif.issuerWarningMessage) - .hasText( - `You are updating the global issuer config. This will overwrite Vault's current issuer and may affect other configurations using this value. Continue?`, - 'modal shows message about overwriting value without the noRead: "if it exists" adage' - ); - await click(SES.wif.issuerWarningCancel); - assert.dom(SES.wif.issuerWarningModal).doesNotExist('issuer modal is removed on cancel'); - assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); - assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.'); - assert.true(this.transitionStub.notCalled, 'Does not redirect'); - }); - - test('it shows modal when updating issuer and calls correct APIs on save', async function (assert) { - const newIssuer = 'http://bar.foo'; - this.server.post('/identity/oidc/config', (schema, req) => { - const payload = JSON.parse(req.requestBody); - assert.deepEqual(payload, { issuer: newIssuer }, 'payload for issuer is correct'); - return { - id: 'identity-oidc-config', // id needs to match the id on secret-engine-helpers createIssuerConfig - data: null, - warnings: [ - 'If "issuer" is set explicitly, all tokens must be validated against that address, including those issued by secondary clusters. Setting issuer to "" will restore the default behavior of using the cluster\'s api_addr as the issuer.', - ], - }; - }); - this.server.post(configUrl('aws', this.id), () => { - assert.notOk(true, 'skips request to config/root due to no changes'); - }); - this.server.post(configUrl('aws-lease', this.id), () => { - assert.notOk(true, 'skips request to config/lease due to no changes'); - }); - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'issuer defaults to empty string'); - await fillIn(GENERAL.inputByAttr('issuer'), newIssuer); - await click(GENERAL.saveButton); - assert.dom(SES.wif.issuerWarningModal).exists('issue warning modal exists'); - await click(SES.wif.issuerWarningSave); - assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); - assert.true( - this.flashSuccessSpy.calledWith('Issuer saved successfully'), - 'Success flash message called for issuer' - ); - assert.ok( - this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), - 'Transitioned to the configuration index route.' - ); - }); - - test('shows modal when modifying the issuer, has correct payload, and shows flash message on fail', async function (assert) { - assert.expect(7); - this.issuer = 'http://foo.bar'; - this.server.post(configUrl('aws', this.id), () => { - assert.true( - true, - 'post request was made to config/root when unsetting the issuer. test should pass.' - ); - }); - this.server.post('/identity/oidc/config', (_, req) => { - const payload = JSON.parse(req.requestBody); - assert.deepEqual(payload, { issuer: this.issuer }, 'correctly sets the issuer'); - return overrideResponse(403); - }); - - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - assert.dom(GENERAL.inputByAttr('issuer')).hasValue(''); - await fillIn(GENERAL.inputByAttr('issuer'), this.issuer); - await fillIn(GENERAL.inputByAttr('roleArn'), 'some-other-value'); - await click(GENERAL.saveButton); - assert.dom(SES.wif.issuerWarningModal).exists('issuer warning modal exists'); - - await click(SES.wif.issuerWarningSave); - assert.true( - this.flashDangerSpy.calledWith('Issuer was not saved: permission denied'), - 'shows danger flash for issuer save' - ); - assert.true( - this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s root configuration.`), - "calls the root flash message not the issuer's" - ); - - assert.ok( - this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), - 'Transitioned to the configuration index route.' - ); - }); - }); - }); - module('isCommunity', function (hooks) { - hooks.beforeEach(function () { - this.version.type = 'community'; - }); - test('it renders fields', async function (assert) { - assert.expect(13); - await this.renderComponent(); - assert.dom(SES.aws.rootForm).exists('it lands on the aws root configuration form.'); - assert.dom(SES.wif.accessTitle).exists('Access section is rendered'); - assert.dom(SES.aws.leaseTitle).exists('Lease section is rendered'); - assert - .dom(SES.wif.accessTypeSection) - .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', true)) { - assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section.`); - } - for (const key of expectedConfigKeys('aws-lease')) { - assert.dom(`[data-test-ttl-form-label="${key}"]`).exists(`${key} shows for Lease section.`); - } - assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist(); - }); - test('it does not send issuer on save', async function (assert) { - assert.expect(4); - await this.renderComponent(); - this.server.post(configUrl('aws', this.id), () => { - assert.true(true, 'post request was made to config/root. test should pass.'); - }); - this.server.post('/identity/oidc/config', () => { - throw new Error('post request was incorrectly made to update issuer'); - }); - await fillInAwsConfig('withAccess'); - await fillInAwsConfig('withLease'); - await click(GENERAL.saveButton); - assert.dom(SES.wif.issuerWarningModal).doesNotExist('modal should not render'); - assert.true( - this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s root configuration.`), - 'Flash message shows that root was saved even if issuer was not' - ); - assert.ok( - this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), - 'Transitioned to the configuration index route.' - ); - }); - }); - }); - module('Edit view', function (hooks) { - hooks.beforeEach(function () { - this.rootConfig = createConfig(this.store, this.id, 'aws'); - this.leaseConfig = createConfig(this.store, this.id, 'aws-lease'); - }); - module('isEnterprise', function (hooks) { - hooks.beforeEach(function () { - this.version.type = 'enterprise'; - }); - - test('it defaults to IAM accessType if IAM fields are already set', async function (assert) { - await this.renderComponent(); - assert.dom(SES.wif.accessType('iam')).isChecked('IAM accessType is checked'); - assert.dom(SES.wif.accessType('iam')).isDisabled('IAM accessType is disabled'); - assert.dom(SES.wif.accessType('wif')).isNotChecked('WIF accessType is not checked'); - assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled'); - assert - .dom(SES.wif.accessTypeSubtext) - .hasText('You cannot edit Access Type if you have already saved access credentials.'); - }); - - test('it defaults to WIF accessType if WIF fields are already set', async function (assert) { - this.rootConfig = createConfig(this.store, this.id, 'aws-wif'); - await this.renderComponent(); - assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked'); - assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled'); - assert.dom(SES.wif.accessType('iam')).isNotChecked('IAM accessType is not checked'); - assert.dom(SES.wif.accessType('iam')).isDisabled('IAM accessType is disabled'); - assert.dom(GENERAL.inputByAttr('roleArn')).hasValue(this.rootConfig.roleArn); - assert - .dom(SES.wif.accessTypeSubtext) - .hasText('You cannot edit Access Type if you have already saved access credentials.'); - assert - .dom(GENERAL.inputByAttr('identityTokenAudience')) - .hasValue(this.rootConfig.identityTokenAudience); - assert.dom(GENERAL.ttl.input('Identity token TTL')).hasValue('2'); // 7200 on payload is 2hrs in ttl picker - }); - - test('it renders issuer if global issuer is already set', async function (assert) { - this.rootConfig = createConfig(this.store, this.id, 'aws-wif'); - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - await this.renderComponent(); - assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked'); - assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled'); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasValue(this.issuerConfig.issuer, 'it has the models issuer value'); - }); - - test('it allows you to change access type if record does not have wif or iam values already set', async function (assert) { - // the model does not have to be new for a user to see the option to change the access type. - // the access type is only disabled if the model has values already set for access type fields. - this.rootConfig = createConfig(this.store, this.id, 'aws-no-access'); - await this.renderComponent(); - assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled'); - assert.dom(SES.wif.accessType('iam')).isNotDisabled('IAM accessType is NOT disabled'); - }); - - test('it shows previously saved root and lease information', async function (assert) { - await this.renderComponent(); - assert.dom(GENERAL.inputByAttr('accessKey')).hasValue(this.rootConfig.accessKey); - assert - .dom(GENERAL.inputByAttr('secretKey')) - .hasValue('**********', 'secretKey is masked on edit the value'); - - await click(GENERAL.toggleGroup('Root config options')); - assert.dom(GENERAL.inputByAttr('region')).hasValue(this.rootConfig.region); - assert.dom(GENERAL.inputByAttr('iamEndpoint')).hasValue(this.rootConfig.iamEndpoint); - assert.dom(GENERAL.inputByAttr('stsEndpoint')).hasValue(this.rootConfig.stsEndpoint); - assert.dom(GENERAL.inputByAttr('maxRetries')).hasValue('1'); - // Check lease config values - assert.dom(GENERAL.ttl.input('Default Lease TTL')).hasValue('50'); - assert.dom(GENERAL.ttl.input('Max Lease TTL')).hasValue('55'); - }); - - test('it requires a double click to change the secret key', async function (assert) { - await this.renderComponent(); - - this.server.post(configUrl('aws', this.id), (schema, req) => { - const payload = JSON.parse(req.requestBody); - assert.strictEqual( - payload.secret_key, - 'new-secret', - 'post request was made to config/root with the updated secret_key.' - ); - }); - - await click(GENERAL.enableField('secretKey')); - await click('[data-test-button="toggle-masked"]'); - await fillIn(GENERAL.inputByAttr('secretKey'), 'new-secret'); - await click(GENERAL.saveButton); - }); - }); - module('isCommunity', function (hooks) { - hooks.beforeEach(function () { - this.version.type = 'community'; - }); - - test('it does not show access types but defaults to iam fields', async function (assert) { - await this.renderComponent(); - assert.dom(SES.wif.accessTypeSection).doesNotExist('Access type section does not render'); - assert.dom(GENERAL.inputByAttr('accessKey')).hasValue(this.rootConfig.accessKey); - assert - .dom(GENERAL.inputByAttr('secretKey')) - .hasValue('**********', 'secretKey is masked on edit the value'); - - await click(GENERAL.toggleGroup('Root config options')); - assert.dom(GENERAL.inputByAttr('region')).hasValue(this.rootConfig.region); - assert.dom(GENERAL.inputByAttr('iamEndpoint')).hasValue(this.rootConfig.iamEndpoint); - assert.dom(GENERAL.inputByAttr('stsEndpoint')).hasValue(this.rootConfig.stsEndpoint); - assert.dom(GENERAL.inputByAttr('maxRetries')).hasValue('1'); - // Check lease config values - assert.dom(GENERAL.ttl.input('Default Lease TTL')).hasValue('50'); - assert.dom(GENERAL.ttl.input('Max Lease TTL')).hasValue('55'); - }); - }); - }); -}); diff --git a/ui/tests/integration/components/secret-engine/configure-azure-test.js b/ui/tests/integration/components/secret-engine/configure-azure-test.js deleted file mode 100644 index 07b3fab00b1b..000000000000 --- a/ui/tests/integration/components/secret-engine/configure-azure-test.js +++ /dev/null @@ -1,426 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import sinon from 'sinon'; -import { setupRenderingTest } from 'vault/tests/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 { render, click, fillIn } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { hbs } from 'ember-cli-htmlbars'; -import { v4 as uuidv4 } from 'uuid'; -import { overrideResponse } from 'vault/tests/helpers/stubs'; -import { - expectedConfigKeys, - createConfig, - configUrl, - fillInAzureConfig, -} from 'vault/tests/helpers/secret-engine/secret-engine-helpers'; -import { capabilitiesStub } from 'vault/tests/helpers/stubs'; - -module('Integration | Component | SecretEngine/ConfigureAzure', function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - this.version = this.owner.lookup('service:version'); - this.flashMessages = this.owner.lookup('service:flash-messages'); - this.flashMessages.registerTypes(['success', 'danger']); - this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success'); - this.flashDangerSpy = sinon.spy(this.flashMessages, 'danger'); - this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo'); - - this.uid = uuidv4(); - this.id = `azure-${this.uid}`; - this.config = this.store.createRecord('azure/config'); - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) - // stub capabilities so that by default user can read and update issuer - this.server.post('/sys/capabilities-self', () => capabilitiesStub('identity/oidc/config', ['sudo'])); - - this.renderComponent = () => { - return render(hbs` - - `); - }; - }); - module('Create view', function () { - module('isEnterprise', function (hooks) { - hooks.beforeEach(function () { - this.version.type = 'enterprise'; - }); - - test('it renders default fields, showing access type options for enterprise users', async function (assert) { - await this.renderComponent(); - assert.dom(SES.configureForm).exists('it lands on the Azure configuration form.'); - 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', true)) { - assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for root section`); - } - assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist(); - }); - - test('it renders wif fields when user selects wif access type', async function (assert) { - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - // check for the wif fields only - 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 { - assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for wif section.`); - } - } - assert.dom(GENERAL.inputByAttr('issuer')).exists('issuer shows for wif section.'); - }); - - test('it clears wif/azure-account inputs after toggling accessType', async function (assert) { - await this.renderComponent(); - await fillInAzureConfig('azure'); - await click(SES.wif.accessType('wif')); - await fillInAzureConfig('withWif'); - await click(SES.wif.accessType('azure')); - - assert - .dom(GENERAL.toggleInput('Root password TTL')) - .isNotChecked('rootPasswordTtl is cleared after toggling accessType'); - assert - .dom(GENERAL.inputByAttr('clientSecret')) - .hasValue('', 'clientSecret is cleared after toggling accessType'); - await click(SES.wif.accessType('wif')); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasValue('', 'issuer shows no value after toggling accessType'); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasAttribute( - 'placeholder', - 'https://vault-test.com', - 'issuer shows no value after toggling accessType' - ); - assert - .dom(GENERAL.inputByAttr('identityTokenAudience')) - .hasValue('', 'idTokenAudience is cleared after toggling accessType'); - assert - .dom(GENERAL.toggleInput('Identity token TTL')) - .isNotChecked('identityTokenTtl is cleared after toggling accessType'); - }); - - test('it does not clear global issuer when toggling accessType', async function (assert) { - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasValue(this.issuerConfig.issuer, 'issuer is what is sent in by the model on first load'); - await fillIn(GENERAL.inputByAttr('issuer'), 'http://ive-changed'); - await click(SES.wif.accessType('azure')); - await click(SES.wif.accessType('wif')); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasValue( - this.issuerConfig.issuer, - 'issuer value is still the same global value after toggling accessType' - ); - }); - - test('it transitions without sending a config or issuer payload on cancel', async function (assert) { - assert.expect(3); - await this.renderComponent(); - this.server.post(configUrl('azure', this.id), () => { - assert.notOk( - true, - 'post request was made to config when user canceled out of flow. test should fail.' - ); - }); - this.server.post('/identity/oidc/config', () => { - assert.notOk( - true, - 'post request was made to save issuer when user canceled out of flow. test should fail.' - ); - }); - await fillInAzureConfig('withWif'); - await click(GENERAL.cancelButton); - - assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); - assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.'); - - assert.ok( - this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), - 'Transitioned to the configuration index route.' - ); - }); - - module('issuer field tests', function () { - test('if issuer API error and user changes issuer value, shows specific warning message', async function (assert) { - this.issuerConfig.queryIssuerError = true; - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - await fillIn(GENERAL.inputByAttr('issuer'), 'http://change.me.no.read'); - await click(GENERAL.saveButton); - assert - .dom(SES.wif.issuerWarningMessage) - .hasText( - `You are updating the global issuer config. This will overwrite Vault's current issuer if it exists and may affect other configurations using this value. Continue?`, - 'modal shows message about overwriting value if it exists' - ); - }); - - test('is shows placeholder issuer, and does not call APIs on canceling out of issuer modal', async function (assert) { - this.server.post('/identity/oidc/config', () => { - assert.notOk(true, 'request should not be made to issuer config endpoint'); - }); - this.server.post(configUrl('azure', this.id), () => { - assert.notOk( - true, - 'post request was made to config/ when user canceled out of flow. test should fail.' - ); - }); - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasAttribute('placeholder', 'https://vault-test.com', 'shows issuer placeholder'); - assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'shows issuer is empty when not passed'); - await fillIn(GENERAL.inputByAttr('issuer'), 'http://bar.foo'); - await click(GENERAL.saveButton); - assert.dom(SES.wif.issuerWarningMessage).exists('issuer modal exists'); - assert - .dom(SES.wif.issuerWarningMessage) - .hasText( - `You are updating the global issuer config. This will overwrite Vault's current issuer and may affect other configurations using this value. Continue?`, - 'modal shows message about overwriting value without the noRead: "if it exists" adage' - ); - await click(SES.wif.issuerWarningCancel); - assert.dom(SES.wif.issuerWarningMessage).doesNotExist('issuer modal is removed on cancel'); - assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); - assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.'); - assert.true(this.transitionStub.notCalled, 'Does not redirect'); - }); - - test('it shows modal when updating issuer and calls correct APIs on save', async function (assert) { - const newIssuer = `http://bar.${uuidv4()}`; - this.server.post('/identity/oidc/config', (schema, req) => { - const payload = JSON.parse(req.requestBody); - assert.deepEqual(payload, { issuer: newIssuer }, 'payload for issuer is correct'); - return { - id: 'identity-oidc-config', // id needs to match the id on secret-engine-helpers createIssuerConfig - data: null, - warnings: [ - 'If "issuer" is set explicitly, all tokens must be validated against that address, including those issued by secondary clusters. Setting issuer to "" will restore the default behavior of using the cluster\'s api_addr as the issuer.', - ], - }; - }); - this.server.post(configUrl('azure', this.id), () => { - assert.notOk(true, 'skips request to config because the model was not changed'); - }); - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'issuer defaults to empty string'); - await fillIn(GENERAL.inputByAttr('issuer'), newIssuer); - await click(GENERAL.saveButton); - - assert.dom(SES.wif.issuerWarningMessage).exists('issue warning modal exists'); - - await click(SES.wif.issuerWarningSave); - assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); - assert.true( - this.flashSuccessSpy.calledWith('Issuer saved successfully'), - 'Success flash message called for Azure issuer' - ); - assert.true( - this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), - 'Transitioned to the configuration index route.' - ); - }); - - test('shows modal when modifying the issuer, has correct payload, and shows flash message on fail', async function (assert) { - assert.expect(7); - this.issuer = 'http://foo.bar'; - this.server.post(configUrl('azure', this.id), () => { - assert.true( - true, - 'post request was made to azure config when unsetting the issuer. test should pass.' - ); - }); - this.server.post('/identity/oidc/config', (_, req) => { - const payload = JSON.parse(req.requestBody); - assert.deepEqual(payload, { issuer: this.issuer }, 'correctly sets the issuer'); - return overrideResponse(403); - }); - - await this.renderComponent(); - await click(SES.wif.accessType('wif')); - assert.dom(GENERAL.inputByAttr('issuer')).hasValue(''); - await fillIn(GENERAL.inputByAttr('issuer'), this.issuer); - await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'some-value'); - await click(GENERAL.saveButton); - assert.dom(SES.wif.issuerWarningMessage).exists('issuer warning modal exists'); - await click(SES.wif.issuerWarningSave); - - assert.true( - this.flashDangerSpy.calledWith('Issuer was not saved: permission denied'), - 'shows danger flash for issuer save' - ); - assert.true( - this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`), - "calls the config flash message not the issuer's" - ); - assert.ok( - this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), - 'Transitioned to the configuration index route.' - ); - }); - }); - }); - module('isCommunity', function (hooks) { - hooks.beforeEach(function () { - this.version.type = 'community'; - }); - - test('it renders fields', async function (assert) { - 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', true)) { - assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for azure account creds section.`); - } - assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist(); - }); - - test('it does not send issuer on save', async function (assert) { - assert.expect(4); - await this.renderComponent(); - this.server.post(configUrl('azure', this.id), () => { - assert.true(true, 'post request was made to config. test should pass.'); - }); - this.server.post('/identity/oidc/config', () => { - throw new Error('post request was incorrectly made to update issuer'); - }); - await fillInAzureConfig('azure'); - await click(GENERAL.saveButton); - assert.dom(SES.wif.issuerWarningMessage).doesNotExist('modal should not render'); - assert.true( - this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`), - 'Flash message shows that config was saved even if issuer was not.' - ); - assert.ok( - this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), - 'Transitioned to the configuration index route.' - ); - }); - }); - }); - - module('Edit view', function () { - module('isEnterprise', function (hooks) { - hooks.beforeEach(function () { - this.version.type = 'enterprise'; - }); - - test('it defaults to Azure accessType if Azure account fields are already set', async function (assert) { - this.config = createConfig(this.store, this.id, 'azure'); - await this.renderComponent(); - assert.dom(SES.wif.accessType('azure')).isChecked('Azure accessType is checked'); - assert.dom(SES.wif.accessType('azure')).isDisabled('Azure accessType is disabled'); - assert.dom(SES.wif.accessType('wif')).isNotChecked('WIF accessType is not checked'); - assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled'); - assert - .dom(SES.wif.accessTypeSubtext) - .hasText('You cannot edit Access Type if you have already saved access credentials.'); - }); - - test('it defaults to WIF accessType if WIF fields are already set', async function (assert) { - this.config = createConfig(this.store, this.id, 'azure-wif'); - await this.renderComponent(); - assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked'); - assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled'); - assert.dom(SES.wif.accessType('azure')).isNotChecked('azure accessType is not checked'); - assert.dom(SES.wif.accessType('azure')).isDisabled('azure accessType is disabled'); - assert.dom(GENERAL.inputByAttr('identityTokenAudience')).hasValue(this.config.identityTokenAudience); - assert - .dom(SES.wif.accessTypeSubtext) - .hasText('You cannot edit Access Type if you have already saved access credentials.'); - assert.dom(GENERAL.ttl.input('Identity token TTL')).hasValue('2'); // 7200 on payload is 2hrs in ttl picker - }); - - test('it renders issuer if global issuer is already set', async function (assert) { - this.config = createConfig(this.store, this.id, 'azure-wif'); - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.issuerConfig.issuer = 'https://foo-bar-blah.com'; - await this.renderComponent(); - assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked'); - assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled'); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasValue( - this.issuerConfig.issuer, - `it has the global issuer value of ${this.issuerConfig.issuer}` - ); - }); - - test('it allows you to change accessType if record does not have wif or azure values already set', async function (assert) { - // the model does not have to be new for a user to see the option to change the access type. - // the access type is only disabled if the model has values already set for access type fields. - this.config = createConfig(this.store, this.id, 'azure-generic'); - await this.renderComponent(); - assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled'); - assert.dom(SES.wif.accessType('azure')).isNotDisabled('Azure accessType is NOT disabled'); - }); - - test('it shows previously saved config information', async function (assert) { - this.config = createConfig(this.store, this.id, 'azure-generic'); - await this.renderComponent(); - assert.dom(GENERAL.inputByAttr('subscriptionId')).hasValue(this.config.subscriptionId); - assert.dom(GENERAL.inputByAttr('clientId')).hasValue(this.config.clientId); - assert.dom(GENERAL.inputByAttr('tenantId')).hasValue(this.config.tenantId); - assert - .dom(GENERAL.inputByAttr('clientSecret')) - .hasValue('**********', 'clientSecret is masked on edit the value'); - }); - - test('it requires a double click to change the client secret', async function (assert) { - this.config = createConfig(this.store, this.id, 'azure'); - await this.renderComponent(); - - this.server.post(configUrl('azure', this.id), (schema, req) => { - const payload = JSON.parse(req.requestBody); - assert.strictEqual( - payload.client_secret, - 'new-secret', - 'post request was made to azure/config with the updated client_secret.' - ); - }); - - await click(GENERAL.enableField('clientSecret')); - await click('[data-test-button="toggle-masked"]'); - await fillIn(GENERAL.inputByAttr('clientSecret'), 'new-secret'); - await click(GENERAL.saveButton); - }); - }); - module('isCommunity', function (hooks) { - hooks.beforeEach(function () { - this.version.type = 'community'; - }); - - test('it does not show access types but defaults to Azure account fields', async function (assert) { - this.config = createConfig(this.store, this.id, 'azure-generic'); - await this.renderComponent(); - assert.dom(SES.wif.accessTypeSection).doesNotExist('Access type section does not render'); - assert.dom(GENERAL.inputByAttr('clientId')).hasValue(this.config.clientId); - assert.dom(GENERAL.inputByAttr('subscriptionId')).hasValue(this.config.subscriptionId); - assert.dom(GENERAL.inputByAttr('tenantId')).hasValue(this.config.tenantId); - }); - }); - }); -}); diff --git a/ui/tests/integration/components/secret-engine/configure-wif-test.js b/ui/tests/integration/components/secret-engine/configure-wif-test.js new file mode 100644 index 000000000000..517c9c03cd48 --- /dev/null +++ b/ui/tests/integration/components/secret-engine/configure-wif-test.js @@ -0,0 +1,861 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import sinon from 'sinon'; +import { setupRenderingTest } from 'vault/tests/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 { render, click, fillIn } from '@ember/test-helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { v4 as uuidv4 } from 'uuid'; +import { hbs } from 'ember-cli-htmlbars'; +import { overrideResponse } from 'vault/tests/helpers/stubs'; +import { + expectedConfigKeys, + createConfig, + configUrl, + fillInAzureConfig, + fillInAwsConfig, +} from 'vault/tests/helpers/secret-engine/secret-engine-helpers'; +import { capabilitiesStub } from 'vault/tests/helpers/stubs'; +import { WIF_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines'; +import waitForError from 'vault/tests/helpers/wait-for-error'; + +const allEnginesArray = allEngines(); // saving as const so we don't invoke the method multiple times via the for loop + +module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function () { + this.store = this.owner.lookup('service:store'); + this.version = this.owner.lookup('service:version'); + this.flashMessages = this.owner.lookup('service:flash-messages'); + this.flashMessages.registerTypes(['success', 'danger']); + this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success'); + this.flashDangerSpy = sinon.spy(this.flashMessages, 'danger'); + this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo'); + this.uid = uuidv4(); + // stub capabilities so that by default user can read and update issuer + this.server.post('/sys/capabilities-self', () => capabilitiesStub('identity/oidc/config', ['sudo'])); + }); + + module('Create view', function () { + module('isEnterprise', function (hooks) { + hooks.beforeEach(function () { + this.version.type = 'enterprise'; + }); + for (const type of WIF_ENGINES) { + test(`${type}: it renders default fields`, async function (assert) { + this.id = `${type}-${this.uid}`; + this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = + type === 'aws' + ? this.store.createRecord('aws/root-config') + : this.store.createRecord(`${type}/config`); + this.secondConfig = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; + this.config.backend = this.id; + this.secondConfig ? (this.secondConfig.backend = this.id) : null; // Add backend to the configs because it's not on the testing snapshot (would come from url) + this.type = type; // required to set on this to pass into the component + + await render(hbs` + + `); + assert.dom(SES.configureForm).exists(`it lands on the ${type} configuration form`); + assert.dom(SES.wif.accessType(type)).isChecked(`defaults to showing ${type} access type checked`); + assert.dom(SES.wif.accessType('wif')).isNotChecked('wif access type is not checked'); + // toggle grouped fields if it exists + const toggleGroup = document.querySelector('[data-test-toggle-group]'); + 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` + ); + } + assert + .dom(GENERAL.inputByAttr('issuer')) + .doesNotExist(`for ${type}, the issuer does not show when wif is not the access type`); + }); + } + for (const type of WIF_ENGINES) { + test(`${type}: it renders wif fields when user selects wif access type`, async function (assert) { + this.id = `${type}-${this.uid}`; + this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = + type === 'aws' + ? this.store.createRecord('aws/root-config') + : this.store.createRecord(`${type}/config`); + this.secondConfig = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; + this.config.backend = this.id; + this.secondConfig ? (this.secondConfig.backend = this.id) : null; // Add backend to the configs because it's not on the testing snapshot (would come from url) + this.type = type; // required to set on this to pass into the component + + await render(hbs` + + `); + await click(SES.wif.accessType('wif')); + // check for the wif fields only + for (const key of expectedConfigKeys(`${type}-wif`, true)) { + if (key === 'Identity token TTL') { + assert.dom(GENERAL.ttl.toggle(key)).exists(`${key} shows for ${type} wif section.`); + } else { + assert.dom(GENERAL.inputByAttr(key)).exists(`${key} shows for ${type} wif section.`); + } + } + assert.dom(GENERAL.inputByAttr('issuer')).exists(`issuer shows for ${type} wif section.`); + }); + } + /* For tests relevant to all engines, they are run once within one of the engine specific modules below */ + module('Azure specific', function () { + test('it clears access type inputs after toggling accessType', async function (assert) { + this.id = `azure-${this.uid}`; + this.displayName = 'Azure'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('azure/config'); + this.config.backend = this.id; + + await render(hbs` + + `); + await fillInAzureConfig('azure'); + await click(SES.wif.accessType('wif')); + await fillInAzureConfig('withWif'); + await click(SES.wif.accessType('azure')); + + assert + .dom(GENERAL.toggleInput('Root password TTL')) + .isNotChecked('rootPasswordTtl is cleared after toggling accessType'); + assert + .dom(GENERAL.inputByAttr('clientSecret')) + .hasValue('', 'clientSecret is cleared after toggling accessType'); + + await click(SES.wif.accessType('wif')); + assert + .dom(GENERAL.inputByAttr('issuer')) + .hasValue('', 'issuer shows no value after toggling accessType'); + assert + .dom(GENERAL.inputByAttr('issuer')) + .hasAttribute( + 'placeholder', + 'https://vault-test.com', + 'issuer shows no value after toggling accessType' + ); + assert + .dom(GENERAL.inputByAttr('identityTokenAudience')) + .hasValue('', 'idTokenAudience is cleared after toggling accessType'); + assert + .dom(GENERAL.toggleInput('Identity token TTL')) + .isNotChecked('identityTokenTtl is cleared after toggling accessType'); + }); + + test('it transitions without sending a config or issuer payload on cancel', async function (assert) { + assert.expect(3); + this.id = `azure-${this.uid}`; + this.displayName = 'Azure'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('azure/config'); + this.config.backend = this.id; + + await render(hbs` + + `); + this.server.post(configUrl('azure', this.id), () => { + assert.notOk( + true, + 'post request was made to config when user canceled out of flow. test should fail.' + ); + }); + this.server.post('/identity/oidc/config', () => { + assert.notOk( + true, + 'post request was made to save issuer when user canceled out of flow. test should fail.' + ); + }); + await fillInAzureConfig('withWif'); + await click(GENERAL.cancelButton); + + assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); + assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.'); + + assert.ok( + this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), + 'Transitioned to the configuration index route.' + ); + }); + + test('it throws an error if the getter isWifPluginConfigured is not defined on the model', async function (assert) { + const promise = waitForError(); + this.id = `azure-${this.uid}`; + this.displayName = 'Azure'; + // creating a config that exists but will not have the attribute isWifPluginConfigured on it + this.config = this.store.createRecord('ssh/ca-config', { backend: this.id }); + await render(hbs` + + `); + const err = await promise; + assert.ok( + err.message.includes( + `'isWifPluginConfigured' is required to be defined on the config model. Must return a boolean.` + ), + 'asserts without isWifPluginConfigured' + ); + }); + }); + + module('AWS specific', function () { + test('it clears access type inputs after toggling accessType', async function (assert) { + this.id = `aws-${this.uid}`; + this.displayName = 'AWS'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('aws/root-config'); + this.config.backend = this.id; + + await render(hbs` + + `); + await fillInAwsConfig('aws'); + await click(SES.wif.accessType('wif')); + await fillInAwsConfig('with-wif'); + await click(SES.wif.accessType('aws')); + + assert + .dom(GENERAL.inputByAttr('accessKey')) + .hasValue('', 'accessKey is cleared after toggling accessType'); + + await click(SES.wif.accessType('wif')); + assert + .dom(GENERAL.inputByAttr('issuer')) + .hasValue('', 'issuer shows no value after toggling accessType'); + assert + .dom(GENERAL.inputByAttr('issuer')) + .hasAttribute( + 'placeholder', + 'https://vault-test.com', + 'issuer shows no value after toggling accessType' + ); + assert + .dom(GENERAL.inputByAttr('identityTokenAudience')) + .hasValue('', 'idTokenAudience is cleared after toggling accessType'); + assert + .dom(GENERAL.toggleInput('Identity token TTL')) + .isNotChecked('identityTokenTtl is cleared after toggling accessType'); + }); + + test('it shows validation error if default lease is entered but max lease is not', async function (assert) { + this.id = `aws-${this.uid}`; + this.displayName = 'AWS'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('aws/root-config'); + this.secondConfig = this.store.createRecord('aws/lease-config'); + this.config.backend = this.secondConfig.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) + + await render(hbs` + + `); + this.server.post(configUrl('aws-lease', this.id), () => { + assert.false( + true, + 'post request was made to config/lease when no data was changed. test should fail.' + ); + }); + this.server.post(configUrl('aws', this.id), () => { + assert.false( + true, + 'post request was made to config/root when no data was changed. test should fail.' + ); + }); + await click(GENERAL.ttl.toggle('Default Lease TTL')); + await fillIn(GENERAL.ttl.input('Default Lease TTL'), '33'); + await click(GENERAL.saveButton); + assert + .dom(GENERAL.inlineError) + .hasText('Lease TTL and Max Lease TTL are both required if one of them is set.'); + assert.dom(SES.configureForm).exists('remains on the configuration form'); + }); + + test('it surfaces the API error if one occurs on root/config, preventing user from transitioning', async function (assert) { + this.id = `aws-${this.uid}`; + this.displayName = 'AWS'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('aws/root-config'); + this.secondConfig = this.store.createRecord('aws/lease-config'); + this.config.backend = this.secondConfig.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) + + await render(hbs` + + `); + this.server.post(configUrl('aws', this.id), () => { + return overrideResponse(400, { errors: ['bad request'] }); + }); + this.server.post(configUrl('aws-lease', this.id), () => { + assert.true( + true, + 'post request was made to config/lease when config/root failed. test should pass.' + ); + }); + // fill in both lease and root endpoints to ensure that both payloads are attempted to be sent + await fillInAwsConfig('withAccess'); + await fillInAwsConfig('withLease'); + await click(GENERAL.saveButton); + assert.dom(GENERAL.messageError).exists('API error surfaced to user'); + assert.dom(GENERAL.inlineError).exists('User shown inline error message'); + }); + + test('it allows user to submit root config even if API error occurs on config/lease config', async function (assert) { + this.id = `aws-${this.uid}`; + this.displayName = 'AWS'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('aws/root-config'); + this.secondConfig = this.store.createRecord('aws/lease-config'); + this.config.backend = this.secondConfig.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) + + await render(hbs` + + `); + this.server.post(configUrl('aws', this.id), () => { + assert.true( + true, + 'post request was made to config/root when config/lease failed. test should pass.' + ); + }); + this.server.post(configUrl('aws-lease', this.id), () => { + return overrideResponse(400, { errors: ['bad request!!'] }); + }); + // fill in both lease and root endpoints to ensure that both payloads are attempted to be sent + await fillInAwsConfig('withAccess'); + await fillInAwsConfig('withLease'); + await click(GENERAL.saveButton); + assert.true( + this.flashDangerSpy.calledWith('Lease configuration was not saved: bad request!!'), + 'Flash message shows that lease was not saved.' + ); + assert.ok( + this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), + 'Transitioned to the configuration index route.' + ); + }); + + test('it allows user to submit root config even if API error occurs on issuer config', async function (assert) { + this.id = `aws-${this.uid}`; + this.displayName = 'AWS'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('aws/root-config'); + this.secondConfig = this.store.createRecord('aws/lease-config'); + this.config.backend = this.secondConfig.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) + + await render(hbs` + + `); + this.server.post(configUrl('aws', this.id), () => { + assert.true(true, 'post request was made to config/root when issuer failed. test should pass.'); + }); + this.server.post('/identity/oidc/config', () => { + return overrideResponse(400, { errors: ['bad request'] }); + }); + await fillInAwsConfig('withWif'); + await click(GENERAL.saveButton); + await click(SES.wif.issuerWarningSave); + + assert.true( + this.flashDangerSpy.calledWith('Issuer was not saved: bad request'), + 'Flash message shows that issuer was not saved' + ); + assert.true( + this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`), + 'Flash message shows that root was saved even if issuer was not' + ); + assert.ok( + this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), + 'Transitioned to the configuration index route.' + ); + }); + + test('it transitions without sending a lease, root, or issuer payload on cancel', async function (assert) { + this.id = `aws-${this.uid}`; + this.displayName = 'AWS'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('aws/root-config'); + this.secondConfig = this.store.createRecord('aws/lease-config'); + this.config.backend = this.secondConfig.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) + + await render(hbs` + + `); + this.server.post(configUrl('aws', this.id), () => { + assert.true( + false, + 'post request was made to config/root when user canceled out of flow. test should fail.' + ); + }); + this.server.post(configUrl('aws-lease', this.id), () => { + assert.true( + false, + 'post request was made to config/lease when user canceled out of flow. test should fail.' + ); + }); + this.server.post('/identity/oidc/config', () => { + assert.true( + false, + 'post request was made to save issuer when user canceled out of flow. test should fail.' + ); + }); + // fill in both lease and root endpoints to ensure that both payloads are attempted to be sent + await fillInAwsConfig('withWif'); + await fillInAwsConfig('withLease'); + await click(GENERAL.cancelButton); + + assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); + assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.'); + assert.ok( + this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), + 'Transitioned to the configuration index route.' + ); + }); + }); + + module('Issuer field tests', function () { + // only need to test one engine because code is the same for all engines. + test('if issuer API error and user changes issuer value, shows specific warning message', async function (assert) { + this.id = `azure-${this.uid}`; + this.displayName = 'Azure'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.issuerConfig.queryIssuerError = true; + this.config = this.store.createRecord('azure/config'); + this.config.backend = this.id; + + await render(hbs` + + `); + await click(SES.wif.accessType('wif')); + await fillIn(GENERAL.inputByAttr('issuer'), 'http://change.me.no.read'); + await click(GENERAL.saveButton); + assert + .dom(SES.wif.issuerWarningMessage) + .hasText( + `You are updating the global issuer config. This will overwrite Vault's current issuer if it exists and may affect other configurations using this value. Continue?`, + 'modal shows message about overwriting value if it exists' + ); + }); + + test('it shows placeholder issuer, and does not call APIs on canceling out of issuer modal', async function (assert) { + this.server.post('/identity/oidc/config', () => { + assert.notOk(true, 'request should not be made to issuer config endpoint'); + }); + this.server.post(configUrl('azure', this.id), () => { + assert.notOk( + true, + 'post request was made to config/ when user canceled out of flow. test should fail.' + ); + }); + this.id = `azure-${this.uid}`; + this.displayName = 'Azure'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('azure/config'); + this.config.backend = this.id; + + await render(hbs` + + `); + await click(SES.wif.accessType('wif')); + assert + .dom(GENERAL.inputByAttr('issuer')) + .hasAttribute('placeholder', 'https://vault-test.com', 'shows issuer placeholder'); + assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'shows issuer is empty when not passed'); + await fillIn(GENERAL.inputByAttr('issuer'), 'http://bar.foo'); + await click(GENERAL.saveButton); + assert.dom(SES.wif.issuerWarningMessage).exists('issuer modal exists'); + assert + .dom(SES.wif.issuerWarningMessage) + .hasText( + `You are updating the global issuer config. This will overwrite Vault's current issuer and may affect other configurations using this value. Continue?`, + 'modal shows message about overwriting value without the noRead: "if it exists" adage' + ); + await click(SES.wif.issuerWarningCancel); + assert.dom(SES.wif.issuerWarningMessage).doesNotExist('issuer modal is removed on cancel'); + assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); + assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.'); + assert.true(this.transitionStub.notCalled, 'Does not redirect'); + }); + + test('it shows modal when updating issuer and calls correct APIs on save', async function (assert) { + const newIssuer = `http://bar.${uuidv4()}`; + this.server.post('/identity/oidc/config', (schema, req) => { + const payload = JSON.parse(req.requestBody); + assert.deepEqual(payload, { issuer: newIssuer }, 'payload for issuer is correct'); + return { + id: 'identity-oidc-config', // id needs to match the id on secret-engine-helpers createIssuerConfig + data: null, + warnings: [ + 'If "issuer" is set explicitly, all tokens must be validated against that address, including those issued by secondary clusters. Setting issuer to "" will restore the default behavior of using the cluster\'s api_addr as the issuer.', + ], + }; + }); + this.server.post(configUrl('azure', this.id), () => { + assert.notOk(true, 'skips request to config because the model was not changed'); + }); + this.id = `azure-${this.uid}`; + this.displayName = 'Azure'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('azure/config'); + this.config.backend = this.id; + + await render(hbs` + + `); + await click(SES.wif.accessType('wif')); + assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'issuer defaults to empty string'); + await fillIn(GENERAL.inputByAttr('issuer'), newIssuer); + await click(GENERAL.saveButton); + assert.dom(SES.wif.issuerWarningMessage).exists('issuer warning modal exists'); + + await click(SES.wif.issuerWarningSave); + assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); + assert.true( + this.flashSuccessSpy.calledWith('Issuer saved successfully'), + 'Success flash message called for issuer' + ); + assert.true( + this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), + 'Transitioned to the configuration index route.' + ); + }); + + test('shows modal when modifying the issuer, has correct payload, and shows flash message on fail', async function (assert) { + assert.expect(7); + this.id = `azure-${this.uid}`; + this.displayName = 'Azure'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('azure/config'); + this.config.backend = this.id; + + this.server.post(configUrl('azure', this.id), () => { + assert.true( + true, + 'post request was made to azure config when unsetting the issuer. test should pass.' + ); + }); + this.server.post('/identity/oidc/config', (_, req) => { + const payload = JSON.parse(req.requestBody); + assert.deepEqual(payload, { issuer: 'http://foo.bar' }, 'correctly sets the issuer'); + return overrideResponse(403); + }); + + await render(hbs` + + `); + await click(SES.wif.accessType('wif')); + assert.dom(GENERAL.inputByAttr('issuer')).hasValue(''); + + await fillIn(GENERAL.inputByAttr('issuer'), 'http://foo.bar'); + await fillIn(GENERAL.inputByAttr('identityTokenAudience'), 'some-value'); + await click(GENERAL.saveButton); + assert.dom(SES.wif.issuerWarningMessage).exists('issuer warning modal exists'); + await click(SES.wif.issuerWarningSave); + + assert.true( + this.flashDangerSpy.calledWith('Issuer was not saved: permission denied'), + 'shows danger flash for issuer save' + ); + assert.true( + this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`), + "calls the config flash message not the issuer's" + ); + assert.ok( + this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), + 'Transitioned to the configuration index route.' + ); + }); + + test('it does not clear global issuer when toggling accessType', async function (assert) { + this.id = `azure-${this.uid}`; + this.displayName = 'Azure'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord(`azure/config`); + this.config.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) + await render(hbs` + + `); + await click(SES.wif.accessType('wif')); + assert + .dom(GENERAL.inputByAttr('issuer')) + .hasValue(this.issuerConfig.issuer, 'issuer is what is sent in by the model on first load'); + await fillIn(GENERAL.inputByAttr('issuer'), 'http://ive-changed'); + await click(SES.wif.accessType('azure')); + await click(SES.wif.accessType('wif')); + assert + .dom(GENERAL.inputByAttr('issuer')) + .hasValue( + this.issuerConfig.issuer, + 'issuer value is still the same global value after toggling accessType' + ); + }); + }); + }); + + module('isCommunity', function (hooks) { + hooks.beforeEach(function () { + this.version.type = 'community'; + }); + + for (const type of WIF_ENGINES) { + test(`${type}: it renders fields`, async function (assert) { + this.id = `${type}-${this.uid}`; + this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = + type === 'aws' + ? this.store.createRecord('aws/root-config') + : type === 'ssh' + ? this.store.createRecord('ssh/ca-config') + : this.store.createRecord(`${type}/config`); + this.secondConfig = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; + this.config.backend = this.id; + this.secondConfig ? (this.secondConfig.backend = this.id) : null; + this.type = type; // required to set on this to pass into the component + + await render(hbs` + + `); + assert.dom(SES.configureForm).exists(`lands on the ${type} configuration form`); + assert + .dom(SES.wif.accessTypeSection) + .doesNotExist('Access type section does not render for a community user'); + // toggle grouped fields if it exists + const toggleGroup = document.querySelector('[data-test-toggle-group]'); + 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.`); + } + assert.dom(GENERAL.inputByAttr('issuer')).doesNotExist(); + }); + } + }); + }); + + module('Edit view', function () { + module('isEnterprise', function (hooks) { + hooks.beforeEach(function () { + this.version.type = 'enterprise'; + }); + for (const type of WIF_ENGINES) { + test(`${type}: it defaults to WIF accessType if WIF fields are already set`, async function (assert) { + this.id = `${type}-${this.uid}`; + this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.config = createConfig(this.store, this.id, `${type}-wif`); + this.type = type; + await render(hbs` + + `); + assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked'); + assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled'); + assert.dom(SES.wif.accessType(type)).isNotChecked(`${type} accessType is not checked`); + assert.dom(SES.wif.accessType(type)).isDisabled(`${type} accessType is disabled`); + assert + .dom(GENERAL.inputByAttr('identityTokenAudience')) + .hasValue(this.config.identityTokenAudience); + assert + .dom(SES.wif.accessTypeSubtext) + .hasText('You cannot edit Access Type if you have already saved access credentials.'); + assert.dom(GENERAL.ttl.input('Identity token TTL')).hasValue('2'); // 7200 on payload is 2hrs in ttl picker + }); + } + + for (const type of WIF_ENGINES) { + test(`${type}: it renders issuer if global issuer is already set`, async function (assert) { + this.id = `${type}-${this.uid}`; + this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.config = createConfig(this.store, this.id, `${type}-wif`); + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.issuerConfig.issuer = 'https://foo-bar-blah.com'; + this.type = type; + await render(hbs` + + `); + + assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked'); + assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled'); + assert + .dom(GENERAL.inputByAttr('issuer')) + .hasValue( + this.issuerConfig.issuer, + `it has the global issuer value of ${this.issuerConfig.issuer}` + ); + }); + } + + module('Azure specific', function () { + test('it defaults to Azure accessType if Azure account fields are already set', async function (assert) { + this.id = `azure-${this.uid}`; + this.config = createConfig(this.store, this.id, 'azure'); + await render(hbs` + + `); + + assert.dom(SES.wif.accessType('azure')).isChecked('Azure accessType is checked'); + assert.dom(SES.wif.accessType('azure')).isDisabled('Azure accessType is disabled'); + assert.dom(SES.wif.accessType('wif')).isNotChecked('WIF accessType is not checked'); + assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled'); + assert + .dom(SES.wif.accessTypeSubtext) + .hasText('You cannot edit Access Type if you have already saved access credentials.'); + }); + + test('it allows you to change accessType if record does not have wif or azure values already set', async function (assert) { + this.id = `azure-${this.uid}`; + this.config = createConfig(this.store, this.id, 'azure-generic'); + await render(hbs` + + `); + + assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled'); + assert.dom(SES.wif.accessType('azure')).isNotDisabled('Azure accessType is NOT disabled'); + }); + + test('it shows previously saved config information', async function (assert) { + this.id = `azure-${this.uid}`; + this.config = createConfig(this.store, this.id, 'azure-generic'); + await render(hbs` + + `); + assert.dom(GENERAL.inputByAttr('subscriptionId')).hasValue(this.config.subscriptionId); + assert.dom(GENERAL.inputByAttr('clientId')).hasValue(this.config.clientId); + assert.dom(GENERAL.inputByAttr('tenantId')).hasValue(this.config.tenantId); + assert + .dom(GENERAL.inputByAttr('clientSecret')) + .hasValue('**********', 'clientSecret is masked on edit the value'); + }); + + test('it requires a double click to change the client secret', async function (assert) { + this.id = `azure-${this.uid}`; + this.config = createConfig(this.store, this.id, 'azure'); + await render(hbs` + + `); + + this.server.post(configUrl('azure', this.id), (schema, req) => { + const payload = JSON.parse(req.requestBody); + assert.strictEqual( + payload.client_secret, + 'new-secret', + 'post request was made to azure/config with the updated client_secret.' + ); + }); + + await click(GENERAL.enableField('clientSecret')); + await click('[data-test-button="toggle-masked"]'); + await fillIn(GENERAL.inputByAttr('clientSecret'), 'new-secret'); + await click(GENERAL.saveButton); + }); + }); + + module('AWS specific', function () { + test('it defaults to IAM accessType if IAM fields are already set', async function (assert) { + this.id = `aws-${this.uid}`; + this.config = createConfig(this.store, this.id, 'aws'); + await render(hbs` + + `); + assert.dom(SES.wif.accessType('aws')).isChecked('IAM accessType is checked'); + assert.dom(SES.wif.accessType('aws')).isDisabled('IAM accessType is disabled'); + assert.dom(SES.wif.accessType('wif')).isNotChecked('WIF accessType is not checked'); + assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled'); + assert + .dom(SES.wif.accessTypeSubtext) + .hasText('You cannot edit Access Type if you have already saved access credentials.'); + }); + + test('it allows you to change access type if record does not have wif or iam values already set', async function (assert) { + this.id = `aws-${this.uid}`; + this.config = createConfig(this.store, this.id, 'aws-no-access'); + await render(hbs` + + `); + assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled'); + assert.dom(SES.wif.accessType('aws')).isNotDisabled('IAM accessType is NOT disabled'); + }); + + test('it shows previously saved root and lease information', async function (assert) { + this.id = `aws-${this.uid}`; + this.config = createConfig(this.store, this.id, 'aws'); + this.secondConfig = createConfig(this.store, this.id, 'aws-lease'); + await render(hbs` + + `); + + assert.dom(GENERAL.inputByAttr('accessKey')).hasValue(this.config.accessKey); + assert + .dom(GENERAL.inputByAttr('secretKey')) + .hasValue('**********', 'secretKey is masked on edit the value'); + + await click(GENERAL.toggleGroup('Root config options')); + assert.dom(GENERAL.inputByAttr('region')).hasValue(this.config.region); + assert.dom(GENERAL.inputByAttr('iamEndpoint')).hasValue(this.config.iamEndpoint); + assert.dom(GENERAL.inputByAttr('stsEndpoint')).hasValue(this.config.stsEndpoint); + assert.dom(GENERAL.inputByAttr('maxRetries')).hasValue('1'); + // Check lease config values + assert.dom(GENERAL.ttl.input('Default Lease TTL')).hasValue('50'); + assert.dom(GENERAL.ttl.input('Max Lease TTL')).hasValue('55'); + }); + + test('it requires a double click to change the secret key', async function (assert) { + this.id = `aws-${this.uid}`; + this.config = createConfig(this.store, this.id, 'aws'); + await render(hbs` + + `); + + this.server.post(configUrl('aws', this.id), (schema, req) => { + const payload = JSON.parse(req.requestBody); + assert.strictEqual( + payload.secret_key, + 'new-secret', + 'post request was made to config/root with the updated secret_key.' + ); + }); + + await click(GENERAL.enableField('secretKey')); + await click('[data-test-button="toggle-masked"]'); + await fillIn(GENERAL.inputByAttr('secretKey'), 'new-secret'); + await click(GENERAL.saveButton); + }); + }); + }); + + module('isCommunity', function (hooks) { + hooks.beforeEach(function () { + this.version.type = 'community'; + }); + for (const type of WIF_ENGINES) { + test(`${type}:it does not show access types but defaults to type account fields`, async function (assert) { + this.id = `${type}-${this.uid}`; + this.config = createConfig(this.store, this.id, `${type}-generic`); + this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.type = type; + await render(hbs` + + `); + assert.dom(SES.wif.accessTypeSection).doesNotExist('Access type section does not render'); + // toggle grouped fields if it exists + const toggleGroup = document.querySelector('[data-test-toggle-group]'); + 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.config[key], `${key} for ${type}: has the expected value set on the config`); + } + }); + } + }); + }); +}); diff --git a/ui/types/vault/models/aws/lease-config.d.ts b/ui/types/vault/models/aws/lease-config.d.ts deleted file mode 100644 index f08995c1c9e7..000000000000 --- a/ui/types/vault/models/aws/lease-config.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Model from '@ember-data/model'; -import type { ModelValidations } from 'vault/vault/app-types'; - -export default class AwsLeaseConfig extends Model { - backend: any; - leaseMax: any; - lease: any; - get attrs(): string[]; - // for some reason the following Model attrs don't exist on the Model definition - changedAttributes(): { - [key: string]: unknown[]; - }; - isNew: boolean; - save(): void; - unloadRecord(): void; - validate(): ModelValidations; -} diff --git a/ui/types/vault/models/aws/root-config.d.ts b/ui/types/vault/models/aws/root-config.d.ts deleted file mode 100644 index 87a300450be1..000000000000 --- a/ui/types/vault/models/aws/root-config.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import type Model from '@ember-data/model'; - -export default class AwsRootConfig extends Model { - backend: any; - accessKey: any; - secretKey: any; - roleArn: any; - identityTokenAudience: any; - identityTokenTtl: any; - region: any; - iamEndpoint: any; - stsEndpoint: any; - maxRetries: any; - get attrs(): any; - get fieldGroupsWif(): any; - get fieldGroupsIam(): any; - formFieldGroups(accessType?: string): { - [key: string]: string[]; - }[]; - // for some reason the following Model attrs don't exist on the Model definition - changedAttributes(): { - [key: string]: unknown[]; - }; - isNew: boolean; - save(): void; - unloadRecord(): void; -} diff --git a/ui/types/vault/models/azure/config.d.ts b/ui/types/vault/models/secret-engine/config.d.ts similarity index 57% rename from ui/types/vault/models/azure/config.d.ts rename to ui/types/vault/models/secret-engine/config.d.ts index 70decffc3440..aee77c60e89e 100644 --- a/ui/types/vault/models/azure/config.d.ts +++ b/ui/types/vault/models/secret-engine/config.d.ts @@ -6,22 +6,43 @@ import type Model from '@ember-data/model'; import type { ModelValidations } from 'vault/app-types'; -export default class AzureConfig extends Model { +export default class SecretEngineConfig extends Model { backend: string; + type: string; + // aws + leaseMax: any; + lease: any; + accessKey: any; + secretKey: any; + roleArn: any; + region: any; + iamEndpoint: any; + stsEndpoint: any; + maxRetries: any; + // azure subscriptionId: string | undefined; tenantId: string | undefined; clientId: string | undefined; clientSecret: string | undefined; - identityTokenAudience: string | undefined; - identityTokenTtl: any; environment: string | undefined; rootPasswordTtl: string | undefined; + // gcp + credentials: string | undefined; + ttl: any; + maxTtl: any; + secretAccountEmail: string | undefined; + modelNameDisplay: string | undefined; + // wif + identityTokenAudience: string | undefined; + identityTokenTtl: any; get displayAttrs(): any; get isWifPluginConfigured(): boolean; - get isAzureAccountConfigured(): boolean; + get isAccountPluginConfigured(): boolean; get fieldGroupsWif(): any; get fieldGroupsAzure(): any; + get fieldGroupsGcp(): any; + get fieldGroupsIam(): any; formFieldGroups(accessType?: string): { [key: string]: string[]; }[]; @@ -31,4 +52,7 @@ export default class AzureConfig extends Model { isNew: boolean; save(): void; unloadRecord(): void; + destroyRecord(): void; + rollbackAttributes(): void; + validate(): ModelValidations; } From eda5a70562016bab2ce9c1d7957044096641bcb0 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Fri, 17 Jan 2025 13:30:50 -0800 Subject: [PATCH 02/16] make access type subtext dynamic based on model type --- .../secret-engine/configure-wif.hbs | 6 +- .../components/secret-engine/configure-wif.ts | 10 +- .../backend/aws/aws-configuration-test.js | 4 +- .../secret-engine/configure-wif-test.js | 272 ++++++++++-------- 4 files changed, 166 insertions(+), 126 deletions(-) diff --git a/ui/app/components/secret-engine/configure-wif.hbs b/ui/app/components/secret-engine/configure-wif.hbs index 53bea2bc94b1..27bce5e04c9f 100644 --- a/ui/app/components/secret-engine/configure-wif.hbs +++ b/ui/app/components/secret-engine/configure-wif.hbs @@ -34,8 +34,8 @@ {{else}} Choose the way to configure access to {{@displayName}}. Access can be configured either using an - {{@displayName}} - account credentials or with the Plugin Workload Identity Federation (WIF). + {{if (eq @type "aws") "IAM access keys" (concat @displayName " account credentials")}}, or with the Plugin + Workload Identity Federation (WIF). {{/if}}

@@ -79,7 +79,7 @@ @groupName="fieldGroupsAccount" /> {{/if}} - {{! secondModel fields should show regardless of the vault version or what access type is selected }} + {{! secondModel fields show regardless of the vault version or what access type is selected }} {{#if @secondModel}}

{{or @secondModel.modelNameDisplay "Additional"}} diff --git a/ui/app/components/secret-engine/configure-wif.ts b/ui/app/components/secret-engine/configure-wif.ts index ee79cdec4638..23abce09f2b8 100644 --- a/ui/app/components/secret-engine/configure-wif.ts +++ b/ui/app/components/secret-engine/configure-wif.ts @@ -37,11 +37,11 @@ import type FlashMessageService from 'vault/services/flash-messages'; /> * * @param {string} backendPath - name of the secret engine, ex: 'azure-123'. - * @param {string} displayName - Azure vs azure or AWS vs aws. Used for display purposes. - * @param {string} type - The type of the engine, ex: 'azure'. - * @param {object} model - The config model for the engine. - * @param {object} [secondModel] - For engines with two config models. Currently, only used by aws - * @param {object} [issuerConfig] - The identity/oidc/config model. Will be passed in if user has an enterprise license. + * @param {string} displayName - used for flash messages, subText and labels. ex: 'Azure'. + * @param {string} type - the type of the engine, ex: 'azure'. + * @param {object} model - the config model for the engine. + * @param {object} [secondModel] - tor engines with two config models. Currently, only used by aws + * @param {object} [issuerConfig] - the identity/oidc/config model. Will be passed in if user has an enterprise license. * @param {string} [modelNameDisplay] - Specific h2 title to display above the second model's fields. Also used in flash message error if saving the second modal fails */ diff --git a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js index 56ec53ca710f..38c7685495ce 100644 --- a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js +++ b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js @@ -65,7 +65,7 @@ module('Acceptance | aws | configuration', function (hooks) { assert.dom(SES.configureForm).exists('it lands on the configuration form.'); assert .dom(SES.secondModelTitle) - .hasText('Lease Configuration', 'it shows the lease configuration section with correct title.'); + .hasText('Lease Configuration', 'it shows the lease configuration section with the lease title.'); // cleanup await runCmd(`delete sys/mounts/${path}`); }); @@ -102,7 +102,7 @@ module('Acceptance | aws | configuration', function (hooks) { assert.strictEqual( this.flashSuccessSpy.args[1][0], `Successfully saved ${path}'s configuration.`, - 'first flash message about the first model config.' + 'first success message is about the first model config.' ); assert.strictEqual( this.flashSuccessSpy.args[2][0], diff --git a/ui/tests/integration/components/secret-engine/configure-wif-test.js b/ui/tests/integration/components/secret-engine/configure-wif-test.js index 517c9c03cd48..5ab2bf2ee38e 100644 --- a/ui/tests/integration/components/secret-engine/configure-wif-test.js +++ b/ui/tests/integration/components/secret-engine/configure-wif-test.js @@ -24,7 +24,7 @@ import { capabilitiesStub } from 'vault/tests/helpers/stubs'; import { WIF_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines'; import waitForError from 'vault/tests/helpers/wait-for-error'; -const allEnginesArray = allEngines(); // saving as const so we don't invoke the method multiple times via the for loop +const allEnginesArray = allEngines(); // saving as const so we don't invoke the method multiple times in the for loop module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { setupRenderingTest(hooks); @@ -48,6 +48,7 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { hooks.beforeEach(function () { this.version.type = 'enterprise'; }); + for (const type of WIF_ENGINES) { test(`${type}: it renders default fields`, async function (assert) { this.id = `${type}-${this.uid}`; @@ -60,7 +61,7 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.secondConfig = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; this.config.backend = this.id; this.secondConfig ? (this.secondConfig.backend = this.id) : null; // Add backend to the configs because it's not on the testing snapshot (would come from url) - this.type = type; // required to set on this to pass into the component + this.type = type; await render(hbs` @@ -84,6 +85,7 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { .doesNotExist(`for ${type}, the issuer does not show when wif is not the access type`); }); } + for (const type of WIF_ENGINES) { test(`${type}: it renders wif fields when user selects wif access type`, async function (assert) { this.id = `${type}-${this.uid}`; @@ -95,8 +97,8 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { : this.store.createRecord(`${type}/config`); this.secondConfig = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; this.config.backend = this.id; - this.secondConfig ? (this.secondConfig.backend = this.id) : null; // Add backend to the configs because it's not on the testing snapshot (would come from url) - this.type = type; // required to set on this to pass into the component + this.secondConfig ? (this.secondConfig.backend = this.id) : null; + this.type = type; await render(hbs` @@ -113,49 +115,9 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { assert.dom(GENERAL.inputByAttr('issuer')).exists(`issuer shows for ${type} wif section.`); }); } - /* For tests relevant to all engines, they are run once within one of the engine specific modules below */ - module('Azure specific', function () { - test('it clears access type inputs after toggling accessType', async function (assert) { - this.id = `azure-${this.uid}`; - this.displayName = 'Azure'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('azure/config'); - this.config.backend = this.id; - - await render(hbs` - - `); - await fillInAzureConfig('azure'); - await click(SES.wif.accessType('wif')); - await fillInAzureConfig('withWif'); - await click(SES.wif.accessType('azure')); - - assert - .dom(GENERAL.toggleInput('Root password TTL')) - .isNotChecked('rootPasswordTtl is cleared after toggling accessType'); - assert - .dom(GENERAL.inputByAttr('clientSecret')) - .hasValue('', 'clientSecret is cleared after toggling accessType'); - - await click(SES.wif.accessType('wif')); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasValue('', 'issuer shows no value after toggling accessType'); - assert - .dom(GENERAL.inputByAttr('issuer')) - .hasAttribute( - 'placeholder', - 'https://vault-test.com', - 'issuer shows no value after toggling accessType' - ); - assert - .dom(GENERAL.inputByAttr('identityTokenAudience')) - .hasValue('', 'idTokenAudience is cleared after toggling accessType'); - assert - .dom(GENERAL.toggleInput('Identity token TTL')) - .isNotChecked('identityTokenTtl is cleared after toggling accessType'); - }); + module('Engine agnostic', function () { + /* This module covers code that is the same for all engines. We run them once against one of the engines.*/ test('it transitions without sending a config or issuer payload on cancel', async function (assert) { assert.expect(3); this.id = `azure-${this.uid}`; @@ -208,6 +170,130 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { 'asserts without isWifPluginConfigured' ); }); + + test('it allows user to submit the config even if API error occurs on issuer config', async function (assert) { + this.id = `aws-${this.uid}`; + this.displayName = 'AWS'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('aws/root-config'); + this.secondConfig = this.store.createRecord('aws/lease-config'); + this.config.backend = this.secondConfig.backend = this.id; + + await render(hbs` + + `); + this.server.post(configUrl('aws', this.id), () => { + assert.true(true, 'post request was made to config/root when issuer failed. test should pass.'); + }); + this.server.post('/identity/oidc/config', () => { + return overrideResponse(400, { errors: ['bad request'] }); + }); + await fillInAwsConfig('withWif'); + await click(GENERAL.saveButton); + await click(SES.wif.issuerWarningSave); + + assert.true( + this.flashDangerSpy.calledWith('Issuer was not saved: bad request'), + 'Flash message shows that issuer was not saved' + ); + assert.true( + this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`), + 'Flash message shows that root was saved even if issuer was not' + ); + assert.ok( + this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), + 'Transitioned to the configuration index route.' + ); + }); + + test('it surfaces the API error if config save fails, and prevents the user from transitioning', async function (assert) { + this.id = `aws-${this.uid}`; + this.displayName = 'AWS'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('aws/root-config'); + this.secondConfig = this.store.createRecord('aws/lease-config'); + this.config.backend = this.secondConfig.backend = this.id; + + await render(hbs` + + `); + this.server.post(configUrl('aws', this.id), () => { + return overrideResponse(400, { errors: ['bad request'] }); + }); + this.server.post(configUrl('aws-lease', this.id), () => { + assert.true( + true, + 'post request was made to config/lease when config/root failed. test should pass.' + ); + }); + // fill in both lease and root endpoints to ensure that both payloads are attempted to be sent + await fillInAwsConfig('withAccess'); + await fillInAwsConfig('withLease'); + await click(GENERAL.saveButton); + assert.dom(GENERAL.messageError).exists('API error surfaced to user'); + assert.dom(GENERAL.inlineError).exists('User shown inline error message'); + }); + }); + + module('Azure specific', function () { + test('it clears access type inputs after toggling accessType', async function (assert) { + this.id = `azure-${this.uid}`; + this.displayName = 'Azure'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('azure/config'); + this.config.backend = this.id; + + await render(hbs` + + `); + await fillInAzureConfig('azure'); + await click(SES.wif.accessType('wif')); + await fillInAzureConfig('withWif'); + await click(SES.wif.accessType('azure')); + + assert + .dom(GENERAL.toggleInput('Root password TTL')) + .isNotChecked('rootPasswordTtl is cleared after toggling accessType'); + assert + .dom(GENERAL.inputByAttr('clientSecret')) + .hasValue('', 'clientSecret is cleared after toggling accessType'); + + await click(SES.wif.accessType('wif')); + assert + .dom(GENERAL.inputByAttr('issuer')) + .hasValue('', 'issuer shows no value after toggling accessType'); + assert + .dom(GENERAL.inputByAttr('issuer')) + .hasAttribute( + 'placeholder', + 'https://vault-test.com', + 'issuer shows no value after toggling accessType' + ); + assert + .dom(GENERAL.inputByAttr('identityTokenAudience')) + .hasValue('', 'idTokenAudience is cleared after toggling accessType'); + assert + .dom(GENERAL.toggleInput('Identity token TTL')) + .isNotChecked('identityTokenTtl is cleared after toggling accessType'); + }); + + test('it shows the correct access type subtext', async function (assert) { + this.id = `azure-${this.uid}`; + this.displayName = 'Azure'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('azure/config'); + this.config.backend = this.id; + + await render(hbs` + + `); + + assert + .dom(SES.wif.accessTypeSubtext) + .hasText( + 'Choose the way to configure access to Azure. Access can be configured either using an Azure account credentials, or with the Plugin Workload Identity Federation (WIF).' + ); + }); }); module('AWS specific', function () { @@ -249,13 +335,31 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { .isNotChecked('identityTokenTtl is cleared after toggling accessType'); }); + test('it shows the correct access type subtext', async function (assert) { + this.id = `aws-${this.uid}`; + this.displayName = 'AWS'; + this.issuerConfig = createConfig(this.store, this.id, 'issuer'); + this.config = this.store.createRecord('aws/root-config'); + this.config.backend = this.id; + + await render(hbs` + + `); + + assert + .dom(SES.wif.accessTypeSubtext) + .hasText( + 'Choose the way to configure access to AWS. Access can be configured either using an IAM access keys, or with the Plugin Workload Identity Federation (WIF).' + ); + }); + test('it shows validation error if default lease is entered but max lease is not', async function (assert) { this.id = `aws-${this.uid}`; this.displayName = 'AWS'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); this.config = this.store.createRecord('aws/root-config'); this.secondConfig = this.store.createRecord('aws/lease-config'); - this.config.backend = this.secondConfig.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) + this.config.backend = this.secondConfig.backend = this.id; await render(hbs` @@ -281,41 +385,13 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { assert.dom(SES.configureForm).exists('remains on the configuration form'); }); - test('it surfaces the API error if one occurs on root/config, preventing user from transitioning', async function (assert) { - this.id = `aws-${this.uid}`; - this.displayName = 'AWS'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('aws/root-config'); - this.secondConfig = this.store.createRecord('aws/lease-config'); - this.config.backend = this.secondConfig.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) - - await render(hbs` - - `); - this.server.post(configUrl('aws', this.id), () => { - return overrideResponse(400, { errors: ['bad request'] }); - }); - this.server.post(configUrl('aws-lease', this.id), () => { - assert.true( - true, - 'post request was made to config/lease when config/root failed. test should pass.' - ); - }); - // fill in both lease and root endpoints to ensure that both payloads are attempted to be sent - await fillInAwsConfig('withAccess'); - await fillInAwsConfig('withLease'); - await click(GENERAL.saveButton); - assert.dom(GENERAL.messageError).exists('API error surfaced to user'); - assert.dom(GENERAL.inlineError).exists('User shown inline error message'); - }); - test('it allows user to submit root config even if API error occurs on config/lease config', async function (assert) { this.id = `aws-${this.uid}`; this.displayName = 'AWS'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); this.config = this.store.createRecord('aws/root-config'); this.secondConfig = this.store.createRecord('aws/lease-config'); - this.config.backend = this.secondConfig.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) + this.config.backend = this.secondConfig.backend = this.id; await render(hbs` @@ -343,48 +419,13 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { ); }); - test('it allows user to submit root config even if API error occurs on issuer config', async function (assert) { - this.id = `aws-${this.uid}`; - this.displayName = 'AWS'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('aws/root-config'); - this.secondConfig = this.store.createRecord('aws/lease-config'); - this.config.backend = this.secondConfig.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) - - await render(hbs` - - `); - this.server.post(configUrl('aws', this.id), () => { - assert.true(true, 'post request was made to config/root when issuer failed. test should pass.'); - }); - this.server.post('/identity/oidc/config', () => { - return overrideResponse(400, { errors: ['bad request'] }); - }); - await fillInAwsConfig('withWif'); - await click(GENERAL.saveButton); - await click(SES.wif.issuerWarningSave); - - assert.true( - this.flashDangerSpy.calledWith('Issuer was not saved: bad request'), - 'Flash message shows that issuer was not saved' - ); - assert.true( - this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`), - 'Flash message shows that root was saved even if issuer was not' - ); - assert.ok( - this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), - 'Transitioned to the configuration index route.' - ); - }); - test('it transitions without sending a lease, root, or issuer payload on cancel', async function (assert) { this.id = `aws-${this.uid}`; this.displayName = 'AWS'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); this.config = this.store.createRecord('aws/root-config'); this.secondConfig = this.store.createRecord('aws/lease-config'); - this.config.backend = this.secondConfig.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) + this.config.backend = this.secondConfig.backend = this.id; await render(hbs` @@ -422,7 +463,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); module('Issuer field tests', function () { - // only need to test one engine because code is the same for all engines. test('if issuer API error and user changes issuer value, shows specific warning message', async function (assert) { this.id = `azure-${this.uid}`; this.displayName = 'Azure'; @@ -579,7 +619,7 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.displayName = 'Azure'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); this.config = this.store.createRecord(`azure/config`); - this.config.backend = this.id; // Add backend to the configs because it's not on the testing snapshot (would come from url) + this.config.backend = this.id; await render(hbs` `); @@ -619,7 +659,7 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.secondConfig = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; this.config.backend = this.id; this.secondConfig ? (this.secondConfig.backend = this.id) : null; - this.type = type; // required to set on this to pass into the component + this.type = type; await render(hbs` @@ -835,7 +875,7 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.version.type = 'community'; }); for (const type of WIF_ENGINES) { - test(`${type}:it does not show access types but defaults to type account fields`, async function (assert) { + test(`${type}:it does not show access type but defaults to type "account" fields`, async function (assert) { this.id = `${type}-${this.uid}`; this.config = createConfig(this.store, this.id, `${type}-generic`); this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; From 06ae272773c5a27f5d510d5762aea7e65ce514b7 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Fri, 17 Jan 2025 13:54:12 -0800 Subject: [PATCH 03/16] clean up --- ui/app/components/secret-engine/configure-wif.ts | 2 +- .../vault/cluster/secrets/backend/configuration/edit.hbs | 2 +- .../secrets/backend/aws/aws-configuration-test.js | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ui/app/components/secret-engine/configure-wif.ts b/ui/app/components/secret-engine/configure-wif.ts index 23abce09f2b8..db58303e7b0c 100644 --- a/ui/app/components/secret-engine/configure-wif.ts +++ b/ui/app/components/secret-engine/configure-wif.ts @@ -131,7 +131,7 @@ export default class ConfigureWif extends Component { const modelAttrChanged = this.modelAttrChanged; const issuerAttrChanged = this.issuerAttrChanged; const secondModelAttrChanged = this.secondModelAttrChanged; - // check if any of the mode(s) or issuer attributes have changed + // check if any of the model(s) or issuer attributes have changed // if no changes, transition and notify user if (!modelAttrChanged && !issuerAttrChanged && !secondModelAttrChanged) { this.flashMessages.info('No changes detected.'); diff --git a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs index d1cb31eec7d5..a33ecf94c3f2 100644 --- a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs +++ b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs @@ -30,7 +30,7 @@ {{#if (eq this.model.type "ssh")}} - {{! This check is preventive. As of writing all other engines using this route——but ssh——are wif engines }} + {{! This "else if" check is preventive. As of writing, all engines using this route, but "ssh", are wif engines }} {{else if this.model.isWifEngine}} Date: Fri, 17 Jan 2025 14:15:49 -0800 Subject: [PATCH 04/16] clean up --- ui/app/models/aws/root-config.js | 2 +- ui/app/models/azure/config.js | 2 +- ui/types/vault/models/secret-engine/config.d.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/app/models/aws/root-config.js b/ui/app/models/aws/root-config.js index 9f0c37007268..405718ac9633 100644 --- a/ui/app/models/aws/root-config.js +++ b/ui/app/models/aws/root-config.js @@ -74,7 +74,7 @@ export default class AwsRootConfig extends Model { 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") + // "filedGroupsWif" and "fieldGroupsAccount" are passed to the FormFieldGroups component to determine which group to show in the form (ex: @groupName="fieldGroupsWif") get fieldGroupsWif() { return fieldToAttrs(this, this.formFieldGroups('wif')); } diff --git a/ui/app/models/azure/config.js b/ui/app/models/azure/config.js index 73796fd148a3..08f9a702ce3f 100644 --- a/ui/app/models/azure/config.js +++ b/ui/app/models/azure/config.js @@ -79,7 +79,7 @@ export default class AzureConfig extends Model { return formFields.filter((attr) => attr.name !== 'clientSecret'); } - // "filedGroupsWif" and "fieldGroupsAzure" are passed to the FormFieldGroups component to determine which group to show in the form (ex: @groupName="fieldGroupsWif") + // "filedGroupsWif" and "fieldGroupsAccount" are passed to the FormFieldGroups component to determine which group to show in the form (ex: @groupName="fieldGroupsWif") get fieldGroupsWif() { return fieldToAttrs(this, this.formFieldGroups('wif')); } diff --git a/ui/types/vault/models/secret-engine/config.d.ts b/ui/types/vault/models/secret-engine/config.d.ts index aee77c60e89e..636f93fd4b9c 100644 --- a/ui/types/vault/models/secret-engine/config.d.ts +++ b/ui/types/vault/models/secret-engine/config.d.ts @@ -37,12 +37,12 @@ export default class SecretEngineConfig extends Model { identityTokenTtl: any; get displayAttrs(): any; + get isConfigured(): boolean; get isWifPluginConfigured(): boolean; get isAccountPluginConfigured(): boolean; get fieldGroupsWif(): any; - get fieldGroupsAzure(): any; - get fieldGroupsGcp(): any; - get fieldGroupsIam(): any; + get fieldGroupsAccount(): any; + formFieldGroups(accessType?: string): { [key: string]: string[]; }[]; From 0c82bba26dea2a5740f0994cf81328b00eb56367 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Fri, 17 Jan 2025 14:22:16 -0800 Subject: [PATCH 05/16] remove model attr for display purposes --- .../components/secret-engine/configure-wif.hbs | 2 +- .../components/secret-engine/configure-wif.ts | 17 ++++++----------- ui/app/models/aws/lease-config.js | 2 -- ui/types/vault/models/secret-engine/config.d.ts | 1 - 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/ui/app/components/secret-engine/configure-wif.hbs b/ui/app/components/secret-engine/configure-wif.hbs index 27bce5e04c9f..9b434935a7b6 100644 --- a/ui/app/components/secret-engine/configure-wif.hbs +++ b/ui/app/components/secret-engine/configure-wif.hbs @@ -82,7 +82,7 @@ {{! secondModel fields show regardless of the vault version or what access type is selected }} {{#if @secondModel}}

- {{or @secondModel.modelNameDisplay "Additional"}} + {{if (eq @type "aws") "Lease" "Additional"}} Configuration

diff --git a/ui/app/components/secret-engine/configure-wif.ts b/ui/app/components/secret-engine/configure-wif.ts index db58303e7b0c..79c963a0fa83 100644 --- a/ui/app/components/secret-engine/configure-wif.ts +++ b/ui/app/components/secret-engine/configure-wif.ts @@ -42,7 +42,6 @@ import type FlashMessageService from 'vault/services/flash-messages'; * @param {object} model - the config model for the engine. * @param {object} [secondModel] - tor engines with two config models. Currently, only used by aws * @param {object} [issuerConfig] - the identity/oidc/config model. Will be passed in if user has an enterprise license. - * @param {string} [modelNameDisplay] - Specific h2 title to display above the second model's fields. Also used in flash message error if saving the second modal fails */ interface Args { @@ -182,22 +181,18 @@ export default class ConfigureWif extends Component { } async saveSecondModel(): Promise { - const { backendPath, secondModel } = this.args; + const { backendPath, secondModel, type } = this.args; + const secondModelName = type === 'aws' ? 'Lease configuration' : 'additional configuration'; try { await secondModel.save(); - this.flashMessages.success( - `Successfully saved ${backendPath}'s ${secondModel.modelNameDisplay?.toLowerCase()} configuration.` - ); + this.flashMessages.success(`Successfully saved ${backendPath}'s ${secondModelName}.`); return true; } catch (error) { this.errorMessage = errorMessage(error); // we transition even if the second model fails. surface a sticky flash message so the user can see it on the next view. - this.flashMessages.danger( - `${secondModel.modelNameDisplay} configuration was not saved: ${this.errorMessage}`, - { - sticky: true, - } - ); + this.flashMessages.danger(`${secondModelName} was not saved: ${this.errorMessage}`, { + sticky: true, + }); return false; } } diff --git a/ui/app/models/aws/lease-config.js b/ui/app/models/aws/lease-config.js index e1ce3f178357..472d3d172beb 100644 --- a/ui/app/models/aws/lease-config.js +++ b/ui/app/models/aws/lease-config.js @@ -43,6 +43,4 @@ export default class AwsLeaseConfig extends Model { get formFields() { return expandAttributeMeta(this, this.configurableParams); } - - modelNameDisplay = 'Lease'; // shows as Lease Configuration } diff --git a/ui/types/vault/models/secret-engine/config.d.ts b/ui/types/vault/models/secret-engine/config.d.ts index 636f93fd4b9c..1613b97c20eb 100644 --- a/ui/types/vault/models/secret-engine/config.d.ts +++ b/ui/types/vault/models/secret-engine/config.d.ts @@ -31,7 +31,6 @@ export default class SecretEngineConfig extends Model { ttl: any; maxTtl: any; secretAccountEmail: string | undefined; - modelNameDisplay: string | undefined; // wif identityTokenAudience: string | undefined; identityTokenTtl: any; From c6e856a146cbc877a92cd32cc1a17729d39f00a2 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Tue, 21 Jan 2025 10:36:36 -0700 Subject: [PATCH 06/16] split out lease to another second config model type and make is-wif-engine helper --- .../secret-engine/configure-wif.hbs | 2 +- .../components/secret-engine/configure-wif.ts | 10 ++++--- ui/app/helpers/is-wif-engine.js | 8 ++++++ .../secrets/backend/configuration/edit.ts | 4 +-- .../secrets/backend/configuration/edit.hbs | 2 +- .../vault/models/secret-engine/config.d.ts | 12 +++----- .../models/secret-engine/second-config.d.ts | 28 +++++++++++++++++++ 7 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 ui/app/helpers/is-wif-engine.js create mode 100644 ui/types/vault/models/secret-engine/second-config.d.ts diff --git a/ui/app/components/secret-engine/configure-wif.hbs b/ui/app/components/secret-engine/configure-wif.hbs index 9b434935a7b6..23e215c1b746 100644 --- a/ui/app/components/secret-engine/configure-wif.hbs +++ b/ui/app/components/secret-engine/configure-wif.hbs @@ -24,7 +24,7 @@ {{/if}}
- {{! Only enterprise users can select a specific access type: "account" or "wif" }} + {{! Only enterprise users can change access type from "account" to "wif" }} {{#if this.version.isEnterprise}}
Access Type diff --git a/ui/app/components/secret-engine/configure-wif.ts b/ui/app/components/secret-engine/configure-wif.ts index 79c963a0fa83..ce543692fb62 100644 --- a/ui/app/components/secret-engine/configure-wif.ts +++ b/ui/app/components/secret-engine/configure-wif.ts @@ -14,6 +14,7 @@ import { assert } from '@ember/debug'; import errorMessage from 'vault/utils/error-message'; import type ConfigModel from 'vault/models/secret-engine/config'; +import type SecondConfigModel from 'vault/models/secret-engine/second-config'; import type IdentityOidcConfigModel from 'vault/models/identity/oidc/config'; import type Router from '@ember/routing/router'; import type StoreService from 'vault/services/store'; @@ -49,7 +50,7 @@ interface Args { displayName: string; type: string; model: ConfigModel; - secondModel: ConfigModel; + secondModel: SecondConfigModel; issuerConfig: IdentityOidcConfigModel; } @@ -108,7 +109,8 @@ export default class ConfigureWif extends Component { waitFor(async (event: Event) => { event?.preventDefault(); this.resetErrors(); - // AWS lease model has model validations we need to check before saving + // currently we only check validations on the second model (for AWS lease config). + // however, if future first models are added and they have validations, we will need to check them here. if (this.args.secondModel && !this.validate(this.args.secondModel)) { return; } @@ -142,7 +144,7 @@ export default class ConfigureWif extends Component { const issuerSaved = issuerAttrChanged ? await this.updateIssuer() : false; if (modelSaved || (!modelAttrChanged && issuerSaved)) { - // if there is a secondModel, attempt to save it. if saving fails, we transition and the failure will surface as a sticky flash message on the configuration details page. + // if there is a second model, attempt to save it. if saving fails, we transition and the failure will surface as a sticky flash message on the configuration details page. if (secondModelAttrChanged) { await this.saveSecondModel(); } @@ -207,7 +209,7 @@ export default class ConfigureWif extends Component { this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath); } - validate(model: ConfigModel) { + validate(model: SecondConfigModel) { const { isValid, state, invalidFormMessage } = model.validate(); this.modelValidations = isValid ? null : state; this.invalidFormAlert = isValid ? '' : invalidFormMessage; diff --git a/ui/app/helpers/is-wif-engine.js b/ui/app/helpers/is-wif-engine.js new file mode 100644 index 000000000000..40d2756e9860 --- /dev/null +++ b/ui/app/helpers/is-wif-engine.js @@ -0,0 +1,8 @@ +import { helper } from '@ember/component/helper'; +import { WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; + +// a helper to use within templates to determine if a secret engine is a WIF engine +// we cannot use the mountable-secret-engines helper for this purpose because the exported helper on that file is for mountableEngines. WIF methods cannot be accessed from within template. +export default helper(function isWifEngine([type]) { + return WIF_ENGINES.includes(type); +}); diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts index 94aa46c358c4..b62d4f30d41a 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts +++ b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts @@ -46,7 +46,6 @@ export default class SecretsBackendConfigurationEdit extends Route { const secretEngineRecord = this.modelFor('vault.cluster.secrets.backend') as SecretEngineModel; const type = secretEngineRecord.type; const displayName = allEngines().find((engine) => engine.type === type)?.displayName; - const isWifEngine = WIF_ENGINES.includes(type); // if the engine type is not configurable, return a 404. if (!secretEngineRecord || !CONFIGURABLE_SECRET_ENGINES.includes(type)) { @@ -96,7 +95,7 @@ export default class SecretsBackendConfigurationEdit extends Route { } // if the type is a WIF engine and it's enterprise, we also fetch the issuer // from a global endpoint which has no associated model/adapter - if (isWifEngine && this.version.isEnterprise) { + if (WIF_ENGINES.includes(type) && this.version.isEnterprise) { try { const response = await this.store.queryRecord('identity/oidc/config', {}); model['identity-oidc-config'] = response; @@ -106,7 +105,6 @@ export default class SecretsBackendConfigurationEdit extends Route { } } model['displayName'] = displayName; - model['isWifEngine'] = isWifEngine; return model; } diff --git a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs index a33ecf94c3f2..512bef19b4c0 100644 --- a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs +++ b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs @@ -31,7 +31,7 @@ {{#if (eq this.model.type "ssh")}} {{! This "else if" check is preventive. As of writing, all engines using this route, but "ssh", are wif engines }} -{{else if this.model.isWifEngine}} +{{else if (is-wif-engine this.model.type)}} Date: Tue, 21 Jan 2025 10:58:14 -0700 Subject: [PATCH 07/16] welp missed the old controller --- .../secrets/backend/configuration/edit.js | 58 +++---------------- ui/app/helpers/is-wif-engine.js | 8 --- .../secrets/backend/configuration/edit.ts | 4 +- .../secrets/backend/configuration/edit.hbs | 4 +- 4 files changed, 12 insertions(+), 62 deletions(-) delete mode 100644 ui/app/helpers/is-wif-engine.js diff --git a/ui/app/controllers/vault/cluster/secrets/backend/configuration/edit.js b/ui/app/controllers/vault/cluster/secrets/backend/configuration/edit.js index 6dbea5516bc4..63cdca50f86d 100644 --- a/ui/app/controllers/vault/cluster/secrets/backend/configuration/edit.js +++ b/ui/app/controllers/vault/cluster/secrets/backend/configuration/edit.js @@ -3,54 +3,14 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { isPresent } from '@ember/utils'; -import { service } from '@ember/service'; import Controller from '@ember/controller'; +import { WIF_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines'; -const CONFIG_ATTRS = { - // ssh - configured: false, - - // aws root config - iamEndpoint: null, - stsEndpoint: null, - accessKey: null, - secretKey: null, - region: '', -}; - -export default Controller.extend(CONFIG_ATTRS, { - queryParams: ['tab'], - tab: '', - flashMessages: service(), - loading: false, - reset() { - this.model.rollbackAttributes(); - this.setProperties(CONFIG_ATTRS); - }, - actions: { - save(method, data) { - this.set('loading', true); - const hasData = Object.keys(data).some((key) => { - return isPresent(data[key]); - }); - if (!hasData) { - return; - } - this.model - .save({ - adapterOptions: { - adapterMethod: method, - data, - }, - }) - .then(() => { - this.reset(); - this.flashMessages.success('The backend configuration saved successfully!'); - }) - .finally(() => { - this.set('loading', false); - }); - }, - }, -}); +export default class SecretsBackendConfigurationEditController extends Controller { + get isWifEngine() { + return WIF_ENGINES.includes(this.model.type); + } + get displayName() { + return allEngines().find((engine) => engine.type === this.model.type)?.displayName; + } +} diff --git a/ui/app/helpers/is-wif-engine.js b/ui/app/helpers/is-wif-engine.js deleted file mode 100644 index 40d2756e9860..000000000000 --- a/ui/app/helpers/is-wif-engine.js +++ /dev/null @@ -1,8 +0,0 @@ -import { helper } from '@ember/component/helper'; -import { WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; - -// a helper to use within templates to determine if a secret engine is a WIF engine -// we cannot use the mountable-secret-engines helper for this purpose because the exported helper on that file is for mountableEngines. WIF methods cannot be accessed from within template. -export default helper(function isWifEngine([type]) { - return WIF_ENGINES.includes(type); -}); diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts index b62d4f30d41a..090e8b3b1758 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts +++ b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts @@ -7,7 +7,7 @@ import AdapterError from '@ember-data/adapter/error'; import { set } from '@ember/object'; import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { CONFIGURABLE_SECRET_ENGINES, WIF_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines'; +import { CONFIGURABLE_SECRET_ENGINES, WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; import errorMessage from 'vault/utils/error-message'; import { action } from '@ember/object'; @@ -45,7 +45,6 @@ export default class SecretsBackendConfigurationEdit extends Route { const { backend } = this.paramsFor('vault.cluster.secrets.backend'); const secretEngineRecord = this.modelFor('vault.cluster.secrets.backend') as SecretEngineModel; const type = secretEngineRecord.type; - const displayName = allEngines().find((engine) => engine.type === type)?.displayName; // if the engine type is not configurable, return a 404. if (!secretEngineRecord || !CONFIGURABLE_SECRET_ENGINES.includes(type)) { @@ -104,7 +103,6 @@ export default class SecretsBackendConfigurationEdit extends Route { model['identity-oidc-config'] = { queryIssuerError: true }; } } - model['displayName'] = displayName; return model; } diff --git a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs index 512bef19b4c0..dee203f52cdc 100644 --- a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs +++ b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs @@ -31,10 +31,10 @@ {{#if (eq this.model.type "ssh")}} {{! This "else if" check is preventive. As of writing, all engines using this route, but "ssh", are wif engines }} -{{else if (is-wif-engine this.model.type)}} +{{else if this.isWifEngine}} Date: Tue, 21 Jan 2025 11:07:58 -0700 Subject: [PATCH 08/16] small removal of overkill comment --- ui/app/components/secret-engine/configure-wif.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/app/components/secret-engine/configure-wif.ts b/ui/app/components/secret-engine/configure-wif.ts index ce543692fb62..94dfb938e43a 100644 --- a/ui/app/components/secret-engine/configure-wif.ts +++ b/ui/app/components/secret-engine/configure-wif.ts @@ -41,7 +41,7 @@ import type FlashMessageService from 'vault/services/flash-messages'; * @param {string} displayName - used for flash messages, subText and labels. ex: 'Azure'. * @param {string} type - the type of the engine, ex: 'azure'. * @param {object} model - the config model for the engine. - * @param {object} [secondModel] - tor engines with two config models. Currently, only used by aws + * @param {object} [secondModel] - for engines with two config models. Currently, only used by aws * @param {object} [issuerConfig] - the identity/oidc/config model. Will be passed in if user has an enterprise license. */ @@ -110,7 +110,6 @@ export default class ConfigureWif extends Component { event?.preventDefault(); this.resetErrors(); // currently we only check validations on the second model (for AWS lease config). - // however, if future first models are added and they have validations, we will need to check them here. if (this.args.secondModel && !this.validate(this.args.secondModel)) { return; } From 3a6e79d078ee6cd95dfc9bb19ff7c9f8dc0d21bb Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Wed, 22 Jan 2025 11:14:19 -0700 Subject: [PATCH 09/16] pr feedback --- .../secret-engine/configure-wif.hbs | 50 ++-- .../components/secret-engine/configure-wif.ts | 103 ++++---- .../secrets/backend/configuration/edit.ts | 31 +-- .../secrets/backend/configuration/edit.hbs | 8 +- .../backend/aws/aws-configuration-test.js | 36 +-- .../backend/azure/azure-configuration-test.js | 13 +- .../backend/gcp/gcp-configuration-test.js | 4 +- .../secret-engine/secret-engine-selectors.ts | 2 +- .../secret-engine/configure-wif-test.js | 245 +++++++++--------- ...ond-config.d.ts => additional-config.d.ts} | 2 +- .../{config.d.ts => mount-config.d.ts} | 2 +- 11 files changed, 240 insertions(+), 256 deletions(-) rename ui/types/vault/models/secret-engine/{second-config.d.ts => additional-config.d.ts} (88%) rename ui/types/vault/models/secret-engine/{config.d.ts => mount-config.d.ts} (95%) diff --git a/ui/app/components/secret-engine/configure-wif.hbs b/ui/app/components/secret-engine/configure-wif.hbs index 23e215c1b746..1c7833a86528 100644 --- a/ui/app/components/secret-engine/configure-wif.hbs +++ b/ui/app/components/secret-engine/configure-wif.hbs @@ -3,21 +3,15 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - + {{! AWS specific note and section header }} {{#if (eq @type "aws")}} -
-

- Note: the client uses the official AWS SDK and will use the specified credentials, environment credentials, shared - file credentials, or IAM role/ECS task credentials in that order. -

-
+

+ Note: the client uses the official AWS SDK and will use the specified credentials, environment credentials, shared file + credentials, or IAM role/ECS task credentials in that order. +

Access to AWS

@@ -33,23 +27,23 @@ You cannot edit Access Type if you have already saved access credentials. {{else}} Choose the way to configure access to - {{@displayName}}. Access can be configured either using an - {{if (eq @type "aws") "IAM access keys" (concat @displayName " account credentials")}}, or with the Plugin - Workload Identity Federation (WIF). + {{@displayName}}. Access can be configured either using + {{if (eq @type "aws") "IAM access keys" (concat @displayName " account credentials")}} + or with the Plugin Workload Identity Federation (WIF). {{/if}}

- + @@ -69,25 +63,29 @@ {{#each @issuerConfig.displayAttrs as |attr|}} {{/each}} - + {{else}} {{! otherwise display account credential fields }} {{/if}} - {{! secondModel fields show regardless of the vault version or what access type is selected }} - {{#if @secondModel}} -

+ {{! additionalConfigModel fields show regardless of the vault version or what access type is selected }} + {{#if @additionalConfigModel}} +

{{if (eq @type "aws") "Lease" "Additional"}} Configuration

- {{#each @secondModel.formFields as |attr|}} - + {{#each @additionalConfigModel.formFields as |attr|}} + {{/each}}
{{/if}} diff --git a/ui/app/components/secret-engine/configure-wif.ts b/ui/app/components/secret-engine/configure-wif.ts index 94dfb938e43a..1b21d53b26b8 100644 --- a/ui/app/components/secret-engine/configure-wif.ts +++ b/ui/app/components/secret-engine/configure-wif.ts @@ -11,10 +11,11 @@ import { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { ValidationMap } from 'vault/vault/app-types'; import { assert } from '@ember/debug'; +import { capitalize } from '@ember/string'; import errorMessage from 'vault/utils/error-message'; -import type ConfigModel from 'vault/models/secret-engine/config'; -import type SecondConfigModel from 'vault/models/secret-engine/second-config'; +import type MountConfigModel from 'vault/vault/models/secret-engine/mount-config'; +import type additionalConfigModel from 'vault/vault/models/secret-engine/additional-config'; import type IdentityOidcConfigModel from 'vault/models/identity/oidc/config'; import type Router from '@ember/routing/router'; import type StoreService from 'vault/services/store'; @@ -22,26 +23,26 @@ import type VersionService from 'vault/services/version'; import type FlashMessageService from 'vault/services/flash-messages'; /** - * @module SecretEngineConfigureWif component is used to configure secret engines that allow the WIF configuration. + * @module SecretEngineConfigureWif component is used to configure secret engines that allow the WIF (Workload Identity Federation) configuration. * The ability to configure WIF fields is an enterprise only feature. * If the user is configuring WIF attributes they will also have the option to update the global issuer config, which is a separate endpoint named identity/oidc/config. - * If a user is on OSS, the account configuration fields will display with no ability to select or see wif fields. + * If a user is on CE, the account configuration fields will display with no ability to select or see wif fields. * * @example * * * @param {string} backendPath - name of the secret engine, ex: 'azure-123'. * @param {string} displayName - used for flash messages, subText and labels. ex: 'Azure'. * @param {string} type - the type of the engine, ex: 'azure'. - * @param {object} model - the config model for the engine. - * @param {object} [secondModel] - for engines with two config models. Currently, only used by aws + * @param {object} mountConfigModel - the config model for the engine. + * @param {object} [additionalConfigModel] - for engines with two config models. Currently, only used by aws * @param {object} [issuerConfig] - the identity/oidc/config model. Will be passed in if user has an enterprise license. */ @@ -49,8 +50,8 @@ interface Args { backendPath: string; displayName: string; type: string; - model: ConfigModel; - secondModel: SecondConfigModel; + mountConfigModel: MountConfigModel; + additionalConfigModel: additionalConfigModel; issuerConfig: IdentityOidcConfigModel; } @@ -71,8 +72,8 @@ export default class ConfigureWif extends Component { constructor(owner: unknown, args: Args) { super(owner, args); // the following checks are only relevant to existing enterprise configurations - if (this.version.isCommunity && this.args.model.isNew) return; - const { isWifPluginConfigured, isAccountPluginConfigured } = this.args.model; + if (this.version.isCommunity && this.args.mountConfigModel.isNew) return; + const { isWifPluginConfigured, isAccountPluginConfigured } = this.args.mountConfigModel; assert( `'isWifPluginConfigured' is required to be defined on the config model. Must return a boolean.`, isWifPluginConfigured !== undefined @@ -84,18 +85,19 @@ export default class ConfigureWif extends Component { get modelAttrChanged() { // "backend" dirties model state so explicity ignore it here - return Object.keys(this.args.model?.changedAttributes()).some((item) => item !== 'backend'); + return Object.keys(this.args.mountConfigModel?.changedAttributes()).some((item) => item !== 'backend'); } get issuerAttrChanged() { return this.args.issuerConfig?.hasDirtyAttributes; } - get secondModelAttrChanged() { - const { secondModel } = this.args; - // required to check for model first otherwise Object.keys will have nothing to iterate over and fails - if (!secondModel) return; - return Object.keys(secondModel.changedAttributes()).some((item) => item !== 'backend'); + get additionalConfigModelAttrChanged() { + const { additionalConfigModel } = this.args; + // required to check for model otherwise Object.keys will have nothing to iterate over and fails + return additionalConfigModel + ? Object.keys(additionalConfigModel.changedAttributes()).some((item) => item !== 'backend') + : false; } @action continueSubmitForm() { @@ -109,8 +111,8 @@ export default class ConfigureWif extends Component { waitFor(async (event: Event) => { event?.preventDefault(); this.resetErrors(); - // currently we only check validations on the second model (for AWS lease config). - if (this.args.secondModel && !this.validate(this.args.secondModel)) { + // currently we only check validations on the additional model (for AWS lease config). + if (this.args.additionalConfigModel && !this.isValid(this.args.additionalConfigModel)) { return; } if (this.issuerAttrChanged) { @@ -130,24 +132,24 @@ export default class ConfigureWif extends Component { waitFor(async () => { const modelAttrChanged = this.modelAttrChanged; const issuerAttrChanged = this.issuerAttrChanged; - const secondModelAttrChanged = this.secondModelAttrChanged; + const additionalConfigModelAttrChanged = this.additionalConfigModelAttrChanged; // check if any of the model(s) or issuer attributes have changed // if no changes, transition and notify user - if (!modelAttrChanged && !issuerAttrChanged && !secondModelAttrChanged) { + if (!modelAttrChanged && !issuerAttrChanged && !additionalConfigModelAttrChanged) { this.flashMessages.info('No changes detected.'); this.transition(); return; } - const modelSaved = modelAttrChanged ? await this.saveModel() : false; + const modelSaved = modelAttrChanged ? await this.saveMountConfigModel() : false; const issuerSaved = issuerAttrChanged ? await this.updateIssuer() : false; if (modelSaved || (!modelAttrChanged && issuerSaved)) { - // if there is a second model, attempt to save it. if saving fails, we transition and the failure will surface as a sticky flash message on the configuration details page. - if (secondModelAttrChanged) { - await this.saveSecondModel(); + // if there is an additional model, attempt to save it. if saving fails, we transition and the failure will surface as a sticky flash message on the configuration details page. + if (additionalConfigModelAttrChanged) { + await this.saveAdditionalConfigModel(); } - // we only prevent a transition if the first model or issuer are edited and fail when saving + // we only prevent a transition if the mount config model or issuer fail when saving this.transition(); } else { return; @@ -168,10 +170,10 @@ export default class ConfigureWif extends Component { } } - async saveModel(): Promise { - const { backendPath, model } = this.args; + async saveMountConfigModel(): Promise { + const { backendPath, mountConfigModel } = this.args; try { - await model.save(); + await mountConfigModel.save(); this.flashMessages.success(`Successfully saved ${backendPath}'s configuration.`); return true; } catch (error) { @@ -181,34 +183,35 @@ export default class ConfigureWif extends Component { } } - async saveSecondModel(): Promise { - const { backendPath, secondModel, type } = this.args; - const secondModelName = type === 'aws' ? 'Lease configuration' : 'additional configuration'; + async saveAdditionalConfigModel() { + const { backendPath, additionalConfigModel, type } = this.args; + const additionalConfigModelName = type === 'aws' ? 'lease configuration' : 'additional configuration'; try { - await secondModel.save(); - this.flashMessages.success(`Successfully saved ${backendPath}'s ${secondModelName}.`); - return true; + await additionalConfigModel.save(); + this.flashMessages.success(`Successfully saved ${backendPath}'s ${additionalConfigModelName}.`); } catch (error) { - this.errorMessage = errorMessage(error); - // we transition even if the second model fails. surface a sticky flash message so the user can see it on the next view. - this.flashMessages.danger(`${secondModelName} was not saved: ${this.errorMessage}`, { - sticky: true, - }); - return false; + // we transition even if the additional config model fails. + // the only error the user sees is a sticky flash message on the next view. + this.flashMessages.danger( + `${capitalize(additionalConfigModelName)} was not saved: ${errorMessage(error)}`, + { + sticky: true, + } + ); } } resetErrors() { this.flashMessages.clearMessages(); - this.errorMessage = ''; - this.invalidFormAlert = ''; + this.errorMessage = this.invalidFormAlert = ''; + this.modelValidations = null; } transition() { this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath); } - validate(model: SecondConfigModel) { + isValid(model: additionalConfigModel) { const { isValid, state, invalidFormMessage } = model.validate(); this.modelValidations = isValid ? null : state; this.invalidFormAlert = isValid ? '' : invalidFormMessage; @@ -218,11 +221,11 @@ export default class ConfigureWif extends Component { @action onChangeAccessType(accessType: string) { this.accessType = accessType; - const { model, type } = this.args; + const { mountConfigModel, type } = this.args; if (accessType === 'account') { // reset all "wif" attributes that are mutually exclusive with "account" attributes // these attributes are the same for each engine - model.identityTokenAudience = model.identityTokenTtl = undefined; + mountConfigModel.identityTokenAudience = mountConfigModel.identityTokenTtl = undefined; // return the issuer to the globally set value (if there is one) on toggle this.args.issuerConfig.rollbackAttributes(); } @@ -230,9 +233,9 @@ export default class ConfigureWif extends Component { // reset all "account" attributes that are mutually exclusive with "wif" attributes // these attributes are different for each engine type === 'azure' - ? (model.clientSecret = model.rootPasswordTtl = undefined) + ? (mountConfigModel.clientSecret = mountConfigModel.rootPasswordTtl = undefined) : type === 'aws' - ? (model.accessKey = undefined) + ? (mountConfigModel.accessKey = undefined) : null; } } @@ -240,7 +243,7 @@ export default class ConfigureWif extends Component { @action onCancel() { this.resetErrors(); - this.args.model.unloadRecord(); + this.args.mountConfigModel.unloadRecord(); this.transition(); } } diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts index 090e8b3b1758..84961ddc6c3a 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts +++ b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts @@ -19,7 +19,7 @@ import type VersionService from 'vault/services/version'; // It generates config models based on the engine type. // Saving and updating of those models are done within the engine specific components. -const CONFIG_ADAPTERS_PATHS: Record = { +const MOUNT_CONFIG_MODEL_NAMES: Record = { aws: ['aws/root-config', 'aws/lease-config'], azure: ['azure/config'], ssh: ['ssh/ca-config'], @@ -29,15 +29,16 @@ export default class SecretsBackendConfigurationEdit extends Route { @service declare readonly store: Store; @service declare readonly version: VersionService; - standardizedModelName(type: string, adapterPath: string) { - if ( - CONFIG_ADAPTERS_PATHS[type] && - CONFIG_ADAPTERS_PATHS[type].length > 1 && - adapterPath === CONFIG_ADAPTERS_PATHS[type][1] - ) { - return 'second-model'; + standardizedModelName(type: string, modelName: string) { + // to determine if there is an additional config model, we check if the modelName is the same as the second element in the array. + const path = + MOUNT_CONFIG_MODEL_NAMES[type] && MOUNT_CONFIG_MODEL_NAMES[type].length > 1 + ? MOUNT_CONFIG_MODEL_NAMES[type][1] + : null; + if (modelName === path) { + return 'additional-config-model'; } else { - return 'first-model'; + return 'mount-config-model'; } } @@ -55,12 +56,12 @@ export default class SecretsBackendConfigurationEdit extends Route { // generate the model based on the engine type. // and pre-set model with type and backend e.g. {type: ssh, id: ssh-123} const model: Record = { type, id: backend }; - for (const adapterPath of CONFIG_ADAPTERS_PATHS[type] as string[]) { + for (const modelName of MOUNT_CONFIG_MODEL_NAMES[type] as string[]) { // create a key that corresponds with the configs model order - // ex: adapterPath = ssh/ca-config, convert to: first-model so that you can pass to component @model={{this.model.first-model}} - const standardizedKey = this.standardizedModelName(type, adapterPath); + // ex: modelName = ssh/ca-config, convert to: mount-config-model so that you can pass to component @model={{this.model.mount-config-model}} + const standardizedKey = this.standardizedModelName(type, modelName); try { - const configModel = await this.store.queryRecord(adapterPath, { + const configModel = await this.store.queryRecord(modelName, { backend, type, }); @@ -68,7 +69,7 @@ export default class SecretsBackendConfigurationEdit extends Route { // so instead of checking a catch or httpStatus, we check if the model is configured based on the getter `isConfigured` on the engine's model // if the engine is not configured we update the record to get the default values if (!configModel.isConfigured && type === 'azure') { - model[standardizedKey] = await this.store.createRecord(adapterPath, { + model[standardizedKey] = await this.store.createRecord(modelName, { backend, type, }); @@ -83,7 +84,7 @@ export default class SecretsBackendConfigurationEdit extends Route { e.httpStatus === 404 || (type === 'ssh' && e.httpStatus === 400 && errorMessage(e) === `keys haven't been configured yet`) ) { - model[standardizedKey] = await this.store.createRecord(adapterPath, { + model[standardizedKey] = await this.store.createRecord(modelName, { backend, type, }); diff --git a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs index dee203f52cdc..8119e89db0ce 100644 --- a/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs +++ b/ui/app/templates/vault/cluster/secrets/backend/configuration/edit.hbs @@ -15,7 +15,7 @@

Configure - {{this.model.displayName}} + {{this.displayName}}

@@ -29,15 +29,15 @@ {{#if (eq this.model.type "ssh")}} - + {{! This "else if" check is preventive. As of writing, all engines using this route, but "ssh", are wif engines }} {{else if this.isWifEngine}} {{/if}} \ No newline at end of file diff --git a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js index e95e6af8dedc..d4f55251c17e 100644 --- a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js +++ b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js @@ -64,7 +64,7 @@ module('Acceptance | aws | configuration', function (hooks) { assert.dom(SES.configureTitle('aws')).hasText('Configure AWS'); assert.dom(SES.configureForm).exists('it lands on the configuration form.'); assert - .dom(SES.secondModelTitle) + .dom(SES.additionalConfigModelTitle) .hasText( 'Lease Configuration', 'it shows the lease configuration section with the "Lease Configuration" title.' @@ -89,9 +89,8 @@ module('Acceptance | aws | configuration', function (hooks) { await enablePage.enable('aws', path); this.server.post(configUrl('aws-lease', path), () => { - assert.false( - true, - 'post request was made to config/lease when no data was changed. test should fail.' + throw new Error( + 'A POST request was made to config/lease when it should not because no data was changed.' ); }); @@ -130,11 +129,11 @@ module('Acceptance | aws | configuration', function (hooks) { const type = 'aws'; this.server.get(`${path}/config/root`, (schema, req) => { const payload = JSON.parse(req.requestBody); - assert.ok(true, 'request made to config/root when navigating to the configuration page.'); + assert.true(true, 'request made to config/root when navigating to the configuration page.'); return { data: { id: path, type, attributes: payload } }; }); this.server.get(`identity/oidc/config`, () => { - assert.false(true, 'request made to return issuer. test should fail.'); + throw new Error(`Request was made to return the issuer when it should not have been.`); }); await enablePage.enable(type, path); createConfig(this.store, path, type); // create the aws root config in the store @@ -151,10 +150,7 @@ module('Acceptance | aws | configuration', function (hooks) { await enablePage.enable('aws', path); this.server.post(configUrl('aws-lease', path), () => { - assert.false( - true, - 'post request was made to config/lease when no data was changed. test should fail.' - ); + throw new Error(`post request was made to config/lease when it should not have been.`); }); await click(SES.configTab); @@ -202,15 +198,13 @@ module('Acceptance | aws | configuration', function (hooks) { await enablePage.enable('aws', path); this.server.post(configUrl('aws', path), () => { - assert.ok('post request was made to config/root when no data was changed. test should fail.'); + assert.true(true, 'post request was made to save aws root config.'); return overrideResponse(400, { errors: ['bad request!'] }); }); this.server.post(configUrl('aws-lease', path), () => { - assert.true( - false, - 'post request was made to config/lease when the first config was not saved. test should fail.' + throw new Error( + `post request was made to config/lease when the first config was not saved. A request to this endpoint should NOT be be made` ); - return overrideResponse(400, { errors: ['bad request!'] }); }); await click(SES.configTab); await click(SES.configure); @@ -226,7 +220,7 @@ module('Acceptance | aws | configuration', function (hooks) { const type = 'aws'; this.server.get(`${path}/config/root`, (schema, req) => { const payload = JSON.parse(req.requestBody); - assert.ok(true, 'request made to config/root when navigating to the configuration page.'); + assert.true(true, 'request made to config/root when navigating to the configuration page.'); return { data: { id: path, type, attributes: payload } }; }); await enablePage.enable(type, path); @@ -303,16 +297,10 @@ module('Acceptance | aws | configuration', function (hooks) { await enablePage.enable(type, path); this.server.post(configUrl(type, path), () => { - assert.false( - true, - 'post request was made to config/root when no data was changed. test should fail.' - ); + throw new Error(`post request was made to config/root when it should not have been.`); }); this.server.post(configUrl('aws-lease', path), () => { - assert.false( - true, - 'post request was made to config/lease when no data was changed. test should fail.' - ); + throw new Error(`post request was made to config/lease when it should not have been.`); }); await click(SES.configTab); diff --git a/ui/tests/acceptance/secrets/backend/azure/azure-configuration-test.js b/ui/tests/acceptance/secrets/backend/azure/azure-configuration-test.js index b67fd22ce5bd..8f98653a3e32 100644 --- a/ui/tests/acceptance/secrets/backend/azure/azure-configuration-test.js +++ b/ui/tests/acceptance/secrets/backend/azure/azure-configuration-test.js @@ -89,7 +89,7 @@ module('Acceptance | Azure | configuration', function (hooks) { environment: 'AZUREPUBLICCLOUD', }; this.server.get(`${path}/config`, () => { - assert.ok(true, 'request made to config when navigating to the configuration page.'); + assert.true(true, 'request made to config when navigating to the configuration page.'); return { data: { id: path, type: this.type, ...azureAccountAttrs } }; }); await enablePage.enable(this.type, path); @@ -129,9 +129,8 @@ module('Acceptance | Azure | configuration', function (hooks) { await enablePage.enable(this.type, path); this.server.post('/identity/oidc/config', () => { - assert.notOk( - true, - 'post request was made to issuer endpoint when on community and data not changed. test should fail.' + throw new Error( + `Request was made to return the issuer when it should not have been because user is on CE.` ); }); @@ -245,7 +244,7 @@ module('Acceptance | Azure | configuration', function (hooks) { environment: 'AZUREPUBLICCLOUD', }; this.server.get(`${path}/config`, () => { - assert.ok(true, 'request made to config when navigating to the configuration page.'); + assert.true(true, 'request made to config when navigating to the configuration page.'); return { data: { id: path, type: this.type, ...wifAttrs } }; }); await enablePage.enable(this.type, path); @@ -268,11 +267,11 @@ module('Acceptance | Azure | configuration', function (hooks) { const path = `azure-${this.uid}`; this.server.get(`${path}/config`, (schema, req) => { const payload = JSON.parse(req.requestBody); - assert.ok(true, 'request made to config/root when navigating to the configuration page.'); + assert.true(true, 'request made to config/root when navigating to the configuration page.'); return { data: { id: path, type: this.type, attributes: payload } }; }); this.server.get(`identity/oidc/config`, () => { - assert.notOk(true, 'request made to return issuer. test should fail.'); + throw new Error(`Request was made to return the issuer when it should not have been.`); }); await createConfig(this.store, path, this.type); // create the azure account config in the store await enablePage.enable(this.type, path); diff --git a/ui/tests/acceptance/secrets/backend/gcp/gcp-configuration-test.js b/ui/tests/acceptance/secrets/backend/gcp/gcp-configuration-test.js index 323285421e8d..460be68b48bb 100644 --- a/ui/tests/acceptance/secrets/backend/gcp/gcp-configuration-test.js +++ b/ui/tests/acceptance/secrets/backend/gcp/gcp-configuration-test.js @@ -72,7 +72,7 @@ module('Acceptance | GCP | configuration', function (hooks) { ttl: 3600, }; this.server.get(`${path}/config`, () => { - assert.ok(true, 'request made to config when navigating to the configuration page.'); + assert.true(true, 'request made to config when navigating to the configuration page.'); return { data: { id: path, type: this.type, ...wifAttrs } }; }); await enablePage.enable(this.type, path); @@ -99,7 +99,7 @@ module('Acceptance | GCP | configuration', function (hooks) { max_ttl: '4 hours', }; this.server.get(`${path}/config`, () => { - assert.ok(true, 'request made to config when navigating to the configuration page.'); + 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); diff --git a/ui/tests/helpers/secret-engine/secret-engine-selectors.ts b/ui/tests/helpers/secret-engine/secret-engine-selectors.ts index e799cd9eb56a..95288035615b 100644 --- a/ui/tests/helpers/secret-engine/secret-engine-selectors.ts +++ b/ui/tests/helpers/secret-engine/secret-engine-selectors.ts @@ -24,7 +24,7 @@ export const SECRET_ENGINE_SELECTORS = { viewBackend: '[data-test-backend-view-link]', warning: '[data-test-warning]', configureForm: '[data-test-configure-form]', - secondModelTitle: '[data-test-second-model-title]', + additionalConfigModelTitle: '[data-test-additional-config-model-title]', wif: { accessTypeSection: '[data-test-access-type-section]', accessTitle: '[data-test-access-title]', diff --git a/ui/tests/integration/components/secret-engine/configure-wif-test.js b/ui/tests/integration/components/secret-engine/configure-wif-test.js index 5ab2bf2ee38e..8e6e2aaa0c27 100644 --- a/ui/tests/integration/components/secret-engine/configure-wif-test.js +++ b/ui/tests/integration/components/secret-engine/configure-wif-test.js @@ -54,17 +54,17 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `${type}-${this.uid}`; this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = + this.mountConfigModel = type === 'aws' ? this.store.createRecord('aws/root-config') : this.store.createRecord(`${type}/config`); - this.secondConfig = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; - this.config.backend = this.id; - this.secondConfig ? (this.secondConfig.backend = this.id) : null; // Add backend to the configs because it's not on the testing snapshot (would come from url) + this.additionalConfigModel = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; + this.mountConfigModel.backend = this.id; + this.additionalConfigModel ? (this.additionalConfigModel.backend = this.id) : null; // Add backend to the configs because it's not on the testing snapshot (would come from url) this.type = type; await render(hbs` - + `); assert.dom(SES.configureForm).exists(`it lands on the ${type} configuration form`); assert.dom(SES.wif.accessType(type)).isChecked(`defaults to showing ${type} access type checked`); @@ -91,17 +91,17 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `${type}-${this.uid}`; this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = + this.mountConfigModel = type === 'aws' ? this.store.createRecord('aws/root-config') : this.store.createRecord(`${type}/config`); - this.secondConfig = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; - this.config.backend = this.id; - this.secondConfig ? (this.secondConfig.backend = this.id) : null; + this.additionalConfigModel = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; + this.mountConfigModel.backend = this.id; + this.additionalConfigModel ? (this.additionalConfigModel.backend = this.id) : null; this.type = type; await render(hbs` - + `); await click(SES.wif.accessType('wif')); // check for the wif fields only @@ -123,22 +123,20 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `azure-${this.uid}`; this.displayName = 'Azure'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('azure/config'); - this.config.backend = this.id; + this.mountConfigModel = this.store.createRecord('azure/config'); + this.mountConfigModel.backend = this.id; await render(hbs` - + `); this.server.post(configUrl('azure', this.id), () => { - assert.notOk( - true, - 'post request was made to config when user canceled out of flow. test should fail.' + throw new Error( + `Request was made to post the config when it should not have been because the user canceled out of the flow.` ); }); this.server.post('/identity/oidc/config', () => { - assert.notOk( - true, - 'post request was made to save issuer when user canceled out of flow. test should fail.' + throw new Error( + `Request was made to save the issuer when it should not have been because the user canceled out of the flow.` ); }); await fillInAzureConfig('withWif'); @@ -158,9 +156,9 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `azure-${this.uid}`; this.displayName = 'Azure'; // creating a config that exists but will not have the attribute isWifPluginConfigured on it - this.config = this.store.createRecord('ssh/ca-config', { backend: this.id }); + this.mountConfigModel = this.store.createRecord('ssh/ca-config', { backend: this.id }); await render(hbs` - + `); const err = await promise; assert.ok( @@ -175,12 +173,12 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `aws-${this.uid}`; this.displayName = 'AWS'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('aws/root-config'); - this.secondConfig = this.store.createRecord('aws/lease-config'); - this.config.backend = this.secondConfig.backend = this.id; + this.mountConfigModel = this.store.createRecord('aws/root-config'); + this.additionalConfigModel = this.store.createRecord('aws/lease-config'); + this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id; await render(hbs` - + `); this.server.post(configUrl('aws', this.id), () => { assert.true(true, 'post request was made to config/root when issuer failed. test should pass.'); @@ -210,12 +208,12 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `aws-${this.uid}`; this.displayName = 'AWS'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('aws/root-config'); - this.secondConfig = this.store.createRecord('aws/lease-config'); - this.config.backend = this.secondConfig.backend = this.id; + this.mountConfigModel = this.store.createRecord('aws/root-config'); + this.additionalConfigModel = this.store.createRecord('aws/lease-config'); + this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id; await render(hbs` - + `); this.server.post(configUrl('aws', this.id), () => { return overrideResponse(400, { errors: ['bad request'] }); @@ -240,11 +238,11 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `azure-${this.uid}`; this.displayName = 'Azure'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('azure/config'); - this.config.backend = this.id; + this.mountConfigModel = this.store.createRecord('azure/config'); + this.mountConfigModel.backend = this.id; await render(hbs` - + `); await fillInAzureConfig('azure'); await click(SES.wif.accessType('wif')); @@ -281,17 +279,17 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `azure-${this.uid}`; this.displayName = 'Azure'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('azure/config'); - this.config.backend = this.id; + this.mountConfigModel = this.store.createRecord('azure/config'); + this.mountConfigModel.backend = this.id; await render(hbs` - + `); assert .dom(SES.wif.accessTypeSubtext) .hasText( - 'Choose the way to configure access to Azure. Access can be configured either using an Azure account credentials, or with the Plugin Workload Identity Federation (WIF).' + 'Choose the way to configure access to Azure. Access can be configured either using Azure account credentials or with the Plugin Workload Identity Federation (WIF).' ); }); }); @@ -301,11 +299,11 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `aws-${this.uid}`; this.displayName = 'AWS'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('aws/root-config'); - this.config.backend = this.id; + this.mountConfigModel = this.store.createRecord('aws/root-config'); + this.mountConfigModel.backend = this.id; await render(hbs` - + `); await fillInAwsConfig('aws'); await click(SES.wif.accessType('wif')); @@ -339,17 +337,17 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `aws-${this.uid}`; this.displayName = 'AWS'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('aws/root-config'); - this.config.backend = this.id; + this.mountConfigModel = this.store.createRecord('aws/root-config'); + this.mountConfigModel.backend = this.id; await render(hbs` - + `); assert .dom(SES.wif.accessTypeSubtext) .hasText( - 'Choose the way to configure access to AWS. Access can be configured either using an IAM access keys, or with the Plugin Workload Identity Federation (WIF).' + 'Choose the way to configure access to AWS. Access can be configured either using IAM access keys or with the Plugin Workload Identity Federation (WIF).' ); }); @@ -357,23 +355,21 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `aws-${this.uid}`; this.displayName = 'AWS'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('aws/root-config'); - this.secondConfig = this.store.createRecord('aws/lease-config'); - this.config.backend = this.secondConfig.backend = this.id; + this.mountConfigModel = this.store.createRecord('aws/root-config'); + this.additionalConfigModel = this.store.createRecord('aws/lease-config'); + this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id; await render(hbs` - + `); this.server.post(configUrl('aws-lease', this.id), () => { - assert.false( - true, - 'post request was made to config/lease when no data was changed. test should fail.' + throw new Error( + `Request was made to post the config/lease when it should not have been because no data was changed.` ); }); this.server.post(configUrl('aws', this.id), () => { - assert.false( - true, - 'post request was made to config/root when no data was changed. test should fail.' + throw new Error( + `Request was made to post the config/root when it should not have been because no data was changed.` ); }); await click(GENERAL.ttl.toggle('Default Lease TTL')); @@ -389,12 +385,12 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `aws-${this.uid}`; this.displayName = 'AWS'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('aws/root-config'); - this.secondConfig = this.store.createRecord('aws/lease-config'); - this.config.backend = this.secondConfig.backend = this.id; + this.mountConfigModel = this.store.createRecord('aws/root-config'); + this.additionalConfigModel = this.store.createRecord('aws/lease-config'); + this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id; await render(hbs` - + `); this.server.post(configUrl('aws', this.id), () => { assert.true( @@ -423,29 +419,26 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `aws-${this.uid}`; this.displayName = 'AWS'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('aws/root-config'); - this.secondConfig = this.store.createRecord('aws/lease-config'); - this.config.backend = this.secondConfig.backend = this.id; + this.mountConfigModel = this.store.createRecord('aws/root-config'); + this.additionalConfigModel = this.store.createRecord('aws/lease-config'); + this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id; await render(hbs` - + `); this.server.post(configUrl('aws', this.id), () => { - assert.true( - false, - 'post request was made to config/root when user canceled out of flow. test should fail.' + throw new Error( + `Request was made to post the config/root when it should not have been because the user canceled out of the flow.` ); }); this.server.post(configUrl('aws-lease', this.id), () => { - assert.true( - false, - 'post request was made to config/lease when user canceled out of flow. test should fail.' + throw new Error( + `Request was made to post the config/lease when it should not have been because the user canceled out of the flow.` ); }); this.server.post('/identity/oidc/config', () => { - assert.true( - false, - 'post request was made to save issuer when user canceled out of flow. test should fail.' + throw new Error( + `Request was made to post the identity/oidc/config when it should not have been because the user canceled out of the flow.` ); }); // fill in both lease and root endpoints to ensure that both payloads are attempted to be sent @@ -468,11 +461,11 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.displayName = 'Azure'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); this.issuerConfig.queryIssuerError = true; - this.config = this.store.createRecord('azure/config'); - this.config.backend = this.id; + this.mountConfigModel = this.store.createRecord('azure/config'); + this.mountConfigModel.backend = this.id; await render(hbs` - + `); await click(SES.wif.accessType('wif')); await fillIn(GENERAL.inputByAttr('issuer'), 'http://change.me.no.read'); @@ -490,19 +483,18 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { assert.notOk(true, 'request should not be made to issuer config endpoint'); }); this.server.post(configUrl('azure', this.id), () => { - assert.notOk( - true, - 'post request was made to config/ when user canceled out of flow. test should fail.' + throw new Error( + `Request was made to post the config when it should not have been because the user canceled out of the flow.` ); }); this.id = `azure-${this.uid}`; this.displayName = 'Azure'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('azure/config'); - this.config.backend = this.id; + this.mountConfigModel = this.store.createRecord('azure/config'); + this.mountConfigModel.backend = this.id; await render(hbs` - + `); await click(SES.wif.accessType('wif')); assert @@ -544,11 +536,11 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `azure-${this.uid}`; this.displayName = 'Azure'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('azure/config'); - this.config.backend = this.id; + this.mountConfigModel = this.store.createRecord('azure/config'); + this.mountConfigModel.backend = this.id; await render(hbs` - + `); await click(SES.wif.accessType('wif')); assert.dom(GENERAL.inputByAttr('issuer')).hasValue('', 'issuer defaults to empty string'); @@ -573,8 +565,8 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `azure-${this.uid}`; this.displayName = 'Azure'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord('azure/config'); - this.config.backend = this.id; + this.mountConfigModel = this.store.createRecord('azure/config'); + this.mountConfigModel.backend = this.id; this.server.post(configUrl('azure', this.id), () => { assert.true( @@ -589,7 +581,7 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); await render(hbs` - + `); await click(SES.wif.accessType('wif')); assert.dom(GENERAL.inputByAttr('issuer')).hasValue(''); @@ -618,10 +610,10 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `azure-${this.uid}`; this.displayName = 'Azure'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = this.store.createRecord(`azure/config`); - this.config.backend = this.id; + this.mountConfigModel = this.store.createRecord(`azure/config`); + this.mountConfigModel.backend = this.id; await render(hbs` - + `); await click(SES.wif.accessType('wif')); assert @@ -650,19 +642,19 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { this.id = `${type}-${this.uid}`; this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.config = + this.mountConfigModel = type === 'aws' ? this.store.createRecord('aws/root-config') : type === 'ssh' ? this.store.createRecord('ssh/ca-config') : this.store.createRecord(`${type}/config`); - this.secondConfig = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; - this.config.backend = this.id; - this.secondConfig ? (this.secondConfig.backend = this.id) : null; + this.additionalConfigModel = type === 'aws' ? this.store.createRecord('aws/lease-config') : null; + this.mountConfigModel.backend = this.id; + this.additionalConfigModel ? (this.additionalConfigModel.backend = this.id) : null; this.type = type; await render(hbs` - + `); assert.dom(SES.configureForm).exists(`lands on the ${type} configuration form`); assert @@ -690,10 +682,10 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { test(`${type}: it defaults to WIF accessType if WIF fields are already set`, async function (assert) { this.id = `${type}-${this.uid}`; this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; - this.config = createConfig(this.store, this.id, `${type}-wif`); + this.mountConfigModel = createConfig(this.store, this.id, `${type}-wif`); this.type = type; await render(hbs` - + `); assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked'); assert.dom(SES.wif.accessType('wif')).isDisabled('WIF accessType is disabled'); @@ -701,7 +693,7 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { assert.dom(SES.wif.accessType(type)).isDisabled(`${type} accessType is disabled`); assert .dom(GENERAL.inputByAttr('identityTokenAudience')) - .hasValue(this.config.identityTokenAudience); + .hasValue(this.mountConfigModel.identityTokenAudience); assert .dom(SES.wif.accessTypeSubtext) .hasText('You cannot edit Access Type if you have already saved access credentials.'); @@ -713,12 +705,12 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { test(`${type}: it renders issuer if global issuer is already set`, async function (assert) { this.id = `${type}-${this.uid}`; this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; - this.config = createConfig(this.store, this.id, `${type}-wif`); + this.mountConfigModel = createConfig(this.store, this.id, `${type}-wif`); this.issuerConfig = createConfig(this.store, this.id, 'issuer'); this.issuerConfig.issuer = 'https://foo-bar-blah.com'; this.type = type; await render(hbs` - + `); assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked'); @@ -735,9 +727,9 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { module('Azure specific', function () { test('it defaults to Azure accessType if Azure account fields are already set', async function (assert) { this.id = `azure-${this.uid}`; - this.config = createConfig(this.store, this.id, 'azure'); + this.mountConfigModel = createConfig(this.store, this.id, 'azure'); await render(hbs` - + `); assert.dom(SES.wif.accessType('azure')).isChecked('Azure accessType is checked'); @@ -751,9 +743,9 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { test('it allows you to change accessType if record does not have wif or azure values already set', async function (assert) { this.id = `azure-${this.uid}`; - this.config = createConfig(this.store, this.id, 'azure-generic'); + this.mountConfigModel = createConfig(this.store, this.id, 'azure-generic'); await render(hbs` - + `); assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled'); @@ -762,13 +754,13 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { test('it shows previously saved config information', async function (assert) { this.id = `azure-${this.uid}`; - this.config = createConfig(this.store, this.id, 'azure-generic'); + this.mountConfigModel = createConfig(this.store, this.id, 'azure-generic'); await render(hbs` - + `); - assert.dom(GENERAL.inputByAttr('subscriptionId')).hasValue(this.config.subscriptionId); - assert.dom(GENERAL.inputByAttr('clientId')).hasValue(this.config.clientId); - assert.dom(GENERAL.inputByAttr('tenantId')).hasValue(this.config.tenantId); + assert.dom(GENERAL.inputByAttr('subscriptionId')).hasValue(this.mountConfigModel.subscriptionId); + assert.dom(GENERAL.inputByAttr('clientId')).hasValue(this.mountConfigModel.clientId); + assert.dom(GENERAL.inputByAttr('tenantId')).hasValue(this.mountConfigModel.tenantId); assert .dom(GENERAL.inputByAttr('clientSecret')) .hasValue('**********', 'clientSecret is masked on edit the value'); @@ -776,9 +768,9 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { test('it requires a double click to change the client secret', async function (assert) { this.id = `azure-${this.uid}`; - this.config = createConfig(this.store, this.id, 'azure'); + this.mountConfigModel = createConfig(this.store, this.id, 'azure'); await render(hbs` - + `); this.server.post(configUrl('azure', this.id), (schema, req) => { @@ -800,9 +792,9 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { module('AWS specific', function () { test('it defaults to IAM accessType if IAM fields are already set', async function (assert) { this.id = `aws-${this.uid}`; - this.config = createConfig(this.store, this.id, 'aws'); + this.mountConfigModel = createConfig(this.store, this.id, 'aws'); await render(hbs` - + `); assert.dom(SES.wif.accessType('aws')).isChecked('IAM accessType is checked'); assert.dom(SES.wif.accessType('aws')).isDisabled('IAM accessType is disabled'); @@ -815,9 +807,9 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { test('it allows you to change access type if record does not have wif or iam values already set', async function (assert) { this.id = `aws-${this.uid}`; - this.config = createConfig(this.store, this.id, 'aws-no-access'); + this.mountConfigModel = createConfig(this.store, this.id, 'aws-no-access'); await render(hbs` - + `); assert.dom(SES.wif.accessType('wif')).isNotDisabled('WIF accessType is NOT disabled'); assert.dom(SES.wif.accessType('aws')).isNotDisabled('IAM accessType is NOT disabled'); @@ -825,21 +817,21 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { test('it shows previously saved root and lease information', async function (assert) { this.id = `aws-${this.uid}`; - this.config = createConfig(this.store, this.id, 'aws'); - this.secondConfig = createConfig(this.store, this.id, 'aws-lease'); + this.mountConfigModel = createConfig(this.store, this.id, 'aws'); + this.additionalConfigModel = createConfig(this.store, this.id, 'aws-lease'); await render(hbs` - + `); - assert.dom(GENERAL.inputByAttr('accessKey')).hasValue(this.config.accessKey); + assert.dom(GENERAL.inputByAttr('accessKey')).hasValue(this.mountConfigModel.accessKey); assert .dom(GENERAL.inputByAttr('secretKey')) .hasValue('**********', 'secretKey is masked on edit the value'); await click(GENERAL.toggleGroup('Root config options')); - assert.dom(GENERAL.inputByAttr('region')).hasValue(this.config.region); - assert.dom(GENERAL.inputByAttr('iamEndpoint')).hasValue(this.config.iamEndpoint); - assert.dom(GENERAL.inputByAttr('stsEndpoint')).hasValue(this.config.stsEndpoint); + assert.dom(GENERAL.inputByAttr('region')).hasValue(this.mountConfigModel.region); + assert.dom(GENERAL.inputByAttr('iamEndpoint')).hasValue(this.mountConfigModel.iamEndpoint); + assert.dom(GENERAL.inputByAttr('stsEndpoint')).hasValue(this.mountConfigModel.stsEndpoint); assert.dom(GENERAL.inputByAttr('maxRetries')).hasValue('1'); // Check lease config values assert.dom(GENERAL.ttl.input('Default Lease TTL')).hasValue('50'); @@ -848,9 +840,9 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { test('it requires a double click to change the secret key', async function (assert) { this.id = `aws-${this.uid}`; - this.config = createConfig(this.store, this.id, 'aws'); + this.mountConfigModel = createConfig(this.store, this.id, 'aws'); await render(hbs` - + `); this.server.post(configUrl('aws', this.id), (schema, req) => { @@ -877,11 +869,11 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { for (const type of WIF_ENGINES) { test(`${type}:it does not show access type but defaults to type "account" fields`, async function (assert) { this.id = `${type}-${this.uid}`; - this.config = createConfig(this.store, this.id, `${type}-generic`); + this.mountConfigModel = createConfig(this.store, this.id, `${type}-generic`); this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; this.type = type; await render(hbs` - + `); assert.dom(SES.wif.accessTypeSection).doesNotExist('Access type section does not render'); // toggle grouped fields if it exists @@ -892,7 +884,10 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { if (key === 'secretKey' || key === 'clientSecret') return; // these keys are not returned by the API assert .dom(GENERAL.inputByAttr(key)) - .hasValue(this.config[key], `${key} for ${type}: has the expected value set on the config`); + .hasValue( + this.mountConfigModel[key], + `${key} for ${type}: has the expected value set on the config` + ); } }); } diff --git a/ui/types/vault/models/secret-engine/second-config.d.ts b/ui/types/vault/models/secret-engine/additional-config.d.ts similarity index 88% rename from ui/types/vault/models/secret-engine/second-config.d.ts rename to ui/types/vault/models/secret-engine/additional-config.d.ts index 1efd931d0d44..18f8088746e7 100644 --- a/ui/types/vault/models/secret-engine/second-config.d.ts +++ b/ui/types/vault/models/secret-engine/additional-config.d.ts @@ -6,7 +6,7 @@ import type Model from '@ember-data/model'; import type { ModelValidations, FormField } from 'vault/app-types'; -export default class SecretEngineSecondConfig extends Model { +export default class SecretEngineAdditionalConfigModel extends Model { backend: string; type: string; // aws lease diff --git a/ui/types/vault/models/secret-engine/config.d.ts b/ui/types/vault/models/secret-engine/mount-config.d.ts similarity index 95% rename from ui/types/vault/models/secret-engine/config.d.ts rename to ui/types/vault/models/secret-engine/mount-config.d.ts index f6cce51a9b48..636ce52d682f 100644 --- a/ui/types/vault/models/secret-engine/config.d.ts +++ b/ui/types/vault/models/secret-engine/mount-config.d.ts @@ -6,7 +6,7 @@ import type Model from '@ember-data/model'; import type { ModelValidations, FormFieldGroups } from 'vault/app-types'; -export default class SecretEngineConfig extends Model { +export default class SecretEngineMountConfigModel extends Model { backend: string; type: string; // aws From ff0689d305c473d70653a841417136249c33439e Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Wed, 22 Jan 2025 12:07:51 -0700 Subject: [PATCH 10/16] save lease config if only thing changed --- .../components/secret-engine/configure-wif.ts | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/ui/app/components/secret-engine/configure-wif.ts b/ui/app/components/secret-engine/configure-wif.ts index 1b21d53b26b8..c44d0714e4be 100644 --- a/ui/app/components/secret-engine/configure-wif.ts +++ b/ui/app/components/secret-engine/configure-wif.ts @@ -15,7 +15,7 @@ import { capitalize } from '@ember/string'; import errorMessage from 'vault/utils/error-message'; import type MountConfigModel from 'vault/vault/models/secret-engine/mount-config'; -import type additionalConfigModel from 'vault/vault/models/secret-engine/additional-config'; +import type AdditionalConfigModel from 'vault/vault/models/secret-engine/additional-config'; import type IdentityOidcConfigModel from 'vault/models/identity/oidc/config'; import type Router from '@ember/routing/router'; import type StoreService from 'vault/services/store'; @@ -33,15 +33,15 @@ import type FlashMessageService from 'vault/services/flash-messages'; @backendPath={{this.model.id}} @displayName="AWS" @type="aws" - @mountConfigModel={{this.model.root-config}} - @additionalConfigModel={{this.model.lease-config}} + @mountConfigModel={{this.model.mount-config-model}} + @additionalConfigModel={{this.model.additional-config-model}} @issuerConfig={{this.model.identity-oidc-config}} /> * * @param {string} backendPath - name of the secret engine, ex: 'azure-123'. * @param {string} displayName - used for flash messages, subText and labels. ex: 'Azure'. * @param {string} type - the type of the engine, ex: 'azure'. - * @param {object} mountConfigModel - the config model for the engine. + * @param {object} mountConfigModel - the config model for the engine. The attr `isWifPluginConfigured` must be added to this config model otherwise this form will assert an error. `isWifPluginConfigured` returns true if any required wif fields have been set. * @param {object} [additionalConfigModel] - for engines with two config models. Currently, only used by aws * @param {object} [issuerConfig] - the identity/oidc/config model. Will be passed in if user has an enterprise license. */ @@ -51,7 +51,7 @@ interface Args { displayName: string; type: string; mountConfigModel: MountConfigModel; - additionalConfigModel: additionalConfigModel; + additionalConfigModel: AdditionalConfigModel; issuerConfig: IdentityOidcConfigModel; } @@ -83,7 +83,7 @@ export default class ConfigureWif extends Component { this.disableAccessType = isWifPluginConfigured || isAccountPluginConfigured; } - get modelAttrChanged() { + get mountConfigModelAttrChanged() { // "backend" dirties model state so explicity ignore it here return Object.keys(this.args.mountConfigModel?.changedAttributes()).some((item) => item !== 'backend'); } @@ -111,7 +111,7 @@ export default class ConfigureWif extends Component { waitFor(async (event: Event) => { event?.preventDefault(); this.resetErrors(); - // currently we only check validations on the additional model (for AWS lease config). + // currently we only check validations on the additional model if (this.args.additionalConfigModel && !this.isValid(this.args.additionalConfigModel)) { return; } @@ -130,23 +130,27 @@ export default class ConfigureWif extends Component { save = task( waitFor(async () => { - const modelAttrChanged = this.modelAttrChanged; + const mountConfigModelChanged = this.mountConfigModelAttrChanged; + const additionalModelAttrChanged = this.additionalConfigModelAttrChanged; const issuerAttrChanged = this.issuerAttrChanged; - const additionalConfigModelAttrChanged = this.additionalConfigModelAttrChanged; // check if any of the model(s) or issuer attributes have changed // if no changes, transition and notify user - if (!modelAttrChanged && !issuerAttrChanged && !additionalConfigModelAttrChanged) { + if (!mountConfigModelChanged && !additionalModelAttrChanged && !issuerAttrChanged) { this.flashMessages.info('No changes detected.'); this.transition(); return; } - const modelSaved = modelAttrChanged ? await this.saveMountConfigModel() : false; + const mountConfigModelSaved = mountConfigModelChanged ? await this.saveMountConfigModel() : false; const issuerSaved = issuerAttrChanged ? await this.updateIssuer() : false; - if (modelSaved || (!modelAttrChanged && issuerSaved)) { - // if there is an additional model, attempt to save it. if saving fails, we transition and the failure will surface as a sticky flash message on the configuration details page. - if (additionalConfigModelAttrChanged) { + if ( + mountConfigModelSaved || + (!mountConfigModelChanged && issuerSaved) || + (!mountConfigModelChanged && additionalModelAttrChanged) + ) { + // if there are changes made to the an additional model, attempt to save it. if saving fails, we transition and the failure will surface as a sticky flash message on the configuration details page. + if (additionalModelAttrChanged) { await this.saveAdditionalConfigModel(); } // we only prevent a transition if the mount config model or issuer fail when saving @@ -190,7 +194,6 @@ export default class ConfigureWif extends Component { await additionalConfigModel.save(); this.flashMessages.success(`Successfully saved ${backendPath}'s ${additionalConfigModelName}.`); } catch (error) { - // we transition even if the additional config model fails. // the only error the user sees is a sticky flash message on the next view. this.flashMessages.danger( `${capitalize(additionalConfigModelName)} was not saved: ${errorMessage(error)}`, @@ -211,7 +214,7 @@ export default class ConfigureWif extends Component { this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath); } - isValid(model: additionalConfigModel) { + isValid(model: AdditionalConfigModel) { const { isValid, state, invalidFormMessage } = model.validate(); this.modelValidations = isValid ? null : state; this.invalidFormAlert = isValid ? '' : invalidFormMessage; From 4257c9f26db2283e6ea9b4e5259b2c56e6df5d0f Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Wed, 22 Jan 2025 14:14:12 -0700 Subject: [PATCH 11/16] error handling in acceptance test --- .../secret-engine/configure-wif.hbs | 4 +- .../components/secret-engine/configure-wif.ts | 4 +- .../secrets/backend/configuration/edit.ts | 4 +- .../backend/aws/aws-configuration-test.js | 155 ++++++++++++++---- .../backend/azure/azure-configuration-test.js | 47 ++++-- 5 files changed, 161 insertions(+), 53 deletions(-) diff --git a/ui/app/components/secret-engine/configure-wif.hbs b/ui/app/components/secret-engine/configure-wif.hbs index 1c7833a86528..50952156a627 100644 --- a/ui/app/components/secret-engine/configure-wif.hbs +++ b/ui/app/components/secret-engine/configure-wif.hbs @@ -5,7 +5,6 @@ - {{! AWS specific note and section header }} {{#if (eq @type "aws")}}

@@ -16,7 +15,6 @@ Access to AWS

{{/if}} -
{{! Only enterprise users can change access type from "account" to "wif" }} {{#if this.version.isEnterprise}} @@ -58,6 +56,8 @@
{{/if}} + {{! surface error message above the beginning of the form fields }} + {{#if (eq this.accessType "wif")}} {{! if access type is "wif" display Issuer and WIF fields }} {{#each @issuerConfig.displayAttrs as |attr|}} diff --git a/ui/app/components/secret-engine/configure-wif.ts b/ui/app/components/secret-engine/configure-wif.ts index c44d0714e4be..310903e9e02f 100644 --- a/ui/app/components/secret-engine/configure-wif.ts +++ b/ui/app/components/secret-engine/configure-wif.ts @@ -41,7 +41,7 @@ import type FlashMessageService from 'vault/services/flash-messages'; * @param {string} backendPath - name of the secret engine, ex: 'azure-123'. * @param {string} displayName - used for flash messages, subText and labels. ex: 'Azure'. * @param {string} type - the type of the engine, ex: 'azure'. - * @param {object} mountConfigModel - the config model for the engine. The attr `isWifPluginConfigured` must be added to this config model otherwise this form will assert an error. `isWifPluginConfigured` returns true if any required wif fields have been set. + * @param {object} mountConfigModel - the config model for the engine. The attr `isWifPluginConfigured` must be added to this config model otherwise this component will assert an error. `isWifPluginConfigured` should return true if any required wif fields have been set. * @param {object} [additionalConfigModel] - for engines with two config models. Currently, only used by aws * @param {object} [issuerConfig] - the identity/oidc/config model. Will be passed in if user has an enterprise license. */ @@ -94,7 +94,7 @@ export default class ConfigureWif extends Component { get additionalConfigModelAttrChanged() { const { additionalConfigModel } = this.args; - // required to check for model otherwise Object.keys will have nothing to iterate over and fails + // required to check for additional model otherwise Object.keys will have nothing to iterate over and fails return additionalConfigModel ? Object.keys(additionalConfigModel.changedAttributes()).some((item) => item !== 'backend') : false; diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts index 84961ddc6c3a..fd94c4c5487a 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts +++ b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts @@ -57,8 +57,8 @@ export default class SecretsBackendConfigurationEdit extends Route { // and pre-set model with type and backend e.g. {type: ssh, id: ssh-123} const model: Record = { type, id: backend }; for (const modelName of MOUNT_CONFIG_MODEL_NAMES[type] as string[]) { - // create a key that corresponds with the configs model order - // ex: modelName = ssh/ca-config, convert to: mount-config-model so that you can pass to component @model={{this.model.mount-config-model}} + // create a key that corresponds with the model order + // ex: modelName = aws/lease-config, convert to: additional-config-model so that you can pass to component @additionalConfigModel={{this.model.additional-config-model}} const standardizedKey = this.standardizedModelName(type, modelName); try { const configModel = await this.store.queryRecord(modelName, { diff --git a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js index d4f55251c17e..346bddef5136 100644 --- a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js +++ b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js @@ -34,10 +34,12 @@ module('Acceptance | aws | configuration', function (hooks) { this.store = this.owner.lookup('service:store'); this.flashSuccessSpy = spy(flash, 'success'); this.flashInfoSpy = spy(flash, 'info'); + this.flashDangerSpy = spy(flash, 'danger'); this.version = this.owner.lookup('service:version'); this.uid = uuidv4(); return authPage.login(); }); + module('isEnterprise', function (hooks) { hooks.beforeEach(function () { this.version.type = 'enterprise'; @@ -192,29 +194,6 @@ module('Acceptance | aws | configuration', function (hooks) { await runCmd(`delete sys/mounts/${path}`); }); - test('it does not save lease AWS configuration if root configuration errored on save', async function (assert) { - assert.expect(1); - const path = `aws-${this.uid}`; - await enablePage.enable('aws', path); - - this.server.post(configUrl('aws', path), () => { - assert.true(true, 'post request was made to save aws root config.'); - return overrideResponse(400, { errors: ['bad request!'] }); - }); - this.server.post(configUrl('aws-lease', path), () => { - throw new Error( - `post request was made to config/lease when the first config was not saved. A request to this endpoint should NOT be be made` - ); - }); - await click(SES.configTab); - await click(SES.configure); - await fillInAwsConfig('withAccess'); - await fillInAwsConfig('withLease'); - await click(GENERAL.saveButton); - // cleanup - await runCmd(`delete sys/mounts/${path}`); - }); - test('it shows AWS mount configuration details', async function (assert) { const path = `aws-${this.uid}`; const type = 'aws'; @@ -277,19 +256,6 @@ module('Acceptance | aws | configuration', function (hooks) { await runCmd(`delete sys/mounts/${path}`); }); - test('it should show API error when AWS configuration read fails', async function (assert) { - assert.expect(1); - const path = `aws-${this.uid}`; - const type = 'aws'; - await enablePage.enable(type, path); - // interrupt get and return API error - this.server.get(configUrl(type, path), () => { - return overrideResponse(400, { errors: ['bad request'] }); - }); - await click(SES.configTab); - assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route'); - }); - test('it should not make a post request if lease or root data was unchanged', async function (assert) { assert.expect(3); const path = `aws-${this.uid}`; @@ -341,6 +307,31 @@ module('Acceptance | aws | configuration', function (hooks) { // cleanup await runCmd(`delete sys/mounts/${path}`); }); + + test('it saves lease configuration if root configuration was not changed', async function (assert) { + assert.expect(1); + const path = `aws-${this.uid}`; + await enablePage.enable('aws', path); + + this.server.post(configUrl('aws', path), () => { + throw new Error( + `Request was made to save the config/root when it should not have been because the user did not make any changes to this config.` + ); + }); + + await click(SES.configTab); + await click(SES.configure); + await fillInAwsConfig('withLease'); + await click(GENERAL.saveButton); + + assert.true( + this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s lease configuration.`), + 'Success flash message is rendered showing the lease configuration was saved.' + ); + assert.strictEqual(currentURL(), `/vault/secrets/${path}/configuration/edit`); + // cleanup + await runCmd(`delete sys/mounts/${path}`); + }); }); module('isCommunity', function (hooks) { @@ -368,5 +359,97 @@ module('Acceptance | aws | configuration', function (hooks) { // cleanup await runCmd(`delete sys/mounts/${path}`); }); + + module('Error handling', function () { + test('it does not try to save lease configuration if root configuration errored on save', async function (assert) { + assert.expect(1); + const path = `aws-${this.uid}`; + await enablePage.enable('aws', path); + + this.server.post(configUrl('aws', path), () => { + assert.true(true, 'post request was made to save aws root config.'); + return overrideResponse(400, { errors: ['bad request!'] }); + }); + this.server.post(configUrl('aws-lease', path), () => { + throw new Error( + `post request was made to config/lease when the first config was not saved. A request to this endpoint should NOT be be made` + ); + }); + await click(SES.configTab); + await click(SES.configure); + await fillInAwsConfig('withAccess'); + await fillInAwsConfig('withLease'); + await click(GENERAL.saveButton); + // cleanup + await runCmd(`delete sys/mounts/${path}`); + }); + + test('it shows a flash message error and transitions if lease configuration errored on save', async function (assert) { + assert.expect(1); + const path = `aws-${this.uid}`; + await enablePage.enable('aws', path); + + this.server.post(configUrl('aws', path), () => { + throw new Error( + `Request was made to save the config/root when it should not have been because the user did not make any changes to this config.` + ); + }); + this.server.post(configUrl('aws-lease', path), () => { + return overrideResponse(400, { errors: ['bad request!'] }); + }); + await click(SES.configTab); + await click(SES.configure); + await fillInAwsConfig('withLease'); + await click(GENERAL.saveButton); + + assert.true( + this.flashDangerSpy.calledWith(`Lease configuration was not saved: bad request!`), + 'flash danger message is rendered showing the lease configuration was NOT saved.' + ); + assert.strictEqual( + currentURL(), + `/vault/secrets/${path}/configuration/index`, + 'lease configuration failed to save but the component transitioned as expected' + ); + // cleanup + await runCmd(`delete sys/mounts/${path}`); + }); + + test('it prevents transition and shows api error if root config errored on save', async function (assert) { + const path = `aws-${this.uid}`; + await enablePage.enable('aws', path); + + this.server.post(configUrl('aws', path), () => { + return overrideResponse(400, { errors: ['welp, that did not work!'] }); + }); + + await click(SES.configTab); + await click(SES.configure); + await fillInAwsConfig('withAccess'); + await click(GENERAL.saveButton); + + assert.dom(GENERAL.messageError).hasText('Error welp, that did not work!', 'API error shows on form'); + assert.strictEqual( + currentURL(), + `/vault/secrets/${path}/configuration/edit`, + 'the form did not transition because the save failed.' + ); + // cleanup + await runCmd(`delete sys/mounts/${path}`); + }); + + test('it should show API error when AWS configuration read fails', async function (assert) { + assert.expect(1); + const path = `aws-${this.uid}`; + const type = 'aws'; + await enablePage.enable(type, path); + // interrupt get and return API error + this.server.get(configUrl(type, path), () => { + return overrideResponse(400, { errors: ['bad request'] }); + }); + await click(SES.configTab); + assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route'); + }); + }); }); }); diff --git a/ui/tests/acceptance/secrets/backend/azure/azure-configuration-test.js b/ui/tests/acceptance/secrets/backend/azure/azure-configuration-test.js index 8f98653a3e32..3f76a3663923 100644 --- a/ui/tests/acceptance/secrets/backend/azure/azure-configuration-test.js +++ b/ui/tests/acceptance/secrets/backend/azure/azure-configuration-test.js @@ -109,17 +109,6 @@ module('Acceptance | Azure | configuration', function (hooks) { // 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 = `azure-${this.uid}`; - // interrupt get and return API error - this.server.get(configUrl(this.type, path), () => { - return overrideResponse(400, { errors: ['bad request'] }); - }); - await enablePage.enable(this.type, path); - assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route'); - }); }); module('create', function () { @@ -225,6 +214,42 @@ module('Acceptance | Azure | configuration', function (hooks) { await runCmd(`delete sys/mounts/${path}`); }); }); + + module('Error handling', function () { + test('it prevents transition and shows api error if config errored on save', async function (assert) { + const path = `azure-${this.uid}`; + await enablePage.enable('azure', path); + + this.server.post(configUrl('azure', path), () => { + return overrideResponse(400, { errors: ['welp, that did not work!'] }); + }); + + await click(SES.configTab); + await click(SES.configure); + await fillInAzureConfig('azure'); + await click(GENERAL.saveButton); + + assert.dom(GENERAL.messageError).hasText('Error welp, that did not work!', 'API error shows on form'); + assert.strictEqual( + currentURL(), + `/vault/secrets/${path}/configuration/edit`, + 'the form did not transition because the save failed.' + ); + // 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 = `azure-${this.uid}`; + // interrupt get and return API error + this.server.get(configUrl(this.type, path), () => { + return overrideResponse(400, { errors: ['bad request'] }); + }); + await enablePage.enable(this.type, path); + assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route'); + }); + }); }); module('isEnterprise', function (hooks) { From c5529b4abe896bdc72ea6ecfa1f5f2c4701821c0 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 23 Jan 2025 09:58:58 -0700 Subject: [PATCH 12/16] test fix --- .../acceptance/secrets/backend/aws/aws-configuration-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js index 346bddef5136..5debf1ade66a 100644 --- a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js +++ b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js @@ -385,7 +385,7 @@ module('Acceptance | aws | configuration', function (hooks) { }); test('it shows a flash message error and transitions if lease configuration errored on save', async function (assert) { - assert.expect(1); + assert.expect(2); const path = `aws-${this.uid}`; await enablePage.enable('aws', path); @@ -408,7 +408,7 @@ module('Acceptance | aws | configuration', function (hooks) { ); assert.strictEqual( currentURL(), - `/vault/secrets/${path}/configuration/index`, + `/vault/secrets/${path}/configuration`, 'lease configuration failed to save but the component transitioned as expected' ); // cleanup From d0c05c016ac49450684d59c053e51e5cc5799669 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 23 Jan 2025 10:08:05 -0700 Subject: [PATCH 13/16] replace notOk with throw --- .../components/secret-engine/configure-wif-test.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/tests/integration/components/secret-engine/configure-wif-test.js b/ui/tests/integration/components/secret-engine/configure-wif-test.js index 8e6e2aaa0c27..be64a7e72cc6 100644 --- a/ui/tests/integration/components/secret-engine/configure-wif-test.js +++ b/ui/tests/integration/components/secret-engine/configure-wif-test.js @@ -480,7 +480,9 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { test('it shows placeholder issuer, and does not call APIs on canceling out of issuer modal', async function (assert) { this.server.post('/identity/oidc/config', () => { - assert.notOk(true, 'request should not be made to issuer config endpoint'); + throw new Error( + 'Request was made to post the identity/oidc/config when it should not have been because user canceled out of the modal.' + ); }); this.server.post(configUrl('azure', this.id), () => { throw new Error( @@ -531,7 +533,9 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }; }); this.server.post(configUrl('azure', this.id), () => { - assert.notOk(true, 'skips request to config because the model was not changed'); + throw new Error( + `Request was made to post the config when it should not have been because no data was changed.` + ); }); this.id = `azure-${this.uid}`; this.displayName = 'Azure'; From 0cec8e41b5587cc475b5561061ec2c9b01a393c2 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 23 Jan 2025 11:32:26 -0700 Subject: [PATCH 14/16] move back error message --- ui/app/components/secret-engine/configure-wif.hbs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/app/components/secret-engine/configure-wif.hbs b/ui/app/components/secret-engine/configure-wif.hbs index 50952156a627..4b22df72562d 100644 --- a/ui/app/components/secret-engine/configure-wif.hbs +++ b/ui/app/components/secret-engine/configure-wif.hbs @@ -5,6 +5,7 @@ + {{! AWS specific note and section header }} {{#if (eq @type "aws")}}

@@ -56,8 +57,6 @@

{{/if}} - {{! surface error message above the beginning of the form fields }} - {{#if (eq this.accessType "wif")}} {{! if access type is "wif" display Issuer and WIF fields }} {{#each @issuerConfig.displayAttrs as |attr|}} From 81e3c15437e1c3157b3d3668518f7967b7fbe723 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Fri, 24 Jan 2025 09:52:15 -0700 Subject: [PATCH 15/16] clean up focused largely on wif component test --- .../secret-engine/configure-wif.hbs | 2 +- .../backend/aws/aws-configuration-test.js | 8 +- .../secret-engine/secret-engine-selectors.ts | 1 + .../secret-engine/configure-wif-test.js | 122 +++++++----------- 4 files changed, 52 insertions(+), 81 deletions(-) diff --git a/ui/app/components/secret-engine/configure-wif.hbs b/ui/app/components/secret-engine/configure-wif.hbs index 4b22df72562d..dd9921bbc0c8 100644 --- a/ui/app/components/secret-engine/configure-wif.hbs +++ b/ui/app/components/secret-engine/configure-wif.hbs @@ -8,7 +8,7 @@ {{! AWS specific note and section header }} {{#if (eq @type "aws")}} -

+

Note: the client uses the official AWS SDK and will use the specified credentials, environment credentials, shared file credentials, or IAM role/ECS task credentials in that order.

diff --git a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js index 5debf1ade66a..365fa7a2945e 100644 --- a/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js +++ b/ui/tests/acceptance/secrets/backend/aws/aws-configuration-test.js @@ -309,7 +309,7 @@ module('Acceptance | aws | configuration', function (hooks) { }); test('it saves lease configuration if root configuration was not changed', async function (assert) { - assert.expect(1); + assert.expect(2); const path = `aws-${this.uid}`; await enablePage.enable('aws', path); @@ -328,7 +328,11 @@ module('Acceptance | aws | configuration', function (hooks) { this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s lease configuration.`), 'Success flash message is rendered showing the lease configuration was saved.' ); - assert.strictEqual(currentURL(), `/vault/secrets/${path}/configuration/edit`); + assert.strictEqual( + currentURL(), + `/vault/secrets/${path}/configuration`, + 'the form transitioned as expected to the details page' + ); // cleanup await runCmd(`delete sys/mounts/${path}`); }); diff --git a/ui/tests/helpers/secret-engine/secret-engine-selectors.ts b/ui/tests/helpers/secret-engine/secret-engine-selectors.ts index 95288035615b..2b49dad26056 100644 --- a/ui/tests/helpers/secret-engine/secret-engine-selectors.ts +++ b/ui/tests/helpers/secret-engine/secret-engine-selectors.ts @@ -6,6 +6,7 @@ export const SECRET_ENGINE_SELECTORS = { configTab: '[data-test-configuration-tab]', configure: '[data-test-secret-backend-configure]', + configureNote: (name: string) => `[data-test-configure-note="${name}"]`, configureTitle: (type: string) => `[data-test-backend-configure-title="${type}"]`, configurationToggle: '[data-test-mount-config-toggle]', createSecret: '[data-test-secret-create]', diff --git a/ui/tests/integration/components/secret-engine/configure-wif-test.js b/ui/tests/integration/components/secret-engine/configure-wif-test.js index be64a7e72cc6..a0189182c81d 100644 --- a/ui/tests/integration/components/secret-engine/configure-wif-test.js +++ b/ui/tests/integration/components/secret-engine/configure-wif-test.js @@ -26,7 +26,7 @@ import waitForError from 'vault/tests/helpers/wait-for-error'; const allEnginesArray = allEngines(); // saving as const so we don't invoke the method multiple times in the for loop -module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { +module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) { setupRenderingTest(hooks); setupMirage(hooks); @@ -115,9 +115,8 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { assert.dom(GENERAL.inputByAttr('issuer')).exists(`issuer shows for ${type} wif section.`); }); } - + /* This module covers code that is the same for all engines. We run them once against one of the engines.*/ module('Engine agnostic', function () { - /* This module covers code that is the same for all engines. We run them once against one of the engines.*/ test('it transitions without sending a config or issuer payload on cancel', async function (assert) { assert.expect(3); this.id = `azure-${this.uid}`; @@ -233,14 +232,15 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); }); - module('Azure specific', function () { - test('it clears access type inputs after toggling accessType', async function (assert) { + module('Azure specific', function (hooks) { + hooks.beforeEach(function () { this.id = `azure-${this.uid}`; this.displayName = 'Azure'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); this.mountConfigModel = this.store.createRecord('azure/config'); this.mountConfigModel.backend = this.id; - + }); + test('it clears access type inputs after toggling accessType', async function (assert) { await render(hbs` `); @@ -276,12 +276,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); test('it shows the correct access type subtext', async function (assert) { - this.id = `azure-${this.uid}`; - this.displayName = 'Azure'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.mountConfigModel = this.store.createRecord('azure/config'); - this.mountConfigModel.backend = this.id; - await render(hbs` `); @@ -292,16 +286,29 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { 'Choose the way to configure access to Azure. Access can be configured either using Azure account credentials or with the Plugin Workload Identity Federation (WIF).' ); }); + + test('it does not show aws specific note', async function (assert) { + await render(hbs` + + `); + + assert + .dom(SES.configureNote('azure')) + .doesNotExist('Note specific to AWS does not show for Azure secret engine when configuring.'); + }); }); - module('AWS specific', function () { - test('it clears access type inputs after toggling accessType', async function (assert) { + module('AWS specific', function (hooks) { + hooks.beforeEach(function () { this.id = `aws-${this.uid}`; this.displayName = 'AWS'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); this.mountConfigModel = this.store.createRecord('aws/root-config'); - this.mountConfigModel.backend = this.id; + this.additionalConfigModel = this.store.createRecord('aws/lease-config'); + this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id; + }); + test('it clears access type inputs after toggling accessType', async function (assert) { await render(hbs` `); @@ -334,12 +341,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); test('it shows the correct access type subtext', async function (assert) { - this.id = `aws-${this.uid}`; - this.displayName = 'AWS'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.mountConfigModel = this.store.createRecord('aws/root-config'); - this.mountConfigModel.backend = this.id; - await render(hbs` `); @@ -352,13 +353,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); test('it shows validation error if default lease is entered but max lease is not', async function (assert) { - this.id = `aws-${this.uid}`; - this.displayName = 'AWS'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.mountConfigModel = this.store.createRecord('aws/root-config'); - this.additionalConfigModel = this.store.createRecord('aws/lease-config'); - this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id; - await render(hbs` `); @@ -382,13 +376,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); test('it allows user to submit root config even if API error occurs on config/lease config', async function (assert) { - this.id = `aws-${this.uid}`; - this.displayName = 'AWS'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.mountConfigModel = this.store.createRecord('aws/root-config'); - this.additionalConfigModel = this.store.createRecord('aws/lease-config'); - this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id; - await render(hbs` `); @@ -416,13 +403,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); test('it transitions without sending a lease, root, or issuer payload on cancel', async function (assert) { - this.id = `aws-${this.uid}`; - this.displayName = 'AWS'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.mountConfigModel = this.store.createRecord('aws/root-config'); - this.additionalConfigModel = this.store.createRecord('aws/lease-config'); - this.mountConfigModel.backend = this.additionalConfigModel.backend = this.id; - await render(hbs` `); @@ -453,17 +433,26 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { 'Transitioned to the configuration index route.' ); }); + + test('it does show aws specific note', async function (assert) { + await render(hbs` + + `); + + assert.dom(SES.configureNote('aws')).exists('Note specific to AWS does show when configuring.'); + }); }); - module('Issuer field tests', function () { - test('if issuer API error and user changes issuer value, shows specific warning message', async function (assert) { + module('Issuer field tests', function (hooks) { + hooks.beforeEach(function () { this.id = `azure-${this.uid}`; this.displayName = 'Azure'; this.issuerConfig = createConfig(this.store, this.id, 'issuer'); this.issuerConfig.queryIssuerError = true; this.mountConfigModel = this.store.createRecord('azure/config'); this.mountConfigModel.backend = this.id; - + }); + test('if issuer API error and user changes issuer value, shows specific warning message', async function (assert) { await render(hbs` `); @@ -489,12 +478,7 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { `Request was made to post the config when it should not have been because the user canceled out of the flow.` ); }); - this.id = `azure-${this.uid}`; - this.displayName = 'Azure'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.mountConfigModel = this.store.createRecord('azure/config'); - this.mountConfigModel.backend = this.id; - + this.issuerConfig.queryIssuerError = false; await render(hbs` `); @@ -537,11 +521,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { `Request was made to post the config when it should not have been because no data was changed.` ); }); - this.id = `azure-${this.uid}`; - this.displayName = 'Azure'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.mountConfigModel = this.store.createRecord('azure/config'); - this.mountConfigModel.backend = this.id; await render(hbs` @@ -566,12 +545,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { test('shows modal when modifying the issuer, has correct payload, and shows flash message on fail', async function (assert) { assert.expect(7); - this.id = `azure-${this.uid}`; - this.displayName = 'Azure'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.mountConfigModel = this.store.createRecord('azure/config'); - this.mountConfigModel.backend = this.id; - this.server.post(configUrl('azure', this.id), () => { assert.true( true, @@ -611,11 +584,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); test('it does not clear global issuer when toggling accessType', async function (assert) { - this.id = `azure-${this.uid}`; - this.displayName = 'Azure'; - this.issuerConfig = createConfig(this.store, this.id, 'issuer'); - this.mountConfigModel = this.store.createRecord(`azure/config`); - this.mountConfigModel.backend = this.id; await render(hbs` `); @@ -728,10 +696,13 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); } - module('Azure specific', function () { - test('it defaults to Azure accessType if Azure account fields are already set', async function (assert) { + module('Azure specific', function (hooks) { + hooks.beforeEach(function () { this.id = `azure-${this.uid}`; this.mountConfigModel = createConfig(this.store, this.id, 'azure'); + }); + + test('it defaults to Azure accessType if Azure account fields are already set', async function (assert) { await render(hbs` `); @@ -746,7 +717,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); test('it allows you to change accessType if record does not have wif or azure values already set', async function (assert) { - this.id = `azure-${this.uid}`; this.mountConfigModel = createConfig(this.store, this.id, 'azure-generic'); await render(hbs` @@ -772,7 +742,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { test('it requires a double click to change the client secret', async function (assert) { this.id = `azure-${this.uid}`; - this.mountConfigModel = createConfig(this.store, this.id, 'azure'); await render(hbs` `); @@ -793,10 +762,12 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); }); - module('AWS specific', function () { - test('it defaults to IAM accessType if IAM fields are already set', async function (assert) { + module('AWS specific', function (hooks) { + hooks.beforeEach(function () { this.id = `aws-${this.uid}`; this.mountConfigModel = createConfig(this.store, this.id, 'aws'); + }); + test('it defaults to IAM accessType if IAM fields are already set', async function (assert) { await render(hbs` `); @@ -810,7 +781,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); test('it allows you to change access type if record does not have wif or iam values already set', async function (assert) { - this.id = `aws-${this.uid}`; this.mountConfigModel = createConfig(this.store, this.id, 'aws-no-access'); await render(hbs` @@ -820,8 +790,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); test('it shows previously saved root and lease information', async function (assert) { - this.id = `aws-${this.uid}`; - this.mountConfigModel = createConfig(this.store, this.id, 'aws'); this.additionalConfigModel = createConfig(this.store, this.id, 'aws-lease'); await render(hbs` @@ -843,8 +811,6 @@ module('Integration | Component | SecretEngine/ConfigureWif', function (hooks) { }); test('it requires a double click to change the secret key', async function (assert) { - this.id = `aws-${this.uid}`; - this.mountConfigModel = createConfig(this.store, this.id, 'aws'); await render(hbs` `); From f7f1ccebf6832273e89ff0df85406f255b1d85e4 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Fri, 24 Jan 2025 10:09:38 -0700 Subject: [PATCH 16/16] replace ok with true --- .../components/secret-engine/configure-wif-test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/tests/integration/components/secret-engine/configure-wif-test.js b/ui/tests/integration/components/secret-engine/configure-wif-test.js index a0189182c81d..6126fb3e66c2 100644 --- a/ui/tests/integration/components/secret-engine/configure-wif-test.js +++ b/ui/tests/integration/components/secret-engine/configure-wif-test.js @@ -144,7 +144,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.'); - assert.ok( + assert.true( this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), 'Transitioned to the configuration index route.' ); @@ -160,7 +160,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) `); const err = await promise; - assert.ok( + assert.true( err.message.includes( `'isWifPluginConfigured' is required to be defined on the config model. Must return a boolean.` ), @@ -197,7 +197,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`), 'Flash message shows that root was saved even if issuer was not' ); - assert.ok( + assert.true( this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), 'Transitioned to the configuration index route.' ); @@ -396,7 +396,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) this.flashDangerSpy.calledWith('Lease configuration was not saved: bad request!!'), 'Flash message shows that lease was not saved.' ); - assert.ok( + assert.true( this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), 'Transitioned to the configuration index route.' ); @@ -428,7 +428,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) assert.true(this.flashDangerSpy.notCalled, 'No danger flash messages called.'); assert.true(this.flashSuccessSpy.notCalled, 'No success flash messages called.'); - assert.ok( + assert.true( this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), 'Transitioned to the configuration index route.' ); @@ -577,7 +577,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) this.flashSuccessSpy.calledWith(`Successfully saved ${this.id}'s configuration.`), "calls the config flash message not the issuer's" ); - assert.ok( + assert.true( this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), 'Transitioned to the configuration index route.' );