mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
Merge remote-tracking branch 'remotes/from/ce/main'
Some checks are pending
build / setup (push) Waiting to run
build / Check ce/* Pull Requests (push) Blocked by required conditions
build / ui (push) Blocked by required conditions
build / artifacts-ce (push) Blocked by required conditions
build / artifacts-ent (push) Blocked by required conditions
build / hcp-image (push) Blocked by required conditions
build / test (push) Blocked by required conditions
build / test-hcp-image (push) Blocked by required conditions
build / completed-successfully (push) Blocked by required conditions
CI / setup (push) Waiting to run
CI / Run Autopilot upgrade tool (push) Blocked by required conditions
CI / Run Go tests (push) Blocked by required conditions
CI / Run Go tests tagged with testonly (push) Blocked by required conditions
CI / Run Go tests with data race detection (push) Blocked by required conditions
CI / Run Go tests with FIPS configuration (push) Blocked by required conditions
CI / Test UI (push) Blocked by required conditions
CI / tests-completed (push) Blocked by required conditions
Run linters / Setup (push) Waiting to run
Run linters / Deprecated functions (push) Blocked by required conditions
Run linters / Code checks (push) Blocked by required conditions
Run linters / Protobuf generate delta (push) Blocked by required conditions
Run linters / Format (push) Blocked by required conditions
Run linters / Semgrep (push) Waiting to run
Check Copywrite Headers / copywrite (push) Waiting to run
Security Scan / scan (push) Waiting to run
Some checks are pending
build / setup (push) Waiting to run
build / Check ce/* Pull Requests (push) Blocked by required conditions
build / ui (push) Blocked by required conditions
build / artifacts-ce (push) Blocked by required conditions
build / artifacts-ent (push) Blocked by required conditions
build / hcp-image (push) Blocked by required conditions
build / test (push) Blocked by required conditions
build / test-hcp-image (push) Blocked by required conditions
build / completed-successfully (push) Blocked by required conditions
CI / setup (push) Waiting to run
CI / Run Autopilot upgrade tool (push) Blocked by required conditions
CI / Run Go tests (push) Blocked by required conditions
CI / Run Go tests tagged with testonly (push) Blocked by required conditions
CI / Run Go tests with data race detection (push) Blocked by required conditions
CI / Run Go tests with FIPS configuration (push) Blocked by required conditions
CI / Test UI (push) Blocked by required conditions
CI / tests-completed (push) Blocked by required conditions
Run linters / Setup (push) Waiting to run
Run linters / Deprecated functions (push) Blocked by required conditions
Run linters / Code checks (push) Blocked by required conditions
Run linters / Protobuf generate delta (push) Blocked by required conditions
Run linters / Format (push) Blocked by required conditions
Run linters / Semgrep (push) Waiting to run
Check Copywrite Headers / copywrite (push) Waiting to run
Security Scan / scan (push) Waiting to run
This commit is contained in:
commit
64d7b4978b
24 changed files with 2727 additions and 156 deletions
3
changelog/_11659.txt
Normal file
3
changelog/_11659.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
**UI: Mount versioned external plugins**: Adds ability to mount previously registered, external plugins and specify a version when enabling secrets engines.
|
||||
```
|
||||
|
|
@ -13,20 +13,105 @@
|
|||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
|
||||
<form {{on "submit" (perform this.mountBackend)}}>
|
||||
{{! Plugin registration type (built-in vs external) }}
|
||||
<Hds::Form::RadioCard::Group @name="plugin-type" class="has-bottom-margin-m" as |RadioGroup|>
|
||||
<RadioGroup.Legend>Plugin registration type</RadioGroup.Legend>
|
||||
{{#each this.pluginTypeOptions as |option|}}
|
||||
<RadioGroup.RadioCard
|
||||
@checked={{eq this.pluginRegistrationType option.type}}
|
||||
{{on "change" (fn this.setPluginType option.type)}}
|
||||
@disabled={{option.disabled}}
|
||||
data-test-radio-card={{option.dataTestAttr}}
|
||||
as |Card|
|
||||
>
|
||||
<Card.Icon @name={{option.icon}} />
|
||||
<Card.Label>{{option.label}}</Card.Label>
|
||||
{{#if option.showBadge}}
|
||||
<Card.Badge @text="Enterprise" data-test-badge="external-enterprise" />
|
||||
{{/if}}
|
||||
<Card.Description>{{option.description}}</Card.Description>
|
||||
{{#if option.showAlert}}
|
||||
<Card.Generic>
|
||||
<Hds::Alert @type="compact" @color="neutral" class="has-top-padding-xs" data-test-inline-alert as |A|>
|
||||
<A.Description>No external plugins for this engine are currently registered in your plugin catalog.</A.Description>
|
||||
</Hds::Alert>
|
||||
</Card.Generic>
|
||||
{{/if}}
|
||||
</RadioGroup.RadioCard>
|
||||
{{/each}}
|
||||
</Hds::Form::RadioCard::Group>
|
||||
|
||||
{{! Plugin version selection (only shows for external plugins) }}
|
||||
{{#if this.shouldShowPluginVersionField}}
|
||||
<div class="field" data-test-field="config.plugin_version">
|
||||
<Hds::Form::Select::Field
|
||||
@isRequired={{true}}
|
||||
name="plugin-version"
|
||||
data-test-select="plugin-version"
|
||||
@value={{this.selectedPluginVersion}}
|
||||
{{on "change" this.onPluginVersionChange}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>Plugin version</F.Label>
|
||||
<F.HelperText data-test-help-text="config.plugin_version">
|
||||
Specifies the semantic version of the plugin to use, e.g. "v1.0.0".
|
||||
{{#if @model.hasUnversionedPlugins}}
|
||||
Un-versioned plugins are not supported, they must be enabled via CLI.
|
||||
{{/if}}
|
||||
{{#if this.pinnedVersionForCurrentPlugin}}
|
||||
{{this.pinnedVersionForCurrentPlugin}}
|
||||
is pinned for this plugin.
|
||||
{{/if}}
|
||||
</F.HelperText>
|
||||
<F.Options>
|
||||
{{#each this.filteredVersionOptions as |version|}}
|
||||
<option value={{version}} data-test-version-option={{version}}>
|
||||
{{version}}
|
||||
{{#if (eq version this.pinnedVersionForCurrentPlugin)}}
|
||||
(pinned)
|
||||
{{/if}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</F.Options>
|
||||
{{#if (get this.formValidations "config.plugin_version.errors.length")}}
|
||||
<F.Error>
|
||||
{{#each (get this.formValidations "config.plugin_version.errors") as |error|}}
|
||||
{{error}}
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::Select::Field>
|
||||
|
||||
{{! Warning when selected version differs from pinned version }}
|
||||
{{#if this.shouldShowPinWarning}}
|
||||
<Hds::Alert @type="inline" @color="warning" class="has-top-margin-s" as |A|>
|
||||
<A.Title>Version differs from pinned</A.Title>
|
||||
<A.Description>
|
||||
You have selected
|
||||
{{this.selectedPluginVersion}}, but version
|
||||
{{this.pinnedVersionForCurrentPlugin}}
|
||||
is pinned for this plugin. Enabling the engine with this version will override the pinned version for this
|
||||
mount.
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<FormFieldGroups
|
||||
@model={{@model}}
|
||||
@model={{@model.form}}
|
||||
@groupName="formFieldGroups"
|
||||
@renderGroup="default"
|
||||
@modelValidations={{this.modelValidations}}
|
||||
@modelValidations={{this.formValidations}}
|
||||
@onKeyUp={{this.onKeyUp}}
|
||||
/>
|
||||
|
||||
<FormFieldGroups @model={{@model}} @renderGroup="Method Options" @groupName="formFieldGroups">
|
||||
<FormFieldGroups @model={{@model.form}} @renderGroup="Method Options" @groupName="formFieldGroups">
|
||||
<:identityTokenKey>
|
||||
<SearchSelectWithModal
|
||||
@id="key"
|
||||
@fallbackComponent="input-search"
|
||||
@inputValue={{@model.data.config.identity_token_key}}
|
||||
@inputValue={{@model.form.data.config.identity_token_key}}
|
||||
@onChange={{this.handleIdentityTokenKeyChange}}
|
||||
@models={{array "oidc/key"}}
|
||||
@selectLimit="1"
|
||||
|
|
|
|||
|
|
@ -3,24 +3,44 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { set } from '@ember/object';
|
||||
import { capitalize } from '@ember/string';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
import type Router from '@ember/routing/router';
|
||||
import type FlashMessagesService from 'ember-cli-flash/services/flash-messages';
|
||||
import type SecretsEngineForm from 'vault/forms/secrets/engine';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type CapabilitiesService from 'vault/services/capabilities';
|
||||
import type Router from '@ember/routing/router';
|
||||
import type SecretsEngineForm from 'vault/forms/secrets/engine';
|
||||
import type { ValidationMap } from 'vault/vault/app-types';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import { isAddonEngine } from 'vault/utils/all-engines-metadata';
|
||||
import { getExternalPluginNameFromBuiltin } from 'vault/utils/external-plugin-helpers';
|
||||
import type { EngineVersionInfo } from 'vault/utils/plugin-catalog-helpers';
|
||||
import { sortVersions } from 'vault/utils/version-utils';
|
||||
import type { ValidationMap } from 'vault/vault/app-types';
|
||||
|
||||
// Extended config interface for plugin mounting
|
||||
interface ExtendedMountConfig {
|
||||
plugin_version?: string;
|
||||
override_pinned_version?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
enum PluginRegistrationType {
|
||||
BUILTIN = 'builtin',
|
||||
EXTERNAL = 'external',
|
||||
}
|
||||
|
||||
interface Args {
|
||||
model: SecretsEngineForm;
|
||||
model: {
|
||||
form: SecretsEngineForm;
|
||||
availableVersions?: EngineVersionInfo[];
|
||||
hasUnversionedPlugins?: boolean;
|
||||
pinnedVersion?: string | null;
|
||||
};
|
||||
onMountSuccess?: (type: string, path: string, useEngineRoute: boolean) => void;
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +48,12 @@ interface Args {
|
|||
* @module Mount::SecretsEngineForm
|
||||
* Modern component for mounting secrets engines using the SecretsEngineForm.
|
||||
*
|
||||
* Plugin version handling:
|
||||
* - Plugin type (built-in/external) is selected via radio cards
|
||||
* - Version dropdown appears only for external plugins
|
||||
* - When version changes, onPluginVersionChange updates model and type
|
||||
* - The model's handlePluginVersionChange method updates the type to use external plugin name if needed
|
||||
*
|
||||
* @example
|
||||
* ```hbs
|
||||
* <Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />
|
||||
|
|
@ -38,10 +64,43 @@ export default class MountSecretsEngineFormComponent extends Component<Args> {
|
|||
@service declare api: ApiService;
|
||||
@service declare capabilities: CapabilitiesService;
|
||||
@service declare router: Router;
|
||||
@service declare version: VersionService;
|
||||
|
||||
@tracked modelValidations: ValidationMap | null = null;
|
||||
@tracked formValidations: ValidationMap | null = null;
|
||||
@tracked invalidFormAlert: string | null = null;
|
||||
@tracked errorMessage: string | string[] = '';
|
||||
@tracked pluginRegistrationType: 'builtin' | 'external' = PluginRegistrationType.BUILTIN;
|
||||
@tracked selectedPluginVersion = '';
|
||||
|
||||
_originalBuiltinType = '';
|
||||
|
||||
// Plugin registration type constants
|
||||
PluginRegistrationType = PluginRegistrationType;
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
|
||||
// Store the original builtin type for restoration when switching back from external
|
||||
this._originalBuiltinType = this.args.model.form.normalizedType;
|
||||
|
||||
// Initialize plugin version
|
||||
this.configObject.plugin_version = '';
|
||||
}
|
||||
|
||||
// Helper to get config object with proper typing
|
||||
get configObject() {
|
||||
return this.args.model.form.data.config as ExtendedMountConfig;
|
||||
}
|
||||
|
||||
// Check if current plugin registration type is builtin
|
||||
get isBuiltinPlugin(): boolean {
|
||||
return this.pluginRegistrationType === PluginRegistrationType.BUILTIN;
|
||||
}
|
||||
|
||||
// Check if current plugin registration type is external
|
||||
get isExternalPlugin(): boolean {
|
||||
return this.pluginRegistrationType === PluginRegistrationType.EXTERNAL;
|
||||
}
|
||||
|
||||
get breadcrumbs() {
|
||||
const breadcrumbs: { label: string; route?: string; icon?: string }[] = [
|
||||
|
|
@ -50,26 +109,171 @@ export default class MountSecretsEngineFormComponent extends Component<Args> {
|
|||
{ label: 'Enable secrets engine', route: 'vault.cluster.secrets.enable' },
|
||||
];
|
||||
|
||||
if (this.args?.model.type) {
|
||||
breadcrumbs.push({ label: capitalize(this.args?.model?.type) });
|
||||
if (this.args?.model?.form?.normalizedType) {
|
||||
breadcrumbs.push({ label: capitalize(this.args?.model?.form?.normalizedType) });
|
||||
}
|
||||
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
get mountForm(): SecretsEngineForm {
|
||||
return this.args.model;
|
||||
get pluginTypeOptions() {
|
||||
return [
|
||||
{
|
||||
type: this.PluginRegistrationType.BUILTIN,
|
||||
icon: 'server',
|
||||
label: 'Built-in plugin',
|
||||
description:
|
||||
'Preregistered plugins shipped with Vault. The plugin version is tied to your Vault version and cannot be specified.',
|
||||
dataTestAttr: 'builtin',
|
||||
disabled: false,
|
||||
showBadge: false,
|
||||
showAlert: false,
|
||||
},
|
||||
{
|
||||
type: this.PluginRegistrationType.EXTERNAL,
|
||||
icon: 'download',
|
||||
label: 'External plugin',
|
||||
description:
|
||||
'External plugins manually registered in your plugin catalog. If multiple versions are registered, you can specify which version to enable.',
|
||||
dataTestAttr: 'external',
|
||||
disabled: this.shouldDisableExternal,
|
||||
showBadge: !this.version.isEnterprise,
|
||||
showAlert: this.shouldShowNoExternalVersionsMessage,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@action
|
||||
onKeyUp(name: string, value: string) {
|
||||
set(this.mountForm.data, name, value);
|
||||
(this.args.model.form.data as any)[name] = value;
|
||||
}
|
||||
|
||||
// Get pinned version for current external plugin
|
||||
get pinnedVersionForCurrentPlugin(): string | null {
|
||||
return this.args.model.pinnedVersion || null;
|
||||
}
|
||||
|
||||
// Check if External radio should be disabled (only built-in versions available or no Enterprise license)
|
||||
get shouldDisableExternal(): boolean {
|
||||
// Disable if no Enterprise license
|
||||
if (!this.version.isEnterprise) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Disable if no external versions available
|
||||
if (!this.args.model.availableVersions) {
|
||||
return true;
|
||||
}
|
||||
return !this.args.model.availableVersions.some((version) => !version.isBuiltin);
|
||||
}
|
||||
|
||||
// Get the external plugin name for the current engine type
|
||||
get externalPluginName(): string | null {
|
||||
const engineType = this.args.model.form.normalizedType;
|
||||
return getExternalPluginNameFromBuiltin(engineType);
|
||||
}
|
||||
|
||||
// Check if we should show info message for disabled external card due to no external versions
|
||||
get shouldShowNoExternalVersionsMessage(): boolean {
|
||||
return this.version.isEnterprise && this.shouldDisableExternal;
|
||||
}
|
||||
|
||||
// Check if plugin version field should be shown
|
||||
get shouldShowPluginVersionField(): boolean {
|
||||
// Only show for external plugins
|
||||
if (!this.isExternalPlugin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only show if we have external versions
|
||||
return this.getExternalVersionList().length > 0;
|
||||
}
|
||||
|
||||
// Get external version options with default pinned version
|
||||
get filteredVersionOptions(): string[] {
|
||||
const versionList = this.getExternalVersionList();
|
||||
if (versionList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Sort versions with pinned version first if it exists and pins are loaded
|
||||
const pinnedVersion = this.pinnedVersionForCurrentPlugin;
|
||||
if (pinnedVersion && versionList.includes(pinnedVersion)) {
|
||||
const sortedVersions = [pinnedVersion, ...versionList.filter((v) => v !== pinnedVersion)];
|
||||
return sortedVersions;
|
||||
}
|
||||
|
||||
// Sort by semantic version (highest first)
|
||||
return sortVersions(versionList, true);
|
||||
}
|
||||
|
||||
// Extract common version list filtering logic
|
||||
private getExternalVersionList(): string[] {
|
||||
const versions = this.args.model.availableVersions;
|
||||
if (!versions || !Array.isArray(versions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter external versions and exclude empty strings
|
||||
const externalVersions = versions.filter((version) => !version.isBuiltin && version.version !== '');
|
||||
return externalVersions.map((version) => version.version);
|
||||
}
|
||||
|
||||
// Check if the currently selected version differs from the pinned version
|
||||
get shouldShowPinWarning(): boolean {
|
||||
if (!this.isExternalPlugin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pinnedVersion = this.pinnedVersionForCurrentPlugin;
|
||||
const currentVersion = this.selectedPluginVersion;
|
||||
|
||||
// If there's no pinned version, no warning needed
|
||||
if (!pinnedVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the current version is undefined/empty, no warning needed
|
||||
if (!currentVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show warning if there's a pinned version and it's different from current selection
|
||||
return pinnedVersion !== currentVersion;
|
||||
}
|
||||
|
||||
// Update override flag based on version selection
|
||||
updateOverridePinnedVersionFlag() {
|
||||
// For builtin plugins, ensure override flag is not sent
|
||||
if (this.isBuiltinPlugin) {
|
||||
delete this.configObject.override_pinned_version;
|
||||
return;
|
||||
}
|
||||
|
||||
const pinnedVersion = this.pinnedVersionForCurrentPlugin;
|
||||
const currentVersion = this.configObject.plugin_version;
|
||||
|
||||
if (pinnedVersion && currentVersion && pinnedVersion !== currentVersion) {
|
||||
// User selected a version different from pinned - include both parameters
|
||||
this.configObject.plugin_version = currentVersion;
|
||||
this.configObject.override_pinned_version = true;
|
||||
} else if (pinnedVersion && currentVersion === pinnedVersion) {
|
||||
// User is using the pinned version - omit both parameters (backend will use pin)
|
||||
delete this.configObject.plugin_version;
|
||||
delete this.configObject.override_pinned_version;
|
||||
} else {
|
||||
// No pinned version exists - include plugin_version but not override flag
|
||||
this.configObject.plugin_version = currentVersion;
|
||||
delete this.configObject.override_pinned_version;
|
||||
}
|
||||
}
|
||||
|
||||
// Save KV configuration if applicable
|
||||
@action
|
||||
async saveKvConfig(path: string, formData: SecretsEngineForm['data']) {
|
||||
const { options, kv_config = {} } = formData;
|
||||
const { max_versions, cas_required, delete_version_after } = kv_config;
|
||||
const isKvV2 = options?.version === 2 && ['kv', 'generic'].includes(this.mountForm.normalizedType);
|
||||
const isKvV2 = options?.version === 2 && ['kv', 'generic'].includes(this.args.model.form.normalizedType);
|
||||
const hasConfig = max_versions || cas_required || delete_version_after;
|
||||
|
||||
if (isKvV2 && hasConfig) {
|
||||
|
|
@ -91,6 +295,8 @@ export default class MountSecretsEngineFormComponent extends Component<Args> {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle mount errors
|
||||
@action
|
||||
async onMountError(status: number, errors: unknown[] | undefined, message: string) {
|
||||
if (status === 403) {
|
||||
this.flashMessages.danger(
|
||||
|
|
@ -114,20 +320,26 @@ export default class MountSecretsEngineFormComponent extends Component<Args> {
|
|||
@task
|
||||
*mountBackend(event: Event) {
|
||||
event.preventDefault();
|
||||
const mountModel = this.mountForm;
|
||||
const mountModel = this.args.model.form;
|
||||
const { type } = mountModel;
|
||||
const { path } = mountModel.data;
|
||||
|
||||
// Handle plugin version change before validation in case onKeyUp wasn't called
|
||||
if (this.args.model.availableVersions && this.configObject.plugin_version) {
|
||||
mountModel.handlePluginVersionChange(this.args.model.availableVersions);
|
||||
}
|
||||
|
||||
// Only submit form if validations pass
|
||||
const { isValid, state, invalidFormMessage, data } = mountModel.toJSON();
|
||||
|
||||
if (!isValid) {
|
||||
this.modelValidations = state;
|
||||
this.formValidations = state;
|
||||
this.invalidFormAlert = invalidFormMessage;
|
||||
return;
|
||||
}
|
||||
|
||||
this.errorMessage = '';
|
||||
this.modelValidations = null;
|
||||
this.formValidations = null;
|
||||
this.invalidFormAlert = null;
|
||||
|
||||
try {
|
||||
|
|
@ -163,7 +375,7 @@ export default class MountSecretsEngineFormComponent extends Component<Args> {
|
|||
@action
|
||||
handleIdentityTokenKeyChange(value: string[] | string): void {
|
||||
// if array, it's coming from the search-select component, otherwise it hit the fallback component and will come in as a string.
|
||||
const { config } = this.mountForm.data;
|
||||
const { config } = this.args.model.form.data;
|
||||
config.identity_token_key = Array.isArray(value) ? value[0] : value;
|
||||
}
|
||||
|
||||
|
|
@ -171,4 +383,70 @@ export default class MountSecretsEngineFormComponent extends Component<Args> {
|
|||
goBack() {
|
||||
this.router.transitionTo('vault.cluster.secrets.enable');
|
||||
}
|
||||
|
||||
// Set default plugin version for external plugins
|
||||
private setDefaultPluginVersion() {
|
||||
const versionList = this.getExternalVersionList();
|
||||
if (versionList.length === 0) {
|
||||
this.configObject.plugin_version = '';
|
||||
this.selectedPluginVersion = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for pinned version first (pins should be loaded from constructor)
|
||||
const pinnedVersion = this.pinnedVersionForCurrentPlugin;
|
||||
|
||||
if (pinnedVersion && versionList.includes(pinnedVersion)) {
|
||||
// Use pinned version if available in catalog
|
||||
this.selectedPluginVersion = pinnedVersion;
|
||||
this.configObject.plugin_version = pinnedVersion;
|
||||
} else {
|
||||
// Use highest semantic version from catalog if no pin or pin not available
|
||||
const sortedVersions = sortVersions(versionList, true);
|
||||
const topVersion = sortedVersions[0] || '';
|
||||
this.selectedPluginVersion = topVersion;
|
||||
this.configObject.plugin_version = topVersion;
|
||||
}
|
||||
|
||||
// Update override flag based on final selection
|
||||
this.updateOverridePinnedVersionFlag();
|
||||
}
|
||||
|
||||
@action
|
||||
setPluginType(type: 'builtin' | 'external') {
|
||||
this.pluginRegistrationType = type;
|
||||
|
||||
// Update the model type based on selection
|
||||
if (type === PluginRegistrationType.BUILTIN) {
|
||||
// Use the stored original built-in type (e.g., 'keymgmt')
|
||||
this.args.model.form.type = this._originalBuiltinType;
|
||||
// Clear plugin version and override flag for built-in plugins
|
||||
this.selectedPluginVersion = '';
|
||||
this.configObject.plugin_version = '';
|
||||
delete this.configObject.override_pinned_version;
|
||||
} else {
|
||||
// Use the external plugin name (e.g., 'vault-plugin-secrets-keymgmt')
|
||||
this.args.model.form.type = this.externalPluginName || '';
|
||||
|
||||
// Set appropriate plugin version based on available versions
|
||||
this.setDefaultPluginVersion();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onPluginVersionChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
const value = target.value;
|
||||
|
||||
this.selectedPluginVersion = value;
|
||||
this.configObject.plugin_version = value;
|
||||
|
||||
// Update override flag when user manually changes version
|
||||
this.updateOverridePinnedVersionFlag();
|
||||
|
||||
// Update the type based on the selected version
|
||||
if (this.args.model.availableVersions) {
|
||||
this.args.model.form.handlePluginVersionChange(this.args.model.availableVersions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,32 +6,41 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
|
||||
import {
|
||||
supportedSecretBackends,
|
||||
SupportedSecretBackendsEnum,
|
||||
} from 'vault/helpers/supported-secret-backends';
|
||||
import engineDisplayData from 'vault/helpers/engines-display-data';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
import type SecretsEngineForm from 'vault/forms/secrets/engine';
|
||||
import type Router from '@ember/routing/router';
|
||||
import type { EngineVersionInfo } from 'vault/utils/plugin-catalog-helpers';
|
||||
|
||||
const SUPPORTED_BACKENDS = supportedSecretBackends();
|
||||
|
||||
export default class VaultClusterSecretsEnableCreateController extends Controller {
|
||||
@service declare router: Router;
|
||||
|
||||
declare model: SecretsEngineForm;
|
||||
declare model: {
|
||||
form: SecretsEngineForm;
|
||||
availableVersions: EngineVersionInfo[];
|
||||
};
|
||||
|
||||
@action
|
||||
onMountSuccess(type: string, path: string, useEngineRoute = false) {
|
||||
let transition;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (SUPPORTED_BACKENDS.includes(type as any)) {
|
||||
const engineInfo = engineDisplayData(type);
|
||||
if (engineInfo && useEngineRoute) {
|
||||
const engineInfo = engineDisplayData(type);
|
||||
const effectiveType = getEffectiveEngineType(type);
|
||||
|
||||
if (engineInfo && SUPPORTED_BACKENDS.includes(effectiveType as SupportedSecretBackendsEnum)) {
|
||||
if (useEngineRoute && engineInfo.engineRoute) {
|
||||
transition = this.router.transitionTo(
|
||||
`vault.cluster.secrets.backend.${engineInfo.engineRoute}`,
|
||||
path
|
||||
);
|
||||
} else if (engineInfo) {
|
||||
} else {
|
||||
// For keymgmt, we need to land on provider tab by default using query params
|
||||
const queryParams = engineInfo.type === 'keymgmt' ? { tab: 'provider' } : {};
|
||||
const queryParams = effectiveType === 'keymgmt' ? { tab: 'provider' } : {};
|
||||
transition = this.router.transitionTo('vault.cluster.secrets.backend.index', path, { queryParams });
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Controller from '@ember/controller';
|
|||
import { action } from '@ember/object';
|
||||
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
|
||||
import engineDisplayData from 'vault/helpers/engines-display-data';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
|
||||
const SUPPORTED_BACKENDS = supportedSecretBackends();
|
||||
|
||||
|
|
@ -22,16 +23,19 @@ export default class SecretEnableController extends Controller {
|
|||
@action
|
||||
onMountSuccess(type, path, useEngineRoute = false) {
|
||||
let transition;
|
||||
if (SUPPORTED_BACKENDS.includes(type)) {
|
||||
const engineInfo = engineDisplayData(type);
|
||||
if (useEngineRoute) {
|
||||
|
||||
const effectiveType = getEffectiveEngineType(type);
|
||||
const engineInfo = engineDisplayData(type);
|
||||
|
||||
if (SUPPORTED_BACKENDS.includes(effectiveType)) {
|
||||
if (useEngineRoute && engineInfo?.engineRoute) {
|
||||
transition = this.router.transitionTo(
|
||||
`vault.cluster.secrets.backend.${engineInfo.engineRoute}`,
|
||||
path
|
||||
);
|
||||
} else {
|
||||
// For keymgmt, we need to land on provider tab by default using query params
|
||||
const queryParams = engineInfo.type === 'keymgmt' ? { tab: 'provider' } : {};
|
||||
const queryParams = effectiveType === 'keymgmt' ? { tab: 'provider' } : {};
|
||||
transition = this.router.transitionTo('vault.cluster.secrets.backend.index', path, { queryParams });
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -3,15 +3,22 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Form from 'vault/forms/form';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import Form from 'vault/forms/form';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
import { WHITESPACE_WARNING } from 'vault/utils/forms/validators';
|
||||
|
||||
import type { Validations } from 'vault/app-types';
|
||||
import type { SecretsEngineFormData } from 'vault/secrets/engine';
|
||||
import type { EngineVersionInfo } from 'vault/utils/plugin-catalog-helpers';
|
||||
import type { AuthMethodFormData } from 'vault/vault/auth/methods';
|
||||
|
||||
type ConfigWithPluginVersion = {
|
||||
plugin_version?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
// common fields and validations shared between secrets engine and auth methods (mounts)
|
||||
// used in form classes for consistency and to avoid duplication
|
||||
export default class MountForm<T extends SecretsEngineFormData | AuthMethodFormData> extends Form<T> {
|
||||
|
|
@ -79,9 +86,83 @@ export default class MountForm<T extends SecretsEngineFormData | AuthMethodFormD
|
|||
}),
|
||||
};
|
||||
|
||||
// namespaces introduced types with a `ns_` prefix for built-in engines so we will strip that out for consistency
|
||||
// normalizes type for UI configuration purposes by:
|
||||
// 1. stripping `ns_` prefix (for namespaced types)
|
||||
// 2. mapping external plugins to their builtin equivalents for consistent UI experience
|
||||
get normalizedType() {
|
||||
return (this.type || '').replace(/^ns_/, '');
|
||||
const baseType = (this.type || '').replace(/^ns_/, '');
|
||||
return getEffectiveEngineType(baseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up plugin version configuration for the form.
|
||||
* Since plugin version is handled manually in the template, this method
|
||||
* only manages the data model setup.
|
||||
*
|
||||
* @param availableVersions - Array of available plugin versions
|
||||
*/
|
||||
setupPluginVersionField(availableVersions: EngineVersionInfo[] | null | undefined) {
|
||||
if (!availableVersions || availableVersions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize plugin_version as empty (default option)
|
||||
(this.data.config as ConfigWithPluginVersion).plugin_version = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the form data with the selected plugin version information.
|
||||
* For external plugins, this also updates the engine type to match the plugin name,
|
||||
* enabling proper mounting of external plugins with their specific names.
|
||||
*
|
||||
* @param versionInfo - The selected version information containing plugin name, version, and builtin status
|
||||
*/
|
||||
setPluginVersionData(versionInfo: EngineVersionInfo) {
|
||||
// Set the version in config
|
||||
(this.data.config as ConfigWithPluginVersion).plugin_version = versionInfo.version;
|
||||
|
||||
// For external plugins, update the type to the plugin name
|
||||
if (!versionInfo.isBuiltin) {
|
||||
this.type = versionInfo.pluginName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the version information object that matches a user-selected value.
|
||||
* This bridges the gap between the selected version value and the underlying plugin metadata needed for mounting.
|
||||
*
|
||||
* @param selectedValue - The selected version value from the UI dropdown (actual semantic version)
|
||||
* @param availableVersions - Available version options from the plugin catalog
|
||||
* @returns The matching version info or undefined if no match found
|
||||
*/
|
||||
findVersionByLabel(
|
||||
selectedValue: string,
|
||||
availableVersions: EngineVersionInfo[]
|
||||
): EngineVersionInfo | undefined {
|
||||
// Handle the empty value (default option) - return undefined so we don't send plugin_version
|
||||
if (!selectedValue || selectedValue === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return availableVersions.find((v) => v.version === selectedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles plugin version changes and updates the type if needed
|
||||
* This method should be called whenever the plugin version field changes
|
||||
*/
|
||||
handlePluginVersionChange(availableVersions: EngineVersionInfo[]) {
|
||||
const config = this.data.config as ConfigWithPluginVersion;
|
||||
const selectedVersion = config?.plugin_version;
|
||||
if (!selectedVersion || !availableVersions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the selected version info
|
||||
const selectedVersionInfo = this.findVersionByLabel(selectedVersion, availableVersions);
|
||||
if (selectedVersionInfo) {
|
||||
this.setPluginVersionData(selectedVersionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
|
@ -95,6 +176,13 @@ export default class MountForm<T extends SecretsEngineFormData | AuthMethodFormD
|
|||
listing_visibility: config?.listing_visibility ? 'unauth' : 'hidden',
|
||||
},
|
||||
};
|
||||
|
||||
// Remove plugin_version if it's empty (let server choose default)
|
||||
const configWithPluginVersion = data.config as ConfigWithPluginVersion;
|
||||
if (!configWithPluginVersion.plugin_version || configWithPluginVersion.plugin_version === '') {
|
||||
delete configWithPluginVersion.plugin_version;
|
||||
}
|
||||
|
||||
// options are only relevant for kv/generic engines
|
||||
if (!['kv', 'generic'].includes(this.type)) {
|
||||
delete data.options;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
*/
|
||||
|
||||
import MountForm from 'vault/forms/mount';
|
||||
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
|
||||
import { isKnownExternalPlugin } from 'vault/utils/external-plugin-helpers';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
import FormFieldGroup from 'vault/utils/forms/field-group';
|
||||
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
|
||||
import type { EngineVersionInfo } from 'vault/utils/plugin-catalog-helpers';
|
||||
import { isValidVersion } from 'vault/utils/version-utils';
|
||||
|
||||
import type { SecretsEngineFormData } from 'vault/secrets/engine';
|
||||
import type Form from 'vault/forms/form';
|
||||
import type { SecretsEngineFormData } from 'vault/secrets/engine';
|
||||
|
||||
export default class SecretsEngineForm extends MountForm<SecretsEngineFormData> {
|
||||
constructor(...args: ConstructorParameters<typeof Form>) {
|
||||
|
|
@ -20,6 +23,59 @@ export default class SecretsEngineForm extends MountForm<SecretsEngineFormData>
|
|||
{ type: 'number', message: 'Maximum versions must be a number.' },
|
||||
{ type: 'length', options: { min: 1, max: 16 }, message: 'You cannot go over 16 characters.' },
|
||||
];
|
||||
// add validation for plugin_version when mounting external plugins
|
||||
this.validations['config.plugin_version'] = [
|
||||
{
|
||||
validator: this.validatePluginVersionForExternalPlugins,
|
||||
message: 'Plugin version is required when mounting external plugins.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Custom validator for plugin version when mounting external plugins
|
||||
validatePluginVersionForExternalPlugins = (data: any) => {
|
||||
const pluginVersion = data?.config?.plugin_version;
|
||||
const pluginType = this.type;
|
||||
|
||||
// Check if this is a known external plugin using the proper mapping
|
||||
const isExternalPluginType = pluginType && isKnownExternalPlugin(pluginType);
|
||||
|
||||
if (isExternalPluginType) {
|
||||
// For external plugins, plugin_version is required UNLESS it's omitted due to pinned version
|
||||
// When using pinned version, the frontend omits plugin_version entirely (it gets deleted)
|
||||
// So we allow external plugin types to not have plugin_version (pinned version scenario)
|
||||
// But if plugin_version IS provided, it must be valid
|
||||
if (pluginVersion !== undefined && pluginVersion !== null) {
|
||||
return isValidVersion(pluginVersion);
|
||||
}
|
||||
// Allow external plugin types without plugin_version (pinned version case)
|
||||
return true;
|
||||
}
|
||||
|
||||
// For non-external plugin types, if a version is specified, validate it
|
||||
if (pluginVersion && pluginVersion.trim() && pluginVersion !== 'null') {
|
||||
return isValidVersion(pluginVersion);
|
||||
}
|
||||
|
||||
// For all other cases (builtin plugins without version), allow
|
||||
return true;
|
||||
};
|
||||
|
||||
// Method to handle plugin version changes and update the type accordingly
|
||||
handlePluginVersionChange(availableVersions: EngineVersionInfo[]) {
|
||||
const config = this.data.config as { plugin_version?: string };
|
||||
const pluginVersion = config?.plugin_version;
|
||||
|
||||
if (pluginVersion && availableVersions) {
|
||||
// Find the matching version info
|
||||
const versionInfo = availableVersions.find((v) => v.version === pluginVersion && !v.isBuiltin);
|
||||
|
||||
if (versionInfo) {
|
||||
// Use the external plugin name format
|
||||
const externalPluginName = versionInfo.pluginName;
|
||||
this.type = externalPluginName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method to apply type-specific side effects - called when type changes
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ export default class SecretEngineModel extends Model {
|
|||
'config.auditNonHmacResponseKeys',
|
||||
'config.passthroughRequestHeaders',
|
||||
'config.allowedResponseHeaders',
|
||||
'config.plugin_version',
|
||||
];
|
||||
|
||||
switch (this.engineType) {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,18 @@
|
|||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import SecretsEngineForm from 'vault/forms/secrets/engine';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type PluginCatalogService from 'vault/services/plugin-catalog';
|
||||
import { getExternalPluginNameFromBuiltin } from 'vault/utils/external-plugin-helpers';
|
||||
import { getAllVersionsForEngineType, type EngineVersionInfo } from 'vault/utils/plugin-catalog-helpers';
|
||||
|
||||
export default class VaultClusterSecretsEnableCreateRoute extends Route {
|
||||
model(params: { mount_type: string }) {
|
||||
@service('plugin-catalog') declare readonly pluginCatalog: PluginCatalogService;
|
||||
@service declare api: ApiService;
|
||||
|
||||
async model(params: { mount_type: string }) {
|
||||
const { mount_type } = params;
|
||||
|
||||
const defaults = {
|
||||
|
|
@ -27,6 +35,52 @@ export default class VaultClusterSecretsEnableCreateRoute extends Route {
|
|||
// Apply type-specific defaults (e.g., PKI max lease TTL)
|
||||
form.applyTypeSpecificDefaults();
|
||||
|
||||
return form;
|
||||
// Fetch plugin catalog data to get available versions for this engine type
|
||||
const pluginCatalogResponse = await this.pluginCatalog.fetchPluginCatalog();
|
||||
let availableVersions: EngineVersionInfo[] = [];
|
||||
let hasUnversionedPlugins = false;
|
||||
|
||||
if (pluginCatalogResponse.data?.detailed) {
|
||||
const versionResult = getAllVersionsForEngineType(
|
||||
pluginCatalogResponse.data.detailed,
|
||||
mount_type,
|
||||
'secret'
|
||||
);
|
||||
|
||||
availableVersions = versionResult.versions;
|
||||
hasUnversionedPlugins = versionResult.hasUnversionedPlugins;
|
||||
|
||||
// Set up the plugin version field with available versions
|
||||
form.setupPluginVersionField(availableVersions);
|
||||
}
|
||||
|
||||
// Get pinned version for this plugin type
|
||||
let pinnedVersion: string | null = null;
|
||||
|
||||
// Only fetch external pinned version if there are external versions available
|
||||
const hasExternalVersions = availableVersions.some((version) => !version.isBuiltin);
|
||||
if (hasExternalVersions) {
|
||||
try {
|
||||
// Convert builtin type to external plugin name for API call
|
||||
const externalPluginName = getExternalPluginNameFromBuiltin(mount_type);
|
||||
if (externalPluginName) {
|
||||
const response = await this.api.sys.pluginsCatalogPinsReadPinnedVersion(
|
||||
externalPluginName,
|
||||
'secret'
|
||||
);
|
||||
pinnedVersion = response?.version || null;
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently handle errors - pins are optional
|
||||
pinnedVersion = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
availableVersions,
|
||||
hasUnversionedPlugins,
|
||||
pinnedVersion,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,3 +63,20 @@ export function isKnownExternalPlugin(pluginName: string): boolean {
|
|||
export function getEffectiveEngineType(pluginType: string): string {
|
||||
return getBuiltinTypeFromExternalPlugin(pluginType) || pluginType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the external plugin name for a given builtin engine type.
|
||||
* This function performs a reverse lookup on the external plugin mapping.
|
||||
*
|
||||
* @param builtinType - The builtin engine type (e.g., "keymgmt")
|
||||
* @returns The external plugin name if a mapping exists, otherwise null
|
||||
*/
|
||||
export function getExternalPluginNameFromBuiltin(builtinType: string): string | null {
|
||||
// Find the external plugin name that maps to this builtin type
|
||||
for (const [externalName, mappedBuiltin] of Object.entries(EXTERNAL_PLUGIN_TO_BUILTIN_MAP)) {
|
||||
if (mappedBuiltin === builtinType) {
|
||||
return externalName;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
*/
|
||||
|
||||
import { isEmpty } from '@ember/utils';
|
||||
import type { EngineDisplayData } from './all-engines-metadata';
|
||||
import type { PluginCatalogPlugin } from 'vault/services/plugin-catalog';
|
||||
import type { EngineDisplayData } from './all-engines-metadata';
|
||||
import { getBuiltinTypeFromExternalPlugin, isKnownExternalPlugin } from './external-plugin-helpers';
|
||||
|
||||
/**
|
||||
* Constants for plugin catalog functionality
|
||||
|
|
@ -127,8 +128,14 @@ export function enhanceEnginesWithCatalogData(
|
|||
|
||||
// Process secret engines from the detailed array
|
||||
secretEnginesDetailed.forEach((plugin) => {
|
||||
// Skip if this plugin already exists in static metadata
|
||||
if (staticEngineTypes.has(plugin.name)) {
|
||||
// Skip if this plugin already exists in static metadata or is a builtin plugin
|
||||
if (staticEngineTypes.has(plugin.name) || plugin.builtin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip plugins that have known builtin mappings - these should appear in their
|
||||
// respective categories (e.g., KV, AWS) rather than in the "External" category
|
||||
if (isKnownExternalPlugin(plugin.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +150,7 @@ export function enhanceEnginesWithCatalogData(
|
|||
return plugin.name.includes(engine.type) || plugin.name.includes(engine.type.replace('-', ''));
|
||||
});
|
||||
|
||||
// Create external engine metadata with defaults
|
||||
// Only create external engines for custom external plugins (external plugins without mappings to builtin Vault plugins)
|
||||
const externalEngine: EnhancedEngineDisplayData = {
|
||||
type: plugin.name,
|
||||
displayName: plugin.name
|
||||
|
|
@ -212,3 +219,87 @@ export function getPluginVersionsFromEngineType(list: PluginCatalogPlugin[] | un
|
|||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Version information for a specific plugin engine
|
||||
*/
|
||||
export interface EngineVersionInfo {
|
||||
version: string;
|
||||
pluginName: string;
|
||||
isBuiltin: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result containing version information and unversioned plugin detection
|
||||
*/
|
||||
export interface EngineVersionResult {
|
||||
versions: EngineVersionInfo[];
|
||||
hasUnversionedPlugins: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all available plugin versions for a specific engine type from the catalog.
|
||||
* This enables users to choose between builtin and external plugin variants when mounting
|
||||
* secrets engines, supporting both standard Vault engines and custom external plugins.
|
||||
*
|
||||
* The function handles the mapping between external plugin names (e.g., "vault-plugin-secrets-kv")
|
||||
* and their corresponding engine types (e.g., "kv") to provide a unified version selection experience.
|
||||
*
|
||||
* @param secretEnginesDetailed - Array of detailed secret engine info from catalog API
|
||||
* @param engineType - The engine type to get versions for (e.g., 'kv', 'aws')
|
||||
* @param pluginType - Optional plugin type filter ('secret', 'auth', 'database')
|
||||
* @returns Object containing version information array and flag for unversioned plugins
|
||||
*/
|
||||
export function getAllVersionsForEngineType(
|
||||
secretEnginesDetailed: PluginCatalogPlugin[] | undefined,
|
||||
engineType: string,
|
||||
pluginType = 'secret'
|
||||
): EngineVersionResult {
|
||||
if (
|
||||
!engineType ||
|
||||
!secretEnginesDetailed ||
|
||||
typeof engineType !== 'string' ||
|
||||
!Array.isArray(secretEnginesDetailed)
|
||||
) {
|
||||
return { versions: [], hasUnversionedPlugins: false };
|
||||
}
|
||||
|
||||
let hasUnversionedPlugins = false;
|
||||
const filteredVersions: EngineVersionInfo[] = [];
|
||||
|
||||
secretEnginesDetailed.forEach((plugin) => {
|
||||
// Basic validation
|
||||
if (!plugin?.name || typeof plugin?.builtin !== 'boolean' || typeof plugin?.version !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter by plugin type (secret, auth, database)
|
||||
if (plugin.type !== pluginType) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this plugin matches the engine type
|
||||
const isDirectMatch = plugin.name === engineType;
|
||||
const builtin = getBuiltinTypeFromExternalPlugin(plugin.name);
|
||||
const isExternalMatch = builtin === engineType;
|
||||
|
||||
if (!isDirectMatch && !isExternalMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for unversioned plugins (empty version strings)
|
||||
if (plugin.version === '') {
|
||||
hasUnversionedPlugins = true;
|
||||
return; // Don't include in versions array
|
||||
}
|
||||
|
||||
// Include versioned plugins
|
||||
filteredVersions.push({
|
||||
version: plugin.version,
|
||||
pluginName: plugin.name,
|
||||
isBuiltin: plugin.builtin,
|
||||
});
|
||||
});
|
||||
|
||||
return { versions: filteredVersions, hasUnversionedPlugins };
|
||||
}
|
||||
|
|
|
|||
130
ui/app/utils/version-utils.ts
Normal file
130
ui/app/utils/version-utils.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { isKnownExternalPlugin } from 'vault/utils/external-plugin-helpers';
|
||||
|
||||
/**
|
||||
* Utility functions for semantic version handling
|
||||
*/
|
||||
|
||||
/**
|
||||
* Clean a version string by removing prefixes and suffixes
|
||||
* @param version - The version string to clean (e.g., "v1.2.3+ent")
|
||||
* @returns The cleaned version string (e.g., "1.2.3")
|
||||
*/
|
||||
export function cleanVersion(version: string): string {
|
||||
return version.replace(/^v/, '').split(/[+-]/)[0] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a version string into numeric parts
|
||||
* @param version - The version string to parse
|
||||
* @returns Array of numeric version parts
|
||||
*/
|
||||
export function parseVersion(version: string): number[] {
|
||||
const cleanVer = cleanVersion(version);
|
||||
return cleanVer.split('.').map((n) => parseInt(n) || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two version strings using semantic version rules
|
||||
* @param a - First version to compare
|
||||
* @param b - Second version to compare
|
||||
* @returns Negative if a < b, positive if a > b, 0 if equal
|
||||
*/
|
||||
export function compareVersions(a: string, b: string): number {
|
||||
const aParts = parseVersion(a);
|
||||
const bParts = parseVersion(b);
|
||||
|
||||
const maxLength = Math.max(aParts.length, bParts.length);
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const aPart = aParts[i] || 0;
|
||||
const bPart = bParts[i] || 0;
|
||||
|
||||
if (aPart !== bPart) {
|
||||
return aPart - bPart;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort an array of version strings in semantic version order
|
||||
* @param versions - Array of version strings to sort
|
||||
* @param descending - If true, sort highest version first (default: false)
|
||||
* @returns New sorted array (does not mutate original)
|
||||
*/
|
||||
export function sortVersions(versions: string[], descending = false): string[] {
|
||||
const sorted = versions.slice().sort((a, b) => compareVersions(a, b));
|
||||
return descending ? sorted.reverse() : sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the highest version from an array of version strings
|
||||
* @param versions - Array of version strings
|
||||
* @returns The highest version string, or null if array is empty
|
||||
*/
|
||||
export function getHighestVersion(versions: string[]): string | null {
|
||||
if (versions.length === 0) return null;
|
||||
|
||||
const sorted = sortVersions(versions, true);
|
||||
return sorted[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if version A is greater than version B
|
||||
* @param a - First version
|
||||
* @param b - Second version
|
||||
* @returns True if a > b
|
||||
*/
|
||||
export function isVersionGreater(a: string, b: string): boolean {
|
||||
return compareVersions(a, b) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two versions are equal
|
||||
* @param a - First version
|
||||
* @param b - Second version
|
||||
* @returns True if versions are equal
|
||||
*/
|
||||
export function areVersionsEqual(a: string, b: string): boolean {
|
||||
return compareVersions(a, b) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a version string is valid and non-empty
|
||||
* @param version - The version string to validate
|
||||
* @returns True if the version is valid
|
||||
*/
|
||||
export function isValidVersion(version: string): boolean {
|
||||
if (!version || typeof version !== 'string') return false;
|
||||
|
||||
const trimmed = version.trim();
|
||||
if (trimmed === '' || trimmed === 'null') return false;
|
||||
|
||||
// Basic semantic version pattern check (allows prefixes like 'v' and suffixes like '+ent')
|
||||
const semverPattern = /^v?\d+(\.\d+)*([+-].+)?$/;
|
||||
const cleanVer = cleanVersion(trimmed);
|
||||
|
||||
return semverPattern.test(`v${cleanVer}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a plugin version is required for external plugins
|
||||
* @param pluginType - The plugin type (e.g., 'keymgmt' or 'vault-plugin-secrets-keymgmt')
|
||||
* @param pluginVersion - The plugin version string
|
||||
* @returns True if the plugin version requirement is satisfied
|
||||
*/
|
||||
export function isPluginVersionValidForType(pluginType: string, pluginVersion?: string): boolean {
|
||||
if (!pluginType) return false;
|
||||
|
||||
if (isKnownExternalPlugin(pluginType)) {
|
||||
// External plugins require a valid version
|
||||
return isValidVersion(pluginVersion || '');
|
||||
} else {
|
||||
// Builtin plugins should not have a version specified
|
||||
return !pluginVersion || pluginVersion.trim() === '' || pluginVersion.trim() === 'null';
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,6 @@ module('Acceptance | auth config form', function (hooks) {
|
|||
'config.audit_non_hmac_response_keys',
|
||||
'config.passthrough_request_headers',
|
||||
'config.allowed_response_headers',
|
||||
'config.plugin_version',
|
||||
];
|
||||
this.tokensGroup = {
|
||||
Tokens: [
|
||||
|
|
|
|||
|
|
@ -3,39 +3,39 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test, skip } from 'qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
click,
|
||||
currentRouteName,
|
||||
currentURL,
|
||||
fillIn,
|
||||
find,
|
||||
findAll,
|
||||
fillIn,
|
||||
typeIn,
|
||||
visit,
|
||||
waitUntil,
|
||||
waitFor,
|
||||
waitUntil,
|
||||
} from '@ember/test-helpers';
|
||||
import { module, skip, test } from 'qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { setupApplicationTest } from 'vault/tests/helpers';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import {
|
||||
createPolicyCmd,
|
||||
createTokenCmd,
|
||||
deleteEngineCmd,
|
||||
mountEngineCmd,
|
||||
runCmd,
|
||||
createTokenCmd,
|
||||
tokenWithPolicyCmd,
|
||||
} from 'vault/tests/helpers/commands';
|
||||
import { personas } from 'vault/tests/helpers/kv/policy-generator';
|
||||
import { grantAccess, setupControlGroup } from 'vault/tests/helpers/control-groups';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import {
|
||||
addSecretMetadataCmd,
|
||||
writeSecret,
|
||||
writeVersionedSecret,
|
||||
} from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { setupControlGroup, grantAccess } from 'vault/tests/helpers/control-groups';
|
||||
import { personas } from 'vault/tests/helpers/kv/policy-generator';
|
||||
|
||||
const secretPath = `my-#:$=?-secret`;
|
||||
// This doesn't encode in a normal way, so hardcoding it here until we sort that out
|
||||
|
|
@ -44,6 +44,9 @@ const secretPathUrlEncoded = `my-%23:$=%3F-secret`;
|
|||
const ALL_TABS = ['Overview', 'Secret', 'Metadata', 'Paths', 'Version History'];
|
||||
const navToBackend = async (backend) => {
|
||||
await visit(`/vault/secrets-engines`);
|
||||
// Use search to find the specific backend instead of relying on pagination
|
||||
await fillIn(GENERAL.inputSearch('secret-engine-path'), backend);
|
||||
await waitUntil(() => find(`${GENERAL.tableData(`${backend}/`, 'path')} a`));
|
||||
return click(`${GENERAL.tableData(`${backend}/`, 'path')} a`);
|
||||
};
|
||||
const assertPolicyGenerator = async (assert, expectedPaths) => {
|
||||
|
|
|
|||
|
|
@ -3,18 +3,19 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, click, fillIn } from '@ember/test-helpers';
|
||||
import { click, fillIn, render } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { allowAllCapabilitiesStub, noopStub } from 'vault/tests/helpers/stubs';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { module, test } from 'qunit';
|
||||
import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { allowAllCapabilitiesStub, noopStub } from 'vault/tests/helpers/stubs';
|
||||
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
|
||||
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
import AuthMethodForm from 'vault/forms/auth/method';
|
||||
import SecretsEngineForm from 'vault/forms/secrets/engine';
|
||||
|
||||
module('Integration | Component | mount backend form', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
|
@ -39,7 +40,6 @@ module('Integration | Component | mount backend form', function (hooks) {
|
|||
});
|
||||
|
||||
test('it renders default state', async function (assert) {
|
||||
assert.expect(15);
|
||||
await render(
|
||||
hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
);
|
||||
|
|
@ -103,8 +103,6 @@ module('Integration | Component | mount backend form', function (hooks) {
|
|||
});
|
||||
|
||||
test('it calls mount success', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.server.post('/sys/auth/foo', () => {
|
||||
assert.ok(true, 'it calls enable on an auth method');
|
||||
return [204, { 'Content-Type': 'application/json' }];
|
||||
|
|
@ -124,4 +122,319 @@ module('Integration | Component | mount backend form', function (hooks) {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('Plugin Version Selection Integration (Community)', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
// Get version service for mocking in individual tests
|
||||
this.version = this.owner.lookup('service:version');
|
||||
|
||||
// Mock plugin pins API endpoint
|
||||
this.server.get('/sys/plugins/pins', () => {
|
||||
return {
|
||||
data: {
|
||||
pinned_versions: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Set up secrets engine form with KV type already selected
|
||||
const defaults = {
|
||||
config: {},
|
||||
kv_config: {
|
||||
max_versions: 0,
|
||||
cas_required: false,
|
||||
delete_version_after: 0,
|
||||
},
|
||||
options: { version: 2 },
|
||||
};
|
||||
this.form = new SecretsEngineForm(defaults, { isNew: true });
|
||||
this.form.type = 'kv'; // Pre-select KV type to skip type selection
|
||||
this.form.data.path = 'test-path'; // Set a test path
|
||||
|
||||
// Mock mount success handler
|
||||
this.onMountSuccess = sinon.spy();
|
||||
|
||||
// Mock available versions (as would be passed from route)
|
||||
this.availableVersions = [
|
||||
{
|
||||
version: 'v1.16.1+builtin',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: true,
|
||||
sha256: 'abc123',
|
||||
},
|
||||
{
|
||||
version: 'v0.25.0',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: false,
|
||||
sha256: 'def456',
|
||||
},
|
||||
];
|
||||
|
||||
this.renderComponent = () =>
|
||||
render(hbs`
|
||||
<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>
|
||||
`);
|
||||
|
||||
// Helper function to create a fresh model with new available versions
|
||||
this.createFreshModel = (type = 'kv', availableVersions = this.availableVersions) => {
|
||||
const defaults = {
|
||||
config: {},
|
||||
kv_config: {
|
||||
max_versions: 0,
|
||||
cas_required: false,
|
||||
delete_version_after: 0,
|
||||
},
|
||||
options: { version: 2 },
|
||||
};
|
||||
this.form = new SecretsEngineForm(defaults, { isNew: true });
|
||||
this.form.type = type;
|
||||
this.form.data.path = 'test-path';
|
||||
|
||||
// Update the model structure with new data
|
||||
this.model = {
|
||||
form: this.form,
|
||||
availableVersions: availableVersions,
|
||||
hasUnversionedPlugins: false,
|
||||
pinnedVersion: null, // Will be set per test as needed
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize with default model
|
||||
this.createFreshModel();
|
||||
});
|
||||
|
||||
test('plugin version field is hidden when only builtin versions available', async function (assert) {
|
||||
// Mock enterprise mode (even with enterprise, no external versions means no version field)
|
||||
this.version.type = 'enterprise';
|
||||
|
||||
// Mock single builtin version response
|
||||
this.availableVersions = [
|
||||
{
|
||||
version: 'v1.16.1+builtin',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: true,
|
||||
sha256: 'abc123',
|
||||
},
|
||||
];
|
||||
|
||||
// Create a fresh model for this test with only builtin versions
|
||||
this.createFreshModel('kv', this.availableVersions);
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
// External radio card should be disabled when no external versions are available
|
||||
assert
|
||||
.dom(`input${GENERAL.radioCardByAttr('external')}`)
|
||||
.isDisabled('external radio card is disabled when only builtin versions available');
|
||||
|
||||
// With only builtin versions, external radio should be disabled and version field hidden
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.doesNotExist('plugin version field is hidden when only builtin versions available');
|
||||
|
||||
// Check for user messaging about why external option is disabled
|
||||
assert
|
||||
.dom(GENERAL.inlineAlert)
|
||||
.exists('info message explains why external option is disabled when no external versions available');
|
||||
});
|
||||
|
||||
test('external radio card is disabled for community version', async function (assert) {
|
||||
// Mock version service to simulate community mode
|
||||
this.version.type = 'community';
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
// External radio card should be disabled in community mode
|
||||
assert
|
||||
.dom(`input${GENERAL.radioCardByAttr('external')}`)
|
||||
.isDisabled('external radio card is disabled for community version');
|
||||
|
||||
// Enterprise badge should be visible for community users
|
||||
assert
|
||||
.dom(GENERAL.badge('external-enterprise'))
|
||||
.hasText('Enterprise', 'Enterprise badge is shown for community users');
|
||||
|
||||
// Plugin version field should not be visible
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.doesNotExist('plugin version field is hidden when external card is disabled');
|
||||
});
|
||||
|
||||
module('Plugin Version Selection Integration (Ent)', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
// Set enterprise mode for all tests in this module
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
test('plugin version field shows when multiple versions available', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
// External radio card should not be disabled in enterprise mode
|
||||
assert
|
||||
.dom(`input${GENERAL.radioCardByAttr('external')}`)
|
||||
.isNotDisabled('external radio card is enabled for enterprise version');
|
||||
|
||||
// Initially, version field should not be visible (builtin is default)
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.doesNotExist('plugin version field is hidden initially with builtin selection');
|
||||
|
||||
// Click External radio card to enable version selection
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
// Now version field should appear
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.exists('plugin version field appears when external is selected');
|
||||
|
||||
// HDS Select component generates a different DOM structure
|
||||
assert
|
||||
.dom(`${GENERAL.fieldByAttr('config.plugin_version')} .hds-form-select`)
|
||||
.exists('plugin version field uses HDS select component');
|
||||
});
|
||||
|
||||
test('selecting default version omits plugin_version from payload', async function (assert) {
|
||||
// Mock successful mount request
|
||||
this.server.post('/sys/mounts/test-path', (schema, request) => {
|
||||
const payload = JSON.parse(request.requestBody);
|
||||
const hasPluginVersion =
|
||||
Object.prototype.hasOwnProperty.call(payload, 'config') &&
|
||||
Object.prototype.hasOwnProperty.call(payload.config, 'plugin_version');
|
||||
assert.notOk(
|
||||
hasPluginVersion,
|
||||
'plugin_version is not included in payload when default is selected'
|
||||
);
|
||||
assert.strictEqual(payload.type, 'kv', 'correct engine type is sent');
|
||||
return {};
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
// Builtin is default selection (no plugin version field visible), so just submit
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
|
||||
test('builtin plugin type selected by default sends correct payload', async function (assert) {
|
||||
// Mock successful mount request
|
||||
this.server.post('/sys/mounts/test-path', (schema, request) => {
|
||||
const payload = JSON.parse(request.requestBody);
|
||||
// With builtin selected (default), no plugin_version should be sent
|
||||
const hasPluginVersion =
|
||||
Object.prototype.hasOwnProperty.call(payload, 'config') &&
|
||||
Object.prototype.hasOwnProperty.call(payload.config, 'plugin_version');
|
||||
assert.notOk(hasPluginVersion, 'plugin_version is not included for builtin selection');
|
||||
assert.strictEqual(payload.type, 'kv', 'type remains builtin type for builtin plugins');
|
||||
return {};
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
// Builtin is selected by default, no version field shown, just submit
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
|
||||
test('selecting external version includes plugin_version in payload with external type', async function (assert) {
|
||||
// Mock successful mount request
|
||||
this.server.post('/sys/mounts/test-path', (schema, request) => {
|
||||
const payload = JSON.parse(request.requestBody);
|
||||
assert.strictEqual(
|
||||
payload.config.plugin_version,
|
||||
'v0.25.0',
|
||||
'plugin_version is included for external version'
|
||||
);
|
||||
assert.strictEqual(
|
||||
payload.type,
|
||||
'vault-plugin-secrets-kv',
|
||||
'type is external plugin name for external plugins'
|
||||
);
|
||||
return {};
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
// Click External radio card to enable version selection
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
// Select the external version from the dropdown
|
||||
await fillIn(GENERAL.selectByAttr('plugin-version'), 'v0.25.0');
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
|
||||
test('external radio card is enabled but version field is hidden when plugin has empty version', async function (assert) {
|
||||
// Create availableVersions with a plugin that has an empty version (registered without version)
|
||||
this.availableVersions = [
|
||||
{
|
||||
version: '', // Empty version when plugin registered without version
|
||||
pluginName: 'vault-plugin-secrets-keymgmt',
|
||||
isBuiltin: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Create a fresh model for this test with empty version plugins
|
||||
this.createFreshModel('keymgmt', this.availableVersions);
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
// External radio card should NOT be disabled
|
||||
assert
|
||||
.dom(`input${GENERAL.radioCardByAttr('external')}`)
|
||||
.isNotDisabled('external radio card is enabled when plugin has empty version');
|
||||
|
||||
// Click External radio card to enable version selection
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
// Version field should NOT appear since only empty versions exist
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.doesNotExist('plugin version field is hidden when only empty version plugins exist');
|
||||
});
|
||||
|
||||
test('version field shows with filtered options when plugin has both empty and non-empty versions', async function (assert) {
|
||||
// Create availableVersions with both empty and non-empty versions
|
||||
this.availableVersions = [
|
||||
{
|
||||
version: 'v1.16.1+builtin',
|
||||
pluginName: 'vault-plugin-secrets-keymgmt',
|
||||
isBuiltin: true,
|
||||
},
|
||||
{
|
||||
version: '', // Empty version (should be filtered out)
|
||||
pluginName: 'vault-plugin-secrets-keymgmt',
|
||||
isBuiltin: false,
|
||||
},
|
||||
{
|
||||
version: 'v1.0.0', // Non-empty external version
|
||||
pluginName: 'vault-plugin-secrets-keymgmt',
|
||||
isBuiltin: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Create a fresh model for this test with mixed versions
|
||||
this.createFreshModel('keymgmt', this.availableVersions);
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
// External radio card should NOT be disabled
|
||||
assert
|
||||
.dom(`input${GENERAL.radioCardByAttr('external')}`)
|
||||
.isNotDisabled('external radio card is enabled when plugin has valid external versions');
|
||||
|
||||
// Click External radio card to enable version selection
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
// Version field should appear since we have non-empty external versions
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.exists('plugin version field appears when non-empty external versions exist');
|
||||
|
||||
assert
|
||||
.dom(`${GENERAL.fieldByAttr('config.plugin_version')} option[value="v1.0.0"]`)
|
||||
.exists('non-empty external version option exists');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, click, typeIn, fillIn } from '@ember/test-helpers';
|
||||
import { click, fillIn, render, typeIn } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { module, test } from 'qunit';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import {
|
||||
allowAllCapabilitiesStub,
|
||||
capabilitiesStub,
|
||||
noopStub,
|
||||
overrideResponse,
|
||||
} from 'vault/tests/helpers/stubs';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
|
||||
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
|
@ -43,7 +43,13 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
},
|
||||
options: { version: 2 },
|
||||
};
|
||||
this.model = new SecretsEngineForm(defaults, { isNew: true });
|
||||
this.form = new SecretsEngineForm(defaults, { isNew: true });
|
||||
|
||||
this.model = {
|
||||
form: this.form,
|
||||
availableVersions: [],
|
||||
hasUnversionedPlugins: false,
|
||||
};
|
||||
});
|
||||
|
||||
test('it renders secret engine form', async function (assert) {
|
||||
|
|
@ -56,8 +62,8 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
});
|
||||
|
||||
test('it changes path when type is set', async function (assert) {
|
||||
this.model.type = 'azure';
|
||||
this.model.data.path = 'azure'; // Set path to match type as would happen in the route
|
||||
this.form.type = 'azure';
|
||||
this.form.data.path = 'azure'; // Set path to match type as would happen in the route
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
);
|
||||
|
|
@ -65,8 +71,8 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
});
|
||||
|
||||
test('it keeps custom path value', async function (assert) {
|
||||
this.model.type = 'kv';
|
||||
this.model.data.path = 'custom-path';
|
||||
this.form.type = 'kv';
|
||||
this.form.data.path = 'custom-path';
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
);
|
||||
|
|
@ -83,8 +89,8 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
const spy = sinon.spy();
|
||||
this.set('onMountSuccess', spy);
|
||||
|
||||
this.model.type = 'ssh';
|
||||
this.model.data.path = 'foo';
|
||||
this.form.type = 'ssh';
|
||||
this.form.data.path = 'foo';
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
|
|
@ -101,7 +107,7 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
|
||||
module('KV engine', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.model.type = 'kv';
|
||||
this.form.type = 'kv';
|
||||
});
|
||||
|
||||
test('it shows KV specific fields when type is kv', async function (assert) {
|
||||
|
|
@ -150,12 +156,12 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
module('WIF secret engines', function () {
|
||||
test('it shows identity_token_key when type is a WIF engine and hides when its not', async function (assert) {
|
||||
// Test AWS (a WIF engine)
|
||||
this.model.type = 'aws';
|
||||
this.model.applyTypeSpecificDefaults();
|
||||
this.form.type = 'aws';
|
||||
this.form.applyTypeSpecificDefaults();
|
||||
|
||||
// Initialize config object for WIF engines
|
||||
if (!this.model.data.config) {
|
||||
this.model.data.config = {};
|
||||
if (!this.form.data.config) {
|
||||
this.form.data.config = {};
|
||||
}
|
||||
|
||||
await render(
|
||||
|
|
@ -163,18 +169,18 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
);
|
||||
|
||||
// First check if the Method Options group is being rendered at all
|
||||
assert.dom('[data-test-button="Method Options"]').exists('Method Options toggle button exists');
|
||||
assert.dom(GENERAL.button('Method Options')).exists('Method Options toggle button exists');
|
||||
|
||||
// Click to expand Method Options if it's collapsed
|
||||
await click('[data-test-button="Method Options"]');
|
||||
await click(GENERAL.button('Method Options'));
|
||||
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.identity_token_key'))
|
||||
.exists('Identity token key field shows for AWS engine');
|
||||
|
||||
// Test KV (not a WIF engine)
|
||||
this.model.type = 'kv';
|
||||
this.model.applyTypeSpecificDefaults();
|
||||
this.form.type = 'kv';
|
||||
this.form.applyTypeSpecificDefaults();
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
|
|
@ -186,11 +192,11 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
});
|
||||
|
||||
test('it updates identity_token_key if user has changed it', async function (assert) {
|
||||
this.model.type = WIF_ENGINES[0]; // Use first WIF engine
|
||||
this.model.applyTypeSpecificDefaults();
|
||||
this.form.type = WIF_ENGINES[0]; // Use first WIF engine
|
||||
this.form.applyTypeSpecificDefaults();
|
||||
// Initialize config object
|
||||
if (!this.model.data.config) {
|
||||
this.model.data.config = {};
|
||||
if (!this.form.data.config) {
|
||||
this.form.data.config = {};
|
||||
}
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
|
|
@ -200,7 +206,7 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
await click(GENERAL.button('Method Options'));
|
||||
|
||||
assert.strictEqual(
|
||||
this.model.data.config.identity_token_key,
|
||||
this.form.data.config.identity_token_key,
|
||||
undefined,
|
||||
'On init identity_token_key is not set on the model'
|
||||
);
|
||||
|
|
@ -209,7 +215,7 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
await typeIn(GENERAL.inputSearch('key'), 'specialKey');
|
||||
|
||||
assert.strictEqual(
|
||||
this.model.data.config.identity_token_key,
|
||||
this.form.data.config.identity_token_key,
|
||||
'specialKey',
|
||||
'updates model with custom identity_token_key'
|
||||
);
|
||||
|
|
@ -218,14 +224,541 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
|
||||
module('PKI engine', function () {
|
||||
test('it sets default max lease TTL for PKI', async function (assert) {
|
||||
this.model.type = 'pki';
|
||||
this.model.applyTypeSpecificDefaults();
|
||||
this.form.type = 'pki';
|
||||
this.form.applyTypeSpecificDefaults();
|
||||
|
||||
assert.strictEqual(
|
||||
this.model.data.config.max_lease_ttl,
|
||||
this.form.data.config.max_lease_ttl,
|
||||
'3650d',
|
||||
'sets PKI default max lease TTL to 10 years'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('Plugin registration and versioning', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.form.type = 'keymgmt';
|
||||
this.form.data.path = 'keymgmt';
|
||||
|
||||
// Mock version service for enterprise checks
|
||||
this.versionService = this.owner.lookup('service:version');
|
||||
sinon.stub(this.versionService, 'isEnterprise').value(true);
|
||||
|
||||
// Setup available versions for testing and add to model structure
|
||||
const availableVersions = [
|
||||
{ version: '1.0.0', pluginName: 'vault-plugin-secrets-keymgmt', isBuiltin: false },
|
||||
{ version: '1.1.0', pluginName: 'vault-plugin-secrets-keymgmt', isBuiltin: false },
|
||||
{ version: '2.0.0', pluginName: 'vault-plugin-secrets-keymgmt', isBuiltin: false },
|
||||
{ version: '', pluginName: 'keymgmt', isBuiltin: true }, // Built-in version
|
||||
];
|
||||
this.availableVersions = availableVersions;
|
||||
this.model.availableVersions = availableVersions;
|
||||
});
|
||||
|
||||
test('it renders plugin type selection radio cards', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
);
|
||||
|
||||
assert.dom(`input${GENERAL.radioCardByAttr('builtin')}`).exists('shows built-in plugin radio card');
|
||||
assert.dom(`input${GENERAL.radioCardByAttr('external')}`).exists('shows external plugin radio card');
|
||||
assert
|
||||
.dom(`input${GENERAL.radioCardByAttr('builtin')}`)
|
||||
.isChecked('built-in plugin is selected by default');
|
||||
assert
|
||||
.dom(`input${GENERAL.radioCardByAttr('external')}`)
|
||||
.isNotChecked('external plugin is not selected by default');
|
||||
});
|
||||
|
||||
test('it defaults to built-in plugin type', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.doesNotExist('plugin version field is hidden for built-in');
|
||||
assert.strictEqual(this.form.type, 'keymgmt', 'model type remains as built-in name');
|
||||
});
|
||||
|
||||
test('it shows plugin version field when external plugin is selected', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.exists('plugin version field appears for external');
|
||||
assert.dom(GENERAL.selectByAttr('plugin-version')).exists('plugin version select is rendered');
|
||||
assert.strictEqual(
|
||||
this.form.type,
|
||||
'vault-plugin-secrets-keymgmt',
|
||||
'model type updates to external plugin name'
|
||||
);
|
||||
});
|
||||
|
||||
test('it populates version dropdown with sorted options', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
// Note: version option selectors may need custom data-test attributes
|
||||
assert.dom('[data-test-version-option="2.0.0"]').exists('includes version 2.0.0');
|
||||
assert.dom('[data-test-version-option="1.1.0"]').exists('includes version 1.1.0');
|
||||
assert.dom('[data-test-version-option="1.0.0"]').exists('includes version 1.0.0');
|
||||
});
|
||||
|
||||
test('it disables external plugin when no enterprise license', async function (assert) {
|
||||
this.versionService.isEnterprise = false;
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(`input${GENERAL.radioCardByAttr('external')}`)
|
||||
.isDisabled('external plugin is disabled without enterprise');
|
||||
assert.dom('.hds-badge').hasText('Enterprise', 'shows enterprise badge');
|
||||
});
|
||||
|
||||
test('it disables external plugin when no external versions available', async function (assert) {
|
||||
this.model.availableVersions = [{ version: '', pluginName: 'keymgmt', isBuiltin: true }];
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
assert
|
||||
.dom(`input${GENERAL.radioCardByAttr('external')}`)
|
||||
.isDisabled('external plugin is disabled when no external versions');
|
||||
});
|
||||
|
||||
test('it updates plugin version when selection changes', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
await fillIn(GENERAL.selectByAttr('plugin-version'), '1.0.0');
|
||||
|
||||
assert.strictEqual(
|
||||
this.form.data.config.plugin_version,
|
||||
'1.0.0',
|
||||
'updates model config with selected version'
|
||||
);
|
||||
});
|
||||
|
||||
test('it clears plugin version when switching back to built-in', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
// Select external and set version
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
await fillIn(GENERAL.selectByAttr('plugin-version'), '1.0.0');
|
||||
|
||||
// Switch back to built-in
|
||||
await click(`input${GENERAL.radioCardByAttr('builtin')}`);
|
||||
|
||||
assert.strictEqual(this.form.data.config.plugin_version, '', 'clears plugin version for built-in');
|
||||
assert.strictEqual(this.form.type, 'keymgmt', 'resets model type to built-in name');
|
||||
assert.dom(GENERAL.fieldByAttr('config.plugin_version')).doesNotExist('hides plugin version field');
|
||||
});
|
||||
|
||||
test('it shows unversioned plugins warning when hasUnversionedPlugins is true', async function (assert) {
|
||||
this.model.hasUnversionedPlugins = true;
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
assert
|
||||
.dom(GENERAL.helpTextByAttr('config.plugin_version'))
|
||||
.containsText(
|
||||
'Un-versioned plugins are not supported, they must be enabled via CLI',
|
||||
'shows unversioned plugins warning'
|
||||
);
|
||||
});
|
||||
|
||||
test('it hides unversioned plugins warning when hasUnversionedPlugins is false', async function (assert) {
|
||||
this.model.hasUnversionedPlugins = false;
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.doesNotContainText('Un-versioned plugins are not supported', 'hides unversioned plugins warning');
|
||||
});
|
||||
|
||||
test('it hides unversioned plugins warning when hasUnversionedPlugins is not provided', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.doesNotContainText(
|
||||
'Un-versioned plugins are not supported',
|
||||
'hides unversioned plugins warning when property not provided'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('Plugin pins integration', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.form.type = 'keymgmt';
|
||||
this.form.data.path = 'keymgmt';
|
||||
this.versionService = this.owner.lookup('service:version');
|
||||
sinon.stub(this.versionService, 'isEnterprise').value(true);
|
||||
|
||||
const availableVersions = [
|
||||
{ version: '1.0.0', pluginName: 'vault-plugin-secrets-keymgmt', isBuiltin: false },
|
||||
{ version: '1.1.0', pluginName: 'vault-plugin-secrets-keymgmt', isBuiltin: false },
|
||||
{ version: '2.0.0', pluginName: 'vault-plugin-secrets-keymgmt', isBuiltin: false },
|
||||
];
|
||||
this.availableVersions = availableVersions;
|
||||
this.model.availableVersions = availableVersions;
|
||||
|
||||
// Add pinned version to model data for tests
|
||||
this.model.pinnedVersion = '1.1.0';
|
||||
});
|
||||
|
||||
test('it shows pinned version first in dropdown', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
// Check that pinned version is selected by default
|
||||
assert
|
||||
.dom(GENERAL.selectByAttr('plugin-version'))
|
||||
.hasValue('1.1.0', 'pinned version is selected by default');
|
||||
|
||||
// Check pinned label appears
|
||||
assert
|
||||
.dom('[data-test-version-option="1.1.0"]')
|
||||
.hasText('1.1.0 (pinned)', 'shows pinned label in dropdown');
|
||||
});
|
||||
|
||||
test('it shows pinned version in helper text', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
assert
|
||||
.dom(`${GENERAL.fieldByAttr('config.plugin_version')} .hds-form-helper-text`)
|
||||
.containsText('1.1.0 is pinned', 'shows pinned version in helper text');
|
||||
});
|
||||
|
||||
test('it shows warning when selecting non-pinned version', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
// Wait for external plugin to be selected and version field to appear
|
||||
|
||||
await fillIn(GENERAL.selectByAttr('plugin-version'), '2.0.0');
|
||||
// Wait for warning logic to process
|
||||
|
||||
assert.dom('.hds-alert').exists('shows warning alert');
|
||||
assert
|
||||
.dom('.hds-alert .hds-alert__title')
|
||||
.hasText('Version differs from pinned', 'shows correct warning title');
|
||||
assert
|
||||
.dom('.hds-alert .hds-alert__description')
|
||||
.containsText(
|
||||
'You have selected 2.0.0, but version 1.1.0 is pinned',
|
||||
'shows correct warning description'
|
||||
);
|
||||
});
|
||||
|
||||
test('it does not show warning when using pinned version', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
// Pinned version should be selected by default, no need to change
|
||||
|
||||
assert.dom('.hds-alert--color-warning').doesNotExist('does not show warning when using pinned version');
|
||||
});
|
||||
|
||||
test('it handles plugins with no pins correctly', async function (assert) {
|
||||
// Clear pinned version
|
||||
this.model.pinnedVersion = null;
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
// Should default to highest semantic version
|
||||
assert
|
||||
.dom(GENERAL.selectByAttr('plugin-version'))
|
||||
.hasValue('2.0.0', 'defaults to highest version when no pins');
|
||||
assert
|
||||
.dom(`${GENERAL.fieldByAttr('config.plugin_version')} .hds-form-helper-text`)
|
||||
.doesNotContainText('pinned', 'does not show pinned text when no pins');
|
||||
assert.dom('.hds-alert--color-warning').doesNotExist('does not show warning when no pins');
|
||||
});
|
||||
});
|
||||
|
||||
module('Plugin version configuration handling', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.form.type = 'keymgmt';
|
||||
this.form.data.path = 'keymgmt';
|
||||
this.versionService = this.owner.lookup('service:version');
|
||||
sinon.stub(this.versionService, 'isEnterprise').value(true);
|
||||
|
||||
const availableVersions = [
|
||||
{ version: '1.0.0', pluginName: 'vault-plugin-secrets-keymgmt', isBuiltin: false },
|
||||
{ version: '2.0.0', pluginName: 'vault-plugin-secrets-keymgmt', isBuiltin: false },
|
||||
];
|
||||
this.availableVersions = availableVersions;
|
||||
this.model.availableVersions = availableVersions;
|
||||
|
||||
// No pinned version for this test
|
||||
this.model.pinnedVersion = null;
|
||||
});
|
||||
|
||||
test('it includes plugin_version in config for external plugins', async function (assert) {
|
||||
this.server.post('/sys/mounts/keymgmt', (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(
|
||||
payload.config.plugin_version,
|
||||
'2.0.0',
|
||||
'includes plugin_version in mount request'
|
||||
);
|
||||
assert.false(
|
||||
Object.hasOwn(payload.config, 'override_pinned_version'),
|
||||
'does not include override flag when no pins'
|
||||
);
|
||||
return [204, { 'Content-Type': 'application/json' }];
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
|
||||
test('it includes override flag when using non-pinned version', async function (assert) {
|
||||
// Set pinned version for keymgmt plugin
|
||||
this.model.pinnedVersion = '1.0.0';
|
||||
|
||||
this.server.post('/sys/mounts/keymgmt', (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(payload.config.plugin_version, '2.0.0', 'includes selected plugin_version');
|
||||
assert.true(
|
||||
payload.config.override_pinned_version,
|
||||
'includes override flag when using non-pinned version'
|
||||
);
|
||||
return [204, { 'Content-Type': 'application/json' }];
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
await fillIn(GENERAL.selectByAttr('plugin-version'), '2.0.0');
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
|
||||
test('it omits plugin_version when using pinned version', async function (assert) {
|
||||
// Set pinned version for keymgmt plugin
|
||||
this.model.pinnedVersion = '1.0.0';
|
||||
|
||||
this.server.post('/sys/mounts/keymgmt', (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.false(
|
||||
Object.hasOwn(payload.config, 'plugin_version'),
|
||||
'omits plugin_version when using pinned version'
|
||||
);
|
||||
assert.false(
|
||||
Object.hasOwn(payload.config, 'override_pinned_version'),
|
||||
'omits override flag when using pinned version'
|
||||
);
|
||||
return [204, { 'Content-Type': 'application/json' }];
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
// The pinned version (1.0.0) should be auto-selected
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
|
||||
test('it does not include plugin_version for built-in plugins', async function (assert) {
|
||||
this.server.post('/sys/mounts/keymgmt', (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.false(
|
||||
Object.hasOwn(payload.config, 'plugin_version'),
|
||||
'does not include plugin_version for built-in'
|
||||
);
|
||||
assert.false(
|
||||
Object.hasOwn(payload.config, 'override_pinned_version'),
|
||||
'does not include override flag for built-in'
|
||||
);
|
||||
assert.strictEqual(payload.type, 'keymgmt', 'uses built-in type name');
|
||||
return [204, { 'Content-Type': 'application/json' }];
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
// Built-in is selected by default
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
});
|
||||
|
||||
module('Error handling and edge cases', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.form.type = 'keymgmt';
|
||||
this.form.data.path = 'keymgmt';
|
||||
this.versionService = this.owner.lookup('service:version');
|
||||
sinon.stub(this.versionService, 'isEnterprise').value(true);
|
||||
|
||||
// No pinned version for error handling tests
|
||||
this.model.pinnedVersion = null;
|
||||
});
|
||||
|
||||
test('it handles empty available versions gracefully', async function (assert) {
|
||||
this.model.availableVersions = [];
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
// External should be disabled
|
||||
assert
|
||||
.dom(`input${GENERAL.radioCardByAttr('external')}`)
|
||||
.isDisabled('external plugin disabled when no versions');
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.doesNotExist('plugin version field hidden when no external versions');
|
||||
});
|
||||
|
||||
test('it handles missing availableVersions argument', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
);
|
||||
|
||||
// External should be disabled
|
||||
assert
|
||||
.dom(`input${GENERAL.radioCardByAttr('external')}`)
|
||||
.isDisabled('external plugin disabled when availableVersions not provided');
|
||||
});
|
||||
|
||||
test('it shows version field immediately when pinned version available', async function (assert) {
|
||||
// Set pinned version
|
||||
this.model.pinnedVersion = '1.0.0';
|
||||
|
||||
// Set up available versions for this test
|
||||
this.model.availableVersions = [
|
||||
{ version: '1.0.0', pluginName: 'vault-plugin-secrets-keymgmt', isBuiltin: false },
|
||||
];
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm
|
||||
@model={{this.model}}
|
||||
@onMountSuccess={{this.onMountSuccess}}
|
||||
/>`
|
||||
);
|
||||
|
||||
await click(`input${GENERAL.radioCardByAttr('external')}`);
|
||||
|
||||
// Version field should show even before pins are loaded, since versions are available
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.exists('version field shows when external selected and versions available');
|
||||
|
||||
// Field should remain visible since pinned version is available immediately
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('config.plugin_version'))
|
||||
.exists('version field remains visible with pinned version');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import engineDisplayData from 'vault/helpers/engines-display-data';
|
||||
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
|
||||
|
||||
module('Unit | Helper | engineDisplayData', function () {
|
||||
test('it returns correct display data for a known engine type', function (assert) {
|
||||
const awsData = engineDisplayData('aws');
|
||||
const expected = ALL_ENGINES.find((e) => e.type === 'aws');
|
||||
assert.propEqual(awsData, expected, 'Returns correct display data for aws');
|
||||
});
|
||||
|
||||
test('it returns correct display data for an ent only engine', function (assert) {
|
||||
const kmipData = engineDisplayData('kmip');
|
||||
assert.true(kmipData.requiresEnterprise, 'KMIP requires enterprise');
|
||||
assert.strictEqual(kmipData.displayName, 'KMIP', 'KMIP displayName is correct');
|
||||
});
|
||||
|
||||
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, 'not-an-engine', 'it returns methodType type');
|
||||
assert.propEqual(mountCategory, ['secret', 'auth'], 'mountCategory is correct');
|
||||
assert.strictEqual(glyph, 'lock', 'default glyph is a lock');
|
||||
});
|
||||
|
||||
test('it returns fallback display data for empty string', function (assert) {
|
||||
const { displayName, type, mountCategory, glyph } = engineDisplayData('');
|
||||
assert.strictEqual(displayName, 'Unknown plugin', 'it returns fallback displayName for empty string');
|
||||
assert.strictEqual(type, 'unknown', 'it returns fallback type for empty string');
|
||||
assert.propEqual(mountCategory, ['secret', 'auth'], 'mountCategory is correct');
|
||||
assert.strictEqual(glyph, 'lock', 'default glyph is a lock');
|
||||
});
|
||||
|
||||
test('it returns fallback display data for undefined', function (assert) {
|
||||
const { displayName, type, mountCategory, glyph } = engineDisplayData(undefined);
|
||||
assert.strictEqual(displayName, 'Unknown plugin', 'it returns fallback displayName for undefined');
|
||||
assert.strictEqual(type, 'unknown', 'it returns fallback type for undefined');
|
||||
assert.propEqual(mountCategory, ['secret', 'auth'], 'mountCategory is correct');
|
||||
assert.strictEqual(glyph, 'lock', 'default glyph is a lock');
|
||||
});
|
||||
|
||||
test('it returns fallback display data for null', function (assert) {
|
||||
const { displayName, type, mountCategory, glyph } = engineDisplayData(null);
|
||||
assert.strictEqual(displayName, 'Unknown plugin', 'it returns fallback displayName for null');
|
||||
assert.strictEqual(type, 'unknown', 'it returns fallback type for null');
|
||||
assert.propEqual(mountCategory, ['secret', 'auth'], 'mountCategory is correct');
|
||||
assert.strictEqual(glyph, 'lock', 'default glyph is a lock');
|
||||
});
|
||||
});
|
||||
44
ui/tests/unit/components/mount/secrets-engine-form-test.js
Normal file
44
ui/tests/unit/components/mount/secrets-engine-form-test.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { module, test } from 'qunit';
|
||||
import SecretsEngineForm from 'vault/forms/secrets/engine';
|
||||
import { getExternalPluginNameFromBuiltin } from 'vault/utils/external-plugin-helpers';
|
||||
|
||||
module('Unit | Component | mount/secrets-engine-form', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
// Setup default model
|
||||
const defaults = {
|
||||
config: { listing_visibility: false },
|
||||
options: { version: 2 },
|
||||
};
|
||||
this.model = new SecretsEngineForm(defaults, { isNew: true });
|
||||
this.model.type = 'keymgmt';
|
||||
this.model.data.path = 'keymgmt';
|
||||
|
||||
this.availableVersions = [
|
||||
{ version: '1.0.0', isBuiltin: false },
|
||||
{ version: '1.1.0', isBuiltin: false },
|
||||
{ version: '2.0.0', isBuiltin: false },
|
||||
{ version: '', isBuiltin: true },
|
||||
];
|
||||
});
|
||||
|
||||
test('getExternalPluginNameFromBuiltin returns correct name for keymgmt', function (assert) {
|
||||
const externalName = getExternalPluginNameFromBuiltin('keymgmt');
|
||||
assert.strictEqual(
|
||||
externalName,
|
||||
'vault-plugin-secrets-keymgmt',
|
||||
'generates correct external plugin name for keymgmt'
|
||||
);
|
||||
});
|
||||
|
||||
test('model normalizedType returns correct value', function (assert) {
|
||||
assert.strictEqual(this.model.normalizedType, 'keymgmt', 'returns correct normalized type');
|
||||
});
|
||||
});
|
||||
325
ui/tests/unit/forms/mount-test.js
Normal file
325
ui/tests/unit/forms/mount-test.js
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import MountForm from 'vault/forms/mount';
|
||||
|
||||
module('Unit | Form | mount', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
module('Plugin Version Selection', function () {
|
||||
test('toJSON omits plugin_version when default is selected (empty value)', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.type = 'kv';
|
||||
form.data = {
|
||||
path: 'test-kv',
|
||||
description: 'Test KV engine',
|
||||
config: {
|
||||
plugin_version: '', // Empty string represents default selection
|
||||
max_lease_ttl: '8760h',
|
||||
},
|
||||
};
|
||||
|
||||
const result = form.toJSON();
|
||||
|
||||
assert.strictEqual(result.data.type, 'kv', 'type is set correctly');
|
||||
assert.notOk(
|
||||
Object.prototype.hasOwnProperty.call(result.data.config, 'plugin_version'),
|
||||
'plugin_version is omitted from config when empty'
|
||||
);
|
||||
});
|
||||
|
||||
test('toJSON omits plugin_version when undefined', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.type = 'kv';
|
||||
form.data = {
|
||||
path: 'test-kv',
|
||||
description: 'Test KV engine',
|
||||
config: {
|
||||
max_lease_ttl: '8760h',
|
||||
// plugin_version not set
|
||||
},
|
||||
};
|
||||
|
||||
const result = form.toJSON();
|
||||
|
||||
assert.strictEqual(result.data.type, 'kv', 'type is set correctly');
|
||||
assert.notOk(
|
||||
Object.prototype.hasOwnProperty.call(result.data.config, 'plugin_version'),
|
||||
'plugin_version is omitted from config when undefined'
|
||||
);
|
||||
});
|
||||
|
||||
test('toJSON includes plugin_version for builtin plugin selection', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.type = 'kv'; // Builtin type should remain as 'kv'
|
||||
form.data = {
|
||||
path: 'test-kv',
|
||||
description: 'Test KV engine',
|
||||
config: {
|
||||
plugin_version: 'v1.16.1+builtin',
|
||||
max_lease_ttl: '8760h',
|
||||
},
|
||||
};
|
||||
|
||||
const result = form.toJSON();
|
||||
|
||||
assert.strictEqual(result.data.type, 'kv', 'type remains builtin type for builtin plugins');
|
||||
assert.strictEqual(
|
||||
result.data.config.plugin_version,
|
||||
'v1.16.1+builtin',
|
||||
'plugin_version is included for builtin plugin'
|
||||
);
|
||||
assert.strictEqual(result.data.path, 'test-kv', 'other data is preserved');
|
||||
});
|
||||
|
||||
test('toJSON includes plugin_version for external plugin selection', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.type = 'vault-plugin-secrets-kv'; // External plugin type
|
||||
form.data = {
|
||||
path: 'test-external-kv',
|
||||
description: 'Test external KV engine',
|
||||
config: {
|
||||
plugin_version: 'v0.25.0',
|
||||
max_lease_ttl: '8760h',
|
||||
},
|
||||
};
|
||||
|
||||
const result = form.toJSON();
|
||||
|
||||
assert.strictEqual(
|
||||
result.data.type,
|
||||
'vault-plugin-secrets-kv',
|
||||
'type is set to external plugin name for external plugins'
|
||||
);
|
||||
assert.strictEqual(
|
||||
result.data.config.plugin_version,
|
||||
'v0.25.0',
|
||||
'plugin_version is included for external plugin'
|
||||
);
|
||||
assert.strictEqual(result.data.path, 'test-external-kv', 'other data is preserved');
|
||||
});
|
||||
});
|
||||
|
||||
module('setPluginVersionData', function () {
|
||||
test('sets config.plugin_version and preserves builtin type for builtin plugins', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.type = 'kv';
|
||||
form.data = { config: {} };
|
||||
|
||||
const builtinVersionInfo = {
|
||||
version: 'v1.16.1+builtin',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: true,
|
||||
sha256: 'abc123',
|
||||
};
|
||||
|
||||
form.setPluginVersionData(builtinVersionInfo);
|
||||
|
||||
assert.strictEqual(form.data.config.plugin_version, 'v1.16.1+builtin', 'plugin_version is set');
|
||||
assert.strictEqual(form.type, 'kv', 'type remains builtin for builtin plugins');
|
||||
assert.ok(form.data.config, 'config object is preserved');
|
||||
});
|
||||
|
||||
test('sets config.plugin_version and updates type for external plugins', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.type = 'kv'; // Initially set to builtin
|
||||
form.data = { config: {} };
|
||||
|
||||
const externalVersionInfo = {
|
||||
version: 'v0.25.0',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: false,
|
||||
sha256: 'def456',
|
||||
};
|
||||
|
||||
form.setPluginVersionData(externalVersionInfo);
|
||||
|
||||
assert.strictEqual(form.data.config.plugin_version, 'v0.25.0', 'plugin_version is set');
|
||||
assert.strictEqual(
|
||||
form.type,
|
||||
'vault-plugin-secrets-kv',
|
||||
'type is updated to plugin name for external plugins'
|
||||
);
|
||||
assert.ok(form.data.config, 'config object is preserved');
|
||||
});
|
||||
});
|
||||
|
||||
module('findVersionByLabel', function () {
|
||||
test('returns undefined for empty string (default selection)', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.data = { config: {} };
|
||||
|
||||
const availableVersions = [
|
||||
{
|
||||
version: 'v1.16.1+builtin',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: true,
|
||||
sha256: 'abc123',
|
||||
},
|
||||
{
|
||||
version: 'v0.25.0',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: false,
|
||||
sha256: 'def456',
|
||||
},
|
||||
];
|
||||
|
||||
const result = form.findVersionByLabel('', availableVersions);
|
||||
|
||||
assert.strictEqual(result, undefined, 'returns undefined for empty string (default)');
|
||||
});
|
||||
|
||||
test('returns undefined for null/undefined selectedValue', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.data = { config: {} };
|
||||
|
||||
const availableVersions = [
|
||||
{
|
||||
version: 'v1.16.1+builtin',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: true,
|
||||
sha256: 'abc123',
|
||||
},
|
||||
];
|
||||
|
||||
assert.strictEqual(
|
||||
form.findVersionByLabel(null, availableVersions),
|
||||
undefined,
|
||||
'returns undefined for null'
|
||||
);
|
||||
assert.strictEqual(
|
||||
form.findVersionByLabel(undefined, availableVersions),
|
||||
undefined,
|
||||
'returns undefined for undefined'
|
||||
);
|
||||
});
|
||||
|
||||
test('finds matching version info by version string', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.data = { config: {} };
|
||||
|
||||
const builtinVersion = {
|
||||
version: 'v1.16.1+builtin',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: true,
|
||||
sha256: 'abc123',
|
||||
};
|
||||
const externalVersion = {
|
||||
version: 'v0.25.0',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: false,
|
||||
sha256: 'def456',
|
||||
};
|
||||
const availableVersions = [builtinVersion, externalVersion];
|
||||
|
||||
const builtinResult = form.findVersionByLabel('v1.16.1+builtin', availableVersions);
|
||||
const externalResult = form.findVersionByLabel('v0.25.0', availableVersions);
|
||||
|
||||
assert.deepEqual(builtinResult, builtinVersion, 'finds builtin version correctly');
|
||||
assert.deepEqual(externalResult, externalVersion, 'finds external version correctly');
|
||||
});
|
||||
|
||||
test('returns undefined for non-matching version', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.data = { config: {} };
|
||||
|
||||
const availableVersions = [
|
||||
{
|
||||
version: 'v1.16.1+builtin',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: true,
|
||||
sha256: 'abc123',
|
||||
},
|
||||
];
|
||||
|
||||
const result = form.findVersionByLabel('v999.999.999', availableVersions);
|
||||
|
||||
assert.strictEqual(result, undefined, 'returns undefined for non-matching version');
|
||||
});
|
||||
});
|
||||
|
||||
module('setupPluginVersionField', function () {
|
||||
test('does nothing when no versions available', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.data = { config: {} };
|
||||
|
||||
form.setupPluginVersionField(null);
|
||||
|
||||
// Since the field is handled in the template now, just verify the method doesn't throw
|
||||
assert.ok(true, 'setupPluginVersionField handles null versions gracefully');
|
||||
});
|
||||
|
||||
test('does nothing when only one version available', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.data = { config: {} };
|
||||
|
||||
const singleVersion = [
|
||||
{
|
||||
version: 'v1.16.1+builtin',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: true,
|
||||
sha256: 'abc123',
|
||||
},
|
||||
];
|
||||
|
||||
form.setupPluginVersionField(singleVersion);
|
||||
|
||||
// Since the field is handled in the template now, just verify the method doesn't throw
|
||||
assert.ok(true, 'setupPluginVersionField handles single version gracefully');
|
||||
});
|
||||
|
||||
test('initializes plugin_version config when multiple versions available', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const form = new MountForm({});
|
||||
form.data = { config: {} };
|
||||
|
||||
const multipleVersions = [
|
||||
{
|
||||
version: 'v1.16.1+builtin',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: true,
|
||||
sha256: 'abc123',
|
||||
},
|
||||
{
|
||||
version: 'v0.25.0',
|
||||
pluginName: 'vault-plugin-secrets-kv',
|
||||
isBuiltin: false,
|
||||
sha256: 'def456',
|
||||
},
|
||||
];
|
||||
|
||||
form.setupPluginVersionField(multipleVersions);
|
||||
|
||||
assert.strictEqual(form.data.config.plugin_version, '', 'plugin_version initialized as empty string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -5,14 +5,25 @@
|
|||
|
||||
import { module, test } from 'qunit';
|
||||
import engineDisplayData, { unknownEngineMetadata } from 'vault/helpers/engines-display-data';
|
||||
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
|
||||
|
||||
module('Unit | Helper | engines-display-data', function () {
|
||||
test('it returns metadata for builtin engines', function (assert) {
|
||||
test('it returns correct display data for known engine types', function (assert) {
|
||||
// Test keymgmt engine
|
||||
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 aws engine with ALL_ENGINES comparison
|
||||
const awsData = engineDisplayData('aws');
|
||||
const expectedAws = ALL_ENGINES.find((e) => e.type === 'aws');
|
||||
assert.propEqual(awsData, expectedAws, 'Returns correct display data for aws');
|
||||
|
||||
// Test enterprise-only engine
|
||||
const kmipData = engineDisplayData('kmip');
|
||||
assert.true(kmipData.requiresEnterprise, 'KMIP requires enterprise');
|
||||
assert.strictEqual(kmipData.displayName, 'KMIP', 'KMIP displayName is correct');
|
||||
});
|
||||
|
||||
test('it returns metadata for external plugins that map to builtins', function (assert) {
|
||||
|
|
@ -51,13 +62,40 @@ module('Unit | Helper | engines-display-data', function () {
|
|||
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');
|
||||
// Test empty string
|
||||
assert.strictEqual(emptyData.type, 'unknown', 'returns unknown type for empty string');
|
||||
assert.strictEqual(emptyData.displayName, 'Unknown plugin', 'uses default name for empty string');
|
||||
assert.propEqual(
|
||||
emptyData.mountCategory,
|
||||
['secret', 'auth'],
|
||||
'mountCategory is correct for empty string'
|
||||
);
|
||||
assert.strictEqual(emptyData.glyph, 'lock', 'default glyph is a lock for empty string');
|
||||
|
||||
assert.strictEqual(nullData.type, unknownMetadata.type, 'returns unknown for null');
|
||||
assert.strictEqual(undefinedData.type, unknownMetadata.type, 'returns unknown for undefined');
|
||||
// Test null
|
||||
assert.strictEqual(nullData.type, 'unknown', 'returns unknown type for null');
|
||||
assert.strictEqual(nullData.displayName, 'Unknown plugin', 'uses default name for null');
|
||||
assert.propEqual(nullData.mountCategory, ['secret', 'auth'], 'mountCategory is correct for null');
|
||||
assert.strictEqual(nullData.glyph, 'lock', 'default glyph is a lock for null');
|
||||
|
||||
// Test undefined
|
||||
assert.strictEqual(undefinedData.type, 'unknown', 'returns unknown type for undefined');
|
||||
assert.strictEqual(undefinedData.displayName, 'Unknown plugin', 'uses default name for undefined');
|
||||
assert.propEqual(
|
||||
undefinedData.mountCategory,
|
||||
['secret', 'auth'],
|
||||
'mountCategory is correct for undefined'
|
||||
);
|
||||
assert.strictEqual(undefinedData.glyph, 'lock', 'default glyph is a lock for undefined');
|
||||
});
|
||||
|
||||
test('it returns fallback display data for unknown engine types', function (assert) {
|
||||
const unknownData = engineDisplayData('not-an-engine');
|
||||
assert.strictEqual(unknownData.displayName, 'not-an-engine', 'uses passed type as fallback displayName');
|
||||
assert.strictEqual(unknownData.type, 'not-an-engine', 'returns methodType type');
|
||||
assert.propEqual(unknownData.mountCategory, ['secret', 'auth'], 'mountCategory is correct');
|
||||
assert.strictEqual(unknownData.glyph, 'lock', 'default glyph is a lock');
|
||||
});
|
||||
|
||||
test('it handles case sensitivity correctly', function (assert) {
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ module('Unit | Model | secret-engine', function (hooks) {
|
|||
'config.auditNonHmacResponseKeys',
|
||||
'config.passthroughRequestHeaders',
|
||||
'config.allowedResponseHeaders',
|
||||
'config.plugin_version',
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
@ -188,6 +189,7 @@ module('Unit | Model | secret-engine', function (hooks) {
|
|||
'config.auditNonHmacResponseKeys',
|
||||
'config.passthroughRequestHeaders',
|
||||
'config.allowedResponseHeaders',
|
||||
'config.plugin_version',
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
@ -215,6 +217,7 @@ module('Unit | Model | secret-engine', function (hooks) {
|
|||
'config.auditNonHmacResponseKeys',
|
||||
'config.passthroughRequestHeaders',
|
||||
'config.allowedResponseHeaders',
|
||||
'config.plugin_version',
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
@ -239,6 +242,7 @@ module('Unit | Model | secret-engine', function (hooks) {
|
|||
'config.auditNonHmacResponseKeys',
|
||||
'config.passthroughRequestHeaders',
|
||||
'config.allowedResponseHeaders',
|
||||
'config.plugin_version',
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
@ -262,6 +266,7 @@ module('Unit | Model | secret-engine', function (hooks) {
|
|||
'config.auditNonHmacResponseKeys',
|
||||
'config.passthroughRequestHeaders',
|
||||
'config.allowedResponseHeaders',
|
||||
'config.plugin_version',
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
@ -286,6 +291,7 @@ module('Unit | Model | secret-engine', function (hooks) {
|
|||
'config.auditNonHmacResponseKeys',
|
||||
'config.passthroughRequestHeaders',
|
||||
'config.allowedResponseHeaders',
|
||||
'config.plugin_version',
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
@ -313,6 +319,7 @@ module('Unit | Model | secret-engine', function (hooks) {
|
|||
'config.auditNonHmacResponseKeys',
|
||||
'config.passthroughRequestHeaders',
|
||||
'config.allowedResponseHeaders',
|
||||
'config.plugin_version',
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import { module, test } from 'qunit';
|
|||
import {
|
||||
EXTERNAL_PLUGIN_TO_BUILTIN_MAP,
|
||||
getBuiltinTypeFromExternalPlugin,
|
||||
isKnownExternalPlugin,
|
||||
getEffectiveEngineType,
|
||||
getExternalPluginNameFromBuiltin,
|
||||
isKnownExternalPlugin,
|
||||
} from 'vault/utils/external-plugin-helpers';
|
||||
|
||||
module('Unit | Utility | external-plugin-helpers', function () {
|
||||
|
|
@ -118,6 +119,77 @@ module('Unit | Utility | external-plugin-helpers', function () {
|
|||
});
|
||||
});
|
||||
|
||||
module('getExternalPluginNameFromBuiltin', function () {
|
||||
test('it returns external plugin name for known builtin types', function (assert) {
|
||||
assert.strictEqual(
|
||||
getExternalPluginNameFromBuiltin('keymgmt'),
|
||||
'vault-plugin-secrets-keymgmt',
|
||||
'returns vault-plugin-secrets-keymgmt for keymgmt'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
getExternalPluginNameFromBuiltin('azure'),
|
||||
'vault-plugin-secrets-azure',
|
||||
'returns vault-plugin-secrets-azure for azure'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
getExternalPluginNameFromBuiltin('gcp'),
|
||||
'vault-plugin-secrets-gcp',
|
||||
'returns vault-plugin-secrets-gcp for gcp'
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns null for unknown builtin types', function (assert) {
|
||||
assert.strictEqual(
|
||||
getExternalPluginNameFromBuiltin('unknown-engine'),
|
||||
null,
|
||||
'returns null for unknown builtin type'
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns null for external plugin names', function (assert) {
|
||||
assert.strictEqual(
|
||||
getExternalPluginNameFromBuiltin('vault-plugin-secrets-keymgmt'),
|
||||
null,
|
||||
'returns null for external plugin name'
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns null for empty string', function (assert) {
|
||||
assert.strictEqual(getExternalPluginNameFromBuiltin(''), null, 'returns null for empty string');
|
||||
});
|
||||
|
||||
test('it handles case sensitivity correctly', function (assert) {
|
||||
assert.strictEqual(
|
||||
getExternalPluginNameFromBuiltin('KEYMGMT'),
|
||||
null,
|
||||
'returns null for uppercase builtin type'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
getExternalPluginNameFromBuiltin('KeyMgmt'),
|
||||
null,
|
||||
'returns null for mixed case builtin type'
|
||||
);
|
||||
});
|
||||
|
||||
test('it works with all mapped builtin types', function (assert) {
|
||||
// Test that every builtin type in the map can be reverse-looked up
|
||||
const builtinTypes = Object.values(EXTERNAL_PLUGIN_TO_BUILTIN_MAP);
|
||||
const uniqueBuiltinTypes = [...new Set(builtinTypes)];
|
||||
|
||||
uniqueBuiltinTypes.forEach((builtinType) => {
|
||||
const externalName = getExternalPluginNameFromBuiltin(builtinType);
|
||||
assert.ok(externalName, `found external name for builtin type: ${builtinType}`);
|
||||
assert.true(
|
||||
externalName.startsWith('vault-plugin-'),
|
||||
`external name ${externalName} follows expected pattern`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module('future extensibility', function () {
|
||||
test('mapping can be easily extended', function (assert) {
|
||||
// Test that we can add more mappings (conceptually)
|
||||
|
|
@ -129,5 +201,24 @@ module('Unit | Utility | external-plugin-helpers', function () {
|
|||
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');
|
||||
});
|
||||
|
||||
test('reverse lookup works with extended mappings', function (assert) {
|
||||
// Test conceptual extensibility of the reverse lookup
|
||||
// This verifies that the reverse lookup algorithm is robust
|
||||
const originalFunction = getExternalPluginNameFromBuiltin('keymgmt');
|
||||
assert.strictEqual(
|
||||
originalFunction,
|
||||
'vault-plugin-secrets-keymgmt',
|
||||
'reverse lookup works for existing mappings'
|
||||
);
|
||||
|
||||
// Test that non-existent mappings return null as expected
|
||||
const nonExistentResult = getExternalPluginNameFromBuiltin('hypothetical-auth');
|
||||
assert.strictEqual(
|
||||
nonExistentResult,
|
||||
null,
|
||||
'reverse lookup correctly returns null for non-mapped types'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@
|
|||
|
||||
import { module, test } from 'qunit';
|
||||
import {
|
||||
enhanceEnginesWithCatalogData,
|
||||
categorizeEnginesByStatus,
|
||||
enhanceEnginesWithCatalogData,
|
||||
getAllVersionsForEngineType,
|
||||
MOUNT_CATEGORIES,
|
||||
PLUGIN_TYPES,
|
||||
PLUGIN_CATEGORIES,
|
||||
PLUGIN_TYPES,
|
||||
} from 'vault/utils/plugin-catalog-helpers';
|
||||
|
||||
module('Unit | Utility | plugin-catalog-helpers', function () {
|
||||
|
|
@ -147,6 +148,55 @@ module('Unit | Utility | plugin-catalog-helpers', function () {
|
|||
assert.false(externalPlugin.builtin, 'external plugin is not builtin');
|
||||
});
|
||||
|
||||
test('it excludes external plugins with builtin mappings from external category', function (assert) {
|
||||
const staticEngines = [
|
||||
{
|
||||
type: 'kv',
|
||||
displayName: 'KV',
|
||||
pluginCategory: PLUGIN_CATEGORIES.GENERIC,
|
||||
mountCategory: [MOUNT_CATEGORIES.SECRET],
|
||||
},
|
||||
];
|
||||
|
||||
const catalogData = [
|
||||
{
|
||||
name: 'kv',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: true,
|
||||
},
|
||||
{
|
||||
name: 'vault-plugin-secrets-kv', // This has a builtin mapping
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: false,
|
||||
version: '2.1.0',
|
||||
},
|
||||
{
|
||||
name: 'truly-external-plugin', // This does not have a builtin mapping
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: false,
|
||||
version: '1.0.0',
|
||||
},
|
||||
];
|
||||
|
||||
const result = enhanceEnginesWithCatalogData(staticEngines, catalogData);
|
||||
|
||||
// Should only add the truly external plugin, not the one with builtin mapping
|
||||
assert.strictEqual(result.length, 2, 'adds only truly external plugin');
|
||||
|
||||
const kvEngine = result.find((engine) => engine.type === 'kv');
|
||||
const externalKv = result.find((engine) => engine.type === 'vault-plugin-secrets-kv');
|
||||
const trulyExternal = result.find((engine) => engine.type === 'truly-external-plugin');
|
||||
|
||||
assert.ok(kvEngine, 'KV engine is present');
|
||||
assert.notOk(externalKv, 'external KV plugin is not added as separate engine');
|
||||
assert.ok(trulyExternal, 'truly external plugin is present');
|
||||
assert.strictEqual(
|
||||
trulyExternal.pluginCategory,
|
||||
PLUGIN_CATEGORIES.EXTERNAL,
|
||||
'truly external plugin is in external category'
|
||||
);
|
||||
});
|
||||
|
||||
test('it matches external plugins with existing static engine glyphs', function (assert) {
|
||||
const staticEngines = [
|
||||
{
|
||||
|
|
@ -282,4 +332,282 @@ module('Unit | Utility | plugin-catalog-helpers', function () {
|
|||
assert.strictEqual(PLUGIN_CATEGORIES.EXTERNAL, 'external', 'EXTERNAL category is correct');
|
||||
});
|
||||
});
|
||||
|
||||
module('getAllVersionsForEngineType', function () {
|
||||
test('it returns empty array when no catalog data provided', function (assert) {
|
||||
const result = getAllVersionsForEngineType(undefined, 'kv', 'secret');
|
||||
assert.deepEqual(
|
||||
result,
|
||||
{ versions: [], hasUnversionedPlugins: false },
|
||||
'returns empty result for undefined catalog data'
|
||||
);
|
||||
|
||||
const result2 = getAllVersionsForEngineType([], 'kv', 'secret');
|
||||
assert.deepEqual(
|
||||
result2,
|
||||
{ versions: [], hasUnversionedPlugins: false },
|
||||
'returns empty result for empty catalog data'
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns versions for direct engine type matches', function (assert) {
|
||||
const catalogData = [
|
||||
{
|
||||
name: 'kv',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: true,
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'kv',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: true,
|
||||
version: '2.0.0',
|
||||
},
|
||||
];
|
||||
|
||||
const result = getAllVersionsForEngineType(catalogData, 'kv', 'secret');
|
||||
|
||||
assert.strictEqual(result.versions.length, 2, 'returns both versions');
|
||||
assert.strictEqual(result.versions[0].version, '1.0.0', 'includes first version');
|
||||
assert.strictEqual(result.versions[1].version, '2.0.0', 'includes second version');
|
||||
assert.strictEqual(result.versions[0].pluginName, 'kv', 'includes plugin name');
|
||||
assert.true(result.versions[0].isBuiltin, 'marks builtin correctly');
|
||||
assert.false(result.hasUnversionedPlugins, 'no unversioned plugins detected');
|
||||
});
|
||||
|
||||
test('it returns versions for external plugins that map to engine types', function (assert) {
|
||||
const catalogData = [
|
||||
{
|
||||
name: 'kv',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: true,
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'vault-plugin-secrets-kv',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: false,
|
||||
version: '2.1.0',
|
||||
},
|
||||
];
|
||||
|
||||
const result = getAllVersionsForEngineType(catalogData, 'kv', 'secret');
|
||||
|
||||
assert.strictEqual(result.versions.length, 2, 'returns both builtin and external versions');
|
||||
|
||||
const builtinVersion = result.versions.find((v) => v.isBuiltin);
|
||||
const externalVersion = result.versions.find((v) => !v.isBuiltin);
|
||||
|
||||
assert.ok(builtinVersion, 'includes builtin version');
|
||||
assert.ok(externalVersion, 'includes external version');
|
||||
assert.strictEqual(builtinVersion.pluginName, 'kv', 'builtin uses engine name');
|
||||
assert.strictEqual(
|
||||
externalVersion.pluginName,
|
||||
'vault-plugin-secrets-kv',
|
||||
'external uses full plugin name'
|
||||
);
|
||||
assert.false(result.hasUnversionedPlugins, 'no unversioned plugins detected');
|
||||
});
|
||||
|
||||
test('it excludes external plugins that do not map to the engine type', function (assert) {
|
||||
const catalogData = [
|
||||
{
|
||||
name: 'kv',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: true,
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'vault-plugin-secrets-aws',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: false,
|
||||
version: '1.5.0',
|
||||
},
|
||||
];
|
||||
|
||||
const result = getAllVersionsForEngineType(catalogData, 'kv', 'secret');
|
||||
|
||||
assert.strictEqual(result.versions.length, 1, 'only includes matching plugins');
|
||||
assert.strictEqual(result.versions[0].pluginName, 'kv', 'includes only KV engine');
|
||||
assert.false(result.hasUnversionedPlugins, 'no unversioned plugins detected');
|
||||
});
|
||||
|
||||
test('it filters by plugin type correctly', function (assert) {
|
||||
const catalogData = [
|
||||
{
|
||||
name: 'gcp',
|
||||
type: 'auth',
|
||||
builtin: true,
|
||||
version: 'v0.22.0+builtin',
|
||||
},
|
||||
{
|
||||
name: 'gcp',
|
||||
type: 'secret',
|
||||
builtin: true,
|
||||
version: 'v0.23.0+builtin',
|
||||
},
|
||||
{
|
||||
name: 'vault-plugin-secrets-gcp',
|
||||
type: 'secret',
|
||||
builtin: false,
|
||||
version: 'v0.23.0',
|
||||
},
|
||||
];
|
||||
|
||||
// Test filtering for secret plugins only
|
||||
const secretResult = getAllVersionsForEngineType(catalogData, 'gcp', 'secret');
|
||||
assert.strictEqual(secretResult.versions.length, 2, 'returns only secret type plugins');
|
||||
assert.true(
|
||||
secretResult.versions.every(
|
||||
(plugin) => plugin.pluginName === 'gcp' || plugin.pluginName === 'vault-plugin-secrets-gcp'
|
||||
),
|
||||
'includes correct secret plugins'
|
||||
);
|
||||
assert.false(secretResult.hasUnversionedPlugins, 'no unversioned plugins detected');
|
||||
|
||||
// Test filtering for auth plugins only
|
||||
const authResult = getAllVersionsForEngineType(catalogData, 'gcp', 'auth');
|
||||
assert.strictEqual(authResult.versions.length, 1, 'returns only auth type plugins');
|
||||
assert.strictEqual(authResult.versions[0].pluginName, 'gcp', 'includes auth plugin');
|
||||
assert.false(authResult.hasUnversionedPlugins, 'no unversioned plugins detected');
|
||||
});
|
||||
|
||||
test('it handles invalid catalog data gracefully', function (assert) {
|
||||
const invalidCatalogData = [
|
||||
null, // null entry
|
||||
{ name: 'kv' }, // missing required fields
|
||||
{ name: 'aws', version: '1.0.0' }, // missing builtin field
|
||||
{
|
||||
name: 'pki',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: true,
|
||||
version: '1.0.0',
|
||||
}, // valid entry
|
||||
];
|
||||
|
||||
const result = getAllVersionsForEngineType(invalidCatalogData, 'pki');
|
||||
|
||||
assert.strictEqual(result.versions.length, 1, 'filters out invalid entries');
|
||||
assert.strictEqual(result.versions[0].pluginName, 'pki', 'includes only valid entry');
|
||||
assert.false(result.hasUnversionedPlugins, 'no unversioned plugins detected');
|
||||
});
|
||||
|
||||
test('it excludes unversioned plugins but detects them', function (assert) {
|
||||
const catalogData = [
|
||||
{
|
||||
name: 'vault-plugin-secrets-keymgmt',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: false,
|
||||
version: '', // Empty string when plugin registered without version
|
||||
sha256: '9433b2b37d30abf8f7cbf8c3e616dfc263034789681081ea4ba7918673d80086',
|
||||
},
|
||||
{
|
||||
name: 'vault-plugin-secrets-keymgmt',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: false,
|
||||
version: '1.5.0',
|
||||
},
|
||||
];
|
||||
|
||||
const result = getAllVersionsForEngineType(catalogData, 'keymgmt', 'secret');
|
||||
|
||||
assert.strictEqual(result.versions.length, 1, 'excludes unversioned plugin from versions');
|
||||
assert.true(result.hasUnversionedPlugins, 'detects presence of unversioned plugins');
|
||||
|
||||
const versionedPlugin = result.versions[0];
|
||||
assert.strictEqual(versionedPlugin.version, '1.5.0', 'includes only versioned plugin');
|
||||
assert.false(versionedPlugin.isBuiltin, 'versioned plugin is not builtin');
|
||||
assert.strictEqual(
|
||||
versionedPlugin.pluginName,
|
||||
'vault-plugin-secrets-keymgmt',
|
||||
'correct plugin name for versioned'
|
||||
);
|
||||
});
|
||||
|
||||
test('it detects unversioned plugins for builtin engines', function (assert) {
|
||||
const catalogData = [
|
||||
{
|
||||
name: 'kv',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: true,
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'kv',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: false,
|
||||
version: '', // Unversioned external kv plugin
|
||||
},
|
||||
];
|
||||
|
||||
const result = getAllVersionsForEngineType(catalogData, 'kv', 'secret');
|
||||
|
||||
assert.strictEqual(result.versions.length, 1, 'only includes versioned plugins');
|
||||
assert.true(result.hasUnversionedPlugins, 'detects unversioned plugin');
|
||||
assert.strictEqual(result.versions[0].version, '1.0.0', 'includes builtin version');
|
||||
assert.true(result.versions[0].isBuiltin, 'included plugin is builtin');
|
||||
});
|
||||
|
||||
test('it handles multiple unversioned plugins for same engine type', function (assert) {
|
||||
const catalogData = [
|
||||
{
|
||||
name: 'vault-plugin-secrets-custom',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: false,
|
||||
version: '', // First unversioned plugin
|
||||
},
|
||||
{
|
||||
name: 'custom',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: false,
|
||||
version: '', // Second unversioned plugin (direct match)
|
||||
},
|
||||
{
|
||||
name: 'custom',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: true,
|
||||
version: '2.0.0', // Versioned plugin
|
||||
},
|
||||
];
|
||||
|
||||
const result = getAllVersionsForEngineType(catalogData, 'custom', 'secret');
|
||||
|
||||
assert.strictEqual(result.versions.length, 1, 'excludes all unversioned plugins');
|
||||
assert.true(result.hasUnversionedPlugins, 'detects multiple unversioned plugins');
|
||||
assert.strictEqual(result.versions[0].version, '2.0.0', 'includes only versioned plugin');
|
||||
});
|
||||
|
||||
test('it handles invalid engine type parameters', function (assert) {
|
||||
const catalogData = [
|
||||
{
|
||||
name: 'kv',
|
||||
type: PLUGIN_TYPES.SECRET,
|
||||
builtin: true,
|
||||
version: '1.0.0',
|
||||
},
|
||||
];
|
||||
|
||||
const result1 = getAllVersionsForEngineType(catalogData, null);
|
||||
assert.deepEqual(
|
||||
result1,
|
||||
{ versions: [], hasUnversionedPlugins: false },
|
||||
'returns empty result for null engine type'
|
||||
);
|
||||
|
||||
const result2 = getAllVersionsForEngineType(catalogData, '');
|
||||
assert.deepEqual(
|
||||
result2,
|
||||
{ versions: [], hasUnversionedPlugins: false },
|
||||
'returns empty result for empty engine type'
|
||||
);
|
||||
|
||||
const result3 = getAllVersionsForEngineType(catalogData, undefined);
|
||||
assert.deepEqual(
|
||||
result3,
|
||||
{ versions: [], hasUnversionedPlugins: false },
|
||||
'returns empty result for undefined engine type'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
128
ui/tests/unit/utils/version-utils-test.js
Normal file
128
ui/tests/unit/utils/version-utils-test.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import {
|
||||
areVersionsEqual,
|
||||
cleanVersion,
|
||||
compareVersions,
|
||||
getHighestVersion,
|
||||
isValidVersion,
|
||||
isVersionGreater,
|
||||
parseVersion,
|
||||
sortVersions,
|
||||
} from 'vault/utils/version-utils';
|
||||
|
||||
module('Unit | Utility | version-utils', function () {
|
||||
test('cleanVersion removes prefixes and suffixes correctly', function (assert) {
|
||||
assert.strictEqual(cleanVersion('v1.2.3'), '1.2.3', 'removes v prefix');
|
||||
assert.strictEqual(cleanVersion('1.2.3+ent'), '1.2.3', 'removes +ent suffix');
|
||||
assert.strictEqual(cleanVersion('v1.2.3+builtin'), '1.2.3', 'removes v prefix and +builtin suffix');
|
||||
assert.strictEqual(cleanVersion('v1.2.3-beta1+ent'), '1.2.3', 'removes v prefix and -beta1+ent suffix');
|
||||
assert.strictEqual(cleanVersion('1.2.3'), '1.2.3', 'leaves clean version unchanged');
|
||||
});
|
||||
|
||||
test('parseVersion converts version strings to numeric arrays', function (assert) {
|
||||
assert.deepEqual(parseVersion('1.2.3'), [1, 2, 3], 'parses basic version');
|
||||
assert.deepEqual(parseVersion('v1.0.0+ent'), [1, 0, 0], 'parses version with prefix and suffix');
|
||||
assert.deepEqual(parseVersion('1.2'), [1, 2], 'parses two-part version');
|
||||
assert.deepEqual(parseVersion('1.2.3.4'), [1, 2, 3, 4], 'parses four-part version');
|
||||
assert.deepEqual(parseVersion('1.0.x'), [1, 0, 0], 'handles non-numeric parts as 0');
|
||||
});
|
||||
|
||||
test('compareVersions works correctly', function (assert) {
|
||||
// Equal versions
|
||||
assert.strictEqual(compareVersions('1.2.3', '1.2.3'), 0, '1.2.3 equals 1.2.3');
|
||||
assert.strictEqual(compareVersions('v1.2.3+ent', '1.2.3'), 0, 'v1.2.3+ent equals 1.2.3');
|
||||
|
||||
// First version greater
|
||||
assert.ok(compareVersions('1.2.4', '1.2.3') > 0, '1.2.4 > 1.2.3');
|
||||
assert.ok(compareVersions('1.3.0', '1.2.9') > 0, '1.3.0 > 1.2.9');
|
||||
assert.ok(compareVersions('2.0.0', '1.9.9') > 0, '2.0.0 > 1.9.9');
|
||||
|
||||
// Second version greater
|
||||
assert.ok(compareVersions('1.2.3', '1.2.4') < 0, '1.2.3 < 1.2.4');
|
||||
assert.ok(compareVersions('1.2.9', '1.3.0') < 0, '1.2.9 < 1.3.0');
|
||||
assert.ok(compareVersions('1.9.9', '2.0.0') < 0, '1.9.9 < 2.0.0');
|
||||
|
||||
// Different lengths
|
||||
assert.ok(compareVersions('1.2.3', '1.2') > 0, '1.2.3 > 1.2');
|
||||
assert.ok(compareVersions('1.2', '1.2.1') < 0, '1.2 < 1.2.1');
|
||||
});
|
||||
|
||||
test('sortVersions sorts correctly', function (assert) {
|
||||
const versions = ['v1.0.0+ent', 'v0.18.0+ent', 'v0.19.0+ent', 'v1.1.0+ent'];
|
||||
|
||||
// Ascending order (default)
|
||||
const ascending = sortVersions(versions);
|
||||
assert.deepEqual(
|
||||
ascending,
|
||||
['v0.18.0+ent', 'v0.19.0+ent', 'v1.0.0+ent', 'v1.1.0+ent'],
|
||||
'sorts ascending'
|
||||
);
|
||||
|
||||
// Descending order
|
||||
const descending = sortVersions(versions, true);
|
||||
assert.deepEqual(
|
||||
descending,
|
||||
['v1.1.0+ent', 'v1.0.0+ent', 'v0.19.0+ent', 'v0.18.0+ent'],
|
||||
'sorts descending'
|
||||
);
|
||||
|
||||
// Original array unchanged
|
||||
assert.deepEqual(
|
||||
versions,
|
||||
['v1.0.0+ent', 'v0.18.0+ent', 'v0.19.0+ent', 'v1.1.0+ent'],
|
||||
'original array unchanged'
|
||||
);
|
||||
});
|
||||
|
||||
test('getHighestVersion returns the latest version', function (assert) {
|
||||
const versions = ['v1.0.0+ent', 'v0.18.0+ent', 'v0.19.0+ent', 'v1.1.0+ent'];
|
||||
assert.strictEqual(getHighestVersion(versions), 'v1.1.0+ent', 'returns highest version');
|
||||
assert.strictEqual(getHighestVersion([]), null, 'returns null for empty array');
|
||||
assert.strictEqual(getHighestVersion(['v1.0.0']), 'v1.0.0', 'returns single version');
|
||||
});
|
||||
|
||||
test('isVersionGreater compares versions correctly', function (assert) {
|
||||
assert.true(isVersionGreater('1.2.4', '1.2.3'), '1.2.4 > 1.2.3');
|
||||
assert.true(isVersionGreater('v1.0.0+ent', '0.9.0'), 'v1.0.0+ent > 0.9.0');
|
||||
assert.false(isVersionGreater('1.2.3', '1.2.4'), '1.2.3 not > 1.2.4');
|
||||
assert.false(isVersionGreater('1.2.3', '1.2.3'), '1.2.3 not > 1.2.3');
|
||||
});
|
||||
|
||||
test('areVersionsEqual compares versions correctly', function (assert) {
|
||||
assert.true(areVersionsEqual('1.2.3', '1.2.3'), '1.2.3 equals 1.2.3');
|
||||
assert.true(areVersionsEqual('v1.2.3+ent', '1.2.3'), 'v1.2.3+ent equals 1.2.3');
|
||||
assert.false(areVersionsEqual('1.2.3', '1.2.4'), '1.2.3 not equal 1.2.4');
|
||||
});
|
||||
|
||||
test('edge cases are handled correctly', function (assert) {
|
||||
// Empty strings
|
||||
assert.strictEqual(compareVersions('', ''), 0, 'empty strings are equal');
|
||||
assert.strictEqual(cleanVersion(''), '', 'empty string returns empty');
|
||||
|
||||
// Only prefixes/suffixes
|
||||
assert.strictEqual(cleanVersion('v'), '', 'only prefix returns empty');
|
||||
assert.strictEqual(cleanVersion('+ent'), '', 'only suffix returns empty');
|
||||
});
|
||||
|
||||
test('isValidVersion validates version strings correctly', function (assert) {
|
||||
// Valid versions
|
||||
assert.true(isValidVersion('0.17'), 'Basic semver is valid');
|
||||
assert.true(isValidVersion('0.17.0'), 'Full semver is valid');
|
||||
assert.true(isValidVersion('v0.17.1'), 'Version with v prefix is valid');
|
||||
assert.true(isValidVersion('1.2.3+ent'), 'Version with build metadata is valid');
|
||||
assert.true(isValidVersion('2.0.0-beta'), 'Version with pre-release is valid');
|
||||
|
||||
// Invalid versions
|
||||
assert.false(isValidVersion(''), 'Empty string is invalid');
|
||||
assert.false(isValidVersion(' '), 'Whitespace only is invalid');
|
||||
assert.false(isValidVersion('null'), 'String "null" is invalid');
|
||||
assert.false(isValidVersion('invalid'), 'Non-numeric string is invalid');
|
||||
assert.false(isValidVersion(null), 'null is invalid');
|
||||
assert.false(isValidVersion(undefined), 'undefined is invalid');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue