/** * 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 { next } from '@ember/runloop'; import type Router from '@ember/routing/router'; import type ApiService from 'vault/services/api'; import type VersionService from 'vault/services/version'; import type FlashMessageService from 'vault/services/flash-messages'; import type Owner from '@ember/owner'; import type AwsConfigForm from 'vault/forms/secrets/aws-config'; import type AzureConfigForm from 'vault/forms/secrets/azure-config'; import type GcpConfigForm from 'vault/forms/secrets/gcp-config'; /** * @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 * * * @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} configForm - the config form for the engine. The field `isWifPluginConfigured` must be added to the form, otherwise this component will assert an error. `isWifPluginConfigured` should return true if any required wif fields have been set. */ type ConfigForm = AwsConfigForm | AzureConfigForm | GcpConfigForm; interface Args { backendPath: string; displayName: string; type: string; configForm: ConfigForm; } export default class ConfigureWif extends Component { @service declare readonly router: Router; @service declare readonly api: ApiService; @service declare readonly version: VersionService; @service declare readonly flashMessages: FlashMessageService; @tracked errorMessage = ''; @tracked invalidFormAlert = ''; @tracked saveIssuerWarning = ''; @tracked modelValidations: ValidationMap | null = null; disableAccessType = false; originalIssuer: string | undefined; constructor(owner: Owner, args: Args) { super(owner, args); // the following checks are only relevant to existing enterprise configurations const { isNew, data, isWifPluginConfigured, isAccountPluginConfigured } = this.args.configForm; if (this.version.isEnterprise && !isNew) { assert( `'isWifPluginConfigured' is required to be defined on the config model. Must return a boolean.`, isWifPluginConfigured !== undefined ); next(() => { this.args.configForm.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; } // cache the issuer to check if it has been changed later this.originalIssuer = data.issuer; } @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(); const { isValid, state, invalidFormMessage, data } = this.args.configForm.toJSON(); if (!isValid) { this.modelValidations = isValid ? null : state; this.invalidFormAlert = isValid ? '' : invalidFormMessage; return; } if (this.originalIssuer !== data.issuer) { // 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.originalIssuer ? '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 () => { try { const { data } = this.args.configForm.toJSON(); const { issuer } = data; await this.saveConfig(data); if (this.originalIssuer !== issuer) { await this.updateIssuer(issuer as string); } this.flashMessages.success(`Successfully saved ${this.args.backendPath}'s configuration.`); this.transition(); } catch (e) { const { message } = await this.api.parseError(e); this.errorMessage = message; this.invalidFormAlert = 'There was an error submitting this form.'; } }) ); async updateIssuer(issuer: string) { try { await this.api.identity.oidcConfigure({ issuer }); } catch (e) { const { message } = await this.api.parseError(e, 'Check Vault logs for details.'); this.flashMessages.danger(`Issuer was not saved: ${message}`); } } async saveConfig(data: ConfigForm['data']) { const { backendPath, type } = this.args; if (type === 'aws') { await this.api.secrets.awsConfigureRootIamCredentials(backendPath, data); try { const { lease, lease_max } = data as { lease: string; lease_max: string }; await this.api.secrets.awsConfigureLease(backendPath, { lease, lease_max }); } catch (e) { const { message } = await this.api.parseError(e); this.flashMessages.danger(`Error saving lease configuration: ${message}`); } } else if (type === 'azure') { await this.api.secrets.azureConfigure(backendPath, data); } else if (type === 'gcp') { await this.api.secrets.googleCloudConfigure(backendPath, data); } } resetErrors() { this.flashMessages.clearMessages(); this.errorMessage = this.invalidFormAlert = ''; this.modelValidations = null; } transition() { this.router.transitionTo('vault.cluster.secrets.backend.configuration', this.args.backendPath); } @action onChangeAccessType(accessType: 'account' | 'wif') { const { configForm, type } = this.args; configForm.accessType = accessType; if (accessType === 'account') { // reset all "wif" attributes that are mutually exclusive with "account" attributes // these attributes are the same for each engine configForm.data.identity_token_audience = configForm.data.identity_token_ttl = undefined; } else if (accessType === 'wif') { // reset all "account" attributes that are mutually exclusive with "wif" attributes // these attributes are different for each engine if (type === 'azure') { (configForm as AzureConfigForm).data.client_secret = undefined; } else if (type === 'aws') { (configForm as AwsConfigForm).data.access_key = undefined; } } } @action onCancel() { this.resetErrors(); this.transition(); } }