mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
* [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:
parent
7c607b36d3
commit
91025c9ce7
32 changed files with 1920 additions and 161 deletions
3
changelog/_11244.txt
Normal file
3
changelog/_11244.txt
Normal 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
|
||||
```
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
51
ui/app/helpers/exit-configuration-route.ts
Normal file
51
ui/app/helpers/exit-configuration-route.ts
Normal 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);
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
35
ui/app/utils/backend-route-helpers.ts
Normal file
35
ui/app/utils/backend-route-helpers.ts
Normal 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;
|
||||
}
|
||||
65
ui/app/utils/external-plugin-helpers.ts
Normal file
65
ui/app/utils/external-plugin-helpers.ts
Normal 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;
|
||||
}
|
||||
114
ui/app/utils/model-helpers/secret-engine-helpers.ts
Normal file
114
ui/app/utils/model-helpers/secret-engine-helpers.ts
Normal 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';
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
82
ui/tests/unit/helpers/engines-display-data-test.js
Normal file
82
ui/tests/unit/helpers/engines-display-data-test.js
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
129
ui/tests/unit/helpers/exit-configuration-route-test.js
Normal file
129
ui/tests/unit/helpers/exit-configuration-route-test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
152
ui/tests/unit/models/secret-engine-external-plugins-test.js
Normal file
152
ui/tests/unit/models/secret-engine-external-plugins-test.js
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
122
ui/tests/unit/utils/all-engines-metadata-test.js
Normal file
122
ui/tests/unit/utils/all-engines-metadata-test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
127
ui/tests/unit/utils/backend-route-helpers-test.js
Normal file
127
ui/tests/unit/utils/backend-route-helpers-test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
133
ui/tests/unit/utils/external-plugin-helpers-test.js
Normal file
133
ui/tests/unit/utils/external-plugin-helpers-test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
166
ui/tests/unit/utils/model-helpers/secret-engine-helpers-test.js
Normal file
166
ui/tests/unit/utils/model-helpers/secret-engine-helpers-test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
297
ui/tests/unit/utils/transform-engine-logic-test.js
Normal file
297
ui/tests/unit/utils/transform-engine-logic-test.js
Normal 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}'`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue