vault/ui/app/components/mount-backend-form.ts
Vault Automation 7d026fa5a8
[VAULT-37521] UI: decouple auth and secret engines (#9307) (#9347)
* [VAULT-37521] UI: decouple auth and secret engines

* add copyright header

* address acceptance test failure

Co-authored-by: Shannon Roberts (Beagin) <beagins@users.noreply.github.com>
2025-09-15 18:22:35 +00:00

157 lines
5.1 KiB
TypeScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';
import { action, set } from '@ember/object';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
import { MOUNT_CATEGORIES } from 'vault/utils/plugin-catalog-helpers';
import type FlashMessageService from 'vault/services/flash-messages';
import type Store from '@ember-data/store';
import type AuthMethodForm from 'vault/forms/auth/method';
import type CapabilitiesService from 'vault/services/capabilities';
import type ApiService from 'vault/services/api';
import type { ApiError } from '@ember-data/adapter/error';
import type { ValidationMap } from 'vault/vault/app-types';
/**
* @module MountBackendForm
* The `MountBackendForm` is used to mount authentication methods.
*
* @example ```js
* <MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />```
*
* @param {AuthMethodForm} mountModel - The authentication method form.
* @param {function} onMountSuccess - A function that transitions once the Mount has been successfully posted.
*
*/
interface Args {
mountModel: AuthMethodForm;
onMountSuccess: (type: string, path: string, useEngineRoute: boolean) => void;
}
const AUTH_MOUNT_CATEGORY = MOUNT_CATEGORIES.AUTH;
export default class MountBackendForm extends Component<Args> {
@service declare readonly store: Store;
@service declare readonly flashMessages: FlashMessageService;
@service declare readonly capabilities: CapabilitiesService;
@service declare readonly api: ApiService;
// validation related properties
@tracked modelValidations: ValidationMap | null = null;
@tracked invalidFormAlert: string | null = null;
@tracked errorMessage: string | string[] = '';
get mountForm(): AuthMethodForm {
return this.args.mountModel;
}
get showEnable(): boolean {
return !!this.mountForm.type;
}
constructor(owner: unknown, args: Args) {
super(owner, args);
}
checkPathChange(backendType: string) {
if (!backendType) return;
const { data } = this.mountForm;
// Always use auth mount category since this component only handles auth methods
const mountsByType = filterEnginesByMountCategory({
mountCategory: AUTH_MOUNT_CATEGORY,
isEnterprise: true,
}).map((engine) => engine.type);
// if the current path has not been altered by user (is empty or matches a default mount type),
// change it here to match the new type
if (!data.path || mountsByType.includes(data.path)) {
data.path = backendType;
}
}
checkModelWarnings() {
// check for warnings on change
// since we only show errors on submit we need to clear those out and only send warning state
const mountModel = this.mountForm;
const { state } = mountModel.toJSON();
for (const key in state) {
if (state[key]) {
state[key].errors = [];
}
}
this.modelValidations = state;
this.invalidFormAlert = null;
}
async onMountError(status: number, errors: ApiError[], message: string) {
if (status === 403) {
this.flashMessages.danger(
'You do not have access to the sys/auth endpoint. The auth method was not mounted.'
);
} else if (errors) {
this.errorMessage = errors.map((e) => {
if (typeof e === 'object') return e.title || e.message || JSON.stringify(e);
return e;
});
} else if (message) {
this.errorMessage = message;
} else {
this.errorMessage = 'An error occurred, check the vault logs.';
}
}
@task
@waitFor
*mountBackend(event: Event) {
event.preventDefault();
const mountModel = this.mountForm;
const { type } = mountModel;
const { path } = mountModel.data;
// only submit form if validations pass
const { isValid, state, invalidFormMessage, data } = mountModel.toJSON();
if (!isValid) {
this.modelValidations = state;
this.invalidFormAlert = invalidFormMessage;
return;
}
try {
yield this.api.sys.authEnableMethod(path, data);
this.flashMessages.success(`Successfully mounted the ${mountModel.type} auth method at ${path}.`);
this.args.onMountSuccess(type, path, false);
} catch (error) {
const { status, response, message } = yield this.api.parseError(error);
this.onMountError(status, response.errors, message);
}
}
@action
onKeyUp(name: string, value: string) {
set(this.mountForm.data, name, value);
this.checkModelWarnings();
}
@action
setMountType(value: string) {
this.mountForm.type = value;
this.checkPathChange(value);
}
@action
handleIdentityTokenKeyChange(value: string[] | string): void {
// if array, it's coming from the search-select component, otherwise it hit the fallback component and will come in as a string.
const { config } = this.mountForm.data;
config.identity_token_key = Array.isArray(value) ? value[0] : value;
}
}