vault/ui/app/components/secret-engine/configure-aws.ts
Angel Garbarino 3abca46464
WIF sidebranch (#28148)
* manual cherry pick to deal with all the merge things

* changelog

* test fixes

* Update 28148.txt

* fix tests failures after main merge

* fix test failures after main merge

* Add Access Type and conditionally render WIF fields (#28149)

* initial work.

* remove access_type

* better no model logic well kind of

* rollback attrs

* remove defaults

* stopping point

* wip changing back to sidebranch

* hustling shuffling and serializing

* some of the component test coverage

* disable acces type if editing

* test coverage

* hide max retries that sneaky bugger

* cleanup

* cleanup

* Update root-config.js

* remove flash message check, locally passes great but on ci flaky

* clean up

* thank you chelsea

* test clean up per enterprise vs community

* address pr comments

* welp a miss add

* UI (sidebranch) WIF Issuer field (#28187)

* Add type declaration files for aws config models

* use updated task syntax for save method on configure-aws

* fix types on edit route

* fetch issuer on configure edit page if aws + enterprise

* track issuer within configure-aws component

* add placeholder support on form-field

* Add warning if issuer changed from previous value or could not be read

* cleanup

* preliminary tests

* dont use while loop so we can test the modal

* tests

* cleanup

* fix tests

* remove extra tracked value and duplicate changed attrs check

* modal footer

---------

Co-authored-by: Angel Garbarino <argarbarino@gmail.com>

* Display issuer on Configuration details (#28209)

* display issuer on configuration details

* workflow complete, now on to testing

* handle issuer things

* fix all the broken tests things

* add test coveragE:

* cleanup

* rename model/adapter

* Update configure-aws.ts

* Update aws-configuration-test.js

* 90 percent there for pr comments

* last one for tonight

* a few more because why not

* hasDirtyAttributes fixes

* revert back to previous noRead->queryIssuerError

---------

Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
2024-08-29 12:17:51 -06:00

233 lines
8.7 KiB
TypeScript

/**
* 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 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<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();
}
}