Backport [VAULT-40813] ManageDropdown component into ce/main (#12508)

* [VAULT-40813] ManageDropdown component (#12295)

* [VAULT-40813] ManageDropdown component

* address pr comments, add routing test coverage

* fix test: generate policy is an enterprise-only feature

---------

Co-authored-by: Shannon Roberts (Beagin) <beagins@users.noreply.github.com>
Co-authored-by: Shannon Roberts <shannon.roberts@hashicorp.com>
This commit is contained in:
Vault Automation 2026-02-25 11:37:43 -07:00 committed by GitHub
parent ad9144da7e
commit b593ca128e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1490 additions and 891 deletions

View file

@ -0,0 +1,6 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'core/components/manage-dropdown';

View file

@ -3,21 +3,21 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { action, set } from '@ember/object';
import { service } from '@ember/service';
import { waitFor } from '@ember/test-waiters';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';
import { action, set } from '@ember/object';
import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata';
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 { ApiError } from '@ember-data/adapter/error';
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 CapabilitiesService from 'vault/services/capabilities';
import type FlashMessageService from 'vault/services/flash-messages';
import type { ValidationMap } from 'vault/vault/app-types';
/**

View file

@ -3,18 +3,18 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata';
import keys from 'core/utils/keys';
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
import {
enhanceEnginesWithCatalogData,
categorizeEnginesByStatus,
enhanceEnginesWithCatalogData,
MOUNT_CATEGORIES,
PLUGIN_TYPES,
PLUGIN_CATEGORIES,
PLUGIN_TYPES,
} from 'vault/utils/plugin-catalog-helpers';
/**

View file

@ -140,7 +140,7 @@
@text="Disable engines"
@color="critical"
@icon="trash"
{{on "click" (fn (mut this.enginesToDisable) this.selectedItems)}}
{{on "click" (fn this.setEnginesToDisable this.selectedItems)}}
/>
</Hds::Layout::Flex>
{{/if}}
@ -171,28 +171,7 @@
<:popupMenu as |rowData|>
{{#let (this.getEngineResourceData rowData.path) as |backendData|}}
<Hds::Dropdown @isInline={{true}} as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="{{if backendData.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
<dd.Interactive
@route={{backendData.backendConfigurationLink}}
@model={{backendData.id}}
data-test-popup-menu="View configuration"
@icon="settings"
>View configuration</dd.Interactive>
{{#if (not-eq backendData.type "cubbyhole")}}
<dd.Interactive
@color="critical"
{{on "click" (fn (mut this.engineToDisable) backendData)}}
data-test-popup-menu="Delete"
@icon="trash"
>Delete</dd.Interactive>
{{/if}}
</Hds::Dropdown>
<ManageDropdown @model={{backendData}} @variant="icon" @configRoute={{backendData.backendConfigurationLink}} />
{{/let}}
</:popupMenu>
</ListTable>
@ -201,16 +180,6 @@
{{/if}}
{{! End Table Section }}
{{#if this.engineToDisable}}
<ConfirmModal
@color="critical"
@confirmMessage="Any data in this engine will be permanently deleted."
@confirmTitle="Disable engine?"
@onClose={{fn (mut this.engineToDisable) null}}
@onConfirm={{perform this.disableEngine this.engineToDisable}}
/>
{{/if}}
{{#if this.enginesToDisable}}
<ConfirmModal
@color="critical"

View file

@ -3,20 +3,20 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { dropTask } from 'ember-concurrency';
import engineDisplayData from 'vault/helpers/engines-display-data';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
import type RouterService from '@ember/routing/router-service';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
import type ApiService from 'vault/services/api';
import type FlashMessageService from 'vault/services/flash-messages';
import type NamespaceService from 'vault/services/namespace';
import type RouterService from '@ember/routing/router-service';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
import type VersionService from 'vault/services/version';
import type WizardService from 'vault/services/wizard';
import { WIZARD_ID } from '../wizard/secret-engines/secret-engines-wizard';
@ -45,7 +45,6 @@ export default class SecretEngineList extends Component<Args> {
@service declare readonly wizard: WizardService;
@tracked secretEngineOptions: Array<string> | [] = [];
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
@tracked enginesToDisable: Array<SecretsEngineResource> | null = null;
@tracked engineTypeFilters: Array<string> = [];
@ -289,6 +288,16 @@ export default class SecretEngineList extends Component<Args> {
this.selectedItems = tableData.selectedRowsKeys;
}
@action
setEnginesToDisable(engines: Array<SecretsEngineResource>) {
this.enginesToDisable = engines;
}
@action
clearEnginesToDisable() {
this.enginesToDisable = null;
}
async disableSingleEngine(engine: SecretsEngineResource) {
const { engineType, id, path } = engine;
try {
@ -302,14 +311,13 @@ export default class SecretEngineList extends Component<Args> {
}
}
@dropTask
*disableMultipleEngines(enginePathsToDisable: Array<string>) {
disableMultipleEngines = dropTask(async (enginePathsToDisable: Array<string>) => {
const enginesToDisable = this.displayableBackends.filter((engine: SecretsEngineResource) =>
enginePathsToDisable.includes(engine.path)
);
try {
for (const engine of enginesToDisable) {
yield this.disableSingleEngine(engine);
await this.disableSingleEngine(engine);
}
// Navigate once all operations are complete
@ -317,15 +325,5 @@ export default class SecretEngineList extends Component<Args> {
} finally {
this.enginesToDisable = null;
}
}
@dropTask
*disableEngine(engine: SecretsEngineResource) {
try {
yield this.disableSingleEngine(engine);
this.router.transitionTo('vault.cluster.secrets.backends');
} finally {
this.engineToDisable = undefined;
}
}
});
}

View file

@ -3,19 +3,19 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
import {
enhanceEnginesWithCatalogData,
categorizeEnginesByStatus,
MOUNT_CATEGORIES,
PLUGIN_TYPES,
PLUGIN_CATEGORIES,
} from 'vault/utils/plugin-catalog-helpers';
import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata';
import type { PluginCatalogData } from 'vault/services/plugin-catalog';
import {
categorizeEnginesByStatus,
enhanceEnginesWithCatalogData,
MOUNT_CATEGORIES,
PLUGIN_CATEGORIES,
PLUGIN_TYPES,
} from 'vault/utils/plugin-catalog-helpers';
import type VersionService from 'vault/services/version';

View file

@ -3,14 +3,13 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { or } from '@ember/object/computed';
import { computed } from '@ember/object';
import { service } from '@ember/service';
import Controller from '@ember/controller';
import BackendCrumbMixin from 'vault/mixins/backend-crumb';
import { computed } from '@ember/object';
import { or } from '@ember/object/computed';
import { service } from '@ember/service';
import ListController from 'core/mixins/list-controller';
import { keyIsFolder } from 'core/utils/key-utils';
import { task } from 'ember-concurrency';
import BackendCrumbMixin from 'vault/mixins/backend-crumb';
export default Controller.extend(ListController, BackendCrumbMixin, {
flashMessages: service(),
@ -68,20 +67,4 @@ export default Controller.extend(ListController, BackendCrumbMixin, {
});
},
},
disableEngine: task(function* (engine) {
const { engineType, id, path } = engine;
try {
yield this.api.sys.mountsDisableSecretsEngine(id);
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
this.router.transitionTo('vault.cluster.secrets.backends');
} catch (err) {
const { message } = yield this.api.parseError(err);
this.flashMessages.danger(
`There was an error disabling the ${engineType} Secrets Engines at ${path}: ${message}.`
);
} finally {
this.engineToDisable = null;
}
}).drop(),
});

View file

@ -3,8 +3,8 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { ALL_ENGINES } from 'core/utils/all-engines-metadata';
import MountForm from 'vault/forms/mount';
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
import { isKnownExternalPlugin } from 'vault/utils/external-plugin-helpers';
import FormField from 'vault/utils/forms/field';
import FormFieldGroup from 'vault/utils/forms/field-group';

View file

@ -3,60 +3,4 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { ALL_ENGINES, type EngineDisplayData } from 'vault/utils/all-engines-metadata';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
/**
* Default metadata for unknown engine plugins
*/
export const unknownEngineMetadata = (methodType?: string): EngineDisplayData => ({
type: methodType || 'unknown',
displayName: methodType || 'Unknown plugin',
glyph: 'lock',
mountCategory: ['secret', 'auth'],
});
/**
* Helper function to retrieve engine metadata for a given `methodType`.
* It searches the `ALL_ENGINES` array for an engine with a matching type and returns its metadata object.
* The `ALL_ENGINES` array includes secret and auth engines, including those supported only in enterprise.
* These details (such as mount type and enterprise licensing) are included in the returned engine object.
*
* For external plugins that have a builtin mapping (e.g., "vault-plugin-secrets-keymgmt" -> "keymgmt"),
* this function returns the metadata for the corresponding builtin engine, preserving the original
* external plugin name in the type field.
*
* Example usage:
* const engineMetadata = engineDisplayData('kmip');
* if (engineMetadata?.requiresEnterprise) {
* console.log(`This mount: ${engineMetadata.engineType} requires an enterprise license`);
* }
*
* @param {string} methodType - The engine type (sometimes called backend) to look up (e.g., "aws", "azure", "vault-plugin-secrets-keymgmt").
* @returns {Object} - The engine metadata, which includes information about its mount type (e.g., secret or auth)
* and whether it requires an enterprise license. For unknown engines, returns a default unknown plugin object.
*/
export default function engineDisplayData(methodType: string): EngineDisplayData {
// First try to find an exact match
const builtinEngine = ALL_ENGINES?.find((t) => t.type === methodType);
if (builtinEngine) {
return builtinEngine;
}
// If no direct match, check if this is a known external plugin and use its builtin mapping
const effectiveType = getEffectiveEngineType(methodType);
if (effectiveType !== methodType) {
// This is a known external plugin with a builtin mapping
const mappedEngine = ALL_ENGINES?.find((t) => t.type === effectiveType);
if (mappedEngine) {
// Return the mapped engine metadata but preserve the original external plugin type
return {
...mappedEngine,
type: methodType, // Keep the original external plugin name for identification
};
}
}
// Return default unknown plugin metadata
return unknownEngineMetadata(methodType);
}
export { default } from 'core/helpers/engines-display-data';

View file

@ -6,11 +6,11 @@
import Model, { attr, hasMany } from '@ember-data/model';
import ArrayProxy from '@ember/array/proxy';
import PromiseProxyMixin from '@ember/object/promise-proxy-mixin';
import { withModelValidations } from 'vault/decorators/model-validations';
import { isPresent } from '@ember/utils';
import { service } from '@ember/service';
import { isPresent } from '@ember/utils';
import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata';
import { withModelValidations } from 'vault/decorators/model-validations';
import { addManyToArray, addToArray } from 'vault/helpers/add-to-array';
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
const validations = {
name: [{ type: 'presence', message: 'Name is required' }],

View file

@ -4,15 +4,14 @@
*/
import Model, { attr, belongsTo } from '@ember-data/model';
import { computed } from '@ember/object'; // eslint-disable-line
import { equal } from '@ember/object/computed'; // eslint-disable-line
import { withModelValidations } from 'vault/decorators/model-validations';
import { ALL_ENGINES, isAddonEngine } from 'core/utils/all-engines-metadata';
import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { WHITESPACE_WARNING } from 'vault/utils/forms/validators';
import { ALL_ENGINES, INTERNAL_ENGINE_TYPES, isAddonEngine } from 'vault/utils/all-engines-metadata';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
import { withModelValidations } from 'vault/decorators/model-validations';
import engineDisplayData from 'vault/helpers/engines-display-data';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { INTERNAL_ENGINE_TYPES } from 'vault/utils/all-engines-metadata';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
import { WHITESPACE_WARNING } from 'vault/utils/forms/validators';
const LINKED_BACKENDS = supportedSecretBackends();

View file

@ -3,14 +3,14 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { baseResourceFactory } from 'vault/resources/base-factory';
import engineDisplayData from 'vault/helpers/engines-display-data';
import {
supportedSecretBackends,
SupportedSecretBackendsEnum,
} from 'vault/helpers/supported-secret-backends';
import { baseResourceFactory } from 'vault/resources/base-factory';
import { INTERNAL_ENGINE_TYPES, isAddonEngine } from 'vault/utils/all-engines-metadata';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
import engineDisplayData from 'vault/helpers/engines-display-data';
import type { Mount } from 'vault/mount';

View file

@ -3,19 +3,19 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { assert } from '@ember/debug';
import { set } from '@ember/object';
import { hash } from 'rsvp';
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { filterEnginesByMountCategory, isAddonEngine } from 'core/utils/all-engines-metadata';
import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs';
import { hash } from 'rsvp';
import engineDisplayData from 'vault/helpers/engines-display-data';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { isAddonEngine, filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
import { getEnginePathParam } from 'vault/utils/backend-route-helpers';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
import { getModelTypeForEngine } from 'vault/utils/model-helpers/secret-engine-helpers';
import { service } from '@ember/service';
import { normalizePath } from 'vault/utils/path-encoding-helpers';
import { getEnginePathParam } from 'vault/utils/backend-route-helpers';
import { assert } from '@ember/debug';
import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs';
import engineDisplayData from 'vault/helpers/engines-display-data';
const SUPPORTED_BACKENDS = supportedSecretBackends();

View file

@ -9,24 +9,14 @@
(options-for-backend this.backendType this.tab) (engines-display-data this.backendType)
as |options engineDisplayData|
}}
<Hds::Dropdown as |D|>
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
<D.Interactive
@icon="settings"
@route={{if
engineDisplayData.isConfigurable
"vault.cluster.secrets.backend.configuration.plugin-settings"
"vault.cluster.secrets.backend.configuration.general-settings"
}}
data-test-popup-menu="Configure"
>Configure</D.Interactive>
<D.Interactive
{{on "click" (fn (mut this.engineToDisable) this.backendModel)}}
@color="critical"
@icon="trash"
data-test-popup-menu="Delete"
>Delete</D.Interactive>
</Hds::Dropdown>
<ManageDropdown
@model={{this.backendModel}}
@configRoute={{if
engineDisplayData.isConfigurable
"vault.cluster.secrets.backend.configuration.plugin-settings"
"vault.cluster.secrets.backend.configuration.general-settings"
}}
/>
<Hds::Button
@text={{options.create}}
@ -176,14 +166,4 @@
{{/if}}
{{/if}}
{{/if}}
{{/let}}
{{#if this.engineToDisable}}
<ConfirmModal
@color="critical"
@confirmMessage="Any data in this engine will be permanently deleted."
@confirmTitle="Disable engine?"
@onClose={{fn (mut this.engineToDisable) null}}
@onConfirm={{perform this.disableEngine this.engineToDisable}}
/>
{{/if}}
{{/let}}

View file

@ -3,355 +3,4 @@
* SPDX-License-Identifier: BUSL-1.1
*/
/**
* Metadata configuration for secret and auth engines, including enterprise.
*
* This file defines and exports engine metadata, including its
* displayName, mountCategory, requiresEnterprise, and other relevant properties. It serves as a
* centralized source of truth for engine-related configurations.
*
* Key responsibilities:
* - Define metadata for all engines.
* - Provide utility functions or constants for accessing engine-specific data.
* - Facilitate dynamic engine rendering and behavior based on metadata.
*
* Example usage:
* If an enterprise license is present, return all secret engines;
* otherwise, return only the secret engines supported in OSS.
* return filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: this.version.isEnterprise });
*/
export interface EngineDisplayData {
pluginCategory?: string; // The plugin category is used to group engines in the UI. e.g., 'cloud', 'infra', 'generic'
displayName: string;
engineRoute?: string; // engines that have their own Ember engine will have this route defined.
glyph?: string;
isWIF?: boolean; // flag for 'Workload Identity Federation' engines. - https://developer.hashicorp.com/hcp/docs/hcp/iam/service-principal/workload-identity-federation
mountCategory: string[];
requiredFeature?: string; // flag for engines that require the ADP (Advanced Data Protection) feature. - https://www.hashicorp.com/en/blog/advanced-data-protection-adp-now-available-in-hcp-vault
requiresEnterprise?: boolean;
isConfigurable?: boolean; // for secret engines that have additional configuration pages and actions.
isOnlyMountable?: boolean; // The UI only supports configuration views for these secrets engines. The CLI must be used to manage other engine resources (i.e. roles, credentials).
type: string;
value?: string;
configRoute?: string; // override for custom route if not "configuration.plugin-settings" (used for Ember engines)
}
/**
* @param mountCategory - Given mount category to filter by, e.g., 'auth' or 'secret'.
* @param isEnterprise - Optional boolean to indicate if enterprise engines should be included in the results.
* @returns Filtered array of engines that match the given mount category
*/
export function filterEnginesByMountCategory({
mountCategory,
isEnterprise = false,
}: {
mountCategory: 'auth' | 'secret';
isEnterprise: boolean;
}) {
return isEnterprise
? ALL_ENGINES.filter((engine) => engine.mountCategory.includes(mountCategory))
: ALL_ENGINES.filter(
(engine) => engine.mountCategory.includes(mountCategory) && !engine.requiresEnterprise
);
}
export function isAddonEngine(type: string, version: number) {
if (type === 'kv' && version === 1) {
return false;
}
const engineRoute = ALL_ENGINES.find((engine) => engine.type === type)?.engineRoute;
return !!engineRoute;
}
// The "sys/mounts" and "sys/internal/ui/mounts" endpoints return a "secret/" key containing
// all mounts enabled in Vault. Some types are internal Vault APIs, not user-mountable secrets engines,
// and should be filtered in some scenarios, such as listing secrets engines.
export const INTERNAL_ENGINE_TYPES = ['system', 'identity', 'agent_registry'];
export const ALL_ENGINES: EngineDisplayData[] = [
{
pluginCategory: 'cloud',
displayName: 'AliCloud',
glyph: 'alibaba-color',
mountCategory: ['auth', 'secret'],
type: 'alicloud',
},
{
pluginCategory: 'generic',
displayName: 'AppRole',
glyph: 'cpu',
mountCategory: ['auth'],
type: 'approle',
value: 'approle',
},
{
pluginCategory: 'cloud',
displayName: 'AWS',
glyph: 'aws-color',
isConfigurable: true,
isWIF: true,
mountCategory: ['auth', 'secret'],
type: 'aws',
},
{
pluginCategory: 'cloud',
displayName: 'Azure',
glyph: 'azure-color',
isOnlyMountable: true,
isConfigurable: true,
isWIF: true,
mountCategory: ['auth', 'secret'],
type: 'azure',
},
{
pluginCategory: 'infra',
displayName: 'Consul',
glyph: 'consul-color',
mountCategory: ['secret'],
type: 'consul',
},
{
displayName: 'Cubbyhole',
type: 'cubbyhole',
mountCategory: ['secret'],
},
{
pluginCategory: 'infra',
displayName: 'Databases',
glyph: 'database',
mountCategory: ['secret'],
type: 'database',
},
{
pluginCategory: 'cloud',
displayName: 'GitHub',
glyph: 'github-color',
mountCategory: ['auth'],
type: 'github',
value: 'github',
},
{
pluginCategory: 'cloud',
displayName: 'Google Cloud',
glyph: 'gcp-color',
isOnlyMountable: true,
isConfigurable: true,
isWIF: true,
mountCategory: ['auth', 'secret'],
type: 'gcp',
},
{
pluginCategory: 'cloud',
displayName: 'Google Cloud KMS',
glyph: 'gcp-color',
mountCategory: ['secret'],
type: 'gcpkms',
},
{
pluginCategory: 'generic',
displayName: 'JWT',
glyph: 'jwt',
mountCategory: ['auth'],
type: 'jwt',
value: 'jwt',
},
{
pluginCategory: 'generic',
displayName: 'KV',
engineRoute: 'kv.list',
configRoute: 'kv.configuration', // only utilized to display config data for kvv2, not in conjunction with isConfigurable as templates determine whether engine is kv v1 or v2
glyph: 'key-values',
mountCategory: ['secret'],
type: 'kv',
},
{
pluginCategory: 'generic',
displayName: 'KMIP',
engineRoute: 'kmip.scopes.index',
configRoute: 'kmip.configuration',
isConfigurable: true,
glyph: 'lock',
mountCategory: ['secret'],
requiredFeature: 'KMIP',
requiresEnterprise: true,
type: 'kmip',
},
{
pluginCategory: 'generic',
displayName: 'Transform',
glyph: 'transform-data',
mountCategory: ['secret'],
requiredFeature: 'Transform Secrets Engine',
requiresEnterprise: true,
type: 'transform',
},
{
pluginCategory: 'cloud',
displayName: 'Key Management',
glyph: 'key',
mountCategory: ['secret'],
requiredFeature: 'Key Management Secrets Engine',
requiresEnterprise: true,
type: 'keymgmt',
},
{
pluginCategory: 'generic',
displayName: 'Kubernetes',
engineRoute: 'kubernetes.overview',
configRoute: 'kubernetes.configuration',
glyph: 'kubernetes-color',
isConfigurable: true,
mountCategory: ['auth', 'secret'],
type: 'kubernetes',
},
{
pluginCategory: 'generic',
displayName: 'LDAP',
isConfigurable: true,
engineRoute: 'ldap.overview',
configRoute: 'ldap.configuration',
glyph: 'folder-users',
mountCategory: ['auth', 'secret'],
type: 'ldap',
},
{
pluginCategory: 'infra',
displayName: 'Nomad',
glyph: 'nomad-color',
mountCategory: ['secret'],
type: 'nomad',
},
{
pluginCategory: 'generic',
displayName: 'OIDC',
glyph: 'openid-color',
mountCategory: ['auth'],
type: 'oidc',
value: 'oidc',
},
{
pluginCategory: 'infra',
displayName: 'Okta',
glyph: 'okta-color',
mountCategory: ['auth'],
type: 'okta',
value: 'okta',
},
{
pluginCategory: 'generic',
displayName: 'PKI Certificates',
isConfigurable: true,
engineRoute: 'pki.overview',
configRoute: 'pki.configuration',
glyph: 'certificate',
mountCategory: ['secret'],
type: 'pki',
},
{
pluginCategory: 'infra',
displayName: 'RADIUS',
glyph: 'mainframe',
mountCategory: ['auth'],
type: 'radius',
value: 'radius',
},
{
pluginCategory: 'infra',
displayName: 'RabbitMQ',
glyph: 'rabbitmq-color',
mountCategory: ['secret'],
type: 'rabbitmq',
},
{
pluginCategory: 'generic',
displayName: 'SAML',
glyph: 'saml-color',
mountCategory: ['auth'],
requiresEnterprise: true,
type: 'saml',
value: 'saml',
},
{
pluginCategory: 'generic',
displayName: 'SSH',
glyph: 'terminal-screen',
isConfigurable: true,
mountCategory: ['secret'],
type: 'ssh',
},
{
pluginCategory: 'generic',
displayName: 'TLS Certificates',
glyph: 'certificate',
mountCategory: ['auth'],
type: 'cert',
value: 'cert',
},
{
pluginCategory: 'generic',
displayName: 'TOTP',
glyph: 'history',
mountCategory: ['secret'],
type: 'totp',
},
{
pluginCategory: 'generic',
displayName: 'Transit',
glyph: 'swap-horizontal',
mountCategory: ['secret'],
type: 'transit',
},
{
displayName: 'Token',
type: 'token',
mountCategory: ['auth'],
},
{
pluginCategory: 'generic',
displayName: 'Userpass',
glyph: 'users',
mountCategory: ['auth'],
type: 'userpass',
value: 'userpass',
},
// TODO: enable builtin plugins after confirming with Product
//
// {
// pluginCategory: 'generic',
// displayName: 'Ad',
// glyph: 'folder',
// isOldEngine: true,
// isOnlyMountable: true,
// mountCategory: ['secret'],
// type: 'ad',
// },
// {
// pluginCategory: 'cloud',
// displayName: 'MongoDB Atlas',
// glyph: 'mongodb-color',
// isOldEngine: true,
// isOnlyMountable: true,
// mountCategory: ['secret'],
// type: 'mongodbatlas',
// },
// {
// pluginCategory: 'infra',
// displayName: 'OpenLDAP',
// glyph: 'folder-users',
// isOldEngine: true,
// isOnlyMountable: true,
// mountCategory: ['secret'],
// type: 'openldap',
// },
// {
// pluginCategory: 'infra',
// displayName: 'Terraform',
// glyph: 'terraform-color',
// isOldEngine: true,
// isOnlyMountable: true,
// mountCategory: ['secret'],
// type: 'terraform',
// },
];
export * from 'core/utils/all-engines-metadata';

View file

@ -0,0 +1,46 @@
{{!
Copyright IBM Corp. 2016, 2025
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Dropdown as |D|>
{{#if this.isIcon}}
<D.ToggleIcon
@icon="more-horizontal"
@text="{{if @model.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{else}}
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
{{/if}}
{{! Yield point for engine-specific custom menu items (e.g., KV's Generate policy) }}
{{yield D}}
<D.Interactive
@icon="settings"
@route={{@configRoute}}
@model={{this.configureRouteModel}}
data-test-popup-menu="Configure"
>Configure</D.Interactive>
{{#if this.shouldShowDelete}}
<D.Interactive
{{on "click" (fn this.handleDeleteClick @model)}}
@color="critical"
@icon="trash"
data-test-popup-menu="Delete"
>Delete</D.Interactive>
{{/if}}
</Hds::Dropdown>
{{#if this.engineToDisable}}
<ConfirmModal
@color="critical"
@confirmMessage="Any data in this engine will be permanently deleted."
@confirmTitle="Disable engine?"
@onClose={{this.handleModalClose}}
@onConfirm={{this.handleModalConfirm}}
/>
{{/if}}

View file

@ -0,0 +1,117 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import type RouterService from '@ember/routing/router-service';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
import type ApiService from 'vault/services/api';
import type FlashMessageService from 'vault/services/flash-messages';
/**
* @module ManageDropdown
* Reusable component for displaying the Manage dropdown used in secret engine headers & secret engine mount list.
*
* @example
* // In main app page headers and list components — uses the resource getter for the full absolute route
* <ManageDropdown
* @model={{this.backendModel}}
* @configRoute={{this.backendConfigurationLink}}
* />
*
* // In Ember engine templates (pki, kubernetes, ldap, kmip, kv) — pass the short relative route,
* // since HDS @route resolves relative to the engine's router mount
* <ManageDropdown
* @model={{@model}}
* @configRoute="configuration"
* />
*
* // With custom menu items (like KV's Generate policy) — icon variant in a Ember engine list
* <ManageDropdown
* @model={{@backendModel}}
* @configRoute="configuration"
* as |D|
* >
* <D.Interactive @icon="shield-check" {{on "click" openFlyout}}>Generate policy</D.Interactive>
* </ManageDropdown>
*
* @param {SecretsEngineResource} model - The secrets engine resource containing the engine details
* @param {string} configRoute - Route for the Configure action.
* @param {string} variant - Set to "icon" for "..." icon button, otherwise shows "Manage" text button (default)
*/
interface Args {
model: SecretsEngineResource;
configRoute: string;
variant?: 'icon';
}
export default class ManageDropdown extends Component<Args> {
@service declare readonly router: RouterService;
@service('app-router') declare readonly appRouter: RouterService;
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
get isIcon() {
return this.args.variant === 'icon';
}
get configureRouteModel() {
return this.args.model.id;
}
get shouldShowDelete() {
// Don't show delete for cubbyhole engine
return this.args.model.type !== 'cubbyhole';
}
transitionToBackends() {
// First try using the router service, which is available in most contexts
if (this.router) {
this.router.transitionTo('vault.cluster.secrets.backends');
return;
}
// Fallback for ember-engine components which use appRouter instead of router service
if (this.appRouter) {
this.appRouter.transitionTo('vault.cluster.secrets.backends');
}
}
@action
handleDeleteClick(engine: SecretsEngineResource) {
this.engineToDisable = engine;
}
@action
handleModalClose() {
this.engineToDisable = undefined;
}
@action
async handleModalConfirm() {
if (this.engineToDisable) {
const { engineType, id, path } = this.engineToDisable;
try {
await this.api.sys.mountsDisableSecretsEngine(id);
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
this.transitionToBackends();
} catch (error) {
const { message } = await this.api.parseError(error);
this.flashMessages.danger(
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
);
} finally {
this.engineToDisable = undefined;
}
}
}
}

View file

@ -0,0 +1,62 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { ALL_ENGINES, type EngineDisplayData } from 'core/utils/all-engines-metadata';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
/**
* Default metadata for unknown engine plugins
*/
export const unknownEngineMetadata = (methodType?: string): EngineDisplayData => ({
type: methodType || 'unknown',
displayName: methodType || 'Unknown plugin',
glyph: 'lock',
mountCategory: ['secret', 'auth'],
});
/**
* Helper function to retrieve engine metadata for a given `methodType`.
* It searches the `ALL_ENGINES` array for an engine with a matching type and returns its metadata object.
* The `ALL_ENGINES` array includes secret and auth engines, including those supported only in enterprise.
* These details (such as mount type and enterprise licensing) are included in the returned engine object.
*
* For external plugins that have a builtin mapping (e.g., "vault-plugin-secrets-keymgmt" -> "keymgmt"),
* this function returns the metadata for the corresponding builtin engine, preserving the original
* external plugin name in the type field.
*
* Example usage:
* const engineMetadata = engineDisplayData('kmip');
* if (engineMetadata?.requiresEnterprise) {
* console.log(`This mount: ${engineMetadata.engineType} requires an enterprise license`);
* }
*
* @param {string} methodType - The engine type (sometimes called backend) to look up (e.g., "aws", "azure", "vault-plugin-secrets-keymgmt").
* @returns {Object} - The engine metadata, which includes information about its mount type (e.g., secret or auth)
* and whether it requires an enterprise license. For unknown engines, returns a default unknown plugin object.
*/
export default function engineDisplayData(methodType: string): EngineDisplayData {
// First try to find an exact match
const builtinEngine = ALL_ENGINES?.find((t) => t.type === methodType);
if (builtinEngine) {
return builtinEngine;
}
// If no direct match, check if this is a known external plugin and use its builtin mapping
const effectiveType = getEffectiveEngineType(methodType);
if (effectiveType !== methodType) {
// This is a known external plugin with a builtin mapping
const mappedEngine = ALL_ENGINES?.find((t) => t.type === effectiveType);
if (mappedEngine) {
// Return the mapped engine metadata but preserve the original external plugin type
return {
...mappedEngine,
type: methodType, // Keep the original external plugin name for identification
};
}
}
// Return default unknown plugin metadata
return unknownEngineMetadata(methodType);
}

View file

@ -0,0 +1,353 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
/**
* Metadata configuration for secret and auth engines, including enterprise.
*
* This file defines and exports engine metadata, including its
* displayName, mountCategory, requiresEnterprise, and other relevant properties. It serves as a
* centralized source of truth for engine-related configurations.
*
* Key responsibilities:
* - Define metadata for all engines.
* - Provide utility functions or constants for accessing engine-specific data.
* - Facilitate dynamic engine rendering and behavior based on metadata.
*
* Example usage:
* If an enterprise license is present, return all secret engines;
* otherwise, return only the secret engines supported in OSS.
* return filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: this.version.isEnterprise });
*/
export interface EngineDisplayData {
pluginCategory?: string; // The plugin category is used to group engines in the UI. e.g., 'cloud', 'infra', 'generic'
displayName: string;
engineRoute?: string; // engines that have their own Ember engine will have this route defined.
glyph?: string;
isWIF?: boolean; // flag for 'Workload Identity Federation' engines. - https://developer.hashicorp.com/hcp/docs/hcp/iam/service-principal/workload-identity-federation
mountCategory: string[];
requiredFeature?: string; // flag for engines that require the ADP (Advanced Data Protection) feature. - https://www.hashicorp.com/en/blog/advanced-data-protection-adp-now-available-in-hcp-vault
requiresEnterprise?: boolean;
isConfigurable?: boolean; // for secret engines that have additional configuration pages and actions.
isOnlyMountable?: boolean; // The UI only supports configuration views for these secrets engines. The CLI must be used to manage other engine resources (i.e. roles, credentials).
type: string;
value?: string;
configRoute?: string; // override for custom route if not "configuration.plugin-settings" (used for Ember engines)
}
/**
* @param mountCategory - Given mount category to filter by, e.g., 'auth' or 'secret'.
* @param isEnterprise - Optional boolean to indicate if enterprise engines should be included in the results.
* @returns Filtered array of engines that match the given mount category
*/
export function filterEnginesByMountCategory({
mountCategory,
isEnterprise = false,
}: {
mountCategory: 'auth' | 'secret';
isEnterprise: boolean;
}) {
return isEnterprise
? ALL_ENGINES.filter((engine) => engine.mountCategory.includes(mountCategory))
: ALL_ENGINES.filter(
(engine) => engine.mountCategory.includes(mountCategory) && !engine.requiresEnterprise
);
}
export function isAddonEngine(type: string, version: number) {
if (type === 'kv' && version === 1) {
return false;
}
const engineRoute = ALL_ENGINES.find((engine) => engine.type === type)?.engineRoute;
return !!engineRoute;
}
// The "sys/mounts" and "sys/internal/ui/mounts" endpoints return a "secret/" key containing
// all mounts enabled in Vault. Some types are internal Vault APIs, not user-mountable secrets engines,
// and should be filtered in some scenarios, such as listing secrets engines.
export const INTERNAL_ENGINE_TYPES = ['system', 'identity', 'agent_registry'];
export const ALL_ENGINES: EngineDisplayData[] = [
{
pluginCategory: 'cloud',
displayName: 'AliCloud',
glyph: 'alibaba-color',
mountCategory: ['auth', 'secret'],
type: 'alicloud',
},
{
pluginCategory: 'generic',
displayName: 'AppRole',
glyph: 'cpu',
mountCategory: ['auth'],
type: 'approle',
value: 'approle',
},
{
pluginCategory: 'cloud',
displayName: 'AWS',
glyph: 'aws-color',
isConfigurable: true,
isWIF: true,
mountCategory: ['auth', 'secret'],
type: 'aws',
},
{
pluginCategory: 'cloud',
displayName: 'Azure',
glyph: 'azure-color',
isOnlyMountable: true,
isConfigurable: true,
isWIF: true,
mountCategory: ['auth', 'secret'],
type: 'azure',
},
{
pluginCategory: 'infra',
displayName: 'Consul',
glyph: 'consul-color',
mountCategory: ['secret'],
type: 'consul',
},
{
displayName: 'Cubbyhole',
type: 'cubbyhole',
mountCategory: ['secret'],
},
{
pluginCategory: 'infra',
displayName: 'Databases',
glyph: 'database',
mountCategory: ['secret'],
type: 'database',
},
{
pluginCategory: 'cloud',
displayName: 'GitHub',
glyph: 'github-color',
mountCategory: ['auth'],
type: 'github',
value: 'github',
},
{
pluginCategory: 'cloud',
displayName: 'Google Cloud',
glyph: 'gcp-color',
isOnlyMountable: true,
isConfigurable: true,
isWIF: true,
mountCategory: ['auth', 'secret'],
type: 'gcp',
},
{
pluginCategory: 'cloud',
displayName: 'Google Cloud KMS',
glyph: 'gcp-color',
mountCategory: ['secret'],
type: 'gcpkms',
},
{
pluginCategory: 'generic',
displayName: 'JWT',
glyph: 'jwt',
mountCategory: ['auth'],
type: 'jwt',
value: 'jwt',
},
{
pluginCategory: 'generic',
displayName: 'KV',
engineRoute: 'kv.list',
configRoute: 'kv.configuration', // only utilized to display config data for kvv2, not in conjunction with isConfigurable as templates determine whether engine is kv v1 or v2
glyph: 'key-values',
mountCategory: ['secret'],
type: 'kv',
},
{
pluginCategory: 'generic',
displayName: 'KMIP',
engineRoute: 'kmip.scopes.index',
configRoute: 'kmip.configuration',
isConfigurable: true,
glyph: 'lock',
mountCategory: ['secret'],
requiredFeature: 'KMIP',
requiresEnterprise: true,
type: 'kmip',
},
{
pluginCategory: 'generic',
displayName: 'Transform',
glyph: 'transform-data',
mountCategory: ['secret'],
requiredFeature: 'Transform Secrets Engine',
requiresEnterprise: true,
type: 'transform',
},
{
pluginCategory: 'cloud',
displayName: 'Key Management',
glyph: 'key',
mountCategory: ['secret'],
requiredFeature: 'Key Management Secrets Engine',
requiresEnterprise: true,
type: 'keymgmt',
},
{
pluginCategory: 'generic',
displayName: 'Kubernetes',
engineRoute: 'kubernetes.overview',
configRoute: 'kubernetes.configuration',
glyph: 'kubernetes-color',
isConfigurable: true,
mountCategory: ['auth', 'secret'],
type: 'kubernetes',
},
{
pluginCategory: 'generic',
displayName: 'LDAP',
isConfigurable: true,
engineRoute: 'ldap.overview',
configRoute: 'ldap.configuration',
glyph: 'folder-users',
mountCategory: ['auth', 'secret'],
type: 'ldap',
},
{
pluginCategory: 'infra',
displayName: 'Nomad',
glyph: 'nomad-color',
mountCategory: ['secret'],
type: 'nomad',
},
{
pluginCategory: 'generic',
displayName: 'OIDC',
glyph: 'openid-color',
mountCategory: ['auth'],
type: 'oidc',
value: 'oidc',
},
{
pluginCategory: 'infra',
displayName: 'Okta',
glyph: 'okta-color',
mountCategory: ['auth'],
type: 'okta',
value: 'okta',
},
{
pluginCategory: 'generic',
displayName: 'PKI Certificates',
isConfigurable: true,
engineRoute: 'pki.overview',
configRoute: 'pki.configuration',
glyph: 'certificate',
mountCategory: ['secret'],
type: 'pki',
},
{
pluginCategory: 'infra',
displayName: 'RADIUS',
glyph: 'mainframe',
mountCategory: ['auth'],
type: 'radius',
value: 'radius',
},
{
pluginCategory: 'infra',
displayName: 'RabbitMQ',
glyph: 'rabbitmq-color',
mountCategory: ['secret'],
type: 'rabbitmq',
},
{
pluginCategory: 'generic',
displayName: 'SAML',
glyph: 'saml-color',
mountCategory: ['auth'],
requiresEnterprise: true,
type: 'saml',
value: 'saml',
},
{
pluginCategory: 'generic',
displayName: 'SSH',
glyph: 'terminal-screen',
isConfigurable: true,
mountCategory: ['secret'],
type: 'ssh',
},
{
pluginCategory: 'generic',
displayName: 'TLS Certificates',
glyph: 'certificate',
mountCategory: ['auth'],
type: 'cert',
value: 'cert',
},
{
pluginCategory: 'generic',
displayName: 'TOTP',
glyph: 'history',
mountCategory: ['secret'],
type: 'totp',
},
{
pluginCategory: 'generic',
displayName: 'Transit',
glyph: 'swap-horizontal',
mountCategory: ['secret'],
type: 'transit',
},
{
displayName: 'Token',
type: 'token',
mountCategory: ['auth'],
},
{
pluginCategory: 'generic',
displayName: 'Userpass',
glyph: 'users',
mountCategory: ['auth'],
type: 'userpass',
value: 'userpass',
},
// TODO: enable builtin plugins after confirming with Product
//
// {
// pluginCategory: 'generic',
// displayName: 'Ad',
// glyph: 'folder',
// isOnlyMountable: true,
// mountCategory: ['secret'],
// type: 'ad',
// },
// {
// pluginCategory: 'cloud',
// displayName: 'MongoDB Atlas',
// glyph: 'mongodb-color',
// isOnlyMountable: true,
// mountCategory: ['secret'],
// type: 'mongodbatlas',
// },
// {
// pluginCategory: 'infra',
// displayName: 'OpenLDAP',
// glyph: 'folder-users',
// isOnlyMountable: true,
// mountCategory: ['secret'],
// type: 'openldap',
// },
// {
// pluginCategory: 'infra',
// displayName: 'Terraform',
// glyph: 'terraform-color',
// isOnlyMountable: true,
// mountCategory: ['secret'],
// type: 'terraform',
// },
];

View file

@ -0,0 +1,6 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'core/components/manage-dropdown';

View file

@ -0,0 +1,6 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'core/helpers/engines-display-data';

View file

@ -7,21 +7,7 @@
<KmipBreadcrumb @currentRoute={{this.secretMountPath.currentPath}} />
</:breadcrumbs>
<:actions>
<Hds::Dropdown as |D|>
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
<D.Interactive
@icon="settings"
@route="configuration"
@model={{@secretsEngine.id}}
data-test-popup-menu="Configure"
>Configure</D.Interactive>
<D.Interactive
{{on "click" (fn (mut this.engineToDisable) @secretsEngine)}}
@color="critical"
@icon="trash"
data-test-popup-menu="Delete"
>Delete</D.Interactive>
</Hds::Dropdown>
<ManageDropdown @model={{@secretsEngine}} @configRoute="configuration" />
</:actions>
</Page::Header>
<KmipTabs />
@ -117,14 +103,4 @@
<Hds::Link::Standalone @icon="plus" @text="Create a scope" @route="scopes.create" />
</EmptyState>
{{/if}}
{{/if}}
{{#if this.engineToDisable}}
<ConfirmModal
@color="critical"
@confirmMessage="Any data in this engine will be permanently deleted."
@confirmTitle="Disable engine?"
@onClose={{fn (mut this.engineToDisable) null}}
@onConfirm={{perform this.disableEngine this.engineToDisable}}
/>
{{/if}}

View file

@ -3,21 +3,21 @@
* 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 { getOwner } from '@ember/owner';
import { task } from 'ember-concurrency';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import type RouterService from '@ember/routing/router-service';
import type SecretMountPath from 'vault/services/secret-mount-path';
import type ApiService from 'vault/services/api';
import type { CapabilitiesMap, EngineOwner } from 'vault/app-types';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
import type ApiService from 'vault/services/api';
import FlashMessageService from 'vault/services/flash-messages';
import type SecretMountPath from 'vault/services/secret-mount-path';
interface Args {
secretsEngine: SecretsEngineResource;
scopes: string[];
capabilities: CapabilitiesMap;
}
@ -27,7 +27,6 @@ export default class KmipScopesPageComponent extends Component<Args> {
@service declare readonly secretMountPath: SecretMountPath;
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
@tracked scopeToDelete: string | null = null;
@ -56,22 +55,4 @@ export default class KmipScopesPageComponent extends Component<Args> {
this.flashMessages.danger(`Error deleting scope ${this.scopeToDelete}: ${message}`);
}
}
@task
*disableEngine(engine: SecretsEngineResource) {
const { engineType, id, path } = engine;
try {
yield this.api.sys.mountsDisableSecretsEngine(id);
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
this.router.transitionTo('vault.cluster.secrets.backends');
} catch (err) {
const { message } = yield this.api.parseError(err);
this.flashMessages.danger(
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
);
} finally {
this.engineToDisable = undefined;
}
}
}

View file

@ -15,21 +15,7 @@
{{#if @configRoute}}
<Hds::Button @color="secondary" @route="overview" @text="Exit configuration" data-test-button="Exit configuration" />
{{else}}
<Hds::Dropdown as |D|>
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
<D.Interactive
@icon="settings"
@route={{if @promptConfig "configure" "configuration"}}
@model={{@model.id}}
data-test-popup-menu="Configure"
>Configure</D.Interactive>
<D.Interactive
{{on "click" (fn (mut this.engineToDisable) @model)}}
@color="critical"
@icon="trash"
data-test-popup-menu="Delete"
>Delete</D.Interactive>
</Hds::Dropdown>
<ManageDropdown @model={{@model}} @configRoute={{if @promptConfig "configure" "configuration"}} />
{{/if}}
</:actions>
</Page::Header>

View file

@ -1,57 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
import type RouterService from '@ember/routing/router-service';
import type FlashMessageService from 'vault/services/flash-messages';
import type ApiService from 'vault/services/api';
/**
* @module KubernetesHeader handles the ldap page header.
*
* @example
* <SecretEngine::KubernetesHeader
* @model={{this.model}}
* />
*
* @param {object} secretsEngine - A model contains a ldap secret engine resource.
* @param {object} config - A model contains the configuration of the ldap secret engine.
*/
interface Args {
secretsEngine: SecretsEngineResource;
config: Record<string, unknown>;
}
export default class KubernetesHeader extends Component<Args> {
@service('app-router') declare readonly router: RouterService;
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
@task
*disableEngine(engine: SecretsEngineResource) {
const { engineType, id, path } = engine;
try {
yield this.api.sys.mountsDisableSecretsEngine(id);
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
this.router.transitionTo('vault.cluster.secrets.backends');
} catch (err) {
const { message } = yield this.api.parseError(err);
this.flashMessages.danger(
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
);
} finally {
this.engineToDisable = undefined;
}
}
}

View file

@ -11,8 +11,7 @@
<Hds::Badge @text="version 2" data-test-badge />
</:badges>
<:actions>
<Hds::Dropdown as |D|>
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
<ManageDropdown @model={{@backendModel}} @configRoute="configuration" as |D|>
<CodeGenerator::Policy::Flyout @onClose={{D.close}}>
<:customTrigger as |openFlyout|>
<D.Interactive @icon="shield-check" {{on "click" openFlyout}} data-test-popup-menu="Generate policy">
@ -20,19 +19,7 @@
</D.Interactive>
</:customTrigger>
</CodeGenerator::Policy::Flyout>
<D.Interactive
@icon="settings"
@route="configuration"
@model={{@backendModel.id}}
data-test-popup-menu="Configure"
>Configure</D.Interactive>
<D.Interactive
{{on "click" (fn (mut this.engineToDisable) @backendModel)}}
@color="critical"
@icon="trash"
data-test-popup-menu="Delete"
>Delete</D.Interactive>
</Hds::Dropdown>
</ManageDropdown>
<Hds::Button
@text="Create secret"
@ -197,14 +184,4 @@
/>
{{/if}}
{{/if}}
{{/if}}
{{#if this.engineToDisable}}
<ConfirmModal
@color="critical"
@confirmMessage="Any data in this engine will be permanently deleted."
@confirmTitle="Disable engine?"
@onClose={{fn (mut this.engineToDisable) null}}
@onConfirm={{perform this.disableEngine this.engineToDisable}}
/>
{{/if}}

View file

@ -3,14 +3,13 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { getOwner } from '@ember/owner';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { ancestorKeysForKey } from 'core/utils/key-utils';
import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs';
import { task } from 'ember-concurrency';
/**
* @module List
@ -32,7 +31,6 @@ export default class KvListPageComponent extends Component {
@tracked secretPath;
@tracked metadataToDelete = null; // set to the metadata intended to delete
@tracked engineToDisable = undefined;
// used for KV list and list-directory view
// ex: beep/
@ -59,24 +57,6 @@ export default class KvListPageComponent extends Component {
};
}
@task
*disableEngine(engine) {
const { engineType, id, path } = engine;
try {
yield this.api.sys.mountsDisableSecretsEngine(id);
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
this.router.transitionTo('vault.cluster.secrets.backends');
} catch (err) {
const { message } = yield this.api.parseError(err);
this.flashMessages.danger(
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
);
} finally {
this.engineToDisable = undefined;
}
}
@action
async onDelete(secretPath) {
try {

View file

@ -15,21 +15,7 @@
{{#if @configRoute}}
<Hds::Button @color="secondary" @route="overview" @text="Exit configuration" data-test-button="Exit configuration" />
{{else}}
<Hds::Dropdown as |D|>
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
<D.Interactive
@icon="settings"
@route={{if @promptConfig "configure" "configuration"}}
@model={{@model.id}}
data-test-popup-menu="Configure"
>Configure</D.Interactive>
<D.Interactive
{{on "click" (fn (mut this.engineToDisable) @model)}}
@color="critical"
@icon="trash"
data-test-popup-menu="Delete"
>Delete</D.Interactive>
</Hds::Dropdown>
<ManageDropdown @model={{@model}} @configRoute={{if @promptConfig "configure" "configuration"}} />
{{/if}}
</:actions>
</Page::Header>
@ -51,16 +37,6 @@
</nav>
{{/if}}
{{#if this.engineToDisable}}
<ConfirmModal
@color="critical"
@confirmMessage="Any data in this engine will be permanently deleted."
@confirmTitle="Disable engine?"
@onClose={{fn (mut this.engineToDisable) null}}
@onConfirm={{perform this.disableEngine this.engineToDisable}}
/>
{{/if}}
<Toolbar aria-label="nav for managing LDAP">
<ToolbarFilters aria-label="filter for LDAP items">
{{yield to="toolbarFilters"}}

View file

@ -1,57 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
import type RouterService from '@ember/routing/router-service';
import type FlashMessageService from 'vault/services/flash-messages';
import type ApiService from 'vault/services/api';
/**
* @module LdapHeader handles the ldap page header.
*
* @example
* <SecretEngine::LdapHeader
* @model={{this.model}}
* />
*
* @param {object} secretsEngine - A model contains a ldap secret engine resource.
* @param {object} config - A model contains the configuration of the ldap secret engine.
*/
interface Args {
secretsEngine: SecretsEngineResource;
config: Record<string, unknown>;
}
export default class LdapHeader extends Component<Args> {
@service('app-router') declare readonly router: RouterService;
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
@task
*disableEngine(engine: SecretsEngineResource) {
const { engineType, id, path } = engine;
try {
yield this.api.sys.mountsDisableSecretsEngine(id);
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
this.router.transitionTo('vault.cluster.secrets.backends');
} catch (err) {
const { message } = yield this.api.parseError(err);
this.flashMessages.danger(
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
);
} finally {
this.engineToDisable = undefined;
}
}
}

View file

@ -17,21 +17,7 @@
{{#if @configRoute}}
<Hds::Button @color="secondary" @route="overview" @text="Exit configuration" data-test-button="Exit configuration" />
{{else}}
<Hds::Dropdown as |D|>
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
<D.Interactive
@icon="settings"
@route="configuration"
@model={{@backend.id}}
data-test-popup-menu="Configure"
>Configure</D.Interactive>
<D.Interactive
{{on "click" (fn (mut this.engineToDisable) @backend)}}
@color="critical"
@icon="trash"
data-test-popup-menu="Delete"
>Delete</D.Interactive>
</Hds::Dropdown>
<ManageDropdown @model={{@backend}} @configRoute="configuration" />
{{/if}}
</:actions>
</Page::Header>
@ -58,14 +44,4 @@
<li><LinkTo @route="tidy" @model={{@backend.id}} data-test-secret-list-tab="Tidy">Tidy</LinkTo></li>
</ul>
</nav>
{{/if}}
{{#if this.engineToDisable}}
<ConfirmModal
@color="critical"
@confirmMessage="Any data in this engine will be permanently deleted."
@confirmTitle="Disable engine?"
@onClose={{fn (mut this.engineToDisable) null}}
@onConfirm={{perform this.disableEngine this.engineToDisable}}
/>
{{/if}}

View file

@ -4,16 +4,12 @@
*/
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import Component from '@glimmer/component';
import { task } from 'ember-concurrency';
import type { PATH_MAP } from 'vault/utils/constants/capabilities';
import type ApiService from 'vault/services/api';
import type CapabilitiesService from 'vault/services/capabilities';
import type FlashMessageService from 'vault/services/flash-messages';
import type RouterService from '@ember/routing/router-service';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
import type CapabilitiesService from 'vault/services/capabilities';
import type { PATH_MAP } from 'vault/utils/constants/capabilities';
/**
* @module PkiPageHeader
@ -25,7 +21,7 @@ import type SecretsEngineResource from 'vault/resources/secrets/engine';
*/
interface Args {
backend: { id: string };
backend: SecretsEngineResource;
}
const ROUTE_PATH_MAP = {
@ -36,12 +32,8 @@ const ROUTE_PATH_MAP = {
export default class PkiPageHeader extends Component<Args> {
@service('app-router') declare readonly router: RouterService;
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@service declare readonly capabilities: CapabilitiesService;
@tracked engineToDisable = undefined;
get breadcrumbs() {
return [
{ label: 'Vault', route: 'vault', icon: 'vault', linkExternal: true },
@ -61,22 +53,4 @@ export default class PkiPageHeader extends Component<Args> {
}
return null;
}
@task
*disableEngine(engine: SecretsEngineResource) {
const { engineType, id, path } = engine;
try {
yield this.api.sys.mountsDisableSecretsEngine(id);
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
this.router.transitionTo('vault.cluster.secrets.backends');
} catch (err) {
const { message } = yield this.api.parseError(err);
this.flashMessages.danger(
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
);
} finally {
this.engineToDisable = undefined;
}
}
}

View file

@ -0,0 +1,518 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { click, currentRouteName, fillIn, findAll, settled, visit } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { module, test } from 'qunit';
import { v4 as uuidv4 } from 'uuid';
import engineDisplayData from 'vault/helpers/engines-display-data';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
import { runCmd } from 'vault/tests/helpers/commands';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
const SECRET_ENGINE_MANAGE_DROPDOWN_ROUTING_CASES = [
{
key: 'alicloud',
type: 'alicloud',
isEnginePathClickable: false,
showManageDropdown: false,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'azure',
type: 'azure',
isEnginePathClickable: true,
showManageDropdown: false,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'gcp',
type: 'gcp',
isEnginePathClickable: true,
showManageDropdown: false,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'gcpkms',
type: 'gcpkms',
isEnginePathClickable: false,
showManageDropdown: false,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'keymgmt',
type: 'keymgmt',
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'kubernetes',
type: 'kubernetes',
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
expectedActionConfigureRoutesOverride: [
// if the engine is configured
'vault.cluster.secrets.backend.kubernetes.configuration',
// if the engine is not configured
'vault.cluster.secrets.backend.kubernetes.configure',
],
expectedLandingConfigureRoutesOverride: [
// if the engine is configured
'vault.cluster.secrets.backend.kubernetes.configuration',
// if the engine is not configured
'vault.cluster.secrets.backend.kubernetes.configure',
],
},
{
key: 'kvv1',
type: 'kv',
version: 1,
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'kvv2',
type: 'kv',
version: 2,
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: true,
showConfigure: true,
showDelete: true,
expectedLandingConfigureRoutesOverride: ['vault.cluster.secrets.backend.kv.configuration'],
},
{
key: 'transform',
type: 'transform',
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'transit',
type: 'transit',
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'kmip',
type: 'kmip',
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
expectedActionConfigureRoutesOverride: [
// if the engine is configured
'vault.cluster.secrets.backend.kmip.configuration',
// if the engine is not configured
'vault.cluster.secrets.backend.kmip.configure',
],
expectedLandingConfigureRoutesOverride: [
// if the engine is configured
'vault.cluster.secrets.backend.kmip.configuration',
// if the engine is not configured
'vault.cluster.secrets.backend.kmip.configure',
],
},
{
key: 'ldap',
type: 'ldap',
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
expectedActionConfigureRoutesOverride: [
// if the engine is configured
'vault.cluster.secrets.backend.ldap.configuration',
// if the engine is not configured
'vault.cluster.secrets.backend.ldap.configure',
],
expectedLandingConfigureRoutesOverride: [
// if the engine is configured
'vault.cluster.secrets.backend.ldap.configuration',
// if the engine is not configured
'vault.cluster.secrets.backend.ldap.configure',
],
},
{
key: 'pki',
type: 'pki',
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
expectedActionConfigureRoutesOverride: [
// if the engine is configured
'vault.cluster.secrets.backend.pki.configuration',
// if the engine is not configured
'vault.cluster.secrets.backend.pki.configuration.create',
],
expectedLandingConfigureRoutesOverride: [
// if the engine is configured
'vault.cluster.secrets.backend.pki.configuration',
// if the engine is not configured
'vault.cluster.secrets.backend.pki.configuration.create',
],
},
{
key: 'ssh',
type: 'ssh',
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'totp',
type: 'totp',
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'aws',
type: 'aws',
isEnginePathClickable: true,
showManageDropdown: true,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'consul',
type: 'consul',
isEnginePathClickable: false,
showManageDropdown: false,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'nomad',
type: 'nomad',
isEnginePathClickable: false,
showManageDropdown: false,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'rabbitmq',
type: 'rabbitmq',
isEnginePathClickable: false,
showManageDropdown: false,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
},
{
key: 'database',
type: 'database',
isEnginePathClickable: true,
showManageDropdown: false,
showGeneratePolicy: false,
showConfigure: true,
showDelete: true,
expectedLandingRouteOverride: 'vault.cluster.secrets.backend.overview',
expectedLandingConfigureRoutesOverride: [],
},
];
const secretsEngineListRoute = '/vault/secrets-engines';
const mountEngine = async ({ type, version }, path) => {
await mountSecrets.visit();
await click(GENERAL.cardContainer(type));
await fillIn(GENERAL.inputByAttr('path'), path);
if (type === 'kv' && version === 1) {
await click(GENERAL.button('Method Options'));
await mountSecrets.version(1);
}
await click(GENERAL.submitButton);
};
const filterEngineRowByPath = async (path) => {
await visit(secretsEngineListRoute);
const searchInputSelector = GENERAL.inputSearch('secret-engine-path');
if (findAll(searchInputSelector).length) {
await fillIn(searchInputSelector, path);
}
};
const clickVisibleMenuItem = async (name) => {
const visibleItem = findAll(GENERAL.menuItem(name)).find((el) => el.offsetParent !== null);
if (!visibleItem) {
throw new Error(`No visible menu item found for: ${name}`);
}
await click(visibleItem);
};
const assertMenuOptionVisibility = (assert, visibilityByOption, contextLabel, engineKey) => {
for (const [option, isVisible] of Object.entries(visibilityByOption)) {
if (isVisible) {
assert.dom(GENERAL.menuItem(option)).exists(`${contextLabel} shows ${option} for ${engineKey}`);
} else {
assert
.dom(GENERAL.menuItem(option))
.doesNotExist(`${contextLabel} does not show ${option} for ${engineKey}`);
}
}
};
const clickVisibleConfirmButton = async () => {
const visibleConfirmButton = findAll(GENERAL.confirmButton).find((el) => el.offsetParent !== null);
if (!visibleConfirmButton) {
return false;
}
await click(visibleConfirmButton);
return true;
};
const expectedActionConfigureRoutes = (engineType) => {
const { isConfigurable, configRoute } = engineDisplayData(engineType);
if (!isConfigurable) {
return ['vault.cluster.secrets.backend.configuration.general-settings'];
}
if (configRoute) {
return [`vault.cluster.secrets.backend.${configRoute}`];
}
return [
// if the engine is configured
'vault.cluster.secrets.backend.configuration.plugin-settings',
// if the engine is not configured
'vault.cluster.secrets.backend.configuration.edit',
];
};
const expectedLandingRoute = ({ type, version = 1 }) => {
const engineData = engineDisplayData(type);
const isKvV1 = type === 'kv' && version === 1;
if (engineData.isOnlyMountable) {
return 'vault.cluster.secrets.backend.configuration.general-settings';
}
if (engineData.engineRoute && !isKvV1) {
return `vault.cluster.secrets.backend.${engineData.engineRoute}`;
}
return 'vault.cluster.secrets.backend.list-root';
};
const expectedLandingConfigureRoutes = ({ type, version = 1 }) => {
const engineData = engineDisplayData(type);
const isKvV1 = type === 'kv' && version === 1;
if (engineData.engineRoute && !isKvV1) {
if (engineData.configRoute) {
return [`vault.cluster.secrets.backend.${engineData.configRoute}`];
}
}
if (engineData.isConfigurable) {
return [
// if the engine is configured
'vault.cluster.secrets.backend.configuration.plugin-settings',
// if the engine is not configured
'vault.cluster.secrets.backend.configuration.edit',
];
}
return ['vault.cluster.secrets.backend.configuration.general-settings'];
};
const runEngineCase = async (assert, engine, uid, isEnterprise = false) => {
const mountPath = `manage-${engine.key}-${uid}`;
const actionConfigureRoutes =
engine.expectedActionConfigureRoutesOverride || expectedActionConfigureRoutes(engine.type);
const expectedManage = {
showManageDropdown: engine.showManageDropdown ?? false,
showGeneratePolicy: (engine.showGeneratePolicy ?? false) && isEnterprise,
showConfigure: engine.showConfigure ?? true,
showDelete: engine.showDelete ?? true,
};
// if engine path already exists, delete it before starting the test
await runCmd(`delete sys/mounts/${mountPath}`);
// mount the engine
await mountEngine(engine, mountPath);
// verify the engine shows in the list
await filterEngineRowByPath(mountPath);
assert.dom(GENERAL.tableRow()).exists(`row renders for ${engine.key}`);
assert.dom(GENERAL.menuTrigger).exists(`Action menu is shown for ${engine.key}`);
await click(GENERAL.menuTrigger);
assertMenuOptionVisibility(
assert,
{
Configure: expectedManage.showConfigure,
Delete: expectedManage.showDelete,
},
'Action menu',
engine.key
);
if (expectedManage.showConfigure) {
// click configure and verify route
await clickVisibleMenuItem('Configure');
await settled();
assert.true(
actionConfigureRoutes.includes(currentRouteName()),
`Action: Configure routes correctly for ${engine.key}`
);
await filterEngineRowByPath(mountPath);
}
if (expectedManage.showDelete) {
// click delete and verify the engine is removed from the list
await filterEngineRowByPath(mountPath);
await click(GENERAL.menuTrigger);
await clickVisibleMenuItem('Delete');
const didConfirmActionDelete = await clickVisibleConfirmButton();
assert.true(didConfirmActionDelete, `Action: Delete shows confirm button for ${engine.key}`);
await settled();
await filterEngineRowByPath(mountPath);
assert.dom(GENERAL.tableRow()).doesNotExist(`Action: Delete removes ${engine.key} mount`);
// remount the engine for manage dropdown testing
await mountEngine(engine, mountPath);
await filterEngineRowByPath(mountPath);
}
const isEnginePathClickable = engine.isEnginePathClickable ?? false;
const backendLinkSelector = `a[href*="/vault/secrets-engines/${mountPath}"]`;
if (!isEnginePathClickable) {
// if the engine path is not expected to be clickable, verify it's not a link and skip the rest of the test
assert.dom(backendLinkSelector).doesNotExist(`EnginePath is not a clickable link for ${engine.key}`);
await runCmd(`delete sys/mounts/${mountPath}`);
return;
}
assert.dom(backendLinkSelector).exists(`EnginePath is a clickable link for ${engine.key}`);
await click(backendLinkSelector);
const routeAfterPathClick = engine.expectedLandingRouteOverride || expectedLandingRoute(engine);
assert.strictEqual(
currentRouteName(),
routeAfterPathClick,
`Engine path click redirects to ${routeAfterPathClick} for ${engine.key}`
);
const shouldShowManageDropdown = expectedManage.showManageDropdown;
if (!shouldShowManageDropdown) {
// if manage dropdown is not expected to show on the landing page, verify it's not shown and skip the rest of the test
assert
.dom(GENERAL.dropdownToggle('Manage'))
.doesNotExist(`Manage dropdown is not shown on landing page for ${engine.key}`);
await runCmd(`delete sys/mounts/${mountPath}`);
return;
}
assert
.dom(GENERAL.dropdownToggle('Manage'))
.exists(`Manage dropdown shows on landing page for ${engine.key}`);
await click(GENERAL.dropdownToggle('Manage'));
assertMenuOptionVisibility(
assert,
{
'Generate policy': expectedManage.showGeneratePolicy,
Configure: expectedManage.showConfigure,
Delete: expectedManage.showDelete,
},
'Manage dropdown',
engine.key
);
if (expectedManage.showConfigure) {
// click configure and verify route
await clickVisibleMenuItem('Configure');
await settled();
const allowedConfigureRoutes =
engine.expectedLandingConfigureRoutesOverride || expectedLandingConfigureRoutes(engine);
assert.true(
allowedConfigureRoutes.includes(currentRouteName()),
`Manage Configure routes correctly for ${engine.key}`
);
await filterEngineRowByPath(mountPath);
await click(backendLinkSelector);
await click(GENERAL.dropdownToggle('Manage'));
}
if (expectedManage.showDelete) {
// click delete and verify the engine is removed from the list
await clickVisibleMenuItem('Delete');
const didConfirmManageDelete = await clickVisibleConfirmButton();
if (!didConfirmManageDelete) {
assert.true(
engine.type === 'kubernetes',
`Manage Delete missing confirm is only expected for kubernetes; got ${engine.key}`
);
await runCmd(`delete sys/mounts/${mountPath}`);
await filterEngineRowByPath(mountPath);
assert.dom(GENERAL.tableRow()).doesNotExist(`Manage Delete removes ${engine.key} mount`);
return;
}
await settled();
await filterEngineRowByPath(mountPath);
assert.dom(GENERAL.tableRow()).doesNotExist(`Manage Delete removes ${engine.key} mount`);
return; // if the delete action is confirmed, the engine should be removed and we can end the test here without needing to clean up again
}
};
module('Acceptance | secrets-engines/manage-dropdown routing', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(function () {
this.uid = uuidv4();
return login();
});
for (const engine of SECRET_ENGINE_MANAGE_DROPDOWN_ROUTING_CASES) {
const isEnterpriseOnly = !!engineDisplayData(engine.type).requiresEnterprise;
const engineLabel = isEnterpriseOnly ? `${engine.key} (enterprise only)` : engine.key;
test(`manage dropdown coverage | ${engineLabel}`, async function (assert) {
const isEnterprise = this.owner.lookup('service:version').isEnterprise;
await runEngineCase(assert, engine, this.uid, isEnterprise);
});
}
});

View file

@ -4,35 +4,34 @@
*/
import {
click,
currentRouteName,
currentURL,
settled,
click,
findAll,
fillIn,
visit,
findAll,
settled,
typeIn,
visit,
waitFor,
} from '@ember/test-helpers';
import { clickTrigger } from 'ember-power-select/test-support/helpers';
import { module, test, skip } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { module, skip, test } from 'qunit';
import { v4 as uuidv4 } from 'uuid';
import { runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
import { create } from 'ember-cli-page-object';
import page from 'vault/tests/pages/settings/mount-secret-backend';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
import { SELECTORS as OIDC } from 'vault/tests/helpers/oidc-config';
import { adminOidcCreateRead, adminOidcCreate } from 'vault/tests/helpers/secret-engine/policy-generator';
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
import engineDisplayData from 'vault/helpers/engines-display-data';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { SELECTORS as OIDC } from 'vault/tests/helpers/oidc-config';
import { adminOidcCreate, adminOidcCreateRead } from 'vault/tests/helpers/secret-engine/policy-generator';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
import { default as mountSecrets, default as page } from 'vault/tests/pages/settings/mount-secret-backend';
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
const consoleComponent = create(consoleClass);
@ -152,6 +151,7 @@ module('Acceptance | secrets-engines/enable', function (hooks) {
await page.secretList();
await settled();
await fillIn(GENERAL.inputSearch('secret-engine-path'), path);
assert
.dom(GENERAL.tableData(`${path}/`, 'path'))
.exists({ count: 1 }, 'renders only one instance of the engine');

View file

@ -32,7 +32,7 @@ export default (test, type) => {
await fillIn(GENERAL.inputSearch('secret-engine-path'), backend);
await click(GENERAL.menuTrigger);
await click(GENERAL.menuItem('View configuration'));
await click(GENERAL.menuItem('Configure'));
assert.strictEqual(
currentRouteName(),
`${BASE_ROUTE}.${this.expectedConfigEditRoute}`,
@ -117,7 +117,7 @@ export default (test, type) => {
await visit(`/vault/secrets-engines`);
await fillIn(GENERAL.inputSearch('secret-engine-path'), backend);
await click(GENERAL.menuTrigger);
await click(GENERAL.menuItem('View configuration'));
await click(GENERAL.menuItem('Configure'));
// For configurable engines, clicking "View configuration" will direct to its plugin settings route
await waitUntil(() => currentRouteName() === `${BASE_ROUTE}.${configRoute}`);

View file

@ -3,16 +3,16 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { currentURL, visit, click, fillIn, currentRouteName, waitUntil } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { click, currentRouteName, currentURL, fillIn, visit, waitUntil } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { module, test } from 'qunit';
import { v4 as uuidv4 } from 'uuid';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
module('Acceptance | secret engine mount settings', function (hooks) {
setupApplicationTest(hooks);
@ -63,7 +63,7 @@ module('Acceptance | secret engine mount settings', function (hooks) {
await visit('/vault/secrets-engines');
await fillIn(GENERAL.inputSearch('secret-engine-path'), path);
await click(GENERAL.menuTrigger);
await click(GENERAL.menuItem('View configuration'));
await click(GENERAL.menuItem('Configure'));
// since ldap hasn't been configured yet, it should redirect to configure page
assert.strictEqual(
currentURL(),
@ -93,7 +93,7 @@ module('Acceptance | secret engine mount settings', function (hooks) {
await visit('/vault/secrets-engines');
await fillIn(GENERAL.inputSearch('secret-engine-path'), path);
await click(GENERAL.menuTrigger);
await click(GENERAL.menuItem('View configuration'));
await click(GENERAL.menuItem('Configure'));
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.configuration.general-settings');
assert.strictEqual(
currentURL(),
@ -125,7 +125,7 @@ module('Acceptance | secret engine mount settings', function (hooks) {
await visit('/vault/secrets-engines');
await fillIn(GENERAL.inputSearch('secret-engine-path'), path);
await click(GENERAL.menuTrigger);
await click(GENERAL.menuItem('View configuration'));
await click(GENERAL.menuItem('Configure'));
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.configuration.edit');
assert.strictEqual(
currentURL(),

View file

@ -3,15 +3,16 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { setupEngine } from 'ember-engines/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { click, fillIn, render } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { setupEngine } from 'ember-engines/test-support';
import { setupRenderingTest } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import { module, test } from 'qunit';
import sinon from 'sinon';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import SecretsEngineResource from 'vault/resources/secrets/engine';
import { getErrorResponse } from 'vault/tests/helpers/api/error-response';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
module('Integration | Component | kmip | Page::Scopes', function (hooks) {
setupRenderingTest(hooks);
@ -20,6 +21,7 @@ module('Integration | Component | kmip | Page::Scopes', function (hooks) {
hooks.beforeEach(function () {
this.backend = 'kmip-test';
this.secretsEngine = new SecretsEngineResource({ path: this.backend, type: 'kmip' });
this.owner.lookup('service:secret-mount-path').update(this.backend);
const { secrets } = this.owner.lookup('service:api');
@ -51,7 +53,7 @@ module('Integration | Component | kmip | Page::Scopes', function (hooks) {
this.renderComponent = () =>
render(
hbs`<Page::Scopes @scopes={{this.scopes}} @capabilities={{this.capabilities}} @filterValue={{this.filterValue}} />`,
hbs`<Page::Scopes @secretsEngine={{this.secretsEngine}} @scopes={{this.scopes}} @capabilities={{this.capabilities}} @filterValue={{this.filterValue}} />`,
{ owner: this.engine }
);
});

View file

@ -0,0 +1,161 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { click, render } from '@ember/test-helpers';
import { setupRenderingTest } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import { module, test } from 'qunit';
import sinon from 'sinon';
import SecretsEngineResource from 'vault/resources/secrets/engine';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
const DEFAULT_MOUNT_DATA = {
accessor: 'test_accessor',
config: {},
description: '',
external_entropy_access: false,
local: false,
plugin_version: '',
running_plugin_version: '',
running_sha256: '',
seal_wrap: false,
uuid: 'test-uuid',
};
const TEST_CASES = [
{
label: 'alicloud',
type: 'alicloud',
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
{
label: 'azure',
type: 'azure',
expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings',
},
{
label: 'gcp',
type: 'gcp',
expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings',
},
{
label: 'gcpkms',
type: 'gcpkms',
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
{
label: 'keymgmt',
type: 'keymgmt',
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
{
label: 'kubernetes',
type: 'kubernetes',
expectedRoute: 'vault.cluster.secrets.backend.kubernetes.configuration',
},
{
label: 'kvv1',
type: 'kv',
version: 1,
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
{
label: 'kvv2',
type: 'kv',
version: 2,
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
{
label: 'transform',
type: 'transform',
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
{
label: 'transit',
type: 'transit',
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
{ label: 'kmip', type: 'kmip', expectedRoute: 'vault.cluster.secrets.backend.kmip.configuration' },
{ label: 'ldap', type: 'ldap', expectedRoute: 'vault.cluster.secrets.backend.ldap.configuration' },
{ label: 'pki', type: 'pki', expectedRoute: 'vault.cluster.secrets.backend.pki.configuration' },
{
label: 'ssh',
type: 'ssh',
expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings',
},
{
label: 'totp',
type: 'totp',
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
{
label: 'aws',
type: 'aws',
expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings',
},
{
label: 'consul',
type: 'consul',
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
{
label: 'nomad',
type: 'nomad',
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
{
label: 'rabbitmq',
type: 'rabbitmq',
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
{
label: 'database',
type: 'database',
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
},
];
module('Integration | Component | manage-dropdown | Configure link', function (hooks) {
setupRenderingTest(hooks);
const makeModel = ({ type, version, id }) => {
const options = version ? { version } : undefined;
return new SecretsEngineResource({
...DEFAULT_MOUNT_DATA,
path: `${id}/`,
type,
options,
});
};
TEST_CASES.forEach(({ label, type, version, expectedRoute }) => {
test(`Configure link routes correctly for ${label}`, async function (assert) {
const routing = this.owner.lookup('service:-routing');
const transitionSpy = sinon.stub(routing, 'transitionTo');
const id = `${label}-integration-test`;
this.model = makeModel({ type, version, id });
await render(
hbs`<ManageDropdown @model={{this.model}} @variant="icon" @configRoute={{this.model.backendConfigurationLink}} />`
);
await click(GENERAL.menuTrigger);
await click(GENERAL.menuItem('Configure'));
assert.true(transitionSpy.called, `Configure action for ${label} triggers a route transition`);
assert.strictEqual(
transitionSpy.firstCall.args[0],
expectedRoute,
`Configure action for ${label} transitions to ${expectedRoute}`
);
assert.true(
JSON.stringify(transitionSpy.firstCall.args).includes(id),
`Configure action for ${label} includes model id ${id}`
);
transitionSpy.restore();
});
});
});

View file

@ -0,0 +1,88 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { setupTest } from 'ember-qunit';
import { module, test } from 'qunit';
import SecretsEngineResource from 'vault/resources/secrets/engine';
const makeResource = ({ type, version }) => {
const options = version ? { version } : undefined;
return new SecretsEngineResource({
accessor: 'test_accessor',
config: {},
description: '',
external_entropy_access: false,
local: false,
options,
path: `${type}-test/`,
plugin_version: '',
running_plugin_version: '',
running_sha256: '',
seal_wrap: false,
type,
uuid: 'test-uuid',
});
};
module('Unit | Component | manage-dropdown', function (hooks) {
setupTest(hooks);
test('backendConfigurationLink: addon engines with configRoute always use their config route', function (assert) {
const kubernetes = makeResource({ type: 'kubernetes' });
assert.strictEqual(
kubernetes.backendConfigurationLink,
'vault.cluster.secrets.backend.kubernetes.configuration',
'kubernetes always routes to its configuration page'
);
const ldap = makeResource({ type: 'ldap' });
assert.strictEqual(
ldap.backendConfigurationLink,
'vault.cluster.secrets.backend.ldap.configuration',
'ldap always routes to its configuration page'
);
const pki = makeResource({ type: 'pki' });
assert.strictEqual(
pki.backendConfigurationLink,
'vault.cluster.secrets.backend.pki.configuration',
'pki always routes to its configuration page'
);
});
test('backendConfigurationLink: configurable engines without configRoute route to plugin-settings', function (assert) {
const ssh = makeResource({ type: 'ssh' });
assert.strictEqual(
ssh.backendConfigurationLink,
'vault.cluster.secrets.backend.configuration.plugin-settings',
'configurable engine routes to the plugin-settings view'
);
});
test('backendConfigurationLink: non-configurable engines always route to general-settings', function (assert) {
const alicloud = makeResource({ type: 'alicloud' });
assert.strictEqual(
alicloud.backendConfigurationLink,
'vault.cluster.secrets.backend.configuration.general-settings'
);
});
test('backendConfigurationLink: KV v1 routes to general-settings', function (assert) {
const kvV1 = makeResource({ type: 'kv', version: 1 });
assert.strictEqual(
kvV1.backendConfigurationLink,
'vault.cluster.secrets.backend.configuration.general-settings'
);
});
test('backendConfigurationLink: KV v2 routes to general-settings (configRoute is display-only)', function (assert) {
const kvV2 = makeResource({ type: 'kv', version: 2 });
assert.strictEqual(
kvV2.backendConfigurationLink,
'vault.cluster.secrets.backend.configuration.general-settings',
"kv's configRoute is skipped because it's for display only"
);
});
});

View file

@ -3,8 +3,8 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import engineDisplayData, { unknownEngineMetadata } from 'core/helpers/engines-display-data';
import { module, test } from 'qunit';
import engineDisplayData, { unknownEngineMetadata } from 'vault/helpers/engines-display-data';
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
module('Unit | Helper | engines-display-data', function () {