mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-22 23:21:08 +02:00
* surface plugin version & removing mountable-auth-methods.js * UI: Removing mountable-secret-engines.js (#30950) * first pass, removing all related imports * fix usage * fix category * fix typos * fix more tests * fix more tests pt2 * attempting WIF const removal * fix wif tests, removing config consts * fixing tests * please * removing fallback * cleanup * fix type ent test * remove isaddon * Revert "remove isaddon" This reverts commit ee114197b7299711e35e3c8e5aca9694063726eb. * adding tab click * update case * fix case, rename to isOnlyMountable * fix backend form * more test fix * adding changelog * pr comments * renaming params, adding requiresADP * updates * updates and pr comments * perhaps update the test
215 lines
7.8 KiB
TypeScript
215 lines
7.8 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 } from '@ember/object';
|
|
import { task } from 'ember-concurrency';
|
|
import { waitFor } from '@ember/test-waiters';
|
|
import { presence } from 'vault/utils/forms/validators';
|
|
import { filterEnginesByMountCategory, isAddonEngine } from 'vault/utils/all-engines-metadata';
|
|
import { assert } from '@ember/debug';
|
|
import { ResponseError } from '@hashicorp/vault-client-typescript';
|
|
import AdapterError from '@ember-data/adapter/error';
|
|
|
|
import type FlashMessageService from 'vault/services/flash-messages';
|
|
import type Store from '@ember-data/store';
|
|
import type { AuthEnableModel } from 'vault/routes/vault/cluster/settings/auth/enable';
|
|
import type SecretsEngineForm from 'vault/forms/secrets/engine';
|
|
import type CapabilitiesService from 'vault/services/capabilities';
|
|
import type ApiService from 'vault/services/api';
|
|
import type { ApiError } from '@ember-data/adapter/error';
|
|
|
|
/**
|
|
* @module MountBackendForm
|
|
* The `MountBackendForm` is used to mount either a secret or auth backend.
|
|
*
|
|
* @example ```js
|
|
* <MountBackendForm @mountCategory="secret" @onMountSuccess={{this.onMountSuccess}} />```
|
|
*
|
|
* @param {function} onMountSuccess - A function that transitions once the Mount has been successfully posted.
|
|
* @param {string} mountCategory - The type of engine to mount, either 'secret' or 'auth'.
|
|
*
|
|
*/
|
|
|
|
type MountModel = SecretsEngineForm | AuthEnableModel;
|
|
|
|
interface Args {
|
|
mountModel: MountModel;
|
|
mountCategory: 'secret' | 'auth';
|
|
onMountSuccess: (type: string, path: string, useEngineRoute: boolean) => void;
|
|
}
|
|
|
|
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 = null;
|
|
@tracked invalidFormAlert = null;
|
|
|
|
@tracked errorMessage: string | string[] = '';
|
|
|
|
constructor(owner: unknown, args: Args) {
|
|
super(owner, args);
|
|
assert(`@mountCategory is required. Must be "auth" or "secret".`, presence(this.args.mountCategory));
|
|
}
|
|
|
|
willDestroy() {
|
|
// components are torn down after store is unloaded and will cause an error if attempt to unload record
|
|
const noTeardown = this.store && !this.store.isDestroying;
|
|
if (noTeardown && this.args.mountCategory === 'auth' && this.args?.mountModel?.isNew) {
|
|
this.args.mountModel.unloadRecord();
|
|
}
|
|
super.willDestroy();
|
|
}
|
|
|
|
checkPathChange(backendType: string) {
|
|
if (!backendType) return;
|
|
const mount = this.args.mountModel;
|
|
const currentPath = mount.path;
|
|
// mountCategory is usually 'secret' or 'auth', but sometimes an empty string is passed in (like when we click the cancel button).
|
|
// In these cases, we should default to returning auth methods.
|
|
const mountsByType = filterEnginesByMountCategory({
|
|
mountCategory: this.args.mountCategory ?? 'auth',
|
|
isEnterprise: true,
|
|
}).map((engine) => engine.type);
|
|
// if the current path has not been altered by user,
|
|
// change it here to match the new type
|
|
if (!currentPath || mountsByType.includes(currentPath)) {
|
|
mount.path = backendType;
|
|
}
|
|
}
|
|
|
|
typeChangeSideEffect(type: string) {
|
|
if (this.args.mountCategory !== 'secret') return;
|
|
// If type PKI, set max lease to ~10years
|
|
this.args.mountModel.config.maxLeaseTtl = type === 'pki' ? '3650d' : 0;
|
|
}
|
|
|
|
checkModelValidity(model: MountModel) {
|
|
const { mountCategory } = this.args;
|
|
const { isValid, state, invalidFormMessage, data } =
|
|
mountCategory === 'secret' ? model.toJSON() : model.validate();
|
|
this.modelValidations = state;
|
|
this.invalidFormAlert = invalidFormMessage;
|
|
return { isValid, data };
|
|
}
|
|
|
|
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 { mountCategory, mountModel } = this.args;
|
|
const { state } = mountCategory === 'secret' ? mountModel.toJSON() : mountModel.validate();
|
|
for (const key in state) {
|
|
state[key].errors = [];
|
|
}
|
|
this.modelValidations = state;
|
|
this.invalidFormAlert = null;
|
|
}
|
|
|
|
async saveKvConfig(path: string, formData: SecretsEngineForm['data']) {
|
|
const { options, kvConfig = {} } = formData;
|
|
const { maxVersions, casRequired, deleteVersionAfter } = kvConfig;
|
|
const isKvV2 = options?.version === 2 && ['kv', 'generic'].includes(this.args.mountModel.engineType);
|
|
const hasConfig = maxVersions || casRequired || deleteVersionAfter;
|
|
|
|
if (isKvV2 && hasConfig) {
|
|
try {
|
|
const { canUpdate } = await this.capabilities.for('kvConfig', { path });
|
|
if (canUpdate) {
|
|
await this.api.secrets.kvV2Configure(path, kvConfig);
|
|
} else {
|
|
this.flashMessages.warning(
|
|
'You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.'
|
|
);
|
|
}
|
|
} catch (e) {
|
|
const { message } = await this.api.parseError(e);
|
|
this.flashMessages.warning(
|
|
`The secret engine was mounted, but the configuration settings were not saved. ${message}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async onMountError(status: number, errors: ApiError[], message: string) {
|
|
if (status === 403) {
|
|
this.flashMessages.danger(
|
|
'You do not have access to the sys/mounts endpoint. The secret engine 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, mountCategory } = this.args;
|
|
const { type, path } = mountModel;
|
|
// only submit form if validations pass
|
|
const { isValid, data: formData } = this.checkModelValidity(mountModel);
|
|
if (!isValid) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (mountCategory === 'secret') {
|
|
yield this.api.sys.mountsEnableSecretsEngine(path, formData);
|
|
yield this.saveKvConfig(path, formData);
|
|
} else {
|
|
yield mountModel.save();
|
|
}
|
|
this.flashMessages.success(
|
|
`Successfully mounted the ${type} ${
|
|
this.args.mountCategory === 'secret' ? 'secrets engine' : 'auth method'
|
|
} at ${path}.`
|
|
);
|
|
// check whether to use the Ember engine route
|
|
const useEngineRoute = isAddonEngine(mountModel.engineType, Number(formData?.options?.version));
|
|
this.args.onMountSuccess(type, path, useEngineRoute);
|
|
} catch (error) {
|
|
if (error instanceof ResponseError) {
|
|
const { status, response, message } = yield this.api.parseError(error);
|
|
this.onMountError(status, response.errors, message);
|
|
} else {
|
|
const err = error as AdapterError;
|
|
this.onMountError(err.httpStatus, err.errors, err.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
@action
|
|
onKeyUp(name: string, value: string) {
|
|
this.args.mountModel[name] = value;
|
|
this.checkModelWarnings();
|
|
}
|
|
|
|
@action
|
|
setMountType(value: string) {
|
|
this.args.mountModel.type = value;
|
|
this.typeChangeSideEffect(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.
|
|
this.args.mountModel.config.identityTokenKey = Array.isArray(value) ? value[0] : value;
|
|
}
|
|
}
|