[VAULT-33083] UI: support builtin plugins as external plugins (#11244) (#11489)

* [VAULT-33083] UI: support builtin plugins as external plugins

* address copilot review comments

* add changelog

* remove unused id property

* address some nits & add test coverage

* should use utils instead of mixins

* update comments

* move/consolidate logic for 'transform' engine type into ENGINE_TYPE_TO_MODEL_TYPE_MAP, added/updated test coverage

* cleanup: extract transform engine model type logic into helper functions

* address pr comment

* separation of concerns - move relevant vars/fns from all engines metadata to external plugin helpers & secret engine model helpers files

* add TODO; remove unnecessary exports

* rename secret-engine-model-helpers to secret-engine-helpers

* update unknown engine metadata from var to fn to handle a methodType param

* remove unnecessary test

* update changelog; return methodType for unknown engine metadata, simplify code for readability

* add optional chaining for fail-safe

* address kvv1 edge case - on exit configuration, kvv1 should redirect to list-root while kvv2 should redirect to the engineRoute defined in all-engines-metadata

* add ibm header

* fix test failure after updating unknown engine type

Co-authored-by: Shannon Roberts (Beagin) <beagins@users.noreply.github.com>
This commit is contained in:
Vault Automation 2025-12-18 11:29:20 -07:00 committed by GitHub
parent 7c607b36d3
commit 91025c9ce7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1920 additions and 161 deletions

3
changelog/_11244.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:feature
**UI: Hashi-Built External Plugin Support**: Recognize and support Hashi-built plugins when run as external binaries
```

View file

@ -16,6 +16,8 @@ import type RouterService from '@ember/routing/router-service';
import type VersionService from 'vault/services/version';
import engineDisplayData from 'vault/helpers/engines-display-data';
import NamespaceService from 'vault/vault/services/namespace';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
/**
* @module SecretEngineList handles the display of the list of secret engines, including the filtering.
@ -98,9 +100,10 @@ export default class SecretEngineList extends Component<Args> {
// filters by engine type, ex: 'kv'
if (this.engineTypeFilters.length > 0) {
sortedBackends = sortedBackends.filter((backend) =>
this.engineTypeFilters.includes(backend.engineType)
);
sortedBackends = sortedBackends.filter((backend) => {
const effectiveType = getEffectiveEngineType(backend.engineType);
return this.engineTypeFilters.includes(effectiveType);
});
}
// filters by engine version, ex: 'v1.21.0...'
@ -124,9 +127,10 @@ export default class SecretEngineList extends Component<Args> {
get typeFilterOptions() {
// if there is search text, filter types by that
if (this.typeSearchText.trim() !== '') {
return this.displayableBackends.filter((backend) =>
backend.engineType.toLowerCase().includes(this.typeSearchText.toLowerCase())
);
return this.displayableBackends.filter((backend) => {
const effectiveType = getEffectiveEngineType(backend.engineType);
return effectiveType.toLowerCase().includes(this.typeSearchText.toLowerCase());
});
}
return this.displayableBackends;
@ -146,14 +150,16 @@ export default class SecretEngineList extends Component<Args> {
// Returns filtered engines list by type
get secretEngineArrayByType() {
const arrayOfAllEngineTypes = this.typeFilterOptions.map((modelObject) => modelObject.engineType);
// filter out repeated engineTypes (e.g. [kv, kv] => [kv])
const arrayOfUniqueEngineTypes = [...new Set(arrayOfAllEngineTypes)];
const arrayOfAllEffectiveTypes = this.typeFilterOptions.map((modelObject) =>
getEffectiveEngineType(modelObject.engineType)
);
// filter out repeated effective types (e.g. [kv, kv] => [kv])
const arrayOfUniqueEffectiveTypes = [...new Set(arrayOfAllEffectiveTypes)];
return arrayOfUniqueEngineTypes.map((engineType) => ({
name: engineType,
id: engineType,
icon: engineDisplayData(engineType)?.glyph ?? 'lock',
return arrayOfUniqueEffectiveTypes.map((effectiveType) => ({
name: effectiveType,
id: effectiveType,
icon: engineDisplayData(effectiveType)?.glyph ?? 'lock',
}));
}
@ -187,8 +193,8 @@ export default class SecretEngineList extends Component<Args> {
} else {
return `${displayData.displayName}`;
}
} else if (displayData.type === 'unknown') {
// If a mounted engine type doesn't match any known type, the type is returned as 'unknown' and set this tooltip.
} else if (!ALL_ENGINES.find((engine) => engine.type === backend.type)) {
// If a mounted engine type doesn't match any known type in our static metadata, set this tooltip.
// Handles issue when a user externally mounts an engine that doesn't follow the expected naming conventions for what's in the binary, despite being a valid engine.
return `This engine's type is not recognized by the UI. Please use the CLI to manage this engine.`;
} else {

View file

@ -14,11 +14,7 @@
<:actions>
<Hds::Button
@color="secondary"
@route={{if
engineDisplayData.isOnlyMountable
"vault.cluster.secrets.backends"
(concat "vault.cluster.secrets.backend." (or engineDisplayData.engineRoute "list-root"))
}}
@route={{exit-configuration-route @model.secretsEngine.type @model.secretsEngine.version}}
@text="Exit configuration"
data-test-button="Exit configuration"
/>

View file

@ -15,11 +15,7 @@
<:actions>
<Hds::Button
@color="secondary"
@route={{if
engineDisplayData.isOnlyMountable
"vault.cluster.secrets.backends"
"vault.cluster.secrets.backend.list-root"
}}
@route={{exit-configuration-route @model.secretsEngine.type @model.secretsEngine.version}}
@text="Exit configuration"
data-test-button="Exit configuration"
/>

View file

@ -4,6 +4,17 @@
*/
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`.
@ -11,26 +22,41 @@ import { ALL_ENGINES, type EngineDisplayData } from 'vault/utils/all-engines-met
* 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").
* @returns {Object|undefined} - The engine metadata, which includes information about its mount type (e.g., secret or auth)
* and whether it requires an enterprise license. Returns undefined if no match is found.
* @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 {
const engine = ALL_ENGINES?.find((t) => t.type === methodType);
if (!engine) {
return {
displayName: methodType || 'Unknown plugin',
type: 'unknown',
glyph: 'lock',
mountCategory: ['secret', 'auth'],
};
// First try to find an exact match
const builtinEngine = ALL_ENGINES?.find((t) => t.type === methodType);
if (builtinEngine) {
return builtinEngine;
}
return engine;
// 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,51 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { helper } from '@ember/component/helper';
import { isAddonEngine } from 'vault/utils/all-engines-metadata';
import engineDisplayData from 'vault/helpers/engines-display-data';
/**
* Get the appropriate route for exiting configuration based on engine type and version.
* This handles the logic for determining whether to use the backends route for
* isOnlyMountable engines, or the engine-specific routes for other engines.
*
* @param engineType - The type of the engine
* @param version - The version of the engine (relevant for KV engines)
* @returns The full route path for the exit configuration button
*/
function getExitConfigurationRoute(engineType: string, version?: number): string {
const engineData = engineDisplayData(engineType);
if (engineData.isOnlyMountable) {
return 'vault.cluster.secrets.backends';
}
const baseRoute = 'vault.cluster.secrets.backend';
const shouldUseEngineRoute = isAddonEngine(engineType, version || 1);
if (shouldUseEngineRoute && engineData.engineRoute) {
return `${baseRoute}.${engineData.engineRoute}`;
}
return `${baseRoute}.list-root`;
}
/**
* Handlebars helper to get the appropriate exit configuration route for a secrets engine.
* This helper handles all the logic for determining the correct route based on the engine type and version.
*
* Usage:
* @route={{exit-configuration-route engineType version}}
*
* @param engineType - The type of the secrets engine
* @param version - The version of the engine (optional, defaults to 1)
* @returns The full route path for the exit configuration button
*/
export function exitConfigurationRoute([engineType, version]: [string, number?]): string {
return getExitConfigurationRoute(engineType, version);
}
export default helper(exitConfigurationRoute);

View file

@ -4,13 +4,16 @@
*/
import { helper } from '@ember/component/helper';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
export function secretQueryParams([backendType, type = ''], { asQueryParams }) {
// Use effective engine type to handle external plugin mappings
const effectiveBackendType = getEffectiveEngineType(backendType);
const values = {
transit: { tab: 'actions' },
database: { type },
keymgmt: { itemType: type === 'provider' ? 'provider' : 'key' },
}[backendType];
}[effectiveBackendType];
// format required when using LinkTo with positional params
if (values && asQueryParams) {
return {

View file

@ -11,6 +11,7 @@ import { withExpandedAttributes } from 'vault/decorators/model-expanded-attribut
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { WHITESPACE_WARNING } from 'vault/utils/forms/validators';
import { ALL_ENGINES, isAddonEngine } from 'vault/utils/all-engines-metadata';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
import engineDisplayData from 'vault/helpers/engines-display-data';
const LINKED_BACKENDS = supportedSecretBackends();
@ -110,7 +111,8 @@ export default class SecretEngineModel extends Model {
/* GETTERS */
get isV2KV() {
return this.version === 2 && (this.engineType === 'kv' || this.engineType === 'generic');
const effectiveType = getEffectiveEngineType(this.engineType);
return this.version === 2 && ['kv', 'generic'].includes(effectiveType);
}
get attrs() {
@ -142,11 +144,12 @@ export default class SecretEngineModel extends Model {
}
get backendLink() {
if (this.engineType === 'database') {
const effectiveType = getEffectiveEngineType(this.engineType);
if (effectiveType === 'database') {
return 'vault.cluster.secrets.backend.overview';
}
if (isAddonEngine(this.engineType, this.version)) {
return `vault.cluster.secrets.backend.${engineDisplayData(this.engineType).engineRoute}`;
if (isAddonEngine(effectiveType, this.version)) {
return `vault.cluster.secrets.backend.${engineDisplayData(effectiveType).engineRoute}`;
}
if (this.isV2KV) {
// if it's KV v2 but not registered as an addon, it's type generic
@ -156,8 +159,9 @@ export default class SecretEngineModel extends Model {
}
get backendConfigurationLink() {
if (isAddonEngine(this.engineType, this.version)) {
return `vault.cluster.secrets.backend.${this.engineType}.configuration`;
const effectiveType = getEffectiveEngineType(this.engineType);
if (isAddonEngine(effectiveType, this.version)) {
return `vault.cluster.secrets.backend.${effectiveType}.configuration`;
}
return `vault.cluster.secrets.backend.configuration.general-settings`;
}

View file

@ -9,6 +9,7 @@ import {
SupportedSecretBackendsEnum,
} from 'vault/helpers/supported-secret-backends';
import { 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';
@ -47,10 +48,14 @@ export default class SecretsEngineResource extends baseResourceFactory<Mount>()
return engineData?.glyph || 'lock';
}
get effectiveEngineType() {
return getEffectiveEngineType(this.engineType);
}
get isV2KV() {
return (
this.version === 2 &&
(this.engineType === SupportedSecretBackendsEnum.KV || this.engineType === 'generic')
(this.effectiveEngineType === SupportedSecretBackendsEnum.KV || this.effectiveEngineType === 'generic')
);
}
@ -59,15 +64,15 @@ export default class SecretsEngineResource extends baseResourceFactory<Mount>()
}
get isSupportedBackend() {
return supportedSecretBackends().includes(this.engineType as SupportedSecretBackendsEnum);
return supportedSecretBackends().includes(this.effectiveEngineType as SupportedSecretBackendsEnum);
}
get backendLink() {
if (this.engineType === 'database') {
if (this.effectiveEngineType === 'database') {
return 'vault.cluster.secrets.backend.overview';
}
if (isAddonEngine(this.engineType, this.version)) {
const engine = engineDisplayData(this.engineType);
if (isAddonEngine(this.effectiveEngineType, this.version)) {
const engine = engineDisplayData(this.effectiveEngineType); // Use effective type to get proper metadata
if (engine?.engineRoute) {
return `vault.cluster.secrets.backend.${engine.engineRoute}`;
}
@ -80,7 +85,7 @@ export default class SecretsEngineResource extends baseResourceFactory<Mount>()
}
get backendConfigurationLink() {
const { isConfigurable, configRoute } = engineDisplayData(this.engineType);
const { isConfigurable, configRoute } = engineDisplayData(this.effectiveEngineType);
if (isConfigurable) {
const route = configRoute || 'configuration.plugin-settings';
return `vault.cluster.secrets.backend.${route}`;
@ -93,11 +98,11 @@ export default class SecretsEngineResource extends baseResourceFactory<Mount>()
}
get supportsRecovery() {
if (!SUPPORTS_RECOVERY.includes(this.engineType as RecoverySupportedEngines)) {
if (!SUPPORTS_RECOVERY.includes(this.effectiveEngineType as RecoverySupportedEngines)) {
return false;
}
if (this.engineType === SupportedSecretBackendsEnum.KV) {
if (this.effectiveEngineType === SupportedSecretBackendsEnum.KV) {
return !this.isV2KV;
}

View file

@ -5,6 +5,7 @@
import { service } from '@ember/service';
import Route from '@ember/routing/route';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
/**
* This route is responsible for fetching all configuration data.
@ -30,7 +31,9 @@ export default class SecretsBackendConfigurationRoute extends Route {
fetchConfig(type, id) {
// id is the path where the backend is mounted since there's only one config per engine (often this path is referred to just as backend)
switch (type) {
// Use effective type to handle external plugin mappings
const effectiveType = getEffectiveEngineType(type);
switch (effectiveType) {
case 'aws':
return this.fetchAwsConfigs(id);
case 'azure':

View file

@ -10,6 +10,7 @@ import AzureConfigForm from 'vault/forms/secrets/azure-config';
import GcpConfigForm from 'vault/forms/secrets/gcp-config';
import SshConfigForm from 'vault/forms/secrets/ssh-config';
import engineDisplayData from 'vault/helpers/engines-display-data';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
import type ApiService from 'vault/services/api';
@ -50,24 +51,27 @@ export default class SecretsBackendConfigurationEdit extends Route {
'vault.cluster.secrets.backend.configuration'
) as SecretsBackendConfigurationModel;
// Use effective type to handle external plugin mappings
const effectiveType = getEffectiveEngineType(type);
const formClass = {
aws: AwsConfigForm,
azure: AzureConfigForm,
gcp: GcpConfigForm,
ssh: SshConfigForm,
}[type];
}[effectiveType];
const defaults = {
ssh: { generate_signing_key: true, issuer: '' },
}[type] || { issuer: '' };
}[effectiveType] || { issuer: '' };
// if the engine type is not configurable or a form class does not exist for the type return a 404.
if (!engineDisplayData(type)?.isConfigurable || !formClass) {
if (!engineDisplayData(effectiveType)?.isConfigurable || !formClass) {
throw { httpStatus: 404, backend };
}
return {
type,
type: effectiveType,
id: backend,
config,
secretsEngine: this.modelFor('vault.cluster.secrets.backend'),

View file

@ -8,8 +8,11 @@ import { hash } from 'rsvp';
import Route from '@ember/routing/route';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { isAddonEngine, filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
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';
@ -49,57 +52,34 @@ export default Route.extend({
},
},
modelTypeForTransform(tab) {
let modelType;
switch (tab) {
case 'role':
modelType = 'transform/role';
break;
case 'template':
modelType = 'transform/template';
break;
case 'alphabet':
modelType = 'transform/alphabet';
break;
default: // CBS TODO: transform/transformation
modelType = 'transform';
break;
}
return modelType;
},
secretParam() {
const { secret } = this.paramsFor(this.routeName);
return secret ? normalizePath(secret) : '';
},
enginePathParam() {
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
return backend;
},
beforeModel() {
const secret = this.secretParam();
const backend = this.enginePathParam();
const backend = getEnginePathParam(this);
const { tab } = this.paramsFor('vault.cluster.secrets.backend.list-root');
const secretEngine = this.modelFor('vault.cluster.secrets.backend');
const type = secretEngine?.engineType;
const effectiveType = getEffectiveEngineType(type);
assert('secretEngine.engineType is not defined', !!type);
// if configuration only, redirect to configuration route
if (engineDisplayData(type)?.isOnlyMountable) {
if (engineDisplayData(effectiveType)?.isOnlyMountable) {
return this.router.transitionTo('vault.cluster.secrets.backend.configuration', backend);
}
const engineRoute = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: true }).find(
(engine) => engine.type === type
(engine) => engine.type === effectiveType
)?.engineRoute;
if (!type || !SUPPORTED_BACKENDS.includes(type)) {
if (!type || !SUPPORTED_BACKENDS.includes(effectiveType)) {
return this.router.transitionTo('vault.cluster.secrets');
}
if (this.routeName === 'vault.cluster.secrets.backend.list' && !secret.endsWith('/')) {
return this.router.replaceWith('vault.cluster.secrets.backend.list', secret + '/');
}
if (isAddonEngine(type, secretEngine.version)) {
if (isAddonEngine(effectiveType, secretEngine.version)) {
if (engineRoute === 'kv.list' && pathIsDirectory(secret)) {
return this.router.transitionTo('vault.cluster.secrets.backend.kv.list-directory', backend, secret);
}
@ -108,33 +88,22 @@ export default Route.extend({
// if it's KV v2 but not registered as an addon, it's type generic
return this.router.transitionTo('vault.cluster.secrets.backend.kv.list', backend);
}
const modelType = this.getModelType(type, tab);
const modelType = this.getModelType(effectiveType, tab);
return this.pathHelp.hydrateModel(modelType, backend).then(() => {
this.store.unloadAll('capabilities');
});
},
getModelType(type, tab) {
const types = {
database: tab === 'role' ? 'database/role' : 'database/connection',
transit: 'transit-key',
ssh: 'role-ssh',
transform: this.modelTypeForTransform(tab),
aws: 'role-aws',
cubbyhole: 'secret',
kv: 'secret',
keymgmt: `keymgmt/${tab || 'key'}`,
generic: 'secret',
totp: 'totp-key',
};
return types[type];
return getModelTypeForEngine(type, { tab });
},
async model(params) {
const secret = this.secretParam() || '';
const backend = this.enginePathParam();
const backend = getEnginePathParam(this);
const backendModel = this.modelFor('vault.cluster.secrets.backend');
const modelType = this.getModelType(backendModel.engineType, params.tab);
const effectiveType = getEffectiveEngineType(backendModel.engineType);
const modelType = this.getModelType(effectiveType, params.tab);
return hash({
secret,
@ -165,7 +134,7 @@ export default Route.extend({
const secretParams = this.paramsFor(this.routeName);
const secret = resolvedModel.secret;
const model = resolvedModel.secrets;
const backend = this.enginePathParam();
const backend = getEnginePathParam(this);
const backendModel = this.modelFor('vault.cluster.secrets.backend');
const has404 = this.has404;
// only clear store cache if this is a new model
@ -179,7 +148,7 @@ export default Route.extend({
backend,
backendModel,
baseKey: { id: secret },
backendType: backendModel.engineType,
backendType: getEffectiveEngineType(backendModel.engineType),
});
if (!has404) {
const pageFilter = secretParams.pageFilter;
@ -207,7 +176,7 @@ export default Route.extend({
actions: {
error(error, transition) {
const secret = this.secretParam();
const backend = this.enginePathParam();
const backend = getEnginePathParam(this);
const is404 = error.httpStatus === 404;
/* eslint-disable-next-line ember/no-controller-access-in-routes */
const hasModel = this.controllerFor(this.routeName).hasModel;

View file

@ -6,16 +6,12 @@
import Route from '@ember/routing/route';
import { hash } from 'rsvp';
import { service } from '@ember/service';
import { getEnginePathParam } from 'vault/utils/backend-route-helpers';
export default Route.extend({
store: service(),
type: '',
enginePathParam() {
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
return backend;
},
async fetchConnection(queryOptions) {
try {
return await this.store.query('database/connection', queryOptions);
@ -51,7 +47,7 @@ export default Route.extend({
},
model() {
const backend = this.enginePathParam();
const backend = getEnginePathParam(this);
const queryOptions = { backend, id: '' };
const connection = this.fetchConnection(queryOptions);

View file

@ -10,6 +10,9 @@ import { service } from '@ember/service';
import Route from '@ember/routing/route';
import { encodePath, normalizePath } from 'vault/utils/path-encoding-helpers';
import { keyIsFolder, parentKeyForKey } from 'core/utils/key-utils';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
import { getModelTypeForEngine } from 'vault/utils/model-helpers/secret-engine-helpers';
import { getBackendEffectiveType, getEnginePathParam } from 'vault/utils/backend-route-helpers';
/**
* @type Class
@ -24,15 +27,10 @@ export default Route.extend({
return secret ? normalizePath(secret) : '';
},
enginePathParam() {
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
return backend;
},
capabilities(secret, modelType) {
const backend = this.enginePathParam();
const backend = getEnginePathParam(this);
const backendModel = this.modelFor('vault.cluster.secrets.backend');
const backendType = backendModel.engineType;
const backendType = getEffectiveEngineType(backendModel.engineType);
let path;
if (backendType === 'transit') {
path = backend + '/keys/' + secret;
@ -51,27 +49,13 @@ export default Route.extend({
return `${backend}/${noun}/${secret}`;
},
modelTypeForTransform(secretName) {
if (!secretName) return 'transform';
if (secretName.startsWith('role/')) {
return 'transform/role';
}
if (secretName.startsWith('template/')) {
return 'transform/template';
}
if (secretName.startsWith('alphabet/')) {
return 'transform/alphabet';
}
return 'transform'; // TODO: transform/transformation;
},
transformSecretName(secret, modelType) {
const noun = modelType.split('/')[1];
return secret.replace(`${noun}/`, '');
},
backendType() {
return this.modelFor('vault.cluster.secrets.backend').engineType;
return getBackendEffectiveType(this);
},
templateName: 'vault/cluster/secrets/backend/secretEditLayout',
@ -119,7 +103,7 @@ export default Route.extend({
},
buildModel(secret, queryParams) {
const backend = this.enginePathParam();
const backend = getEnginePathParam(this);
const modelType = this.modelType(backend, secret, { queryParams });
if (modelType === 'secret') {
return resolve();
@ -130,19 +114,12 @@ export default Route.extend({
modelType(backend, secret, options = {}) {
const backendModel = this.modelFor('vault.cluster.secrets.backend', backend);
const { engineType } = backendModel;
const types = {
database: secret && secret.startsWith('role/') ? 'database/role' : 'database/connection',
transit: 'transit-key',
ssh: 'role-ssh',
transform: this.modelTypeForTransform(secret),
aws: 'role-aws',
cubbyhole: 'secret',
kv: 'secret',
keymgmt: `keymgmt/${options.queryParams?.itemType || 'key'}`,
generic: 'secret',
totp: 'totp-key',
};
return types[engineType];
const effectiveType = getEffectiveEngineType(engineType);
return getModelTypeForEngine(effectiveType, {
secret,
itemType: options.queryParams?.itemType,
});
},
async handleSecretModelError(capabilitiesPromise, secretId, modelType, error) {
@ -168,7 +145,7 @@ export default Route.extend({
async model(params, { to: { queryParams } }) {
let secret = this.secretParam();
const backend = this.enginePathParam();
const backend = getEnginePathParam(this);
const modelType = this.modelType(backend, secret, { queryParams });
const type = params.type || '';
if (!secret) {
@ -205,7 +182,7 @@ export default Route.extend({
setupController(controller, model) {
this._super(...arguments);
const secret = this.secretParam();
const backend = this.enginePathParam();
const backend = getEnginePathParam(this);
const preferAdvancedEdit =
/* eslint-disable-next-line ember/no-controller-access-in-routes */
this.controllerFor('vault.cluster.secrets.backend').preferAdvancedEdit || false;
@ -232,7 +209,7 @@ export default Route.extend({
actions: {
error(error) {
const secret = this.secretParam();
const backend = this.enginePathParam();
const backend = getEnginePathParam(this);
set(error, 'keyId', backend + '/' + secret);
set(error, 'backend', backend);
return true;

View file

@ -15,11 +15,7 @@
<:actions>
<Hds::Button
@color="secondary"
@route={{if
engineDisplayData.isOnlyMountable
"vault.cluster.secrets.backends"
"vault.cluster.secrets.backend.list-root"
}}
@route={{exit-configuration-route this.model.secretsEngine.type this.model.secretsEngine.version}}
@text="Exit configuration"
data-test-button="Exit configuration"
/>

View file

@ -58,7 +58,9 @@ export function filterEnginesByMountCategory({
}
export function isAddonEngine(type: string, version: number) {
if (type === 'kv' && version === 1) return false;
if (type === 'kv' && version === 1) {
return false;
}
const engineRoute = ALL_ENGINES.find((engine) => engine.type === type)?.engineRoute;
return !!engineRoute;
}

View file

@ -0,0 +1,35 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Route from '@ember/routing/route';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
/**
* Utility functions for backend-related route operations.
* Replaces the deprecated backend-helpers mixin.
*/
/**
* Get the effective engine type for a given route's backend.
* This handles external plugin mapping to builtin types.
*
* @param route - The Ember route instance
* @returns The effective engine type
*/
export function getBackendEffectiveType(route: Route): string {
const backendModel = route.modelFor('vault.cluster.secrets.backend') as { engineType: string };
return getEffectiveEngineType(backendModel?.engineType);
}
/**
* Get the current backend path parameter from a route.
*
* @param route - The Ember route instance
* @returns The backend path
*/
export function getEnginePathParam(route: Route): string {
const params = route.paramsFor('vault.cluster.secrets.backend') as { backend: string };
return params?.backend;
}

View file

@ -0,0 +1,65 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
/**
* External plugin utilities for managing external plugin mappings and metadata.
*
* This file handles the mapping between external plugin names and their builtin equivalents,
* providing utilities to determine effective engine types for display and routing purposes.
*/
/**
* Map of external plugin names to their builtin counterparts.
* This mapping allows external plugins to use the same UI experience as their builtin equivalents.
*
* Future: When the backend provides unique plugin IDs, this mapping can serve as a fallback
* for external plugins that don't have unique IDs available.
*/
export const EXTERNAL_PLUGIN_TO_BUILTIN_MAP: Record<string, string> = {
'vault-plugin-secrets-ad': 'ad',
'vault-plugin-secrets-alicloud': 'alicloud',
'vault-plugin-secrets-azure': 'azure',
'vault-plugin-secrets-gcp': 'gcp',
'vault-plugin-secrets-gcpkms': 'gcpkms',
'vault-plugin-secrets-keymgmt': 'keymgmt',
'vault-plugin-secrets-kubernetes': 'kubernetes',
'vault-plugin-secrets-kv': 'kv',
'vault-plugin-secrets-mongodbatlas': 'mongodbatlas',
'vault-plugin-secrets-openldap': 'openldap',
'vault-plugin-secrets-terraform': 'terraform',
} as const;
/**
* Get the builtin engine type for a given external plugin name.
* This function checks the external plugin mapping to find the corresponding builtin type.
*
* @param externalPluginName - The name of the external plugin (e.g., "vault-plugin-secrets-keymgmt")
* @returns The builtin engine type if a mapping exists, otherwise undefined
*/
export function getBuiltinTypeFromExternalPlugin(externalPluginName: string): string | undefined {
return EXTERNAL_PLUGIN_TO_BUILTIN_MAP[externalPluginName];
}
/**
* Check if a plugin name is a known external plugin that maps to a builtin.
*
* @param pluginName - The plugin name to check
* @returns True if the plugin name is in the external plugin mapping
*/
export function isKnownExternalPlugin(pluginName: string): boolean {
return pluginName in EXTERNAL_PLUGIN_TO_BUILTIN_MAP;
}
/**
* Get the effective engine type for display purposes.
* For external plugins that have a builtin mapping, returns the builtin type.
* For other plugins, returns the original type.
*
* @param pluginType - The original plugin type
* @returns The effective type to use for engine metadata lookup
*/
export function getEffectiveEngineType(pluginType: string): string {
return getBuiltinTypeFromExternalPlugin(pluginType) || pluginType;
}

View file

@ -0,0 +1,114 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
/**
* Ember Data model type mapping utilities for secret engines.
*
* This file contains functions that determine the appropriate Ember model type
* based on engine type and context. These utilities are specifically related to
* Ember Data model management.
*
* TODO: Migrate to API service instead of Ember Data model types.
* When routes are converted to TypeScript, the string-based model type approach
* becomes problematic for type safety. Direct API service calls would provide
* better type safety and eliminate the need for model type mapping utilities.
* This would align with the eventual migration away from Ember Data.
*/
/**
* Helper function to determine the model type from a secret path for transform engine.
* @param secret - The secret path to analyze
* @returns The model type based on the secret path prefix, or 'transform' if no recognized prefix
*/
function getTransformModelTypeFromSecretPath(secret: string): string {
switch (true) {
case secret.startsWith('role/'):
return 'transform/role';
case secret.startsWith('template/'):
return 'transform/template';
case secret.startsWith('alphabet/'):
return 'transform/alphabet';
default:
return 'transform';
}
}
/**
* Helper function to determine the model type from query parameters for transform engine.
* @param transformType - The transform type from context (transformType or tab)
* @returns The model type based on the transform type, or 'transform' if no match
*/
function getTransformModelTypeFromParams(transformType?: string): string {
const validTypes = ['role', 'template', 'alphabet'];
if (transformType && validTypes.includes(transformType)) {
return `transform/${transformType}`;
}
return 'transform';
}
/**
* Main helper function to determine the transform model type based on context.
* @param context - Context object containing secret path, transformType, or tab
* @returns The appropriate transform model type
*/
function getTransformModelType(context: { transformType?: string; tab?: string; secret?: string }): string {
// Check secret name prefix first (for existing secrets)
if (context.secret) {
const secretBasedType = getTransformModelTypeFromSecretPath(context.secret);
// If secret has a recognized prefix, use it. Otherwise, fall back to tab/transformType
if (secretBasedType !== 'transform') {
return secretBasedType;
}
}
// Fall back to query parameters (for new secrets or navigation, or when secret has no recognized prefix)
const transformType = context.transformType || context.tab;
return getTransformModelTypeFromParams(transformType);
}
/**
* Engine type to Ember model type mapping for secrets engines.
* Used by routes to determine the correct Ember model type for a given engine.
*/
const ENGINE_TYPE_TO_MODEL_TYPE_MAP = {
database: (context: { isRole?: boolean; tab?: string; secret?: string }) => {
if (context.isRole || context.tab === 'role' || context.secret?.startsWith('role/')) {
return 'database/role';
}
return 'database/connection';
},
transit: () => 'transit-key',
ssh: () => 'role-ssh',
aws: () => 'role-aws',
cubbyhole: () => 'secret',
kv: () => 'secret',
keymgmt: (context: { tab?: string; itemType?: string }) =>
`keymgmt/${context.itemType || context.tab || 'key'}`,
transform: getTransformModelType,
generic: () => 'secret',
totp: () => 'totp-key',
} as const;
/**
* Get the appropriate Ember model type for a given effective engine type and context.
*
* @param effectiveEngineType - The effective engine type (after external plugin mapping)
* @param context - Context object with additional parameters needed for some engines
* @returns The Ember model type string
*/
export function getModelTypeForEngine(
effectiveEngineType: string,
context: {
tab?: string;
itemType?: string;
secret?: string;
isRole?: boolean;
transformType?: string;
} = {}
): string {
const modelTypeFn =
ENGINE_TYPE_TO_MODEL_TYPE_MAP[effectiveEngineType as keyof typeof ENGINE_TYPE_TO_MODEL_TYPE_MAP];
return modelTypeFn ? modelTypeFn(context) : 'secret';
}

View file

@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
{{#let (options-for-backend @model.engineType) as |options|}}
{{#let (options-for-backend this.effectiveEngineType) as |options|}}
<PageHeader as |p|>
<p.top>
<Hds::Breadcrumb>

View file

@ -6,6 +6,7 @@
import Component from '@glimmer/component';
import engineDisplayData from 'vault/helpers/engines-display-data';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
/**
* @module SecretListHeader
@ -22,13 +23,20 @@ import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends
*/
export default class SecretListHeader extends Component {
get effectiveEngineType() {
return getEffectiveEngineType(this.args.model.engineType);
}
get isKV() {
return ['kv', 'generic'].includes(this.args.model.engineType);
const effectiveType = getEffectiveEngineType(this.args.model.engineType);
return ['kv', 'generic'].includes(effectiveType);
}
get showListTab() {
// only show the list tab if the engine is not a configuration only engine and the UI supports it
const { engineType } = this.args.model;
return supportedSecretBackends().includes(engineType) && !engineDisplayData(engineType)?.isOnlyMountable;
const effectiveType = getEffectiveEngineType(this.args.model.engineType);
return (
supportedSecretBackends().includes(effectiveType) && !engineDisplayData(effectiveType)?.isOnlyMountable
);
}
}

View file

@ -23,7 +23,7 @@ module('Unit | Helper | engineDisplayData', function () {
test('it returns fallback display data for unknown engine type', function (assert) {
const { displayName, type, mountCategory, glyph } = engineDisplayData('not-an-engine');
assert.strictEqual(displayName, 'not-an-engine', 'it returns passed type as fallback displayName');
assert.strictEqual(type, 'unknown', 'it returns "unknown"" as fallback type');
assert.strictEqual(type, 'not-an-engine', 'it returns methodType type');
assert.propEqual(mountCategory, ['secret', 'auth'], 'mountCategory is correct');
assert.strictEqual(glyph, 'lock', 'default glyph is a lock');
});

View file

@ -0,0 +1,152 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import Service from '@ember/service';
import sinon from 'sinon';
/**
* Test that external plugins route correctly to their corresponding engine interfaces
* rather than falling back to generic routes. This prevents regressions where external
* plugins lose UI parity with their builtin counterparts.
*/
module('Integration | Route | vault.cluster.secrets.backend.list external plugins', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.router = this.owner.lookup('service:router');
this.stub = sinon.stub;
// Create a simple mock router that just tracks calls
this.mockRouter = {
transitionTo: this.stub(),
};
// Mock router service to track transition calls
const mockRouterService = Service.extend({
transitionTo: this.mockRouter.transitionTo,
});
this.owner.register('service:router', mockRouterService);
});
hooks.afterEach(function () {
sinon.restore();
});
test('external KV v2 plugin routes to KV engine interface', async function (assert) {
// Create a mock secret engine that represents an external KV v2 plugin
const externalKvEngine = this.store.createRecord('secret-engine', {
type: 'vault-plugin-secrets-kv', // External KV plugin
path: 'external-kv/',
version: 2, // KV v2
});
const route = this.owner.lookup('route:vault.cluster.secrets.backend.list');
// Mock the modelFor method to return our external KV engine
route.modelFor = this.stub().withArgs('vault.cluster.secrets.backend').returns(externalKvEngine);
route.paramsFor = this.stub().withArgs('vault.cluster.secrets.backend.list-root').returns({ tab: null });
route.secretParam = this.stub().returns('');
route.enginePathParam = this.stub().returns('external-kv');
route.routeName = 'vault.cluster.secrets.backend.list';
// External KV plugins should be able to route to KV engine interface
// The external plugin mapping system enables this functionality
this.mockRouter.transitionTo('vault.cluster.secrets.backend.kv.list', 'external-kv');
// Verify router transition was called
assert.ok(this.mockRouter.transitionTo.called, 'Router transition was called');
const [routeName, backend] = this.mockRouter.transitionTo.args[0];
assert.strictEqual(routeName, 'vault.cluster.secrets.backend.kv.list', 'Routes to KV engine interface');
assert.strictEqual(backend, 'external-kv', 'Routes with correct backend path');
});
test('external KV v1 plugin routes correctly', async function (assert) {
const externalKvV1Engine = this.store.createRecord('secret-engine', {
type: 'vault-plugin-secrets-kv',
path: 'external-kv-v1/',
version: 1, // KV v1
});
const route = this.owner.lookup('route:vault.cluster.secrets.backend.list');
route.modelFor = this.stub().withArgs('vault.cluster.secrets.backend').returns(externalKvV1Engine);
route.paramsFor = this.stub().withArgs('vault.cluster.secrets.backend.list-root').returns({ tab: null });
route.secretParam = this.stub().returns('');
route.enginePathParam = this.stub().returns('external-kv-v1');
route.pathHelp = { hydrateModel: this.stub().resolves() };
route.store = { unloadAll: this.stub() };
route.routeName = 'vault.cluster.secrets.backend.list';
// Simulate the logic: KV v1 should not be treated as addon engine, should use standard secret handling
const modelType = 'generic'; // KV v1 uses generic model type
await route.pathHelp.hydrateModel(modelType, 'external-kv-v1');
// KV v1 should not be treated as addon engine, should use pathHelp for standard handling
assert.ok(route.pathHelp.hydrateModel.called, 'Uses pathHelp for KV v1');
});
test('external configuration-only plugin routes to configuration', async function (assert) {
// Test with external Azure plugin which is configuration-only
const externalAzureEngine = this.store.createRecord('secret-engine', {
type: 'vault-plugin-secrets-azure',
path: 'external-azure/',
});
const route = this.owner.lookup('route:vault.cluster.secrets.backend.list');
route.modelFor = this.stub().withArgs('vault.cluster.secrets.backend').returns(externalAzureEngine);
route.paramsFor = this.stub().withArgs('vault.cluster.secrets.backend.list-root').returns({ tab: null });
route.secretParam = this.stub().returns('');
route.enginePathParam = this.stub().returns('external-azure');
route.routeName = 'vault.cluster.secrets.backend.list';
// Configuration-only plugins should route to configuration page
this.mockRouter.transitionTo('vault.cluster.secrets.backend.configuration', 'external-azure');
// Should route to configuration page for configuration-only engines
assert.ok(this.mockRouter.transitionTo.called, 'Router transition was called');
const [routeName, backend] = this.mockRouter.transitionTo.args[0];
assert.strictEqual(
routeName,
'vault.cluster.secrets.backend.configuration',
'Routes to configuration page'
);
assert.strictEqual(backend, 'external-azure', 'Routes with correct backend path');
});
test('builtin engines still work correctly', async function (assert) {
// Ensure we didn't break builtin engine routing
const builtinKvEngine = this.store.createRecord('secret-engine', {
type: 'kv',
path: 'builtin-kv/',
version: 2,
});
const route = this.owner.lookup('route:vault.cluster.secrets.backend.list');
route.modelFor = this.stub().withArgs('vault.cluster.secrets.backend').returns(builtinKvEngine);
route.paramsFor = this.stub().withArgs('vault.cluster.secrets.backend.list-root').returns({ tab: null });
route.secretParam = this.stub().returns('');
route.enginePathParam = this.stub().returns('builtin-kv');
route.routeName = 'vault.cluster.secrets.backend.list';
// Test logic: Builtin KV should also route to KV engine interface
// Ensure external plugin mapping doesn't break existing builtin engines
this.mockRouter.transitionTo('vault.cluster.secrets.backend.kv.list', 'builtin-kv');
assert.ok(this.mockRouter.transitionTo.called, 'Router transition was called');
const [routeName, backend] = this.mockRouter.transitionTo.args[0];
assert.strictEqual(routeName, 'vault.cluster.secrets.backend.kv.list', 'Routes to KV engine interface');
assert.strictEqual(backend, 'builtin-kv', 'Routes with correct backend path');
});
});

View file

@ -0,0 +1,82 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import engineDisplayData, { unknownEngineMetadata } from 'vault/helpers/engines-display-data';
module('Unit | Helper | engines-display-data', function () {
test('it returns metadata for builtin engines', function (assert) {
const keymgmtData = engineDisplayData('keymgmt');
assert.strictEqual(keymgmtData.type, 'keymgmt', 'returns correct type for keymgmt');
assert.strictEqual(keymgmtData.displayName, 'Key Management', 'returns correct displayName for keymgmt');
assert.ok(keymgmtData.requiresEnterprise, 'keymgmt requires enterprise');
});
test('it returns metadata for external plugins that map to builtins', function (assert) {
const externalKeymgmtData = engineDisplayData('vault-plugin-secrets-keymgmt');
// Should return keymgmt metadata but with the external plugin type preserved
assert.strictEqual(
externalKeymgmtData.type,
'vault-plugin-secrets-keymgmt',
'preserves external plugin type'
);
assert.strictEqual(externalKeymgmtData.displayName, 'Key Management', 'returns builtin displayName');
assert.ok(externalKeymgmtData.requiresEnterprise, 'inherits enterprise requirement from builtin');
assert.strictEqual(externalKeymgmtData.glyph, 'key', 'inherits glyph from builtin');
});
test('it returns unknown plugin metadata for unmapped external plugins', function (assert) {
const unknownData = engineDisplayData('vault-plugin-secrets-unknown');
const unknownMetadata = unknownEngineMetadata('vault-plugin-secrets-unknown');
assert.strictEqual(unknownData.type, unknownMetadata.type, 'returns unknown type');
assert.strictEqual(
unknownData.displayName,
'vault-plugin-secrets-unknown',
'uses plugin name as displayName'
);
assert.strictEqual(unknownData.glyph, unknownMetadata.glyph, 'uses default lock glyph');
assert.deepEqual(
unknownData.mountCategory,
unknownMetadata.mountCategory,
'has correct mount categories'
);
});
test('it returns unknown plugin metadata for empty/null inputs', function (assert) {
const emptyData = engineDisplayData('');
const nullData = engineDisplayData(null);
const undefinedData = engineDisplayData(undefined);
const unknownMetadata = unknownEngineMetadata();
assert.strictEqual(emptyData.type, unknownMetadata.type, 'returns unknown for empty string');
assert.strictEqual(emptyData.displayName, 'Unknown plugin', 'uses default name for empty string');
assert.strictEqual(nullData.type, unknownMetadata.type, 'returns unknown for null');
assert.strictEqual(undefinedData.type, unknownMetadata.type, 'returns unknown for undefined');
});
test('it handles case sensitivity correctly', function (assert) {
// Should not match due to case sensitivity
const upperCaseData = engineDisplayData('KEYMGMT');
const upperCaseUnknownMetadata = unknownEngineMetadata('KEYMGMT');
const mixedCaseData = engineDisplayData('KeyMgmt');
const mixedCaseUnknownMetadata = unknownEngineMetadata('KeyMgmt');
assert.strictEqual(
upperCaseData.type,
upperCaseUnknownMetadata.type,
'case sensitive - KEYMGMT not recognized'
);
assert.strictEqual(
mixedCaseData.type,
mixedCaseUnknownMetadata.type,
'case sensitive - KeyMgmt not recognized'
);
});
});

View file

@ -0,0 +1,129 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { exitConfigurationRoute } from 'vault/helpers/exit-configuration-route';
module('Unit | Helper | exit-configuration-route', function () {
test('alicloud returns list-root', function (assert) {
const result = exitConfigurationRoute(['alicloud']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('aws returns list-root', function (assert) {
const result = exitConfigurationRoute(['aws']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('azure returns backends route (isOnlyMountable)', function (assert) {
const result = exitConfigurationRoute(['azure']);
assert.strictEqual(result, 'vault.cluster.secrets.backends');
});
test('consul returns list-root', function (assert) {
const result = exitConfigurationRoute(['consul']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('cubbyhole returns list-root', function (assert) {
const result = exitConfigurationRoute(['cubbyhole']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('database returns list-root', function (assert) {
const result = exitConfigurationRoute(['database']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('gcp returns backends route (isOnlyMountable)', function (assert) {
const result = exitConfigurationRoute(['gcp']);
assert.strictEqual(result, 'vault.cluster.secrets.backends');
});
test('gcpkms returns list-root', function (assert) {
const result = exitConfigurationRoute(['gcpkms']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('kv with no version (defaults to v1) returns list-root', function (assert) {
const result = exitConfigurationRoute(['kv']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('kv v1 returns list-root', function (assert) {
const result = exitConfigurationRoute(['kv', 1]);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('kv v2 returns kv.list', function (assert) {
const result = exitConfigurationRoute(['kv', 2]);
assert.strictEqual(result, 'vault.cluster.secrets.backend.kv.list');
});
test('kmip returns kmip.scopes.index', function (assert) {
const result = exitConfigurationRoute(['kmip']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.kmip.scopes.index');
});
test('transform returns list-root', function (assert) {
const result = exitConfigurationRoute(['transform']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('keymgmt returns list-root', function (assert) {
const result = exitConfigurationRoute(['keymgmt']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('kubernetes returns kubernetes.overview', function (assert) {
const result = exitConfigurationRoute(['kubernetes']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.kubernetes.overview');
});
test('ldap returns ldap.overview', function (assert) {
const result = exitConfigurationRoute(['ldap']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.ldap.overview');
});
test('nomad returns list-root', function (assert) {
const result = exitConfigurationRoute(['nomad']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('pki returns pki.overview', function (assert) {
const result = exitConfigurationRoute(['pki']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.pki.overview');
});
test('rabbitmq returns list-root', function (assert) {
const result = exitConfigurationRoute(['rabbitmq']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('ssh returns list-root', function (assert) {
const result = exitConfigurationRoute(['ssh']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('totp returns list-root', function (assert) {
const result = exitConfigurationRoute(['totp']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('transit returns list-root', function (assert) {
const result = exitConfigurationRoute(['transit']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('unknown engine type returns list-root', function (assert) {
const result = exitConfigurationRoute(['unknown-engine']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
test('empty engine type returns list-root', function (assert) {
const result = exitConfigurationRoute(['']);
assert.strictEqual(result, 'vault.cluster.secrets.backend.list-root');
});
});

View file

@ -0,0 +1,140 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { secretQueryParams } from 'vault/helpers/secret-query-params';
/**
* Test the secret-query-params helper to ensure it correctly handles
* external plugin mapping for query parameter generation.
*/
module('Unit | Helper | secret-query-params external plugin support', function () {
module('keymgmt external plugins', function () {
test('generates itemType=key for external keymgmt plugins with key type', function (assert) {
const result = secretQueryParams(['vault-plugin-secrets-keymgmt', 'key'], {});
assert.deepEqual(
result,
{ itemType: 'key' },
'External keymgmt plugin generates correct itemType for key'
);
});
test('generates itemType=provider for external keymgmt plugins with provider type', function (assert) {
const result = secretQueryParams(['vault-plugin-secrets-keymgmt', 'provider'], {});
assert.deepEqual(
result,
{ itemType: 'provider' },
'External keymgmt plugin generates correct itemType for provider'
);
});
test('defaults to itemType=key for external keymgmt plugins with no type', function (assert) {
const result = secretQueryParams(['vault-plugin-secrets-keymgmt'], {});
assert.deepEqual(result, { itemType: 'key' }, 'External keymgmt plugin defaults to key itemType');
});
test('generates same params as builtin keymgmt', function (assert) {
const externalResult = secretQueryParams(['vault-plugin-secrets-keymgmt', 'key'], {});
const builtinResult = secretQueryParams(['keymgmt', 'key'], {});
assert.deepEqual(
externalResult,
builtinResult,
'External keymgmt generates same params as builtin keymgmt'
);
});
});
module('transit external plugins', function () {
test('generates tab=actions for external transit plugins', function (assert) {
// Note: transit external plugin would be vault-plugin-secrets-transit if it existed
const result = secretQueryParams(['transit', ''], {});
assert.deepEqual(result, { tab: 'actions' }, 'Transit plugins generate tab=actions');
});
});
module('database external plugins', function () {
test('generates type parameter for database plugins', function (assert) {
const result = secretQueryParams(['database', 'connection'], {});
assert.deepEqual(result, { type: 'connection' }, 'Database plugins generate correct type parameter');
});
test('passes through type parameter for external database plugins', function (assert) {
// Even though we don't have database external mapping, test the behavior
const result = secretQueryParams(['vault-plugin-database-postgresql', 'role'], {});
// Should return undefined since unmapped external plugins don't generate params
assert.strictEqual(result, undefined, 'Unmapped external plugins return undefined');
});
});
module('asQueryParams formatting', function () {
test('formats external keymgmt params for LinkTo components', function (assert) {
const result = secretQueryParams(['vault-plugin-secrets-keymgmt', 'provider'], { asQueryParams: true });
assert.deepEqual(
result,
{
isQueryParams: true,
values: { itemType: 'provider' },
},
'External keymgmt formats correctly for LinkTo components'
);
});
test('returns undefined when formatted but no params generated', function (assert) {
const result = secretQueryParams(['vault-plugin-secrets-unknown'], { asQueryParams: true });
assert.strictEqual(
result,
undefined,
'Unknown external plugins return undefined even with asQueryParams'
);
});
});
module('unknown external plugins', function () {
test('returns undefined for unmapped external plugins', function (assert) {
const result = secretQueryParams(['vault-plugin-secrets-unknown'], {});
assert.strictEqual(result, undefined, 'Unmapped external plugins return undefined');
});
test('preserves behavior for builtin engines', function (assert) {
const transitResult = secretQueryParams(['transit'], {});
const keymgmtResult = secretQueryParams(['keymgmt'], {});
const unknownResult = secretQueryParams(['unknown'], {});
assert.deepEqual(transitResult, { tab: 'actions' }, 'Builtin transit works');
assert.deepEqual(keymgmtResult, { itemType: 'key' }, 'Builtin keymgmt works');
assert.strictEqual(unknownResult, undefined, 'Unknown builtin returns undefined');
});
});
module('edge cases', function () {
test('handles empty backend type', function (assert) {
const result = secretQueryParams([''], {});
assert.strictEqual(result, undefined, 'Empty backend type returns undefined');
});
test('handles undefined backend type', function (assert) {
const result = secretQueryParams([undefined], {});
assert.strictEqual(result, undefined, 'Undefined backend type returns undefined');
});
test('handles missing type parameter', function (assert) {
const result = secretQueryParams(['vault-plugin-secrets-keymgmt'], {});
assert.deepEqual(result, { itemType: 'key' }, 'Missing type parameter defaults correctly');
});
});
});

View file

@ -0,0 +1,152 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
/**
* Test the secret-engine model to ensure external plugin mapping
* works correctly for key getters that affect routing and UI behavior.
*/
module('Unit | Model | secret-engine external plugin support', function (hooks) {
setupTest(hooks);
module('isV2KV getter', function () {
test('returns true for external KV v2 plugins', function (assert) {
const store = this.owner.lookup('service:store');
const externalKvV2 = store.createRecord('secret-engine', {
type: 'vault-plugin-secrets-kv',
version: 2,
});
assert.true(externalKvV2.isV2KV, 'External KV v2 plugin is recognized as V2 KV');
});
test('returns false for external KV v1 plugins', function (assert) {
const store = this.owner.lookup('service:store');
const externalKvV1 = store.createRecord('secret-engine', {
type: 'vault-plugin-secrets-kv',
version: 1,
});
assert.false(externalKvV1.isV2KV, 'External KV v1 plugin is not V2 KV');
});
test('returns true for builtin KV v2 engines', function (assert) {
const store = this.owner.lookup('service:store');
const builtinKvV2 = store.createRecord('secret-engine', {
type: 'kv',
version: 2,
});
assert.true(builtinKvV2.isV2KV, 'Builtin KV v2 engine is recognized as V2 KV');
});
test('returns true for generic v2 engines', function (assert) {
const store = this.owner.lookup('service:store');
const genericV2 = store.createRecord('secret-engine', {
type: 'generic',
version: 2,
});
assert.true(genericV2.isV2KV, 'Generic v2 engine is recognized as V2 KV');
});
test('returns false for non-KV external plugins', function (assert) {
const store = this.owner.lookup('service:store');
const externalKeymgmt = store.createRecord('secret-engine', {
type: 'vault-plugin-secrets-keymgmt',
version: 1,
});
assert.false(externalKeymgmt.isV2KV, 'External keymgmt plugin is not V2 KV');
});
});
module('backendLink getter', function () {
test('returns KV engine route for external KV v2 plugins', function (assert) {
const store = this.owner.lookup('service:store');
const externalKvV2 = store.createRecord('secret-engine', {
type: 'vault-plugin-secrets-kv',
version: 2,
});
const backendLink = externalKvV2.backendLink;
assert.true(backendLink.includes('kv.list'), `External KV v2 uses KV engine route: ${backendLink}`);
});
test('returns correct route for external database plugins', function (assert) {
const store = this.owner.lookup('service:store');
// Mock external database plugin (though not in our current mapping)
const externalDb = store.createRecord('secret-engine', {
type: 'vault-plugin-database-postgresql',
});
const backendLink = externalDb.backendLink;
// Should fall back to list-root for unmapped plugins
assert.strictEqual(
backendLink,
'vault.cluster.secrets.backend.list-root',
'Unmapped external plugin uses generic route'
);
});
test('handles external keymgmt plugins correctly', function (assert) {
const store = this.owner.lookup('service:store');
const externalKeymgmt = store.createRecord('secret-engine', {
type: 'vault-plugin-secrets-keymgmt',
});
const backendLink = externalKeymgmt.backendLink;
// External keymgmt should route to generic since keymgmt doesn't have engineRoute
assert.strictEqual(
backendLink,
'vault.cluster.secrets.backend.list-root',
'External keymgmt uses list-root route'
);
});
});
module('backendConfigurationLink getter', function () {
test('returns effective type configuration route for external plugins', function (assert) {
const store = this.owner.lookup('service:store');
const externalAzure = store.createRecord('secret-engine', {
type: 'vault-plugin-secrets-azure',
});
const configLink = externalAzure.backendConfigurationLink;
// Note: The old secret-engine model uses isAddonEngine logic, so Azure (not an addon)
// falls back to general-settings rather than plugin-settings
assert.strictEqual(
configLink,
'vault.cluster.secrets.backend.configuration.general-settings',
`External Azure uses general settings route in old model: ${configLink}`
);
});
test('fallback to generic configuration for unmapped plugins', function (assert) {
const store = this.owner.lookup('service:store');
const unknownExternal = store.createRecord('secret-engine', {
type: 'vault-plugin-secrets-unknown',
});
const configLink = unknownExternal.backendConfigurationLink;
assert.strictEqual(
configLink,
'vault.cluster.secrets.backend.configuration.general-settings',
'Unknown external plugin uses generic configuration route'
);
});
});
});

View file

@ -0,0 +1,122 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { ALL_ENGINES, filterEnginesByMountCategory, isAddonEngine } from 'vault/utils/all-engines-metadata';
module('Unit | Utility | all-engines-metadata', function () {
module('ALL_ENGINES', function () {
test('it contains expected engine metadata', function (assert) {
assert.true(Array.isArray(ALL_ENGINES), 'ALL_ENGINES is an array');
assert.true(ALL_ENGINES.length > 0, 'ALL_ENGINES contains engines');
// Check that at least some expected engines are present
const engineTypes = ALL_ENGINES.map((engine) => engine.type);
assert.true(engineTypes.includes('kv'), 'contains kv engine');
assert.true(engineTypes.includes('pki'), 'contains pki engine');
assert.true(engineTypes.includes('transit'), 'contains transit engine');
});
test('all engines have required properties', function (assert) {
ALL_ENGINES.forEach((engine) => {
assert.ok(engine.displayName, `${engine.type} has displayName`);
assert.ok(engine.type, `${engine.type} has type`);
assert.true(Array.isArray(engine.mountCategory), `${engine.type} has mountCategory array`);
assert.true(engine.mountCategory.length > 0, `${engine.type} has at least one mount category`);
});
});
});
module('filterEnginesByMountCategory', function () {
test('filters engines by secret mount category', function (assert) {
const secretEngines = filterEnginesByMountCategory({
mountCategory: 'secret',
isEnterprise: false,
});
assert.true(Array.isArray(secretEngines), 'returns an array');
assert.true(secretEngines.length > 0, 'returns some engines');
// All returned engines should have 'secret' in mountCategory
secretEngines.forEach((engine) => {
assert.true(
engine.mountCategory.includes('secret'),
`${engine.type} should have 'secret' in mountCategory`
);
});
});
test('filters engines by auth mount category', function (assert) {
const authEngines = filterEnginesByMountCategory({
mountCategory: 'auth',
isEnterprise: false,
});
assert.true(Array.isArray(authEngines), 'returns an array');
assert.true(authEngines.length > 0, 'returns some engines');
// All returned engines should have 'auth' in mountCategory
authEngines.forEach((engine) => {
assert.true(
engine.mountCategory.includes('auth'),
`${engine.type} should have 'auth' in mountCategory`
);
});
});
test('excludes enterprise engines when isEnterprise is false', function (assert) {
const ossEngines = filterEnginesByMountCategory({
mountCategory: 'secret',
isEnterprise: false,
});
// Should not contain any engines that require enterprise
ossEngines.forEach((engine) => {
assert.notOk(
engine.requiresEnterprise,
`${engine.type} should not require enterprise when isEnterprise is false`
);
});
});
test('includes enterprise engines when isEnterprise is true', function (assert) {
const allEngines = filterEnginesByMountCategory({
mountCategory: 'secret',
isEnterprise: true,
});
const ossEngines = filterEnginesByMountCategory({
mountCategory: 'secret',
isEnterprise: false,
});
// Enterprise should have same or more engines than OSS
assert.true(
allEngines.length >= ossEngines.length,
'enterprise mode should include same or more engines'
);
});
});
module('isAddonEngine', function () {
test('returns false for kv version 1', function (assert) {
assert.false(isAddonEngine('kv', 1), 'kv version 1 is not an addon engine');
});
test('returns true for engines with engineRoute', function (assert) {
assert.true(isAddonEngine('kv', 2), 'kv version 2 is an addon engine');
assert.true(isAddonEngine('pki', 1), 'pki is an addon engine');
});
test('returns false for engines without engineRoute', function (assert) {
assert.false(isAddonEngine('transit', 1), 'transit is not an addon engine');
assert.false(isAddonEngine('cubbyhole', 1), 'cubbyhole is not an addon engine');
});
test('returns false for unknown engine types', function (assert) {
assert.false(isAddonEngine('unknown-engine', 1), 'unknown engines are not addon engines');
});
});
});

View file

@ -0,0 +1,127 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import Route from '@ember/routing/route';
import { getBackendEffectiveType, getEnginePathParam } from 'vault/utils/backend-route-helpers';
import sinon from 'sinon';
/**
* Test the backend route helper utilities to ensure external plugin mapping
* works correctly.
*/
module('Unit | Utility | backend-route-helpers', function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
// Create a test route
this.owner.register('route:test', Route);
this.route = this.owner.lookup('route:test');
this.stub = sinon.stub;
});
hooks.afterEach(function () {
sinon.restore();
});
module('getBackendEffectiveType', function () {
test('returns effective type for external keymgmt plugins', function (assert) {
// Mock modelFor to return an external keymgmt engine
this.route.modelFor = this.stub().withArgs('vault.cluster.secrets.backend').returns({
engineType: 'vault-plugin-secrets-keymgmt',
});
const effectiveType = getBackendEffectiveType(this.route);
assert.strictEqual(effectiveType, 'keymgmt', 'External keymgmt plugin returns effective type keymgmt');
});
test('returns effective type for external KV plugins', function (assert) {
this.route.modelFor = this.stub().withArgs('vault.cluster.secrets.backend').returns({
engineType: 'vault-plugin-secrets-kv',
});
const effectiveType = getBackendEffectiveType(this.route);
assert.strictEqual(effectiveType, 'kv', 'External KV plugin returns effective type kv');
});
test('returns original type for builtin engines', function (assert) {
this.route.modelFor = this.stub().withArgs('vault.cluster.secrets.backend').returns({
engineType: 'keymgmt',
});
const effectiveType = getBackendEffectiveType(this.route);
assert.strictEqual(effectiveType, 'keymgmt', 'Builtin keymgmt returns original type');
});
test('returns original type for unknown external plugins', function (assert) {
this.route.modelFor = this.stub().withArgs('vault.cluster.secrets.backend').returns({
engineType: 'vault-plugin-secrets-unknown',
});
const effectiveType = getBackendEffectiveType(this.route);
assert.strictEqual(
effectiveType,
'vault-plugin-secrets-unknown',
'Unknown external plugin returns original type'
);
});
test('handles external Azure plugins', function (assert) {
this.route.modelFor = this.stub().withArgs('vault.cluster.secrets.backend').returns({
engineType: 'vault-plugin-secrets-azure',
});
const effectiveType = getBackendEffectiveType(this.route);
assert.strictEqual(effectiveType, 'azure', 'External Azure plugin returns effective type azure');
});
});
module('getEnginePathParam', function () {
test('returns backend parameter from route params', function (assert) {
this.route.paramsFor = this.stub().withArgs('vault.cluster.secrets.backend').returns({
backend: 'external-keymgmt',
});
const enginePath = getEnginePathParam(this.route);
assert.strictEqual(enginePath, 'external-keymgmt', 'Returns backend parameter from route');
});
test('handles different backend paths', function (assert) {
this.route.paramsFor = this.stub().withArgs('vault.cluster.secrets.backend').returns({
backend: 'my-custom-engine-path',
});
const enginePath = getEnginePathParam(this.route);
assert.strictEqual(enginePath, 'my-custom-engine-path', 'Returns custom backend path');
});
});
module('integration with route operations', function () {
test('utility functions can be used together in route logic', function (assert) {
// Mock both functions used in typical route scenarios
this.route.modelFor = this.stub().withArgs('vault.cluster.secrets.backend').returns({
engineType: 'vault-plugin-secrets-keymgmt',
});
this.route.paramsFor = this.stub().withArgs('vault.cluster.secrets.backend').returns({
backend: 'external-keymgmt',
});
const effectiveType = getBackendEffectiveType(this.route);
const enginePath = getEnginePathParam(this.route);
assert.strictEqual(effectiveType, 'keymgmt', 'Gets effective type correctly');
assert.strictEqual(enginePath, 'external-keymgmt', 'Gets engine path correctly');
});
});
});

View file

@ -0,0 +1,133 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import {
EXTERNAL_PLUGIN_TO_BUILTIN_MAP,
getBuiltinTypeFromExternalPlugin,
isKnownExternalPlugin,
getEffectiveEngineType,
} from 'vault/utils/external-plugin-helpers';
module('Unit | Utility | external-plugin-helpers', function () {
module('EXTERNAL_PLUGIN_TO_BUILTIN_MAP', function () {
test('it contains expected mappings', function (assert) {
assert.strictEqual(
EXTERNAL_PLUGIN_TO_BUILTIN_MAP['vault-plugin-secrets-keymgmt'],
'keymgmt',
'maps vault-plugin-secrets-keymgmt to keymgmt'
);
});
test('it is a constant record', function (assert) {
assert.strictEqual(typeof EXTERNAL_PLUGIN_TO_BUILTIN_MAP, 'object', 'is an object');
assert.notStrictEqual(EXTERNAL_PLUGIN_TO_BUILTIN_MAP, null, 'is not null');
});
});
module('getBuiltinTypeFromExternalPlugin', function () {
test('it returns mapped builtin type for known external plugins', function (assert) {
assert.strictEqual(
getBuiltinTypeFromExternalPlugin('vault-plugin-secrets-keymgmt'),
'keymgmt',
'returns keymgmt for vault-plugin-secrets-keymgmt'
);
});
test('it returns undefined for unknown external plugins', function (assert) {
assert.strictEqual(
getBuiltinTypeFromExternalPlugin('vault-plugin-secrets-unknown'),
undefined,
'returns undefined for unknown plugin'
);
});
test('it returns undefined for builtin plugin names', function (assert) {
assert.strictEqual(
getBuiltinTypeFromExternalPlugin('keymgmt'),
undefined,
'returns undefined for builtin plugin name'
);
});
test('it returns undefined for empty string', function (assert) {
assert.strictEqual(
getBuiltinTypeFromExternalPlugin(''),
undefined,
'returns undefined for empty string'
);
});
});
module('isKnownExternalPlugin', function () {
test('it returns true for known external plugins', function (assert) {
assert.true(
isKnownExternalPlugin('vault-plugin-secrets-keymgmt'),
'returns true for vault-plugin-secrets-keymgmt'
);
});
test('it returns false for unknown external plugins', function (assert) {
assert.false(isKnownExternalPlugin('vault-plugin-secrets-unknown'), 'returns false for unknown plugin');
});
test('it returns false for builtin plugin names', function (assert) {
assert.false(isKnownExternalPlugin('keymgmt'), 'returns false for builtin plugin name');
});
test('it returns false for empty string', function (assert) {
assert.false(isKnownExternalPlugin(''), 'returns false for empty string');
});
});
module('getEffectiveEngineType', function () {
test('it returns builtin type for known external plugins', function (assert) {
assert.strictEqual(
getEffectiveEngineType('vault-plugin-secrets-keymgmt'),
'keymgmt',
'returns keymgmt for vault-plugin-secrets-keymgmt'
);
});
test('it returns original type for unknown external plugins', function (assert) {
assert.strictEqual(
getEffectiveEngineType('vault-plugin-secrets-unknown'),
'vault-plugin-secrets-unknown',
'returns original type for unknown plugin'
);
});
test('it returns original type for builtin plugins', function (assert) {
assert.strictEqual(
getEffectiveEngineType('keymgmt'),
'keymgmt',
'returns original type for builtin plugin'
);
});
test('it returns original type for standard engines', function (assert) {
assert.strictEqual(getEffectiveEngineType('kv'), 'kv', 'returns original type for kv engine');
assert.strictEqual(getEffectiveEngineType('pki'), 'pki', 'returns original type for pki engine');
assert.strictEqual(getEffectiveEngineType('aws'), 'aws', 'returns original type for aws engine');
});
test('it handles empty string gracefully', function (assert) {
assert.strictEqual(getEffectiveEngineType(''), '', 'returns empty string for empty input');
});
});
module('future extensibility', function () {
test('mapping can be easily extended', function (assert) {
// Test that we can add more mappings (conceptually)
const testMap = {
...EXTERNAL_PLUGIN_TO_BUILTIN_MAP,
'vault-plugin-auth-example': 'example-auth',
};
assert.strictEqual(testMap['vault-plugin-secrets-keymgmt'], 'keymgmt', 'existing mapping is preserved');
assert.strictEqual(testMap['vault-plugin-auth-example'], 'example-auth', 'new mapping can be added');
});
});
});

View file

@ -0,0 +1,166 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { getModelTypeForEngine } from 'vault/utils/model-helpers/secret-engine-helpers';
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
module('Unit | Utility | model-helpers/secret-engine-helpers', function () {
module('getModelTypeForEngine', function () {
test('returns correct model types for basic engines', function (assert) {
assert.strictEqual(getModelTypeForEngine('transit'), 'transit-key');
assert.strictEqual(getModelTypeForEngine('ssh'), 'role-ssh');
assert.strictEqual(getModelTypeForEngine('aws'), 'role-aws');
assert.strictEqual(getModelTypeForEngine('cubbyhole'), 'secret');
assert.strictEqual(getModelTypeForEngine('kv'), 'secret');
assert.strictEqual(getModelTypeForEngine('generic'), 'secret');
assert.strictEqual(getModelTypeForEngine('totp'), 'totp-key');
});
test('returns correct model types for database engine with context', function (assert) {
assert.strictEqual(
getModelTypeForEngine('database', { isRole: true }),
'database/role',
'returns database/role when isRole is true'
);
assert.strictEqual(
getModelTypeForEngine('database', { tab: 'role' }),
'database/role',
'returns database/role when tab is role'
);
assert.strictEqual(
getModelTypeForEngine('database', { secret: 'role/my-role' }),
'database/role',
'returns database/role when secret starts with role/'
);
assert.strictEqual(
getModelTypeForEngine('database', {}),
'database/connection',
'returns database/connection for empty context'
);
assert.strictEqual(
getModelTypeForEngine('database'),
'database/connection',
'returns database/connection with no context'
);
});
test('returns correct model types for transform engine', function (assert) {
// Test secret name prefix logic (takes priority)
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'role/my-role' }),
'transform/role',
'returns transform/role for secret starting with role/'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'template/my-template' }),
'transform/template',
'returns transform/template for secret starting with template/'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'alphabet/my-alphabet' }),
'transform/alphabet',
'returns transform/alphabet for secret starting with alphabet/'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'other/my-other' }),
'transform',
'returns transform for secret with unknown prefix'
);
// Test query parameter logic (fallback)
assert.strictEqual(
getModelTypeForEngine('transform', { tab: 'role' }),
'transform/role',
'returns transform/role when tab is role'
);
assert.strictEqual(
getModelTypeForEngine('transform', { transformType: 'template' }),
'transform/template',
'returns transform/template when transformType is template'
);
assert.strictEqual(
getModelTypeForEngine('transform', { tab: 'alphabet' }),
'transform/alphabet',
'returns transform/alphabet when tab is alphabet'
);
// Test precedence: secret name should override query params
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'role/my-role', tab: 'template' }),
'transform/role',
'secret name prefix takes precedence over tab parameter'
);
// Test fallback cases
assert.strictEqual(
getModelTypeForEngine('transform', { transformType: 'unknown' }),
'transform',
'returns transform for unknown transformType'
);
assert.strictEqual(
getModelTypeForEngine('transform', {}),
'transform',
'returns transform for empty context'
);
assert.strictEqual(
getModelTypeForEngine('transform'),
'transform',
'returns transform with no context'
);
});
test('returns correct model types for keymgmt engine', function (assert) {
assert.strictEqual(
getModelTypeForEngine('keymgmt', { itemType: 'key' }),
'keymgmt/key',
'returns keymgmt/key when itemType is key'
);
assert.strictEqual(
getModelTypeForEngine('keymgmt', { tab: 'provider' }),
'keymgmt/provider',
'returns keymgmt/provider when tab is provider'
);
assert.strictEqual(
getModelTypeForEngine('keymgmt', { itemType: 'provider' }),
'keymgmt/provider',
'returns keymgmt/provider when itemType is provider'
);
assert.strictEqual(
getModelTypeForEngine('keymgmt', {}),
'keymgmt/key',
'returns keymgmt/key for empty context (default)'
);
assert.strictEqual(
getModelTypeForEngine('keymgmt'),
'keymgmt/key',
'returns keymgmt/key with no context (default)'
);
});
test('returns default "secret" for unknown engines', function (assert) {
assert.strictEqual(
getModelTypeForEngine('unknown-engine'),
'secret',
'returns secret for unknown engine'
);
assert.strictEqual(
getModelTypeForEngine('custom-plugin'),
'secret',
'returns secret for custom plugin'
);
assert.strictEqual(getModelTypeForEngine(''), 'secret', 'returns secret for empty string');
});
test('works with external plugin mapping', function (assert) {
// Test that external plugins get correct model types via effective type mapping
const externalKeymgmtType = getEffectiveEngineType('vault-plugin-secrets-keymgmt');
assert.strictEqual(externalKeymgmtType, 'keymgmt', 'external plugin maps to builtin');
const modelType = getModelTypeForEngine(externalKeymgmtType, { itemType: 'provider' });
assert.strictEqual(modelType, 'keymgmt/provider', 'external plugin gets correct model type');
});
});
});

View file

@ -0,0 +1,297 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { getModelTypeForEngine } from 'vault/utils/model-helpers/secret-engine-helpers';
module('Unit | Utility | transform-engine-logic', function () {
module('Secret prefix-based model type detection', function () {
test('it returns "transform" when secret is empty/null/undefined', function (assert) {
// Check that empty/null/undefined secrets return default transform type
assert.strictEqual(
getModelTypeForEngine('transform', { secret: null }),
'transform',
'returns transform for null secret'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: undefined }),
'transform',
'returns transform for undefined secret'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: '' }),
'transform',
'returns transform for empty string secret'
);
});
test('it returns "transform/role" when secret starts with "role/"', function (assert) {
// Check that role/ prefix returns transform/role
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'role/my-role' }),
'transform/role',
'returns transform/role for role/ prefixed secret'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'role/' }),
'transform/role',
'returns transform/role for just role/ prefix'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'role/test-role-name' }),
'transform/role',
'returns transform/role for complex role name'
);
});
test('it returns "transform/template" when secret starts with "template/"', function (assert) {
// Check that template/ prefix returns transform/template
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'template/my-template' }),
'transform/template',
'returns transform/template for template/ prefixed secret'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'template/' }),
'transform/template',
'returns transform/template for just template/ prefix'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'template/test-template-name' }),
'transform/template',
'returns transform/template for complex template name'
);
});
test('it returns "transform/alphabet" when secret starts with "alphabet/"', function (assert) {
// Check that alphabet/ prefix returns transform/alphabet
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'alphabet/my-alphabet' }),
'transform/alphabet',
'returns transform/alphabet for alphabet/ prefixed secret'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'alphabet/' }),
'transform/alphabet',
'returns transform/alphabet for just alphabet/ prefix'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'alphabet/test-alphabet-name' }),
'transform/alphabet',
'returns transform/alphabet for complex alphabet name'
);
});
test('it returns "transform" as default for other secret names', function (assert) {
// Check that non-recognized prefixes return default transform type
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'some-other-secret' }),
'transform',
'returns transform for non-prefixed secret'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'transformation/test' }),
'transform',
'returns transform for transformation/ prefix (TODO case)'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'random-name' }),
'transform',
'returns transform for random secret name'
);
});
test('it handles edge cases correctly', function (assert) {
// Test cases that might cause issues
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'role' }),
'transform',
'returns transform for just "role" (no slash)'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'template' }),
'transform',
'returns transform for just "template" (no slash)'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'alphabet' }),
'transform',
'returns transform for just "alphabet" (no slash)'
);
assert.strictEqual(
getModelTypeForEngine('transform', { secret: 'role/template/alphabet' }),
'transform/role',
'returns transform/role for complex path starting with role/'
);
});
});
module('Tab-based model type selection', function () {
test('it returns correct model types based on tab parameter', function (assert) {
// Check tab-based model type selection (switch statement logic)
assert.strictEqual(
getModelTypeForEngine('transform', { tab: 'role' }),
'transform/role',
'returns transform/role for role tab'
);
assert.strictEqual(
getModelTypeForEngine('transform', { tab: 'template' }),
'transform/template',
'returns transform/template for template tab'
);
assert.strictEqual(
getModelTypeForEngine('transform', { tab: 'alphabet' }),
'transform/alphabet',
'returns transform/alphabet for alphabet tab'
);
assert.strictEqual(
getModelTypeForEngine('transform', { tab: 'other' }),
'transform',
'returns transform for unknown tab (default case)'
);
assert.strictEqual(
getModelTypeForEngine('transform', { tab: null }),
'transform',
'returns transform for null tab'
);
assert.strictEqual(
getModelTypeForEngine('transform', { tab: undefined }),
'transform',
'returns transform for undefined tab'
);
});
});
module('Context-based transform type resolution', function () {
test('it handles tab context parameter', function (assert) {
// Simplified to use only tab parameter
assert.strictEqual(
getModelTypeForEngine('transform', { tab: 'role' }),
'transform/role',
'uses tab when available'
);
assert.strictEqual(
getModelTypeForEngine('transform', { tab: 'template' }),
'transform/template',
'uses tab when available'
);
assert.strictEqual(
getModelTypeForEngine('transform', { tab: 'alphabet' }),
'transform/alphabet',
'uses tab when available'
);
});
test('it validates tab is in allowed list', function (assert) {
// Check that only allowed tab values return specific types
assert.strictEqual(
getModelTypeForEngine('transform', { tab: 'invalid' }),
'transform',
'returns default for invalid tab'
);
assert.strictEqual(
getModelTypeForEngine('transform', { tab: 'transformation' }),
'transform',
'returns default for "transformation" (not in allowed list)'
);
assert.strictEqual(
getModelTypeForEngine('transform', { tab: '' }),
'transform',
'returns default for empty tab'
);
});
test('it returns default when no valid transform type is found', function (assert) {
// Check default behavior when no valid context is provided
assert.strictEqual(
getModelTypeForEngine('transform', {}),
'transform',
'returns default for empty context'
);
assert.strictEqual(
getModelTypeForEngine('transform', { someOtherParam: 'value' }),
'transform',
'returns default when no transform-related parameters'
);
});
});
module('Combined logic scenarios', function () {
test('secret parameter takes precedence over tab', function (assert) {
// When secret is provided with a prefix, it should override tab
assert.strictEqual(
getModelTypeForEngine('transform', {
secret: 'role/my-role',
tab: 'template',
}),
'transform/role',
'secret prefix overrides tab'
);
assert.strictEqual(
getModelTypeForEngine('transform', {
secret: 'template/my-template',
tab: 'alphabet',
}),
'transform/template',
'secret prefix overrides tab'
);
});
test('tab used when secret has no recognized prefix', function (assert) {
// When secret doesn't have a recognized prefix, fall back to tab
assert.strictEqual(
getModelTypeForEngine('transform', {
secret: 'some-other-secret',
tab: 'role',
}),
'transform/role',
'uses tab when secret has no prefix'
);
assert.strictEqual(
getModelTypeForEngine('transform', {
secret: 'random-name',
tab: 'template',
}),
'transform/template',
'uses tab when secret has no prefix'
);
});
test('handles all original edge cases', function (assert) {
// Comprehensive test covering various combinations
const testCases = [
// Secret-based detection
{ input: { secret: 'role/test' }, expected: 'transform/role' },
{ input: { secret: 'template/test' }, expected: 'transform/template' },
{ input: { secret: 'alphabet/test' }, expected: 'transform/alphabet' },
{ input: { secret: 'other/test' }, expected: 'transform' },
{ input: { secret: '' }, expected: 'transform' },
{ input: { secret: null }, expected: 'transform' },
// Tab-based selection
{ input: { tab: 'role' }, expected: 'transform/role' },
{ input: { tab: 'template' }, expected: 'transform/template' },
{ input: { tab: 'alphabet' }, expected: 'transform/alphabet' },
// Default cases
{ input: {}, expected: 'transform' },
{ input: { tab: 'invalid' }, expected: 'transform' },
// Precedence cases
{ input: { secret: 'role/test', tab: 'template' }, expected: 'transform/role' },
{ input: { secret: 'other', tab: 'role' }, expected: 'transform/role' },
];
testCases.forEach(({ input, expected }) => {
const result = getModelTypeForEngine('transform', input);
assert.strictEqual(
result,
expected,
`getModelTypeForEngine('transform', ${JSON.stringify(input)}) should return '${expected}'`
);
});
});
});
});