mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	One WIF configuration component (#29367)
* make one component and make one test file for that component. remove the two components and associated files the new component replaces * make access type subtext dynamic based on model type * clean up * clean up * remove model attr for display purposes * split out lease to another second config model type and make is-wif-engine helper * welp missed the old controller * small removal of overkill comment * pr feedback * save lease config if only thing changed * error handling in acceptance test * test fix * replace notOk with throw * move back error message * clean up focused largely on wif component test * replace ok with true
This commit is contained in:
		| @@ -1,136 +0,0 @@ | |||||||
| {{! |  | ||||||
|   Copyright (c) HashiCorp, Inc. |  | ||||||
|   SPDX-License-Identifier: BUSL-1.1 |  | ||||||
| ~}} |  | ||||||
|  |  | ||||||
| <form {{on "submit" (perform this.submitForm)}} aria-label="save aws creds" data-test-root-form> |  | ||||||
|   <div class="box is-fullwidth is-shadowless is-marginless"> |  | ||||||
|     <NamespaceReminder @mode="save" @noun="configuration" /> |  | ||||||
|     <MessageError @errorMessage={{this.errorMessageRoot}} /> |  | ||||||
|     <p class="has-text-grey-dark"> |  | ||||||
|       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. |  | ||||||
|     </p> |  | ||||||
|   </div> |  | ||||||
|   {{! Root configuration details }} |  | ||||||
|   <h2 class="title is-5 has-bottom-padding-s has-top-margin-l" data-test-access-title> |  | ||||||
|     Access to AWS |  | ||||||
|   </h2> |  | ||||||
|   <div class="box is-fullwidth is-sideless"> |  | ||||||
|     {{! 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}} |  | ||||||
|       <fieldset class="field form-fieldset" id="protection" data-test-access-type-section> |  | ||||||
|         <legend class="is-label">Access Type</legend> |  | ||||||
|         <p class="sub-text" data-test-access-type-subtext> |  | ||||||
|           {{#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}}</p> |  | ||||||
|         <div> |  | ||||||
|           <RadioButton |  | ||||||
|             id="access-type-iam" |  | ||||||
|             name="iam" |  | ||||||
|             class="radio" |  | ||||||
|             data-test-access-type="iam" |  | ||||||
|             @value="iam" |  | ||||||
|             @groupValue={{this.accessType}} |  | ||||||
|             @onChange={{fn this.onChangeAccessType "iam"}} |  | ||||||
|             @disabled={{this.disableAccessType}} |  | ||||||
|           /> |  | ||||||
|           <label for="access-type-iam">IAM Credentials</label> |  | ||||||
|  |  | ||||||
|           <RadioButton |  | ||||||
|             id="access-type-wif" |  | ||||||
|             name="wif" |  | ||||||
|             class="radio has-left-margin-m" |  | ||||||
|             data-test-access-type="wif" |  | ||||||
|             @value="wif" |  | ||||||
|             @groupValue={{this.accessType}} |  | ||||||
|             @onChange={{fn this.onChangeAccessType "wif"}} |  | ||||||
|             @disabled={{this.disableAccessType}} |  | ||||||
|           /> |  | ||||||
|           <label for="access-type-wif">Workload Identity Federation</label> |  | ||||||
|         </div> |  | ||||||
|       </fieldset> |  | ||||||
|     {{/if}} |  | ||||||
|     {{#if (eq this.accessType "wif")}} |  | ||||||
|       {{! WIF Fields }} |  | ||||||
|       {{#each @issuerConfig.displayAttrs as |attr|}} |  | ||||||
|         <FormField @attr={{attr}} @model={{@issuerConfig}} /> |  | ||||||
|       {{/each}} |  | ||||||
|       <FormFieldGroups |  | ||||||
|         @model={{@rootConfig}} |  | ||||||
|         @mode={{if @rootConfig.isNew "create" "edit"}} |  | ||||||
|         @modelValidations={{this.modelValidationsRoot}} |  | ||||||
|         @groupName="fieldGroupsWif" |  | ||||||
|       /> |  | ||||||
|     {{else}} |  | ||||||
|       {{! IAM Fields }} |  | ||||||
|       <FormFieldGroups |  | ||||||
|         @model={{@rootConfig}} |  | ||||||
|         @mode={{if @rootConfig.isNew "create" "edit"}} |  | ||||||
|         @modelValidations={{this.modelValidationsRoot}} |  | ||||||
|         @useEnableInput={{true}} |  | ||||||
|         @groupName="fieldGroupsIam" |  | ||||||
|       /> |  | ||||||
|     {{/if}} |  | ||||||
|   </div> |  | ||||||
|  |  | ||||||
|   {{! Lease configuration details }} |  | ||||||
|   <h2 class="title is-5 has-bottom-padding-s has-top-margin-l" data-test-lease-title> |  | ||||||
|     Leases |  | ||||||
|   </h2> |  | ||||||
|   <div class="box is-fullwidth is-sideless is-bottomless"> |  | ||||||
|     {{#each @leaseConfig.displayAttrs as |attr|}} |  | ||||||
|       <FormField @attr={{attr}} @model={{@leaseConfig}} @modelValidations={{this.modelValidationsLease}} /> |  | ||||||
|     {{/each}} |  | ||||||
|   </div> |  | ||||||
|  |  | ||||||
|   <div class="box is-fullwidth is-bottomless"> |  | ||||||
|     <div class="control"> |  | ||||||
|       <Hds::Button |  | ||||||
|         @text="Save" |  | ||||||
|         @icon={{if this.save.isRunning "loading"}} |  | ||||||
|         type="submit" |  | ||||||
|         disabled={{this.save.isRunning}} |  | ||||||
|         data-test-save |  | ||||||
|       /> |  | ||||||
|       <Hds::Button |  | ||||||
|         @text="Cancel" |  | ||||||
|         @color="secondary" |  | ||||||
|         class="has-left-margin-s" |  | ||||||
|         disabled={{this.save.isRunning}} |  | ||||||
|         {{on "click" this.onCancel}} |  | ||||||
|         data-test-cancel |  | ||||||
|       /> |  | ||||||
|     </div> |  | ||||||
|     {{#if this.invalidFormAlert}} |  | ||||||
|       <AlertInline |  | ||||||
|         data-test-invalid-form-alert |  | ||||||
|         class="has-top-padding-s" |  | ||||||
|         @type="danger" |  | ||||||
|         @message={{this.invalidFormAlert}} |  | ||||||
|       /> |  | ||||||
|     {{/if}} |  | ||||||
|   </div> |  | ||||||
| </form> |  | ||||||
|  |  | ||||||
| {{#if this.saveIssuerWarning}} |  | ||||||
|   <Hds::Modal @color="warning" @onClose={{action (mut this.saveIssuerWarning) ""}} data-test-issuer-warning as |M|> |  | ||||||
|     <M.Header @icon="alert-circle"> |  | ||||||
|       Are you sure? |  | ||||||
|     </M.Header> |  | ||||||
|     <M.Body> |  | ||||||
|       <p class="has-bottom-margin-s" data-test-issuer-warning-message> |  | ||||||
|         {{this.saveIssuerWarning}} |  | ||||||
|       </p> |  | ||||||
|     </M.Body> |  | ||||||
|     <M.Footer as |F|> |  | ||||||
|       <Hds::ButtonSet> |  | ||||||
|         <Hds::Button @text="Continue" {{on "click" this.continueSubmitForm}} data-test-issuer-save /> |  | ||||||
|         <Hds::Button @text="Cancel" @color="secondary" {{on "click" F.close}} data-test-issuer-cancel /> |  | ||||||
|       </Hds::ButtonSet> |  | ||||||
|     </M.Footer> |  | ||||||
|   </Hds::Modal> |  | ||||||
| {{/if}} |  | ||||||
| @@ -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 |  | ||||||
|  * <SecretEngine::ConfigureAws |  | ||||||
|     @rootConfig={{this.model.aws-root-config}} |  | ||||||
|     @leaseConfig={{this.model.aws-lease-config}} |  | ||||||
|     @backendPath={{this.model.id}} |  | ||||||
|     /> |  | ||||||
|  * ``` |  | ||||||
|  * |  | ||||||
|  * @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<Args> { |  | ||||||
|   @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<boolean> { |  | ||||||
|     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<boolean> { |  | ||||||
|     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<boolean> { |  | ||||||
|     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(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,184 +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 errorMessage from 'vault/utils/error-message'; |  | ||||||
|  |  | ||||||
| import type ConfigModel from 'vault/models/azure/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 SecretEngineConfigureAzure component is used to configure the Azure secret engine |  | ||||||
|  * For enterprise users, they will see an additional option to config WIF attributes in place of Azure account attributes. |  | ||||||
|  * 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. |  | ||||||
|  * @example |  | ||||||
|  * <SecretEngine::ConfigureAzure |  | ||||||
|     @model={{this.model.azure-config}} |  | ||||||
|     @backendPath={{this.model.id}} |  | ||||||
|     @issuerConfig={{this.model.identity-oidc-config}} |  | ||||||
|     /> |  | ||||||
|  *  |  | ||||||
|  * @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 |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| interface Args { |  | ||||||
|   model: ConfigModel; |  | ||||||
|   issuerConfig: IdentityOidcConfigModel; |  | ||||||
|   backendPath: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default class ConfigureAzureComponent extends Component<Args> { |  | ||||||
|   @service declare readonly router: Router; |  | ||||||
|   @service declare readonly store: StoreService; |  | ||||||
|   @service declare readonly version: VersionService; |  | ||||||
|   @service declare readonly flashMessages: FlashMessageService; |  | ||||||
|  |  | ||||||
|   @tracked accessType = 'azure'; |  | ||||||
|   @tracked errorMessage = ''; |  | ||||||
|   @tracked invalidFormAlert = ''; |  | ||||||
|   @tracked saveIssuerWarning = ''; |  | ||||||
|  |  | ||||||
|   disableAccessType = false; |  | ||||||
|  |  | ||||||
|   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, 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; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   get modelAttrChanged() { |  | ||||||
|     // "backend" dirties model state so explicity ignore it here |  | ||||||
|     return Object.keys(this.args.model?.changedAttributes()).some((item) => item !== 'backend'); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   get issuerAttrChanged() { |  | ||||||
|     return this.args.issuerConfig?.hasDirtyAttributes; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @action continueSubmitForm() { |  | ||||||
|     this.saveIssuerWarning = ''; |  | ||||||
|     this.save.perform(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // check if the issuer has been changed to show issuer modal |  | ||||||
|   // continue saving the configuration |  | ||||||
|   submitForm = task( |  | ||||||
|     waitFor(async (event: Event) => { |  | ||||||
|       event?.preventDefault(); |  | ||||||
|       this.resetErrors(); |  | ||||||
|  |  | ||||||
|       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 |  | ||||||
|         this.saveIssuerWarning = `You are updating the global issuer config. This will overwrite Vault's current issuer ${ |  | ||||||
|           this.args.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 () => { |  | ||||||
|       const modelAttrChanged = this.modelAttrChanged; |  | ||||||
|       const issuerAttrChanged = this.issuerAttrChanged; |  | ||||||
|       // check if any of the model or issue attributes have changed |  | ||||||
|       // if no changes, transition and notify user |  | ||||||
|       if (!modelAttrChanged && !issuerAttrChanged) { |  | ||||||
|         this.flashMessages.info('No changes detected.'); |  | ||||||
|         this.transition(); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const modelSaved = modelAttrChanged ? await this.saveModel() : false; |  | ||||||
|       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 |  | ||||||
|         this.transition(); |  | ||||||
|       } else { |  | ||||||
|         // otherwise there was a failure and we should not transition and exit the function |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   async updateIssuer(): Promise<boolean> { |  | ||||||
|     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.')}`); |  | ||||||
|       // remove issuer from the config model if it was not saved |  | ||||||
|       this.args.issuerConfig.rollbackAttributes(); |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async saveModel(): Promise<boolean> { |  | ||||||
|     const { backendPath, model } = this.args; |  | ||||||
|     try { |  | ||||||
|       await model.save(); |  | ||||||
|       this.flashMessages.success(`Successfully saved ${backendPath}'s configuration.`); |  | ||||||
|       return true; |  | ||||||
|     } catch (error) { |  | ||||||
|       this.errorMessage = errorMessage(error); |  | ||||||
|       this.invalidFormAlert = 'There was an error submitting this form.'; |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   resetErrors() { |  | ||||||
|     this.flashMessages.clearMessages(); |  | ||||||
|     this.errorMessage = ''; |  | ||||||
|     this.invalidFormAlert = ''; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   transition() { |  | ||||||
|     this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @action |  | ||||||
|   onChangeAccessType(accessType: string) { |  | ||||||
|     this.accessType = accessType; |  | ||||||
|     const { model } = this.args; |  | ||||||
|     if (accessType === 'azure') { |  | ||||||
|       // reset all WIF attributes |  | ||||||
|       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; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @action |  | ||||||
|   onCancel() { |  | ||||||
|     this.resetErrors(); |  | ||||||
|     this.args.model.unloadRecord(); |  | ||||||
|     this.transition(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -3,11 +3,21 @@ | |||||||
|   SPDX-License-Identifier: BUSL-1.1 |   SPDX-License-Identifier: BUSL-1.1 | ||||||
| ~}} | ~}} | ||||||
| 
 | 
 | ||||||
| <form {{on "submit" (perform this.submitForm)}} aria-label="configure azure credentials" data-test-configure-form> | <form {{on "submit" (perform this.submitForm)}} aria-label="configure {{@type}} credentials" data-test-configure-form> | ||||||
|   <NamespaceReminder @mode="save" @noun="configuration" /> |   <NamespaceReminder @mode="save" @noun="configuration" /> | ||||||
|   <MessageError @errorMessage={{this.errorMessage}} /> |   <MessageError @errorMessage={{this.errorMessage}} /> | ||||||
|  |   {{! AWS specific note and section header }} | ||||||
|  |   {{#if (eq @type "aws")}} | ||||||
|  |     <p class="has-text-grey-dark has-top-bottom-margin" data-test-configure-note={{@type}}> | ||||||
|  |       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. | ||||||
|  |     </p> | ||||||
|  |     <h2 class="title is-5 has-bottom-padding-s has-top-margin-l" data-test-access-title> | ||||||
|  |       Access to AWS | ||||||
|  |     </h2> | ||||||
|  |   {{/if}} | ||||||
|   <div class="box is-fullwidth is-sideless"> |   <div class="box is-fullwidth is-sideless"> | ||||||
|     {{! 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 change access type from "account" to "wif" }} | ||||||
|     {{#if this.version.isEnterprise}} |     {{#if this.version.isEnterprise}} | ||||||
|       <fieldset class="field form-fieldset" id="protection" data-test-access-type-section> |       <fieldset class="field form-fieldset" id="protection" data-test-access-type-section> | ||||||
|         <legend class="is-label">Access Type</legend> |         <legend class="is-label">Access Type</legend> | ||||||
| @@ -15,22 +25,24 @@ | |||||||
|           {{#if this.disableAccessType}} |           {{#if this.disableAccessType}} | ||||||
|             You cannot edit Access Type if you have already saved access credentials. |             You cannot edit Access Type if you have already saved access credentials. | ||||||
|           {{else}} |           {{else}} | ||||||
|             Choose the way to configure access to Azure. Access can be configured either using an Azure account or with the |             Choose the way to configure access to | ||||||
|             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}} |           {{/if}} | ||||||
|         </p> |         </p> | ||||||
|         <div> |         <div> | ||||||
|           <RadioButton |           <RadioButton | ||||||
|             id="access-type-azure" |             id="access-type-{{@type}}" | ||||||
|             name="azure" |             name="account" | ||||||
|             class="radio" |             class="radio" | ||||||
|             data-test-access-type="azure" |             data-test-access-type={{@type}} | ||||||
|             @value="azure" |             @value="account" | ||||||
|             @groupValue={{this.accessType}} |             @groupValue={{this.accessType}} | ||||||
|             @onChange={{fn this.onChangeAccessType "azure"}} |             @onChange={{this.onChangeAccessType}} | ||||||
|             @disabled={{this.disableAccessType}} |             @disabled={{this.disableAccessType}} | ||||||
|           /> |           /> | ||||||
|           <label for="access-type-azure">Azure account credentials</label> |           <label for="access-type-{{@type}}">{{@displayName}} account credentials</label> | ||||||
|           <RadioButton |           <RadioButton | ||||||
|             id="access-type-wif" |             id="access-type-wif" | ||||||
|             name="wif" |             name="wif" | ||||||
| @@ -38,7 +50,7 @@ | |||||||
|             data-test-access-type="wif" |             data-test-access-type="wif" | ||||||
|             @value="wif" |             @value="wif" | ||||||
|             @groupValue={{this.accessType}} |             @groupValue={{this.accessType}} | ||||||
|             @onChange={{fn this.onChangeAccessType "wif"}} |             @onChange={{this.onChangeAccessType}} | ||||||
|             @disabled={{this.disableAccessType}} |             @disabled={{this.disableAccessType}} | ||||||
|           /> |           /> | ||||||
|           <label for="access-type-wif">Workload Identity Federation</label> |           <label for="access-type-wif">Workload Identity Federation</label> | ||||||
| @@ -46,21 +58,38 @@ | |||||||
|       </fieldset> |       </fieldset> | ||||||
|     {{/if}} |     {{/if}} | ||||||
|     {{#if (eq this.accessType "wif")}} |     {{#if (eq this.accessType "wif")}} | ||||||
|       {{! WIF Fields }} |       {{! if access type is "wif" display Issuer and WIF fields }} | ||||||
|       {{#each @issuerConfig.displayAttrs as |attr|}} |       {{#each @issuerConfig.displayAttrs as |attr|}} | ||||||
|         <FormField @attr={{attr}} @model={{@issuerConfig}} /> |         <FormField @attr={{attr}} @model={{@issuerConfig}} /> | ||||||
|       {{/each}} |       {{/each}} | ||||||
|       <FormFieldGroups @model={{@model}} @mode={{if @model.isNew "create" "edit"}} @groupName="fieldGroupsWif" /> |  | ||||||
|     {{else}} |  | ||||||
|       {{! Azure Account Fields }} |  | ||||||
|       <FormFieldGroups |       <FormFieldGroups | ||||||
|         @model={{@model}} |         @model={{@mountConfigModel}} | ||||||
|         @mode={{if @model.isNew "create" "edit"}} |         @mode={{if @mountConfigModel.isNew "create" "edit"}} | ||||||
|  |         @groupName="fieldGroupsWif" | ||||||
|  |       /> | ||||||
|  |     {{else}} | ||||||
|  |       {{! otherwise display account credential fields }} | ||||||
|  |       <FormFieldGroups | ||||||
|  |         @model={{@mountConfigModel}} | ||||||
|  |         @mode={{if @mountConfigModel.isNew "create" "edit"}} | ||||||
|         @useEnableInput={{true}} |         @useEnableInput={{true}} | ||||||
|         @groupName="fieldGroupsAzure" |         @groupName="fieldGroupsAccount" | ||||||
|       /> |       /> | ||||||
|     {{/if}} |     {{/if}} | ||||||
|  |     {{! additionalConfigModel fields show regardless of the vault version or what access type is selected }} | ||||||
|  |     {{#if @additionalConfigModel}} | ||||||
|  |       <h2 class="title is-5 has-bottom-padding-s has-top-margin-l" data-test-additional-config-model-title> | ||||||
|  |         {{if (eq @type "aws") "Lease" "Additional"}} | ||||||
|  |         Configuration | ||||||
|  |       </h2> | ||||||
|  |       <div class="box is-fullwidth is-sideless is-bottomless"> | ||||||
|  |         {{#each @additionalConfigModel.formFields as |attr|}} | ||||||
|  |           <FormField @attr={{attr}} @model={{@additionalConfigModel}} @modelValidations={{this.modelValidations}} /> | ||||||
|  |         {{/each}} | ||||||
|       </div> |       </div> | ||||||
|  |     {{/if}} | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|   <Hds::ButtonSet> |   <Hds::ButtonSet> | ||||||
|     <Hds::Button |     <Hds::Button | ||||||
|       @text="Save" |       @text="Save" | ||||||
							
								
								
									
										252
									
								
								ui/app/components/secret-engine/configure-wif.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								ui/app/components/secret-engine/configure-wif.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,252 @@ | |||||||
|  | /** | ||||||
|  |  * 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 { assert } from '@ember/debug'; | ||||||
|  | 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 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 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 CE, the account configuration fields will display with no ability to select or see wif fields. | ||||||
|  |  *  | ||||||
|  |  * @example | ||||||
|  |  * <SecretEngine::ConfigureWif | ||||||
|  |     @backendPath={{this.model.id}} | ||||||
|  |     @displayName="AWS" | ||||||
|  |     @type="aws" | ||||||
|  |     @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. 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. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | interface Args { | ||||||
|  |   backendPath: string; | ||||||
|  |   displayName: string; | ||||||
|  |   type: string; | ||||||
|  |   mountConfigModel: MountConfigModel; | ||||||
|  |   additionalConfigModel: AdditionalConfigModel; | ||||||
|  |   issuerConfig: IdentityOidcConfigModel; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class ConfigureWif extends Component<Args> { | ||||||
|  |   @service declare readonly router: Router; | ||||||
|  |   @service declare readonly store: StoreService; | ||||||
|  |   @service declare readonly version: VersionService; | ||||||
|  |   @service declare readonly flashMessages: FlashMessageService; | ||||||
|  |  | ||||||
|  |   @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; | ||||||
|  |  | ||||||
|  |   constructor(owner: unknown, args: Args) { | ||||||
|  |     super(owner, args); | ||||||
|  |     // the following checks are only relevant to existing enterprise configurations | ||||||
|  |     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 | ||||||
|  |     ); | ||||||
|  |     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 mountConfigModelAttrChanged() { | ||||||
|  |     // "backend" dirties model state so explicity ignore it here | ||||||
|  |     return Object.keys(this.args.mountConfigModel?.changedAttributes()).some((item) => item !== 'backend'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get issuerAttrChanged() { | ||||||
|  |     return this.args.issuerConfig?.hasDirtyAttributes; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get additionalConfigModelAttrChanged() { | ||||||
|  |     const { additionalConfigModel } = this.args; | ||||||
|  |     // 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; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action continueSubmitForm() { | ||||||
|  |     this.saveIssuerWarning = ''; | ||||||
|  |     this.save.perform(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // check if the issuer has been changed to show issuer modal | ||||||
|  |   // continue saving the configuration | ||||||
|  |   submitForm = task( | ||||||
|  |     waitFor(async (event: Event) => { | ||||||
|  |       event?.preventDefault(); | ||||||
|  |       this.resetErrors(); | ||||||
|  |       // currently we only check validations on the additional model | ||||||
|  |       if (this.args.additionalConfigModel && !this.isValid(this.args.additionalConfigModel)) { | ||||||
|  |         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 | ||||||
|  |         this.saveIssuerWarning = `You are updating the global issuer config. This will overwrite Vault's current issuer ${ | ||||||
|  |           this.args.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 () => { | ||||||
|  |       const mountConfigModelChanged = this.mountConfigModelAttrChanged; | ||||||
|  |       const additionalModelAttrChanged = this.additionalConfigModelAttrChanged; | ||||||
|  |       const issuerAttrChanged = this.issuerAttrChanged; | ||||||
|  |       // check if any of the model(s) or issuer attributes have changed | ||||||
|  |       // if no changes, transition and notify user | ||||||
|  |       if (!mountConfigModelChanged && !additionalModelAttrChanged && !issuerAttrChanged) { | ||||||
|  |         this.flashMessages.info('No changes detected.'); | ||||||
|  |         this.transition(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const mountConfigModelSaved = mountConfigModelChanged ? await this.saveMountConfigModel() : false; | ||||||
|  |       const issuerSaved = issuerAttrChanged ? await this.updateIssuer() : false; | ||||||
|  |  | ||||||
|  |       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 | ||||||
|  |         this.transition(); | ||||||
|  |       } else { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   async updateIssuer(): Promise<boolean> { | ||||||
|  |     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.')}`); | ||||||
|  |       // remove issuer from the config model if it was not saved | ||||||
|  |       this.args.issuerConfig.rollbackAttributes(); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async saveMountConfigModel(): Promise<boolean> { | ||||||
|  |     const { backendPath, mountConfigModel } = this.args; | ||||||
|  |     try { | ||||||
|  |       await mountConfigModel.save(); | ||||||
|  |       this.flashMessages.success(`Successfully saved ${backendPath}'s configuration.`); | ||||||
|  |       return true; | ||||||
|  |     } catch (error) { | ||||||
|  |       this.errorMessage = errorMessage(error); | ||||||
|  |       this.invalidFormAlert = 'There was an error submitting this form.'; | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async saveAdditionalConfigModel() { | ||||||
|  |     const { backendPath, additionalConfigModel, type } = this.args; | ||||||
|  |     const additionalConfigModelName = type === 'aws' ? 'lease configuration' : 'additional configuration'; | ||||||
|  |     try { | ||||||
|  |       await additionalConfigModel.save(); | ||||||
|  |       this.flashMessages.success(`Successfully saved ${backendPath}'s ${additionalConfigModelName}.`); | ||||||
|  |     } catch (error) { | ||||||
|  |       // 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.modelValidations = null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   transition() { | ||||||
|  |     this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   isValid(model: AdditionalConfigModel) { | ||||||
|  |     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 { 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 | ||||||
|  |       mountConfigModel.identityTokenAudience = mountConfigModel.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 "account" attributes that are mutually exclusive with "wif" attributes | ||||||
|  |       // these attributes are different for each engine | ||||||
|  |       type === 'azure' | ||||||
|  |         ? (mountConfigModel.clientSecret = mountConfigModel.rootPasswordTtl = undefined) | ||||||
|  |         : type === 'aws' | ||||||
|  |         ? (mountConfigModel.accessKey = undefined) | ||||||
|  |         : null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   onCancel() { | ||||||
|  |     this.resetErrors(); | ||||||
|  |     this.args.mountConfigModel.unloadRecord(); | ||||||
|  |     this.transition(); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -3,54 +3,14 @@ | |||||||
|  * SPDX-License-Identifier: BUSL-1.1 |  * SPDX-License-Identifier: BUSL-1.1 | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| import { isPresent } from '@ember/utils'; |  | ||||||
| import { service } from '@ember/service'; |  | ||||||
| import Controller from '@ember/controller'; | import Controller from '@ember/controller'; | ||||||
|  | import { WIF_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines'; | ||||||
|  |  | ||||||
| const CONFIG_ATTRS = { | export default class SecretsBackendConfigurationEditController extends Controller { | ||||||
|   // ssh |   get isWifEngine() { | ||||||
|   configured: false, |     return WIF_ENGINES.includes(this.model.type); | ||||||
|  |   } | ||||||
|   // aws root config |   get displayName() { | ||||||
|   iamEndpoint: null, |     return allEngines().find((engine) => engine.type === this.model.type)?.displayName; | ||||||
|   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); |  | ||||||
|         }); |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
|   | |||||||
| @@ -74,21 +74,21 @@ export default class AwsRootConfig extends Model { | |||||||
|     return formFields.filter((attr) => attr.name !== 'secretKey'); |     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() { |   get fieldGroupsWif() { | ||||||
|     return fieldToAttrs(this, this.formFieldGroups('wif')); |     return fieldToAttrs(this, this.formFieldGroups('wif')); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get fieldGroupsIam() { |   get fieldGroupsAccount() { | ||||||
|     return fieldToAttrs(this, this.formFieldGroups('iam')); |     return fieldToAttrs(this, this.formFieldGroups('account')); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   formFieldGroups(accessType = 'iam') { |   formFieldGroups(accessType = 'account') { | ||||||
|     const formFieldGroups = []; |     const formFieldGroups = []; | ||||||
|     if (accessType === 'wif') { |     if (accessType === 'wif') { | ||||||
|       formFieldGroups.push({ default: ['roleArn', 'identityTokenAudience', 'identityTokenTtl'] }); |       formFieldGroups.push({ default: ['roleArn', 'identityTokenAudience', 'identityTokenTtl'] }); | ||||||
|     } |     } | ||||||
|     if (accessType === 'iam') { |     if (accessType === 'account') { | ||||||
|       formFieldGroups.push({ default: ['accessKey', 'secretKey'] }); |       formFieldGroups.push({ default: ['accessKey', 'secretKey'] }); | ||||||
|     } |     } | ||||||
|     formFieldGroups.push({ |     formFieldGroups.push({ | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ export default class AzureConfig extends Model { | |||||||
|     return !!this.identityTokenAudience || !!this.identityTokenTtl; |     return !!this.identityTokenAudience || !!this.identityTokenTtl; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get isAzureAccountConfigured() { |   get isAccountPluginConfigured() { | ||||||
|     // clientSecret is not checked here because it's never return by the API |     // clientSecret is not checked here because it's never return by the API | ||||||
|     // however it is an Azure account field |     // however it is an Azure account field | ||||||
|     return !!this.rootPasswordTtl; |     return !!this.rootPasswordTtl; | ||||||
| @@ -79,16 +79,16 @@ export default class AzureConfig extends Model { | |||||||
|     return formFields.filter((attr) => attr.name !== 'clientSecret'); |     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() { |   get fieldGroupsWif() { | ||||||
|     return fieldToAttrs(this, this.formFieldGroups('wif')); |     return fieldToAttrs(this, this.formFieldGroups('wif')); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get fieldGroupsAzure() { |   get fieldGroupsAccount() { | ||||||
|     return fieldToAttrs(this, this.formFieldGroups('azure')); |     return fieldToAttrs(this, this.formFieldGroups('account')); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   formFieldGroups(accessType = 'azure') { |   formFieldGroups(accessType = 'account') { | ||||||
|     const formFieldGroups = []; |     const formFieldGroups = []; | ||||||
|     formFieldGroups.push({ |     formFieldGroups.push({ | ||||||
|       default: ['subscriptionId', 'tenantId', 'clientId', 'environment'], |       default: ['subscriptionId', 'tenantId', 'clientId', 'environment'], | ||||||
| @@ -98,7 +98,7 @@ export default class AzureConfig extends Model { | |||||||
|         default: ['identityTokenAudience', 'identityTokenTtl'], |         default: ['identityTokenAudience', 'identityTokenTtl'], | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     if (accessType === 'azure') { |     if (accessType === 'account') { | ||||||
|       formFieldGroups.push({ |       formFieldGroups.push({ | ||||||
|         default: ['clientSecret', 'rootPasswordTtl'], |         default: ['clientSecret', 'rootPasswordTtl'], | ||||||
|       }); |       }); | ||||||
|   | |||||||
| @@ -19,8 +19,8 @@ import type VersionService from 'vault/services/version'; | |||||||
| // It generates config models based on the engine type. | // It generates config models based on the engine type. | ||||||
| // Saving and updating of those models are done within the engine specific components. | // Saving and updating of those models are done within the engine specific components. | ||||||
|  |  | ||||||
| const CONFIG_ADAPTERS_PATHS: Record<string, string[]> = { | const MOUNT_CONFIG_MODEL_NAMES: Record<string, string[]> = { | ||||||
|   aws: ['aws/lease-config', 'aws/root-config'], |   aws: ['aws/root-config', 'aws/lease-config'], | ||||||
|   azure: ['azure/config'], |   azure: ['azure/config'], | ||||||
|   ssh: ['ssh/ca-config'], |   ssh: ['ssh/ca-config'], | ||||||
| }; | }; | ||||||
| @@ -29,6 +29,19 @@ export default class SecretsBackendConfigurationEdit extends Route { | |||||||
|   @service declare readonly store: Store; |   @service declare readonly store: Store; | ||||||
|   @service declare readonly version: VersionService; |   @service declare readonly version: VersionService; | ||||||
|  |  | ||||||
|  |   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 'mount-config-model'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   async model() { |   async model() { | ||||||
|     const { backend } = this.paramsFor('vault.cluster.secrets.backend'); |     const { backend } = this.paramsFor('vault.cluster.secrets.backend'); | ||||||
|     const secretEngineRecord = this.modelFor('vault.cluster.secrets.backend') as SecretEngineModel; |     const secretEngineRecord = this.modelFor('vault.cluster.secrets.backend') as SecretEngineModel; | ||||||
| @@ -43,12 +56,12 @@ export default class SecretsBackendConfigurationEdit extends Route { | |||||||
|     // generate the model based on the engine type. |     // generate the model based on the engine type. | ||||||
|     // and pre-set model with type and backend e.g. {type: ssh, id: ssh-123} |     // and pre-set model with type and backend e.g. {type: ssh, id: ssh-123} | ||||||
|     const model: Record<string, unknown> = { type, id: backend }; |     const model: Record<string, unknown> = { type, id: backend }; | ||||||
|     for (const adapterPath of CONFIG_ADAPTERS_PATHS[type] as string[]) { |     for (const modelName of MOUNT_CONFIG_MODEL_NAMES[type] as string[]) { | ||||||
|       // convert the adapterPath with a name that can be passed to the components |       // create a key that corresponds with the model order | ||||||
|       // ex: adapterPath = ssh/ca-config, convert to: ssh-ca-config so that you can pass to component @model={{this.model.ssh-ca-config}} |       // 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 = adapterPath.replace(/\//g, '-'); |       const standardizedKey = this.standardizedModelName(type, modelName); | ||||||
|       try { |       try { | ||||||
|         const configModel = await this.store.queryRecord(adapterPath, { |         const configModel = await this.store.queryRecord(modelName, { | ||||||
|           backend, |           backend, | ||||||
|           type, |           type, | ||||||
|         }); |         }); | ||||||
| @@ -56,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 |         // 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 the engine is not configured we update the record to get the default values | ||||||
|         if (!configModel.isConfigured && type === 'azure') { |         if (!configModel.isConfigured && type === 'azure') { | ||||||
|           model[standardizedKey] = await this.store.createRecord(adapterPath, { |           model[standardizedKey] = await this.store.createRecord(modelName, { | ||||||
|             backend, |             backend, | ||||||
|             type, |             type, | ||||||
|           }); |           }); | ||||||
| @@ -71,7 +84,7 @@ export default class SecretsBackendConfigurationEdit extends Route { | |||||||
|           e.httpStatus === 404 || |           e.httpStatus === 404 || | ||||||
|           (type === 'ssh' && e.httpStatus === 400 && errorMessage(e) === `keys haven't been configured yet`) |           (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, |             backend, | ||||||
|             type, |             type, | ||||||
|           }); |           }); | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ | |||||||
|   <p.levelLeft> |   <p.levelLeft> | ||||||
|     <h1 class="title is-3" data-test-backend-configure-title={{this.model.type}}> |     <h1 class="title is-3" data-test-backend-configure-title={{this.model.type}}> | ||||||
|       Configure |       Configure | ||||||
|       {{get (options-for-backend this.model.type) "displayName"}} |       {{this.displayName}} | ||||||
|     </h1> |     </h1> | ||||||
|   </p.levelLeft> |   </p.levelLeft> | ||||||
| </PageHeader> | </PageHeader> | ||||||
| @@ -28,19 +28,16 @@ | |||||||
|   </ToolbarActions> |   </ToolbarActions> | ||||||
| </Toolbar> | </Toolbar> | ||||||
|  |  | ||||||
| {{#if (eq this.model.type "aws")}} | {{#if (eq this.model.type "ssh")}} | ||||||
|   <SecretEngine::ConfigureAws |   <SecretEngine::ConfigureSsh @model={{this.model.mount-config-model}} @id={{this.model.id}} /> | ||||||
|     @leaseConfig={{this.model.aws-lease-config}} |   {{! This "else if" check is preventive. As of writing, all engines using this route, but "ssh", are wif engines }} | ||||||
|     @rootConfig={{this.model.aws-root-config}} | {{else if this.isWifEngine}} | ||||||
|     @issuerConfig={{this.model.identity-oidc-config}} |   <SecretEngine::ConfigureWif | ||||||
|     @backendPath={{this.model.id}} |  | ||||||
|   /> |  | ||||||
| {{else if (eq this.model.type "azure")}} |  | ||||||
|   <SecretEngine::ConfigureAzure |  | ||||||
|     @model={{this.model.azure-config}} |  | ||||||
|     @backendPath={{this.model.id}} |     @backendPath={{this.model.id}} | ||||||
|  |     @displayName={{this.displayName}} | ||||||
|  |     @type={{this.model.type}} | ||||||
|  |     @mountConfigModel={{this.model.mount-config-model}} | ||||||
|  |     @additionalConfigModel={{this.model.additional-config-model}} | ||||||
|     @issuerConfig={{this.model.identity-oidc-config}} |     @issuerConfig={{this.model.identity-oidc-config}} | ||||||
|   /> |   /> | ||||||
| {{else if (eq this.model.type "ssh")}} |  | ||||||
|   <SecretEngine::ConfigureSsh @model={{this.model.ssh-ca-config}} @id={{this.model.id}} /> |  | ||||||
| {{/if}} | {{/if}} | ||||||
| @@ -34,10 +34,12 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|     this.store = this.owner.lookup('service:store'); |     this.store = this.owner.lookup('service:store'); | ||||||
|     this.flashSuccessSpy = spy(flash, 'success'); |     this.flashSuccessSpy = spy(flash, 'success'); | ||||||
|     this.flashInfoSpy = spy(flash, 'info'); |     this.flashInfoSpy = spy(flash, 'info'); | ||||||
|  |     this.flashDangerSpy = spy(flash, 'danger'); | ||||||
|     this.version = this.owner.lookup('service:version'); |     this.version = this.owner.lookup('service:version'); | ||||||
|     this.uid = uuidv4(); |     this.uid = uuidv4(); | ||||||
|     return authPage.login(); |     return authPage.login(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   module('isEnterprise', function (hooks) { |   module('isEnterprise', function (hooks) { | ||||||
|     hooks.beforeEach(function () { |     hooks.beforeEach(function () { | ||||||
|       this.version.type = 'enterprise'; |       this.version.type = 'enterprise'; | ||||||
| @@ -62,7 +64,13 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|       await click(SES.configure); |       await click(SES.configure); | ||||||
|       assert.strictEqual(currentURL(), `/vault/secrets/${path}/configuration/edit`); |       assert.strictEqual(currentURL(), `/vault/secrets/${path}/configuration/edit`); | ||||||
|       assert.dom(SES.configureTitle('aws')).hasText('Configure AWS'); |       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.additionalConfigModelTitle) | ||||||
|  |         .hasText( | ||||||
|  |           'Lease Configuration', | ||||||
|  |           'it shows the lease configuration section with the "Lease Configuration" title.' | ||||||
|  |         ); | ||||||
|       // cleanup |       // cleanup | ||||||
|       await runCmd(`delete sys/mounts/${path}`); |       await runCmd(`delete sys/mounts/${path}`); | ||||||
|     }); |     }); | ||||||
| @@ -83,9 +91,8 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|       await enablePage.enable('aws', path); |       await enablePage.enable('aws', path); | ||||||
|  |  | ||||||
|       this.server.post(configUrl('aws-lease', path), () => { |       this.server.post(configUrl('aws-lease', path), () => { | ||||||
|         assert.false( |         throw new Error( | ||||||
|           true, |           'A POST request was made to config/lease when it should not because no data was changed.' | ||||||
|           'post request was made to config/lease when no data was changed. test should fail.' |  | ||||||
|         ); |         ); | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
| @@ -93,13 +100,13 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|       await click(SES.configure); |       await click(SES.configure); | ||||||
|       await fillInAwsConfig('withWif'); |       await fillInAwsConfig('withWif'); | ||||||
|       await click(GENERAL.saveButton); |       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); |       await click(SES.wif.issuerWarningSave); | ||||||
|       // three flash messages, the first is about mounting the engine, only care about the last two |       // three flash messages, the first is about mounting the engine, only care about the last two | ||||||
|       assert.strictEqual( |       assert.strictEqual( | ||||||
|         this.flashSuccessSpy.args[1][0], |         this.flashSuccessSpy.args[1][0], | ||||||
|         `Successfully saved ${path}'s root configuration.`, |         `Successfully saved ${path}'s configuration.`, | ||||||
|         'first flash message about the root config.' |         'first flash message about the first model config.' | ||||||
|       ); |       ); | ||||||
|       assert.strictEqual( |       assert.strictEqual( | ||||||
|         this.flashSuccessSpy.args[2][0], |         this.flashSuccessSpy.args[2][0], | ||||||
| @@ -124,11 +131,11 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|       const type = 'aws'; |       const type = 'aws'; | ||||||
|       this.server.get(`${path}/config/root`, (schema, req) => { |       this.server.get(`${path}/config/root`, (schema, req) => { | ||||||
|         const payload = JSON.parse(req.requestBody); |         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 } }; |         return { data: { id: path, type, attributes: payload } }; | ||||||
|       }); |       }); | ||||||
|       this.server.get(`identity/oidc/config`, () => { |       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); |       await enablePage.enable(type, path); | ||||||
|       createConfig(this.store, path, type); // create the aws root config in the store |       createConfig(this.store, path, type); // create the aws root config in the store | ||||||
| @@ -145,10 +152,7 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|       await enablePage.enable('aws', path); |       await enablePage.enable('aws', path); | ||||||
|  |  | ||||||
|       this.server.post(configUrl('aws-lease', path), () => { |       this.server.post(configUrl('aws-lease', path), () => { | ||||||
|         assert.false( |         throw new Error(`post request was made to config/lease when it should not have been.`); | ||||||
|           true, |  | ||||||
|           'post request was made to config/lease when no data was changed. test should fail.' |  | ||||||
|         ); |  | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       await click(SES.configTab); |       await click(SES.configTab); | ||||||
| @@ -156,8 +160,8 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|       await fillInAwsConfig('withAccess'); |       await fillInAwsConfig('withAccess'); | ||||||
|       await click(GENERAL.saveButton); |       await click(GENERAL.saveButton); | ||||||
|       assert.true( |       assert.true( | ||||||
|         this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s root configuration.`), |         this.flashSuccessSpy.calledWith(`Successfully saved ${path}'s configuration.`), | ||||||
|         'Success flash message is rendered showing the root configuration was saved.' |         'Success flash message is rendered showing the configuration was saved.' | ||||||
|       ); |       ); | ||||||
|       assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', 'Access Key has been set.'); |       assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', 'Access Key has been set.'); | ||||||
|       assert |       assert | ||||||
| @@ -190,40 +194,12 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|       await runCmd(`delete sys/mounts/${path}`); |       await runCmd(`delete sys/mounts/${path}`); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     test('it should save lease AWS configuration', async function (assert) { |  | ||||||
|       assert.expect(3); |  | ||||||
|       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.' |  | ||||||
|         ); |  | ||||||
|       }); |  | ||||||
|       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 |  | ||||||
|         .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}`); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     test('it shows AWS mount configuration details', async function (assert) { |     test('it shows AWS mount configuration details', async function (assert) { | ||||||
|       const path = `aws-${this.uid}`; |       const path = `aws-${this.uid}`; | ||||||
|       const type = 'aws'; |       const type = 'aws'; | ||||||
|       this.server.get(`${path}/config/root`, (schema, req) => { |       this.server.get(`${path}/config/root`, (schema, req) => { | ||||||
|         const payload = JSON.parse(req.requestBody); |         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 } }; |         return { data: { id: path, type, attributes: payload } }; | ||||||
|       }); |       }); | ||||||
|       await enablePage.enable(type, path); |       await enablePage.enable(type, path); | ||||||
| @@ -280,19 +256,6 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|       await runCmd(`delete sys/mounts/${path}`); |       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) { |     test('it should not make a post request if lease or root data was unchanged', async function (assert) { | ||||||
|       assert.expect(3); |       assert.expect(3); | ||||||
|       const path = `aws-${this.uid}`; |       const path = `aws-${this.uid}`; | ||||||
| @@ -300,16 +263,10 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|       await enablePage.enable(type, path); |       await enablePage.enable(type, path); | ||||||
|  |  | ||||||
|       this.server.post(configUrl(type, path), () => { |       this.server.post(configUrl(type, path), () => { | ||||||
|         assert.false( |         throw new Error(`post request was made to config/root when it should not have been.`); | ||||||
|           true, |  | ||||||
|           'post request was made to config/root when no data was changed. test should fail.' |  | ||||||
|         ); |  | ||||||
|       }); |       }); | ||||||
|       this.server.post(configUrl('aws-lease', path), () => { |       this.server.post(configUrl('aws-lease', path), () => { | ||||||
|         assert.false( |         throw new Error(`post request was made to config/lease when it should not have been.`); | ||||||
|           true, |  | ||||||
|           'post request was made to config/lease when no data was changed. test should fail.' |  | ||||||
|         ); |  | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       await click(SES.configTab); |       await click(SES.configTab); | ||||||
| @@ -350,6 +307,35 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|       // cleanup |       // cleanup | ||||||
|       await runCmd(`delete sys/mounts/${path}`); |       await runCmd(`delete sys/mounts/${path}`); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     test('it saves lease configuration if root configuration was not changed', async function (assert) { | ||||||
|  |       assert.expect(2); | ||||||
|  |       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`, | ||||||
|  |         'the form transitioned as expected to the details page' | ||||||
|  |       ); | ||||||
|  |       // cleanup | ||||||
|  |       await runCmd(`delete sys/mounts/${path}`); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   module('isCommunity', function (hooks) { |   module('isCommunity', function (hooks) { | ||||||
| @@ -377,5 +363,97 @@ module('Acceptance | aws | configuration', function (hooks) { | |||||||
|       // cleanup |       // cleanup | ||||||
|       await runCmd(`delete sys/mounts/${path}`); |       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(2); | ||||||
|  |         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`, | ||||||
|  |           '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'); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -89,7 +89,7 @@ module('Acceptance | Azure | configuration', function (hooks) { | |||||||
|           environment: 'AZUREPUBLICCLOUD', |           environment: 'AZUREPUBLICCLOUD', | ||||||
|         }; |         }; | ||||||
|         this.server.get(`${path}/config`, () => { |         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 } }; |           return { data: { id: path, type: this.type, ...azureAccountAttrs } }; | ||||||
|         }); |         }); | ||||||
|         await enablePage.enable(this.type, path); |         await enablePage.enable(this.type, path); | ||||||
| @@ -109,17 +109,6 @@ module('Acceptance | Azure | configuration', function (hooks) { | |||||||
|         // cleanup |         // cleanup | ||||||
|         await runCmd(`delete sys/mounts/${path}`); |         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 () { |     module('create', function () { | ||||||
| @@ -129,9 +118,8 @@ module('Acceptance | Azure | configuration', function (hooks) { | |||||||
|         await enablePage.enable(this.type, path); |         await enablePage.enable(this.type, path); | ||||||
|  |  | ||||||
|         this.server.post('/identity/oidc/config', () => { |         this.server.post('/identity/oidc/config', () => { | ||||||
|           assert.notOk( |           throw new Error( | ||||||
|             true, |             `Request was made to return the issuer when it should not have been because user is on CE.` | ||||||
|             'post request was made to issuer endpoint when on community and data not changed. test should fail.' |  | ||||||
|           ); |           ); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
| @@ -226,6 +214,42 @@ module('Acceptance | Azure | configuration', function (hooks) { | |||||||
|         await runCmd(`delete sys/mounts/${path}`); |         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) { |   module('isEnterprise', function (hooks) { | ||||||
| @@ -245,7 +269,7 @@ module('Acceptance | Azure | configuration', function (hooks) { | |||||||
|           environment: 'AZUREPUBLICCLOUD', |           environment: 'AZUREPUBLICCLOUD', | ||||||
|         }; |         }; | ||||||
|         this.server.get(`${path}/config`, () => { |         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 } }; |           return { data: { id: path, type: this.type, ...wifAttrs } }; | ||||||
|         }); |         }); | ||||||
|         await enablePage.enable(this.type, path); |         await enablePage.enable(this.type, path); | ||||||
| @@ -268,11 +292,11 @@ module('Acceptance | Azure | configuration', function (hooks) { | |||||||
|         const path = `azure-${this.uid}`; |         const path = `azure-${this.uid}`; | ||||||
|         this.server.get(`${path}/config`, (schema, req) => { |         this.server.get(`${path}/config`, (schema, req) => { | ||||||
|           const payload = JSON.parse(req.requestBody); |           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 } }; |           return { data: { id: path, type: this.type, attributes: payload } }; | ||||||
|         }); |         }); | ||||||
|         this.server.get(`identity/oidc/config`, () => { |         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 createConfig(this.store, path, this.type); // create the azure account config in the store | ||||||
|         await enablePage.enable(this.type, path); |         await enablePage.enable(this.type, path); | ||||||
|   | |||||||
| @@ -72,7 +72,7 @@ module('Acceptance | GCP | configuration', function (hooks) { | |||||||
|         ttl: 3600, |         ttl: 3600, | ||||||
|       }; |       }; | ||||||
|       this.server.get(`${path}/config`, () => { |       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 } }; |         return { data: { id: path, type: this.type, ...wifAttrs } }; | ||||||
|       }); |       }); | ||||||
|       await enablePage.enable(this.type, path); |       await enablePage.enable(this.type, path); | ||||||
| @@ -99,7 +99,7 @@ module('Acceptance | GCP | configuration', function (hooks) { | |||||||
|         max_ttl: '4 hours', |         max_ttl: '4 hours', | ||||||
|       }; |       }; | ||||||
|       this.server.get(`${path}/config`, () => { |       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 } }; |         return { data: { id: path, type: this.type, ...GCPAccountAttrs } }; | ||||||
|       }); |       }); | ||||||
|       await enablePage.enable(this.type, path); |       await enablePage.enable(this.type, path); | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ | |||||||
| export const SECRET_ENGINE_SELECTORS = { | export const SECRET_ENGINE_SELECTORS = { | ||||||
|   configTab: '[data-test-configuration-tab]', |   configTab: '[data-test-configuration-tab]', | ||||||
|   configure: '[data-test-secret-backend-configure]', |   configure: '[data-test-secret-backend-configure]', | ||||||
|  |   configureNote: (name: string) => `[data-test-configure-note="${name}"]`, | ||||||
|   configureTitle: (type: string) => `[data-test-backend-configure-title="${type}"]`, |   configureTitle: (type: string) => `[data-test-backend-configure-title="${type}"]`, | ||||||
|   configurationToggle: '[data-test-mount-config-toggle]', |   configurationToggle: '[data-test-mount-config-toggle]', | ||||||
|   createSecret: '[data-test-secret-create]', |   createSecret: '[data-test-secret-create]', | ||||||
| @@ -24,6 +25,7 @@ export const SECRET_ENGINE_SELECTORS = { | |||||||
|   viewBackend: '[data-test-backend-view-link]', |   viewBackend: '[data-test-backend-view-link]', | ||||||
|   warning: '[data-test-warning]', |   warning: '[data-test-warning]', | ||||||
|   configureForm: '[data-test-configure-form]', |   configureForm: '[data-test-configure-form]', | ||||||
|  |   additionalConfigModelTitle: '[data-test-additional-config-model-title]', | ||||||
|   wif: { |   wif: { | ||||||
|     accessTypeSection: '[data-test-access-type-section]', |     accessTypeSection: '[data-test-access-type-section]', | ||||||
|     accessTitle: '[data-test-access-title]', |     accessTitle: '[data-test-access-title]', | ||||||
| @@ -35,8 +37,6 @@ export const SECRET_ENGINE_SELECTORS = { | |||||||
|     issuerWarningSave: '[data-test-issuer-save]', |     issuerWarningSave: '[data-test-issuer-save]', | ||||||
|   }, |   }, | ||||||
|   aws: { |   aws: { | ||||||
|     rootForm: '[data-test-root-form]', |  | ||||||
|     leaseTitle: '[data-test-lease-title]', |  | ||||||
|     deleteRole: (role: string) => `[data-test-aws-role-delete="${role}"]`, |     deleteRole: (role: string) => `[data-test-aws-role-delete="${role}"]`, | ||||||
|   }, |   }, | ||||||
|   ssh: { |   ssh: { | ||||||
|   | |||||||
| @@ -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` |  | ||||||
|         <SecretEngine::ConfigureAws @rootConfig={{this.rootConfig}} @leaseConfig={{this.leaseConfig}} @issuerConfig={{this.issuerConfig}} @backendPath={{this.id}} /> |  | ||||||
|         `); |  | ||||||
|     }; |  | ||||||
|   }); |  | ||||||
|   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'); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
| @@ -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` |  | ||||||
|         <SecretEngine::ConfigureAzure @model={{this.config}} @issuerConfig={{this.issuerConfig}} @backendPath={{this.id}} /> |  | ||||||
|         `); |  | ||||||
|     }; |  | ||||||
|   }); |  | ||||||
|   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); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
| @@ -0,0 +1,866 @@ | |||||||
|  | /** | ||||||
|  |  * 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 in 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.mountConfigModel = | ||||||
|  |             type === 'aws' | ||||||
|  |               ? this.store.createRecord('aws/root-config') | ||||||
|  |               : this.store.createRecord(`${type}/config`); | ||||||
|  |           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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/> | ||||||
|  |               `); | ||||||
|  |           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.mountConfigModel = | ||||||
|  |             type === 'aws' | ||||||
|  |               ? this.store.createRecord('aws/root-config') | ||||||
|  |               : this.store.createRecord(`${type}/config`); | ||||||
|  |           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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/> | ||||||
|  |               `); | ||||||
|  |           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.`); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |       /* This module covers code that is the same for all engines. We run them once against one of the engines.*/ | ||||||
|  |       module('Engine agnostic', function () { | ||||||
|  |         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.mountConfigModel = this.store.createRecord('azure/config'); | ||||||
|  |           this.mountConfigModel.backend = this.id; | ||||||
|  |  | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           this.server.post(configUrl('azure', this.id), () => { | ||||||
|  |             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', () => { | ||||||
|  |             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'); | ||||||
|  |           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.true( | ||||||
|  |             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.mountConfigModel = this.store.createRecord('ssh/ca-config', { backend: this.id }); | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           const err = await promise; | ||||||
|  |           assert.true( | ||||||
|  |             err.message.includes( | ||||||
|  |               `'isWifPluginConfigured' is required to be defined on the config model. Must return a boolean.` | ||||||
|  |             ), | ||||||
|  |             '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.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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/> | ||||||
|  |               `); | ||||||
|  |           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.true( | ||||||
|  |             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.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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/> | ||||||
|  |               `); | ||||||
|  |           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 (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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           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) { | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |  | ||||||
|  |           assert | ||||||
|  |             .dom(SES.wif.accessTypeSubtext) | ||||||
|  |             .hasText( | ||||||
|  |               '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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |  | ||||||
|  |           assert | ||||||
|  |             .dom(SES.configureNote('azure')) | ||||||
|  |             .doesNotExist('Note specific to AWS does not show for Azure secret engine when configuring.'); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       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.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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           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 the correct access type subtext', async function (assert) { | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |  | ||||||
|  |           assert | ||||||
|  |             .dom(SES.wif.accessTypeSubtext) | ||||||
|  |             .hasText( | ||||||
|  |               '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).' | ||||||
|  |             ); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         test('it shows validation error if default lease is entered but max lease is not', async function (assert) { | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/> | ||||||
|  |               `); | ||||||
|  |           this.server.post(configUrl('aws-lease', this.id), () => { | ||||||
|  |             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), () => { | ||||||
|  |             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')); | ||||||
|  |           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 allows user to submit root config even if API error occurs on config/lease config', async function (assert) { | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/> | ||||||
|  |               `); | ||||||
|  |           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.true( | ||||||
|  |             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) { | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/> | ||||||
|  |               `); | ||||||
|  |           this.server.post(configUrl('aws', this.id), () => { | ||||||
|  |             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), () => { | ||||||
|  |             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', () => { | ||||||
|  |             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 | ||||||
|  |           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.true( | ||||||
|  |             this.transitionStub.calledWith('vault.cluster.secrets.backend.configuration', this.id), | ||||||
|  |             'Transitioned to the configuration index route.' | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         test('it does show aws specific note', async function (assert) { | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/> | ||||||
|  |               `); | ||||||
|  |  | ||||||
|  |           assert.dom(SES.configureNote('aws')).exists('Note specific to AWS does show when configuring.'); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           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', () => { | ||||||
|  |             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( | ||||||
|  |               `Request was made to post the config when it should not have been because the user canceled out of the flow.` | ||||||
|  |             ); | ||||||
|  |           }); | ||||||
|  |           this.issuerConfig.queryIssuerError = false; | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           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), () => { | ||||||
|  |             throw new Error( | ||||||
|  |               `Request was made to post the config when it should not have been because no data was changed.` | ||||||
|  |             ); | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           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.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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           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.true( | ||||||
|  |             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) { | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           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.mountConfigModel = | ||||||
|  |             type === 'aws' | ||||||
|  |               ? this.store.createRecord('aws/root-config') | ||||||
|  |               : type === 'ssh' | ||||||
|  |               ? this.store.createRecord('ssh/ca-config') | ||||||
|  |               : this.store.createRecord(`${type}/config`); | ||||||
|  |           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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/> | ||||||
|  |               `); | ||||||
|  |           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.mountConfigModel = createConfig(this.store, this.id, `${type}-wif`); | ||||||
|  |           this.type = type; | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} /> | ||||||
|  |               `); | ||||||
|  |           assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked'); | ||||||
|  |           assert.dom(SES.wif.accessType('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.mountConfigModel.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.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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |  | ||||||
|  |           assert.dom(SES.wif.accessType('wif')).isChecked('WIF accessType is checked'); | ||||||
|  |           assert.dom(SES.wif.accessType('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 (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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='Azure' @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |  | ||||||
|  |           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.mountConfigModel = createConfig(this.store, this.id, 'azure-generic'); | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='Azure' @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |  | ||||||
|  |           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.mountConfigModel = createConfig(this.store, this.id, 'azure-generic'); | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='Azure' @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           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'); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         test('it requires a double click to change the client secret', async function (assert) { | ||||||
|  |           this.id = `azure-${this.uid}`; | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='Azure' @type='azure' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |  | ||||||
|  |           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 (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` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='AWS' @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           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.mountConfigModel = createConfig(this.store, this.id, 'aws-no-access'); | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='AWS' @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}}/> | ||||||
|  |               `); | ||||||
|  |           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.additionalConfigModel = createConfig(this.store, this.id, 'aws-lease'); | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='AWS' @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/> | ||||||
|  |               `); | ||||||
|  |  | ||||||
|  |           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.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'); | ||||||
|  |           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 render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName='AWS' @type='aws' @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} /> | ||||||
|  |               `); | ||||||
|  |  | ||||||
|  |           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 type but defaults to type "account" fields`, async function (assert) { | ||||||
|  |           this.id = `${type}-${this.uid}`; | ||||||
|  |           this.mountConfigModel = createConfig(this.store, this.id, `${type}-generic`); | ||||||
|  |           this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; | ||||||
|  |           this.type = type; | ||||||
|  |           await render(hbs` | ||||||
|  |                 <SecretEngine::ConfigureWif @backendPath={{this.id}} @displayName={{this.displayName}} @type={{this.type}} @mountConfigModel={{this.mountConfigModel}} @issuerConfig={{this.issuerConfig}} @additionalConfigModel={{this.additionalConfigModel}}/> | ||||||
|  |               `); | ||||||
|  |           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.mountConfigModel[key], | ||||||
|  |                 `${key} for ${type}: has the expected value set on the config` | ||||||
|  |               ); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										22
									
								
								ui/types/vault/models/aws/lease-config.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								ui/types/vault/models/aws/lease-config.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -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; |  | ||||||
| } |  | ||||||
							
								
								
									
										32
									
								
								ui/types/vault/models/aws/root-config.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								ui/types/vault/models/aws/root-config.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -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; |  | ||||||
| } |  | ||||||
							
								
								
									
										34
									
								
								ui/types/vault/models/azure/config.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								ui/types/vault/models/azure/config.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,34 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Copyright (c) HashiCorp, Inc. |  | ||||||
|  * SPDX-License-Identifier: BUSL-1.1 |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| import type Model from '@ember-data/model'; |  | ||||||
| import type { ModelValidations } from 'vault/app-types'; |  | ||||||
|  |  | ||||||
| export default class AzureConfig extends Model { |  | ||||||
|   backend: string; |  | ||||||
|   subscriptionId: string | undefined; |  | ||||||
|   tenantId: string | undefined; |  | ||||||
|   clientId: string | undefined; |  | ||||||
|   clientSecret: string | undefined; |  | ||||||
|   identityTokenAudience: string | undefined; |  | ||||||
|   identityTokenTtl: any; |  | ||||||
|   environment: string | undefined; |  | ||||||
|   rootPasswordTtl: string | undefined; |  | ||||||
|  |  | ||||||
|   get displayAttrs(): any; |  | ||||||
|   get isWifPluginConfigured(): boolean; |  | ||||||
|   get isAzureAccountConfigured(): boolean; |  | ||||||
|   get fieldGroupsWif(): any; |  | ||||||
|   get fieldGroupsAzure(): any; |  | ||||||
|   formFieldGroups(accessType?: string): { |  | ||||||
|     [key: string]: string[]; |  | ||||||
|   }[]; |  | ||||||
|   changedAttributes(): { |  | ||||||
|     [key: string]: unknown[]; |  | ||||||
|   }; |  | ||||||
|   isNew: boolean; |  | ||||||
|   save(): void; |  | ||||||
|   unloadRecord(): void; |  | ||||||
| } |  | ||||||
							
								
								
									
										28
									
								
								ui/types/vault/models/secret-engine/additional-config.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								ui/types/vault/models/secret-engine/additional-config.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: BUSL-1.1 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import type Model from '@ember-data/model'; | ||||||
|  | import type { ModelValidations, FormField } from 'vault/app-types'; | ||||||
|  |  | ||||||
|  | export default class SecretEngineAdditionalConfigModel extends Model { | ||||||
|  |   backend: string; | ||||||
|  |   type: string; | ||||||
|  |   // aws lease | ||||||
|  |   leaseMax: any; | ||||||
|  |   lease: any; | ||||||
|  |  | ||||||
|  |   get displayAttrs(): any; | ||||||
|  |  | ||||||
|  |   formFields: FormField[]; | ||||||
|  |   changedAttributes(): { | ||||||
|  |     [key: string]: unknown[]; | ||||||
|  |   }; | ||||||
|  |   isNew: boolean; | ||||||
|  |   save(): void; | ||||||
|  |   unloadRecord(): void; | ||||||
|  |   destroyRecord(): void; | ||||||
|  |   rollbackAttributes(): void; | ||||||
|  |   validate(): ModelValidations; | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								ui/types/vault/models/secret-engine/mount-config.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								ui/types/vault/models/secret-engine/mount-config.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) HashiCorp, Inc. | ||||||
|  |  * SPDX-License-Identifier: BUSL-1.1 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import type Model from '@ember-data/model'; | ||||||
|  | import type { ModelValidations, FormFieldGroups } from 'vault/app-types'; | ||||||
|  |  | ||||||
|  | export default class SecretEngineMountConfigModel extends Model { | ||||||
|  |   backend: string; | ||||||
|  |   type: string; | ||||||
|  |   // aws | ||||||
|  |   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; | ||||||
|  |   environment: string | undefined; | ||||||
|  |   rootPasswordTtl: string | undefined; | ||||||
|  |   // gcp | ||||||
|  |   credentials: string | undefined; | ||||||
|  |   ttl: any; | ||||||
|  |   maxTtl: any; | ||||||
|  |   secretAccountEmail: string | undefined; | ||||||
|  |   // wif | ||||||
|  |   identityTokenAudience: string | undefined; | ||||||
|  |   identityTokenTtl: any; | ||||||
|  |  | ||||||
|  |   get displayAttrs(): any; | ||||||
|  |   get isConfigured(): boolean; // used only for secret engines that return a 200 when configuration has not be set. | ||||||
|  |   get isWifPluginConfigured(): boolean; | ||||||
|  |   get isAccountPluginConfigured(): boolean; | ||||||
|  |   get fieldGroupsWif(): any; | ||||||
|  |   get fieldGroupsAccount(): any; | ||||||
|  |  | ||||||
|  |   formFieldGroups: FormFieldGroups[]; | ||||||
|  |  | ||||||
|  |   changedAttributes(): { | ||||||
|  |     [key: string]: unknown[]; | ||||||
|  |   }; | ||||||
|  |   isNew: boolean; | ||||||
|  |   save(): void; | ||||||
|  |   unloadRecord(): void; | ||||||
|  |   destroyRecord(): void; | ||||||
|  |   rollbackAttributes(): void; | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Angel Garbarino
					Angel Garbarino