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