mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
Backport [VAULT-40813] ManageDropdown component into ce/main (#12508)
* [VAULT-40813] ManageDropdown component (#12295) * [VAULT-40813] ManageDropdown component * address pr comments, add routing test coverage * fix test: generate policy is an enterprise-only feature --------- Co-authored-by: Shannon Roberts (Beagin) <beagins@users.noreply.github.com> Co-authored-by: Shannon Roberts <shannon.roberts@hashicorp.com>
This commit is contained in:
parent
ad9144da7e
commit
b593ca128e
39 changed files with 1490 additions and 891 deletions
6
ui/app/components/manage-dropdown.ts
Normal file
6
ui/app/components/manage-dropdown.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'core/components/manage-dropdown';
|
||||
|
|
@ -3,21 +3,21 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { action, set } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { service } from '@ember/service';
|
||||
import { action, set } from '@ember/object';
|
||||
import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
|
||||
import { MOUNT_CATEGORIES } from 'vault/utils/plugin-catalog-helpers';
|
||||
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type { ApiError } from '@ember-data/adapter/error';
|
||||
import type Store from '@ember-data/store';
|
||||
import type AuthMethodForm from 'vault/forms/auth/method';
|
||||
import type CapabilitiesService from 'vault/services/capabilities';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type { ApiError } from '@ember-data/adapter/error';
|
||||
import type CapabilitiesService from 'vault/services/capabilities';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type { ValidationMap } from 'vault/vault/app-types';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata';
|
||||
import keys from 'core/utils/keys';
|
||||
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
|
||||
import {
|
||||
enhanceEnginesWithCatalogData,
|
||||
categorizeEnginesByStatus,
|
||||
enhanceEnginesWithCatalogData,
|
||||
MOUNT_CATEGORIES,
|
||||
PLUGIN_TYPES,
|
||||
PLUGIN_CATEGORIES,
|
||||
PLUGIN_TYPES,
|
||||
} from 'vault/utils/plugin-catalog-helpers';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@
|
|||
@text="Disable engines"
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
{{on "click" (fn (mut this.enginesToDisable) this.selectedItems)}}
|
||||
{{on "click" (fn this.setEnginesToDisable this.selectedItems)}}
|
||||
/>
|
||||
</Hds::Layout::Flex>
|
||||
{{/if}}
|
||||
|
|
@ -171,28 +171,7 @@
|
|||
|
||||
<:popupMenu as |rowData|>
|
||||
{{#let (this.getEngineResourceData rowData.path) as |backendData|}}
|
||||
<Hds::Dropdown @isInline={{true}} as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="{{if backendData.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
<dd.Interactive
|
||||
@route={{backendData.backendConfigurationLink}}
|
||||
@model={{backendData.id}}
|
||||
data-test-popup-menu="View configuration"
|
||||
@icon="settings"
|
||||
>View configuration</dd.Interactive>
|
||||
{{#if (not-eq backendData.type "cubbyhole")}}
|
||||
<dd.Interactive
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.engineToDisable) backendData)}}
|
||||
data-test-popup-menu="Delete"
|
||||
@icon="trash"
|
||||
>Delete</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
<ManageDropdown @model={{backendData}} @variant="icon" @configRoute={{backendData.backendConfigurationLink}} />
|
||||
{{/let}}
|
||||
</:popupMenu>
|
||||
</ListTable>
|
||||
|
|
@ -201,16 +180,6 @@
|
|||
{{/if}}
|
||||
{{! End Table Section }}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{perform this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.enginesToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
|
|
|
|||
|
|
@ -3,20 +3,20 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { dropTask } from 'ember-concurrency';
|
||||
import engineDisplayData from 'vault/helpers/engines-display-data';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type NamespaceService from 'vault/services/namespace';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type WizardService from 'vault/services/wizard';
|
||||
import { WIZARD_ID } from '../wizard/secret-engines/secret-engines-wizard';
|
||||
|
|
@ -45,7 +45,6 @@ export default class SecretEngineList extends Component<Args> {
|
|||
@service declare readonly wizard: WizardService;
|
||||
|
||||
@tracked secretEngineOptions: Array<string> | [] = [];
|
||||
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
|
||||
@tracked enginesToDisable: Array<SecretsEngineResource> | null = null;
|
||||
|
||||
@tracked engineTypeFilters: Array<string> = [];
|
||||
|
|
@ -289,6 +288,16 @@ export default class SecretEngineList extends Component<Args> {
|
|||
this.selectedItems = tableData.selectedRowsKeys;
|
||||
}
|
||||
|
||||
@action
|
||||
setEnginesToDisable(engines: Array<SecretsEngineResource>) {
|
||||
this.enginesToDisable = engines;
|
||||
}
|
||||
|
||||
@action
|
||||
clearEnginesToDisable() {
|
||||
this.enginesToDisable = null;
|
||||
}
|
||||
|
||||
async disableSingleEngine(engine: SecretsEngineResource) {
|
||||
const { engineType, id, path } = engine;
|
||||
try {
|
||||
|
|
@ -302,14 +311,13 @@ export default class SecretEngineList extends Component<Args> {
|
|||
}
|
||||
}
|
||||
|
||||
@dropTask
|
||||
*disableMultipleEngines(enginePathsToDisable: Array<string>) {
|
||||
disableMultipleEngines = dropTask(async (enginePathsToDisable: Array<string>) => {
|
||||
const enginesToDisable = this.displayableBackends.filter((engine: SecretsEngineResource) =>
|
||||
enginePathsToDisable.includes(engine.path)
|
||||
);
|
||||
try {
|
||||
for (const engine of enginesToDisable) {
|
||||
yield this.disableSingleEngine(engine);
|
||||
await this.disableSingleEngine(engine);
|
||||
}
|
||||
|
||||
// Navigate once all operations are complete
|
||||
|
|
@ -317,15 +325,5 @@ export default class SecretEngineList extends Component<Args> {
|
|||
} finally {
|
||||
this.enginesToDisable = null;
|
||||
}
|
||||
}
|
||||
|
||||
@dropTask
|
||||
*disableEngine(engine: SecretsEngineResource) {
|
||||
try {
|
||||
yield this.disableSingleEngine(engine);
|
||||
this.router.transitionTo('vault.cluster.secrets.backends');
|
||||
} finally {
|
||||
this.engineToDisable = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,19 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
|
||||
import {
|
||||
enhanceEnginesWithCatalogData,
|
||||
categorizeEnginesByStatus,
|
||||
MOUNT_CATEGORIES,
|
||||
PLUGIN_TYPES,
|
||||
PLUGIN_CATEGORIES,
|
||||
} from 'vault/utils/plugin-catalog-helpers';
|
||||
import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata';
|
||||
import type { PluginCatalogData } from 'vault/services/plugin-catalog';
|
||||
import {
|
||||
categorizeEnginesByStatus,
|
||||
enhanceEnginesWithCatalogData,
|
||||
MOUNT_CATEGORIES,
|
||||
PLUGIN_CATEGORIES,
|
||||
PLUGIN_TYPES,
|
||||
} from 'vault/utils/plugin-catalog-helpers';
|
||||
|
||||
import type VersionService from 'vault/services/version';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { or } from '@ember/object/computed';
|
||||
import { computed } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import Controller from '@ember/controller';
|
||||
import BackendCrumbMixin from 'vault/mixins/backend-crumb';
|
||||
import { computed } from '@ember/object';
|
||||
import { or } from '@ember/object/computed';
|
||||
import { service } from '@ember/service';
|
||||
import ListController from 'core/mixins/list-controller';
|
||||
import { keyIsFolder } from 'core/utils/key-utils';
|
||||
import { task } from 'ember-concurrency';
|
||||
import BackendCrumbMixin from 'vault/mixins/backend-crumb';
|
||||
|
||||
export default Controller.extend(ListController, BackendCrumbMixin, {
|
||||
flashMessages: service(),
|
||||
|
|
@ -68,20 +67,4 @@ export default Controller.extend(ListController, BackendCrumbMixin, {
|
|||
});
|
||||
},
|
||||
},
|
||||
|
||||
disableEngine: task(function* (engine) {
|
||||
const { engineType, id, path } = engine;
|
||||
try {
|
||||
yield this.api.sys.mountsDisableSecretsEngine(id);
|
||||
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
|
||||
this.router.transitionTo('vault.cluster.secrets.backends');
|
||||
} catch (err) {
|
||||
const { message } = yield this.api.parseError(err);
|
||||
this.flashMessages.danger(
|
||||
`There was an error disabling the ${engineType} Secrets Engines at ${path}: ${message}.`
|
||||
);
|
||||
} finally {
|
||||
this.engineToDisable = null;
|
||||
}
|
||||
}).drop(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { ALL_ENGINES } from 'core/utils/all-engines-metadata';
|
||||
import MountForm from 'vault/forms/mount';
|
||||
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
|
||||
import { isKnownExternalPlugin } from 'vault/utils/external-plugin-helpers';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
import FormFieldGroup from 'vault/utils/forms/field-group';
|
||||
|
|
|
|||
|
|
@ -3,60 +3,4 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { ALL_ENGINES, type EngineDisplayData } from 'vault/utils/all-engines-metadata';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
|
||||
/**
|
||||
* Default metadata for unknown engine plugins
|
||||
*/
|
||||
export const unknownEngineMetadata = (methodType?: string): EngineDisplayData => ({
|
||||
type: methodType || 'unknown',
|
||||
displayName: methodType || 'Unknown plugin',
|
||||
glyph: 'lock',
|
||||
mountCategory: ['secret', 'auth'],
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to retrieve engine metadata for a given `methodType`.
|
||||
* It searches the `ALL_ENGINES` array for an engine with a matching type and returns its metadata object.
|
||||
* The `ALL_ENGINES` array includes secret and auth engines, including those supported only in enterprise.
|
||||
* These details (such as mount type and enterprise licensing) are included in the returned engine object.
|
||||
*
|
||||
* For external plugins that have a builtin mapping (e.g., "vault-plugin-secrets-keymgmt" -> "keymgmt"),
|
||||
* this function returns the metadata for the corresponding builtin engine, preserving the original
|
||||
* external plugin name in the type field.
|
||||
*
|
||||
* Example usage:
|
||||
* const engineMetadata = engineDisplayData('kmip');
|
||||
* if (engineMetadata?.requiresEnterprise) {
|
||||
* console.log(`This mount: ${engineMetadata.engineType} requires an enterprise license`);
|
||||
* }
|
||||
*
|
||||
* @param {string} methodType - The engine type (sometimes called backend) to look up (e.g., "aws", "azure", "vault-plugin-secrets-keymgmt").
|
||||
* @returns {Object} - The engine metadata, which includes information about its mount type (e.g., secret or auth)
|
||||
* and whether it requires an enterprise license. For unknown engines, returns a default unknown plugin object.
|
||||
*/
|
||||
export default function engineDisplayData(methodType: string): EngineDisplayData {
|
||||
// First try to find an exact match
|
||||
const builtinEngine = ALL_ENGINES?.find((t) => t.type === methodType);
|
||||
if (builtinEngine) {
|
||||
return builtinEngine;
|
||||
}
|
||||
|
||||
// If no direct match, check if this is a known external plugin and use its builtin mapping
|
||||
const effectiveType = getEffectiveEngineType(methodType);
|
||||
if (effectiveType !== methodType) {
|
||||
// This is a known external plugin with a builtin mapping
|
||||
const mappedEngine = ALL_ENGINES?.find((t) => t.type === effectiveType);
|
||||
if (mappedEngine) {
|
||||
// Return the mapped engine metadata but preserve the original external plugin type
|
||||
return {
|
||||
...mappedEngine,
|
||||
type: methodType, // Keep the original external plugin name for identification
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Return default unknown plugin metadata
|
||||
return unknownEngineMetadata(methodType);
|
||||
}
|
||||
export { default } from 'core/helpers/engines-display-data';
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
import Model, { attr, hasMany } from '@ember-data/model';
|
||||
import ArrayProxy from '@ember/array/proxy';
|
||||
import PromiseProxyMixin from '@ember/object/promise-proxy-mixin';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
import { isPresent } from '@ember/utils';
|
||||
import { service } from '@ember/service';
|
||||
import { isPresent } from '@ember/utils';
|
||||
import { filterEnginesByMountCategory } from 'core/utils/all-engines-metadata';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
import { addManyToArray, addToArray } from 'vault/helpers/add-to-array';
|
||||
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
|
||||
|
||||
const validations = {
|
||||
name: [{ type: 'presence', message: 'Name is required' }],
|
||||
|
|
|
|||
|
|
@ -4,15 +4,14 @@
|
|||
*/
|
||||
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
import { computed } from '@ember/object'; // eslint-disable-line
|
||||
import { equal } from '@ember/object/computed'; // eslint-disable-line
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
import { ALL_ENGINES, isAddonEngine } from 'core/utils/all-engines-metadata';
|
||||
import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes';
|
||||
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
|
||||
import { WHITESPACE_WARNING } from 'vault/utils/forms/validators';
|
||||
import { ALL_ENGINES, INTERNAL_ENGINE_TYPES, isAddonEngine } from 'vault/utils/all-engines-metadata';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
import engineDisplayData from 'vault/helpers/engines-display-data';
|
||||
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
|
||||
import { INTERNAL_ENGINE_TYPES } from 'vault/utils/all-engines-metadata';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
import { WHITESPACE_WARNING } from 'vault/utils/forms/validators';
|
||||
|
||||
const LINKED_BACKENDS = supportedSecretBackends();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { baseResourceFactory } from 'vault/resources/base-factory';
|
||||
import engineDisplayData from 'vault/helpers/engines-display-data';
|
||||
import {
|
||||
supportedSecretBackends,
|
||||
SupportedSecretBackendsEnum,
|
||||
} from 'vault/helpers/supported-secret-backends';
|
||||
import { baseResourceFactory } from 'vault/resources/base-factory';
|
||||
import { INTERNAL_ENGINE_TYPES, isAddonEngine } from 'vault/utils/all-engines-metadata';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
import engineDisplayData from 'vault/helpers/engines-display-data';
|
||||
|
||||
import type { Mount } from 'vault/mount';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,19 +3,19 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { assert } from '@ember/debug';
|
||||
import { set } from '@ember/object';
|
||||
import { hash } from 'rsvp';
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { filterEnginesByMountCategory, isAddonEngine } from 'core/utils/all-engines-metadata';
|
||||
import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs';
|
||||
import { hash } from 'rsvp';
|
||||
import engineDisplayData from 'vault/helpers/engines-display-data';
|
||||
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
|
||||
import { isAddonEngine, filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
|
||||
import { getEnginePathParam } from 'vault/utils/backend-route-helpers';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
import { getModelTypeForEngine } from 'vault/utils/model-helpers/secret-engine-helpers';
|
||||
import { service } from '@ember/service';
|
||||
import { normalizePath } from 'vault/utils/path-encoding-helpers';
|
||||
import { getEnginePathParam } from 'vault/utils/backend-route-helpers';
|
||||
import { assert } from '@ember/debug';
|
||||
import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs';
|
||||
import engineDisplayData from 'vault/helpers/engines-display-data';
|
||||
|
||||
const SUPPORTED_BACKENDS = supportedSecretBackends();
|
||||
|
||||
|
|
|
|||
|
|
@ -9,24 +9,14 @@
|
|||
(options-for-backend this.backendType this.tab) (engines-display-data this.backendType)
|
||||
as |options engineDisplayData|
|
||||
}}
|
||||
<Hds::Dropdown as |D|>
|
||||
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
|
||||
<D.Interactive
|
||||
@icon="settings"
|
||||
@route={{if
|
||||
engineDisplayData.isConfigurable
|
||||
"vault.cluster.secrets.backend.configuration.plugin-settings"
|
||||
"vault.cluster.secrets.backend.configuration.general-settings"
|
||||
}}
|
||||
data-test-popup-menu="Configure"
|
||||
>Configure</D.Interactive>
|
||||
<D.Interactive
|
||||
{{on "click" (fn (mut this.engineToDisable) this.backendModel)}}
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
data-test-popup-menu="Delete"
|
||||
>Delete</D.Interactive>
|
||||
</Hds::Dropdown>
|
||||
<ManageDropdown
|
||||
@model={{this.backendModel}}
|
||||
@configRoute={{if
|
||||
engineDisplayData.isConfigurable
|
||||
"vault.cluster.secrets.backend.configuration.plugin-settings"
|
||||
"vault.cluster.secrets.backend.configuration.general-settings"
|
||||
}}
|
||||
/>
|
||||
|
||||
<Hds::Button
|
||||
@text={{options.create}}
|
||||
|
|
@ -176,14 +166,4 @@
|
|||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{perform this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
|
|
@ -3,355 +3,4 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
/**
|
||||
* Metadata configuration for secret and auth engines, including enterprise.
|
||||
*
|
||||
* This file defines and exports engine metadata, including its
|
||||
* displayName, mountCategory, requiresEnterprise, and other relevant properties. It serves as a
|
||||
* centralized source of truth for engine-related configurations.
|
||||
*
|
||||
* Key responsibilities:
|
||||
* - Define metadata for all engines.
|
||||
* - Provide utility functions or constants for accessing engine-specific data.
|
||||
* - Facilitate dynamic engine rendering and behavior based on metadata.
|
||||
*
|
||||
* Example usage:
|
||||
* If an enterprise license is present, return all secret engines;
|
||||
* otherwise, return only the secret engines supported in OSS.
|
||||
* return filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: this.version.isEnterprise });
|
||||
*/
|
||||
|
||||
export interface EngineDisplayData {
|
||||
pluginCategory?: string; // The plugin category is used to group engines in the UI. e.g., 'cloud', 'infra', 'generic'
|
||||
displayName: string;
|
||||
engineRoute?: string; // engines that have their own Ember engine will have this route defined.
|
||||
glyph?: string;
|
||||
isWIF?: boolean; // flag for 'Workload Identity Federation' engines. - https://developer.hashicorp.com/hcp/docs/hcp/iam/service-principal/workload-identity-federation
|
||||
mountCategory: string[];
|
||||
requiredFeature?: string; // flag for engines that require the ADP (Advanced Data Protection) feature. - https://www.hashicorp.com/en/blog/advanced-data-protection-adp-now-available-in-hcp-vault
|
||||
requiresEnterprise?: boolean;
|
||||
isConfigurable?: boolean; // for secret engines that have additional configuration pages and actions.
|
||||
isOnlyMountable?: boolean; // The UI only supports configuration views for these secrets engines. The CLI must be used to manage other engine resources (i.e. roles, credentials).
|
||||
type: string;
|
||||
value?: string;
|
||||
configRoute?: string; // override for custom route if not "configuration.plugin-settings" (used for Ember engines)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mountCategory - Given mount category to filter by, e.g., 'auth' or 'secret'.
|
||||
* @param isEnterprise - Optional boolean to indicate if enterprise engines should be included in the results.
|
||||
* @returns Filtered array of engines that match the given mount category
|
||||
*/
|
||||
export function filterEnginesByMountCategory({
|
||||
mountCategory,
|
||||
isEnterprise = false,
|
||||
}: {
|
||||
mountCategory: 'auth' | 'secret';
|
||||
isEnterprise: boolean;
|
||||
}) {
|
||||
return isEnterprise
|
||||
? ALL_ENGINES.filter((engine) => engine.mountCategory.includes(mountCategory))
|
||||
: ALL_ENGINES.filter(
|
||||
(engine) => engine.mountCategory.includes(mountCategory) && !engine.requiresEnterprise
|
||||
);
|
||||
}
|
||||
|
||||
export function isAddonEngine(type: string, version: number) {
|
||||
if (type === 'kv' && version === 1) {
|
||||
return false;
|
||||
}
|
||||
const engineRoute = ALL_ENGINES.find((engine) => engine.type === type)?.engineRoute;
|
||||
return !!engineRoute;
|
||||
}
|
||||
|
||||
// The "sys/mounts" and "sys/internal/ui/mounts" endpoints return a "secret/" key containing
|
||||
// all mounts enabled in Vault. Some types are internal Vault APIs, not user-mountable secrets engines,
|
||||
// and should be filtered in some scenarios, such as listing secrets engines.
|
||||
export const INTERNAL_ENGINE_TYPES = ['system', 'identity', 'agent_registry'];
|
||||
|
||||
export const ALL_ENGINES: EngineDisplayData[] = [
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'AliCloud',
|
||||
glyph: 'alibaba-color',
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'alicloud',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'AppRole',
|
||||
glyph: 'cpu',
|
||||
mountCategory: ['auth'],
|
||||
type: 'approle',
|
||||
value: 'approle',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'AWS',
|
||||
glyph: 'aws-color',
|
||||
isConfigurable: true,
|
||||
isWIF: true,
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'aws',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'Azure',
|
||||
glyph: 'azure-color',
|
||||
isOnlyMountable: true,
|
||||
isConfigurable: true,
|
||||
isWIF: true,
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'azure',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'Consul',
|
||||
glyph: 'consul-color',
|
||||
mountCategory: ['secret'],
|
||||
type: 'consul',
|
||||
},
|
||||
{
|
||||
displayName: 'Cubbyhole',
|
||||
type: 'cubbyhole',
|
||||
mountCategory: ['secret'],
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'Databases',
|
||||
glyph: 'database',
|
||||
mountCategory: ['secret'],
|
||||
type: 'database',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'GitHub',
|
||||
glyph: 'github-color',
|
||||
mountCategory: ['auth'],
|
||||
type: 'github',
|
||||
value: 'github',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'Google Cloud',
|
||||
glyph: 'gcp-color',
|
||||
isOnlyMountable: true,
|
||||
isConfigurable: true,
|
||||
isWIF: true,
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'gcp',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'Google Cloud KMS',
|
||||
glyph: 'gcp-color',
|
||||
mountCategory: ['secret'],
|
||||
type: 'gcpkms',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'JWT',
|
||||
glyph: 'jwt',
|
||||
mountCategory: ['auth'],
|
||||
type: 'jwt',
|
||||
value: 'jwt',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'KV',
|
||||
engineRoute: 'kv.list',
|
||||
configRoute: 'kv.configuration', // only utilized to display config data for kvv2, not in conjunction with isConfigurable as templates determine whether engine is kv v1 or v2
|
||||
glyph: 'key-values',
|
||||
mountCategory: ['secret'],
|
||||
type: 'kv',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'KMIP',
|
||||
engineRoute: 'kmip.scopes.index',
|
||||
configRoute: 'kmip.configuration',
|
||||
isConfigurable: true,
|
||||
glyph: 'lock',
|
||||
mountCategory: ['secret'],
|
||||
requiredFeature: 'KMIP',
|
||||
requiresEnterprise: true,
|
||||
type: 'kmip',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'Transform',
|
||||
glyph: 'transform-data',
|
||||
mountCategory: ['secret'],
|
||||
requiredFeature: 'Transform Secrets Engine',
|
||||
requiresEnterprise: true,
|
||||
type: 'transform',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'Key Management',
|
||||
glyph: 'key',
|
||||
mountCategory: ['secret'],
|
||||
requiredFeature: 'Key Management Secrets Engine',
|
||||
requiresEnterprise: true,
|
||||
type: 'keymgmt',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'Kubernetes',
|
||||
engineRoute: 'kubernetes.overview',
|
||||
configRoute: 'kubernetes.configuration',
|
||||
glyph: 'kubernetes-color',
|
||||
isConfigurable: true,
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'kubernetes',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'LDAP',
|
||||
isConfigurable: true,
|
||||
engineRoute: 'ldap.overview',
|
||||
configRoute: 'ldap.configuration',
|
||||
glyph: 'folder-users',
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'ldap',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'Nomad',
|
||||
glyph: 'nomad-color',
|
||||
mountCategory: ['secret'],
|
||||
type: 'nomad',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'OIDC',
|
||||
glyph: 'openid-color',
|
||||
mountCategory: ['auth'],
|
||||
type: 'oidc',
|
||||
value: 'oidc',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'Okta',
|
||||
glyph: 'okta-color',
|
||||
mountCategory: ['auth'],
|
||||
type: 'okta',
|
||||
value: 'okta',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'PKI Certificates',
|
||||
isConfigurable: true,
|
||||
engineRoute: 'pki.overview',
|
||||
configRoute: 'pki.configuration',
|
||||
glyph: 'certificate',
|
||||
mountCategory: ['secret'],
|
||||
type: 'pki',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'RADIUS',
|
||||
glyph: 'mainframe',
|
||||
mountCategory: ['auth'],
|
||||
type: 'radius',
|
||||
value: 'radius',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'RabbitMQ',
|
||||
glyph: 'rabbitmq-color',
|
||||
mountCategory: ['secret'],
|
||||
type: 'rabbitmq',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'SAML',
|
||||
glyph: 'saml-color',
|
||||
mountCategory: ['auth'],
|
||||
requiresEnterprise: true,
|
||||
type: 'saml',
|
||||
value: 'saml',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'SSH',
|
||||
glyph: 'terminal-screen',
|
||||
isConfigurable: true,
|
||||
mountCategory: ['secret'],
|
||||
type: 'ssh',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'TLS Certificates',
|
||||
glyph: 'certificate',
|
||||
mountCategory: ['auth'],
|
||||
type: 'cert',
|
||||
value: 'cert',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'TOTP',
|
||||
glyph: 'history',
|
||||
mountCategory: ['secret'],
|
||||
type: 'totp',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'Transit',
|
||||
glyph: 'swap-horizontal',
|
||||
mountCategory: ['secret'],
|
||||
type: 'transit',
|
||||
},
|
||||
{
|
||||
displayName: 'Token',
|
||||
type: 'token',
|
||||
mountCategory: ['auth'],
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'Userpass',
|
||||
glyph: 'users',
|
||||
mountCategory: ['auth'],
|
||||
type: 'userpass',
|
||||
value: 'userpass',
|
||||
},
|
||||
|
||||
// TODO: enable builtin plugins after confirming with Product
|
||||
//
|
||||
// {
|
||||
// pluginCategory: 'generic',
|
||||
// displayName: 'Ad',
|
||||
// glyph: 'folder',
|
||||
// isOldEngine: true,
|
||||
// isOnlyMountable: true,
|
||||
// mountCategory: ['secret'],
|
||||
// type: 'ad',
|
||||
// },
|
||||
// {
|
||||
// pluginCategory: 'cloud',
|
||||
// displayName: 'MongoDB Atlas',
|
||||
// glyph: 'mongodb-color',
|
||||
// isOldEngine: true,
|
||||
// isOnlyMountable: true,
|
||||
// mountCategory: ['secret'],
|
||||
// type: 'mongodbatlas',
|
||||
// },
|
||||
// {
|
||||
// pluginCategory: 'infra',
|
||||
// displayName: 'OpenLDAP',
|
||||
// glyph: 'folder-users',
|
||||
// isOldEngine: true,
|
||||
// isOnlyMountable: true,
|
||||
// mountCategory: ['secret'],
|
||||
// type: 'openldap',
|
||||
// },
|
||||
// {
|
||||
// pluginCategory: 'infra',
|
||||
// displayName: 'Terraform',
|
||||
// glyph: 'terraform-color',
|
||||
// isOldEngine: true,
|
||||
// isOnlyMountable: true,
|
||||
// mountCategory: ['secret'],
|
||||
// type: 'terraform',
|
||||
// },
|
||||
];
|
||||
export * from 'core/utils/all-engines-metadata';
|
||||
|
|
|
|||
46
ui/lib/core/addon/components/manage-dropdown.hbs
Normal file
46
ui/lib/core/addon/components/manage-dropdown.hbs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Hds::Dropdown as |D|>
|
||||
{{#if this.isIcon}}
|
||||
<D.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="{{if @model.isSupportedBackend 'supported' 'unsupported'}} secrets engine menu"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
{{else}}
|
||||
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
|
||||
{{/if}}
|
||||
|
||||
{{! Yield point for engine-specific custom menu items (e.g., KV's Generate policy) }}
|
||||
{{yield D}}
|
||||
|
||||
<D.Interactive
|
||||
@icon="settings"
|
||||
@route={{@configRoute}}
|
||||
@model={{this.configureRouteModel}}
|
||||
data-test-popup-menu="Configure"
|
||||
>Configure</D.Interactive>
|
||||
|
||||
{{#if this.shouldShowDelete}}
|
||||
<D.Interactive
|
||||
{{on "click" (fn this.handleDeleteClick @model)}}
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
data-test-popup-menu="Delete"
|
||||
>Delete</D.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{this.handleModalClose}}
|
||||
@onConfirm={{this.handleModalConfirm}}
|
||||
/>
|
||||
{{/if}}
|
||||
117
ui/lib/core/addon/components/manage-dropdown.ts
Normal file
117
ui/lib/core/addon/components/manage-dropdown.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
|
||||
/**
|
||||
* @module ManageDropdown
|
||||
* Reusable component for displaying the Manage dropdown used in secret engine headers & secret engine mount list.
|
||||
*
|
||||
* @example
|
||||
* // In main app page headers and list components — uses the resource getter for the full absolute route
|
||||
* <ManageDropdown
|
||||
* @model={{this.backendModel}}
|
||||
* @configRoute={{this.backendConfigurationLink}}
|
||||
* />
|
||||
*
|
||||
* // In Ember engine templates (pki, kubernetes, ldap, kmip, kv) — pass the short relative route,
|
||||
* // since HDS @route resolves relative to the engine's router mount
|
||||
* <ManageDropdown
|
||||
* @model={{@model}}
|
||||
* @configRoute="configuration"
|
||||
* />
|
||||
*
|
||||
* // With custom menu items (like KV's Generate policy) — icon variant in a Ember engine list
|
||||
* <ManageDropdown
|
||||
* @model={{@backendModel}}
|
||||
* @configRoute="configuration"
|
||||
* as |D|
|
||||
* >
|
||||
* <D.Interactive @icon="shield-check" {{on "click" openFlyout}}>Generate policy</D.Interactive>
|
||||
* </ManageDropdown>
|
||||
*
|
||||
* @param {SecretsEngineResource} model - The secrets engine resource containing the engine details
|
||||
* @param {string} configRoute - Route for the Configure action.
|
||||
* @param {string} variant - Set to "icon" for "..." icon button, otherwise shows "Manage" text button (default)
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
model: SecretsEngineResource;
|
||||
configRoute: string;
|
||||
variant?: 'icon';
|
||||
}
|
||||
|
||||
export default class ManageDropdown extends Component<Args> {
|
||||
@service declare readonly router: RouterService;
|
||||
@service('app-router') declare readonly appRouter: RouterService;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
|
||||
|
||||
get isIcon() {
|
||||
return this.args.variant === 'icon';
|
||||
}
|
||||
|
||||
get configureRouteModel() {
|
||||
return this.args.model.id;
|
||||
}
|
||||
|
||||
get shouldShowDelete() {
|
||||
// Don't show delete for cubbyhole engine
|
||||
return this.args.model.type !== 'cubbyhole';
|
||||
}
|
||||
|
||||
transitionToBackends() {
|
||||
// First try using the router service, which is available in most contexts
|
||||
if (this.router) {
|
||||
this.router.transitionTo('vault.cluster.secrets.backends');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback for ember-engine components which use appRouter instead of router service
|
||||
if (this.appRouter) {
|
||||
this.appRouter.transitionTo('vault.cluster.secrets.backends');
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
handleDeleteClick(engine: SecretsEngineResource) {
|
||||
this.engineToDisable = engine;
|
||||
}
|
||||
|
||||
@action
|
||||
handleModalClose() {
|
||||
this.engineToDisable = undefined;
|
||||
}
|
||||
|
||||
@action
|
||||
async handleModalConfirm() {
|
||||
if (this.engineToDisable) {
|
||||
const { engineType, id, path } = this.engineToDisable;
|
||||
|
||||
try {
|
||||
await this.api.sys.mountsDisableSecretsEngine(id);
|
||||
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
|
||||
this.transitionToBackends();
|
||||
} catch (error) {
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.flashMessages.danger(
|
||||
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
|
||||
);
|
||||
} finally {
|
||||
this.engineToDisable = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
ui/lib/core/addon/helpers/engines-display-data.ts
Normal file
62
ui/lib/core/addon/helpers/engines-display-data.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { ALL_ENGINES, type EngineDisplayData } from 'core/utils/all-engines-metadata';
|
||||
import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers';
|
||||
|
||||
/**
|
||||
* Default metadata for unknown engine plugins
|
||||
*/
|
||||
export const unknownEngineMetadata = (methodType?: string): EngineDisplayData => ({
|
||||
type: methodType || 'unknown',
|
||||
displayName: methodType || 'Unknown plugin',
|
||||
glyph: 'lock',
|
||||
mountCategory: ['secret', 'auth'],
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to retrieve engine metadata for a given `methodType`.
|
||||
* It searches the `ALL_ENGINES` array for an engine with a matching type and returns its metadata object.
|
||||
* The `ALL_ENGINES` array includes secret and auth engines, including those supported only in enterprise.
|
||||
* These details (such as mount type and enterprise licensing) are included in the returned engine object.
|
||||
*
|
||||
* For external plugins that have a builtin mapping (e.g., "vault-plugin-secrets-keymgmt" -> "keymgmt"),
|
||||
* this function returns the metadata for the corresponding builtin engine, preserving the original
|
||||
* external plugin name in the type field.
|
||||
*
|
||||
* Example usage:
|
||||
* const engineMetadata = engineDisplayData('kmip');
|
||||
* if (engineMetadata?.requiresEnterprise) {
|
||||
* console.log(`This mount: ${engineMetadata.engineType} requires an enterprise license`);
|
||||
* }
|
||||
*
|
||||
* @param {string} methodType - The engine type (sometimes called backend) to look up (e.g., "aws", "azure", "vault-plugin-secrets-keymgmt").
|
||||
* @returns {Object} - The engine metadata, which includes information about its mount type (e.g., secret or auth)
|
||||
* and whether it requires an enterprise license. For unknown engines, returns a default unknown plugin object.
|
||||
*/
|
||||
export default function engineDisplayData(methodType: string): EngineDisplayData {
|
||||
// First try to find an exact match
|
||||
const builtinEngine = ALL_ENGINES?.find((t) => t.type === methodType);
|
||||
if (builtinEngine) {
|
||||
return builtinEngine;
|
||||
}
|
||||
|
||||
// If no direct match, check if this is a known external plugin and use its builtin mapping
|
||||
const effectiveType = getEffectiveEngineType(methodType);
|
||||
if (effectiveType !== methodType) {
|
||||
// This is a known external plugin with a builtin mapping
|
||||
const mappedEngine = ALL_ENGINES?.find((t) => t.type === effectiveType);
|
||||
if (mappedEngine) {
|
||||
// Return the mapped engine metadata but preserve the original external plugin type
|
||||
return {
|
||||
...mappedEngine,
|
||||
type: methodType, // Keep the original external plugin name for identification
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Return default unknown plugin metadata
|
||||
return unknownEngineMetadata(methodType);
|
||||
}
|
||||
353
ui/lib/core/addon/utils/all-engines-metadata.ts
Normal file
353
ui/lib/core/addon/utils/all-engines-metadata.ts
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
/**
|
||||
* Metadata configuration for secret and auth engines, including enterprise.
|
||||
*
|
||||
* This file defines and exports engine metadata, including its
|
||||
* displayName, mountCategory, requiresEnterprise, and other relevant properties. It serves as a
|
||||
* centralized source of truth for engine-related configurations.
|
||||
*
|
||||
* Key responsibilities:
|
||||
* - Define metadata for all engines.
|
||||
* - Provide utility functions or constants for accessing engine-specific data.
|
||||
* - Facilitate dynamic engine rendering and behavior based on metadata.
|
||||
*
|
||||
* Example usage:
|
||||
* If an enterprise license is present, return all secret engines;
|
||||
* otherwise, return only the secret engines supported in OSS.
|
||||
* return filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: this.version.isEnterprise });
|
||||
*/
|
||||
|
||||
export interface EngineDisplayData {
|
||||
pluginCategory?: string; // The plugin category is used to group engines in the UI. e.g., 'cloud', 'infra', 'generic'
|
||||
displayName: string;
|
||||
engineRoute?: string; // engines that have their own Ember engine will have this route defined.
|
||||
glyph?: string;
|
||||
isWIF?: boolean; // flag for 'Workload Identity Federation' engines. - https://developer.hashicorp.com/hcp/docs/hcp/iam/service-principal/workload-identity-federation
|
||||
mountCategory: string[];
|
||||
requiredFeature?: string; // flag for engines that require the ADP (Advanced Data Protection) feature. - https://www.hashicorp.com/en/blog/advanced-data-protection-adp-now-available-in-hcp-vault
|
||||
requiresEnterprise?: boolean;
|
||||
isConfigurable?: boolean; // for secret engines that have additional configuration pages and actions.
|
||||
isOnlyMountable?: boolean; // The UI only supports configuration views for these secrets engines. The CLI must be used to manage other engine resources (i.e. roles, credentials).
|
||||
type: string;
|
||||
value?: string;
|
||||
configRoute?: string; // override for custom route if not "configuration.plugin-settings" (used for Ember engines)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mountCategory - Given mount category to filter by, e.g., 'auth' or 'secret'.
|
||||
* @param isEnterprise - Optional boolean to indicate if enterprise engines should be included in the results.
|
||||
* @returns Filtered array of engines that match the given mount category
|
||||
*/
|
||||
export function filterEnginesByMountCategory({
|
||||
mountCategory,
|
||||
isEnterprise = false,
|
||||
}: {
|
||||
mountCategory: 'auth' | 'secret';
|
||||
isEnterprise: boolean;
|
||||
}) {
|
||||
return isEnterprise
|
||||
? ALL_ENGINES.filter((engine) => engine.mountCategory.includes(mountCategory))
|
||||
: ALL_ENGINES.filter(
|
||||
(engine) => engine.mountCategory.includes(mountCategory) && !engine.requiresEnterprise
|
||||
);
|
||||
}
|
||||
|
||||
export function isAddonEngine(type: string, version: number) {
|
||||
if (type === 'kv' && version === 1) {
|
||||
return false;
|
||||
}
|
||||
const engineRoute = ALL_ENGINES.find((engine) => engine.type === type)?.engineRoute;
|
||||
return !!engineRoute;
|
||||
}
|
||||
|
||||
// The "sys/mounts" and "sys/internal/ui/mounts" endpoints return a "secret/" key containing
|
||||
// all mounts enabled in Vault. Some types are internal Vault APIs, not user-mountable secrets engines,
|
||||
// and should be filtered in some scenarios, such as listing secrets engines.
|
||||
export const INTERNAL_ENGINE_TYPES = ['system', 'identity', 'agent_registry'];
|
||||
|
||||
export const ALL_ENGINES: EngineDisplayData[] = [
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'AliCloud',
|
||||
glyph: 'alibaba-color',
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'alicloud',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'AppRole',
|
||||
glyph: 'cpu',
|
||||
mountCategory: ['auth'],
|
||||
type: 'approle',
|
||||
value: 'approle',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'AWS',
|
||||
glyph: 'aws-color',
|
||||
isConfigurable: true,
|
||||
isWIF: true,
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'aws',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'Azure',
|
||||
glyph: 'azure-color',
|
||||
isOnlyMountable: true,
|
||||
isConfigurable: true,
|
||||
isWIF: true,
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'azure',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'Consul',
|
||||
glyph: 'consul-color',
|
||||
mountCategory: ['secret'],
|
||||
type: 'consul',
|
||||
},
|
||||
{
|
||||
displayName: 'Cubbyhole',
|
||||
type: 'cubbyhole',
|
||||
mountCategory: ['secret'],
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'Databases',
|
||||
glyph: 'database',
|
||||
mountCategory: ['secret'],
|
||||
type: 'database',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'GitHub',
|
||||
glyph: 'github-color',
|
||||
mountCategory: ['auth'],
|
||||
type: 'github',
|
||||
value: 'github',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'Google Cloud',
|
||||
glyph: 'gcp-color',
|
||||
isOnlyMountable: true,
|
||||
isConfigurable: true,
|
||||
isWIF: true,
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'gcp',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'Google Cloud KMS',
|
||||
glyph: 'gcp-color',
|
||||
mountCategory: ['secret'],
|
||||
type: 'gcpkms',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'JWT',
|
||||
glyph: 'jwt',
|
||||
mountCategory: ['auth'],
|
||||
type: 'jwt',
|
||||
value: 'jwt',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'KV',
|
||||
engineRoute: 'kv.list',
|
||||
configRoute: 'kv.configuration', // only utilized to display config data for kvv2, not in conjunction with isConfigurable as templates determine whether engine is kv v1 or v2
|
||||
glyph: 'key-values',
|
||||
mountCategory: ['secret'],
|
||||
type: 'kv',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'KMIP',
|
||||
engineRoute: 'kmip.scopes.index',
|
||||
configRoute: 'kmip.configuration',
|
||||
isConfigurable: true,
|
||||
glyph: 'lock',
|
||||
mountCategory: ['secret'],
|
||||
requiredFeature: 'KMIP',
|
||||
requiresEnterprise: true,
|
||||
type: 'kmip',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'Transform',
|
||||
glyph: 'transform-data',
|
||||
mountCategory: ['secret'],
|
||||
requiredFeature: 'Transform Secrets Engine',
|
||||
requiresEnterprise: true,
|
||||
type: 'transform',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'cloud',
|
||||
displayName: 'Key Management',
|
||||
glyph: 'key',
|
||||
mountCategory: ['secret'],
|
||||
requiredFeature: 'Key Management Secrets Engine',
|
||||
requiresEnterprise: true,
|
||||
type: 'keymgmt',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'Kubernetes',
|
||||
engineRoute: 'kubernetes.overview',
|
||||
configRoute: 'kubernetes.configuration',
|
||||
glyph: 'kubernetes-color',
|
||||
isConfigurable: true,
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'kubernetes',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'LDAP',
|
||||
isConfigurable: true,
|
||||
engineRoute: 'ldap.overview',
|
||||
configRoute: 'ldap.configuration',
|
||||
glyph: 'folder-users',
|
||||
mountCategory: ['auth', 'secret'],
|
||||
type: 'ldap',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'Nomad',
|
||||
glyph: 'nomad-color',
|
||||
mountCategory: ['secret'],
|
||||
type: 'nomad',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'OIDC',
|
||||
glyph: 'openid-color',
|
||||
mountCategory: ['auth'],
|
||||
type: 'oidc',
|
||||
value: 'oidc',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'Okta',
|
||||
glyph: 'okta-color',
|
||||
mountCategory: ['auth'],
|
||||
type: 'okta',
|
||||
value: 'okta',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'PKI Certificates',
|
||||
isConfigurable: true,
|
||||
engineRoute: 'pki.overview',
|
||||
configRoute: 'pki.configuration',
|
||||
glyph: 'certificate',
|
||||
mountCategory: ['secret'],
|
||||
type: 'pki',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'RADIUS',
|
||||
glyph: 'mainframe',
|
||||
mountCategory: ['auth'],
|
||||
type: 'radius',
|
||||
value: 'radius',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'infra',
|
||||
displayName: 'RabbitMQ',
|
||||
glyph: 'rabbitmq-color',
|
||||
mountCategory: ['secret'],
|
||||
type: 'rabbitmq',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'SAML',
|
||||
glyph: 'saml-color',
|
||||
mountCategory: ['auth'],
|
||||
requiresEnterprise: true,
|
||||
type: 'saml',
|
||||
value: 'saml',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'SSH',
|
||||
glyph: 'terminal-screen',
|
||||
isConfigurable: true,
|
||||
mountCategory: ['secret'],
|
||||
type: 'ssh',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'TLS Certificates',
|
||||
glyph: 'certificate',
|
||||
mountCategory: ['auth'],
|
||||
type: 'cert',
|
||||
value: 'cert',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'TOTP',
|
||||
glyph: 'history',
|
||||
mountCategory: ['secret'],
|
||||
type: 'totp',
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'Transit',
|
||||
glyph: 'swap-horizontal',
|
||||
mountCategory: ['secret'],
|
||||
type: 'transit',
|
||||
},
|
||||
{
|
||||
displayName: 'Token',
|
||||
type: 'token',
|
||||
mountCategory: ['auth'],
|
||||
},
|
||||
{
|
||||
pluginCategory: 'generic',
|
||||
displayName: 'Userpass',
|
||||
glyph: 'users',
|
||||
mountCategory: ['auth'],
|
||||
type: 'userpass',
|
||||
value: 'userpass',
|
||||
},
|
||||
|
||||
// TODO: enable builtin plugins after confirming with Product
|
||||
//
|
||||
// {
|
||||
// pluginCategory: 'generic',
|
||||
// displayName: 'Ad',
|
||||
// glyph: 'folder',
|
||||
// isOnlyMountable: true,
|
||||
// mountCategory: ['secret'],
|
||||
// type: 'ad',
|
||||
// },
|
||||
// {
|
||||
// pluginCategory: 'cloud',
|
||||
// displayName: 'MongoDB Atlas',
|
||||
// glyph: 'mongodb-color',
|
||||
// isOnlyMountable: true,
|
||||
// mountCategory: ['secret'],
|
||||
// type: 'mongodbatlas',
|
||||
// },
|
||||
// {
|
||||
// pluginCategory: 'infra',
|
||||
// displayName: 'OpenLDAP',
|
||||
// glyph: 'folder-users',
|
||||
// isOnlyMountable: true,
|
||||
// mountCategory: ['secret'],
|
||||
// type: 'openldap',
|
||||
// },
|
||||
// {
|
||||
// pluginCategory: 'infra',
|
||||
// displayName: 'Terraform',
|
||||
// glyph: 'terraform-color',
|
||||
// isOnlyMountable: true,
|
||||
// mountCategory: ['secret'],
|
||||
// type: 'terraform',
|
||||
// },
|
||||
];
|
||||
6
ui/lib/core/app/components/manage-dropdown.js
Normal file
6
ui/lib/core/app/components/manage-dropdown.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'core/components/manage-dropdown';
|
||||
6
ui/lib/core/app/helpers/engines-display-data.js
Normal file
6
ui/lib/core/app/helpers/engines-display-data.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'core/helpers/engines-display-data';
|
||||
|
|
@ -7,21 +7,7 @@
|
|||
<KmipBreadcrumb @currentRoute={{this.secretMountPath.currentPath}} />
|
||||
</:breadcrumbs>
|
||||
<:actions>
|
||||
<Hds::Dropdown as |D|>
|
||||
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
|
||||
<D.Interactive
|
||||
@icon="settings"
|
||||
@route="configuration"
|
||||
@model={{@secretsEngine.id}}
|
||||
data-test-popup-menu="Configure"
|
||||
>Configure</D.Interactive>
|
||||
<D.Interactive
|
||||
{{on "click" (fn (mut this.engineToDisable) @secretsEngine)}}
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
data-test-popup-menu="Delete"
|
||||
>Delete</D.Interactive>
|
||||
</Hds::Dropdown>
|
||||
<ManageDropdown @model={{@secretsEngine}} @configRoute="configuration" />
|
||||
</:actions>
|
||||
</Page::Header>
|
||||
<KmipTabs />
|
||||
|
|
@ -117,14 +103,4 @@
|
|||
<Hds::Link::Standalone @icon="plus" @text="Create a scope" @route="scopes.create" />
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{perform this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
|
@ -3,21 +3,21 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { getOwner } from '@ember/owner';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type { CapabilitiesMap, EngineOwner } from 'vault/app-types';
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import FlashMessageService from 'vault/services/flash-messages';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
|
||||
interface Args {
|
||||
secretsEngine: SecretsEngineResource;
|
||||
scopes: string[];
|
||||
capabilities: CapabilitiesMap;
|
||||
}
|
||||
|
|
@ -27,7 +27,6 @@ export default class KmipScopesPageComponent extends Component<Args> {
|
|||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
|
||||
|
||||
@tracked scopeToDelete: string | null = null;
|
||||
|
||||
|
|
@ -56,22 +55,4 @@ export default class KmipScopesPageComponent extends Component<Args> {
|
|||
this.flashMessages.danger(`Error deleting scope ${this.scopeToDelete}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
*disableEngine(engine: SecretsEngineResource) {
|
||||
const { engineType, id, path } = engine;
|
||||
|
||||
try {
|
||||
yield this.api.sys.mountsDisableSecretsEngine(id);
|
||||
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
|
||||
this.router.transitionTo('vault.cluster.secrets.backends');
|
||||
} catch (err) {
|
||||
const { message } = yield this.api.parseError(err);
|
||||
this.flashMessages.danger(
|
||||
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
|
||||
);
|
||||
} finally {
|
||||
this.engineToDisable = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,21 +15,7 @@
|
|||
{{#if @configRoute}}
|
||||
<Hds::Button @color="secondary" @route="overview" @text="Exit configuration" data-test-button="Exit configuration" />
|
||||
{{else}}
|
||||
<Hds::Dropdown as |D|>
|
||||
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
|
||||
<D.Interactive
|
||||
@icon="settings"
|
||||
@route={{if @promptConfig "configure" "configuration"}}
|
||||
@model={{@model.id}}
|
||||
data-test-popup-menu="Configure"
|
||||
>Configure</D.Interactive>
|
||||
<D.Interactive
|
||||
{{on "click" (fn (mut this.engineToDisable) @model)}}
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
data-test-popup-menu="Delete"
|
||||
>Delete</D.Interactive>
|
||||
</Hds::Dropdown>
|
||||
<ManageDropdown @model={{@model}} @configRoute={{if @promptConfig "configure" "configuration"}} />
|
||||
{{/if}}
|
||||
</:actions>
|
||||
</Page::Header>
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type ApiService from 'vault/services/api';
|
||||
|
||||
/**
|
||||
* @module KubernetesHeader handles the ldap page header.
|
||||
*
|
||||
* @example
|
||||
* <SecretEngine::KubernetesHeader
|
||||
* @model={{this.model}}
|
||||
* />
|
||||
*
|
||||
* @param {object} secretsEngine - A model contains a ldap secret engine resource.
|
||||
* @param {object} config - A model contains the configuration of the ldap secret engine.
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
secretsEngine: SecretsEngineResource;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export default class KubernetesHeader extends Component<Args> {
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
|
||||
|
||||
@task
|
||||
*disableEngine(engine: SecretsEngineResource) {
|
||||
const { engineType, id, path } = engine;
|
||||
|
||||
try {
|
||||
yield this.api.sys.mountsDisableSecretsEngine(id);
|
||||
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
|
||||
this.router.transitionTo('vault.cluster.secrets.backends');
|
||||
} catch (err) {
|
||||
const { message } = yield this.api.parseError(err);
|
||||
this.flashMessages.danger(
|
||||
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
|
||||
);
|
||||
} finally {
|
||||
this.engineToDisable = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,7 @@
|
|||
<Hds::Badge @text="version 2" data-test-badge />
|
||||
</:badges>
|
||||
<:actions>
|
||||
<Hds::Dropdown as |D|>
|
||||
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
|
||||
<ManageDropdown @model={{@backendModel}} @configRoute="configuration" as |D|>
|
||||
<CodeGenerator::Policy::Flyout @onClose={{D.close}}>
|
||||
<:customTrigger as |openFlyout|>
|
||||
<D.Interactive @icon="shield-check" {{on "click" openFlyout}} data-test-popup-menu="Generate policy">
|
||||
|
|
@ -20,19 +19,7 @@
|
|||
</D.Interactive>
|
||||
</:customTrigger>
|
||||
</CodeGenerator::Policy::Flyout>
|
||||
<D.Interactive
|
||||
@icon="settings"
|
||||
@route="configuration"
|
||||
@model={{@backendModel.id}}
|
||||
data-test-popup-menu="Configure"
|
||||
>Configure</D.Interactive>
|
||||
<D.Interactive
|
||||
{{on "click" (fn (mut this.engineToDisable) @backendModel)}}
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
data-test-popup-menu="Delete"
|
||||
>Delete</D.Interactive>
|
||||
</Hds::Dropdown>
|
||||
</ManageDropdown>
|
||||
|
||||
<Hds::Button
|
||||
@text="Create secret"
|
||||
|
|
@ -197,14 +184,4 @@
|
|||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{perform this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
|
@ -3,14 +3,13 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { getOwner } from '@ember/owner';
|
||||
import { service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { ancestorKeysForKey } from 'core/utils/key-utils';
|
||||
import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
/**
|
||||
* @module List
|
||||
|
|
@ -32,7 +31,6 @@ export default class KvListPageComponent extends Component {
|
|||
|
||||
@tracked secretPath;
|
||||
@tracked metadataToDelete = null; // set to the metadata intended to delete
|
||||
@tracked engineToDisable = undefined;
|
||||
|
||||
// used for KV list and list-directory view
|
||||
// ex: beep/
|
||||
|
|
@ -59,24 +57,6 @@ export default class KvListPageComponent extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
@task
|
||||
*disableEngine(engine) {
|
||||
const { engineType, id, path } = engine;
|
||||
|
||||
try {
|
||||
yield this.api.sys.mountsDisableSecretsEngine(id);
|
||||
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
|
||||
this.router.transitionTo('vault.cluster.secrets.backends');
|
||||
} catch (err) {
|
||||
const { message } = yield this.api.parseError(err);
|
||||
this.flashMessages.danger(
|
||||
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
|
||||
);
|
||||
} finally {
|
||||
this.engineToDisable = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async onDelete(secretPath) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -15,21 +15,7 @@
|
|||
{{#if @configRoute}}
|
||||
<Hds::Button @color="secondary" @route="overview" @text="Exit configuration" data-test-button="Exit configuration" />
|
||||
{{else}}
|
||||
<Hds::Dropdown as |D|>
|
||||
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
|
||||
<D.Interactive
|
||||
@icon="settings"
|
||||
@route={{if @promptConfig "configure" "configuration"}}
|
||||
@model={{@model.id}}
|
||||
data-test-popup-menu="Configure"
|
||||
>Configure</D.Interactive>
|
||||
<D.Interactive
|
||||
{{on "click" (fn (mut this.engineToDisable) @model)}}
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
data-test-popup-menu="Delete"
|
||||
>Delete</D.Interactive>
|
||||
</Hds::Dropdown>
|
||||
<ManageDropdown @model={{@model}} @configRoute={{if @promptConfig "configure" "configuration"}} />
|
||||
{{/if}}
|
||||
</:actions>
|
||||
</Page::Header>
|
||||
|
|
@ -51,16 +37,6 @@
|
|||
</nav>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{perform this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<Toolbar aria-label="nav for managing LDAP">
|
||||
<ToolbarFilters aria-label="filter for LDAP items">
|
||||
{{yield to="toolbarFilters"}}
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type ApiService from 'vault/services/api';
|
||||
|
||||
/**
|
||||
* @module LdapHeader handles the ldap page header.
|
||||
*
|
||||
* @example
|
||||
* <SecretEngine::LdapHeader
|
||||
* @model={{this.model}}
|
||||
* />
|
||||
*
|
||||
* @param {object} secretsEngine - A model contains a ldap secret engine resource.
|
||||
* @param {object} config - A model contains the configuration of the ldap secret engine.
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
secretsEngine: SecretsEngineResource;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export default class LdapHeader extends Component<Args> {
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked engineToDisable: SecretsEngineResource | undefined = undefined;
|
||||
|
||||
@task
|
||||
*disableEngine(engine: SecretsEngineResource) {
|
||||
const { engineType, id, path } = engine;
|
||||
|
||||
try {
|
||||
yield this.api.sys.mountsDisableSecretsEngine(id);
|
||||
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
|
||||
this.router.transitionTo('vault.cluster.secrets.backends');
|
||||
} catch (err) {
|
||||
const { message } = yield this.api.parseError(err);
|
||||
this.flashMessages.danger(
|
||||
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
|
||||
);
|
||||
} finally {
|
||||
this.engineToDisable = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,21 +17,7 @@
|
|||
{{#if @configRoute}}
|
||||
<Hds::Button @color="secondary" @route="overview" @text="Exit configuration" data-test-button="Exit configuration" />
|
||||
{{else}}
|
||||
<Hds::Dropdown as |D|>
|
||||
<D.ToggleButton @text="Manage" @color="secondary" data-test-dropdown="Manage" />
|
||||
<D.Interactive
|
||||
@icon="settings"
|
||||
@route="configuration"
|
||||
@model={{@backend.id}}
|
||||
data-test-popup-menu="Configure"
|
||||
>Configure</D.Interactive>
|
||||
<D.Interactive
|
||||
{{on "click" (fn (mut this.engineToDisable) @backend)}}
|
||||
@color="critical"
|
||||
@icon="trash"
|
||||
data-test-popup-menu="Delete"
|
||||
>Delete</D.Interactive>
|
||||
</Hds::Dropdown>
|
||||
<ManageDropdown @model={{@backend}} @configRoute="configuration" />
|
||||
{{/if}}
|
||||
</:actions>
|
||||
</Page::Header>
|
||||
|
|
@ -58,14 +44,4 @@
|
|||
<li><LinkTo @route="tidy" @model={{@backend.id}} data-test-secret-list-tab="Tidy">Tidy</LinkTo></li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.engineToDisable}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Any data in this engine will be permanently deleted."
|
||||
@confirmTitle="Disable engine?"
|
||||
@onClose={{fn (mut this.engineToDisable) null}}
|
||||
@onConfirm={{perform this.disableEngine this.engineToDisable}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
|
@ -4,16 +4,12 @@
|
|||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import Component from '@glimmer/component';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
import type { PATH_MAP } from 'vault/utils/constants/capabilities';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type CapabilitiesService from 'vault/services/capabilities';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type CapabilitiesService from 'vault/services/capabilities';
|
||||
import type { PATH_MAP } from 'vault/utils/constants/capabilities';
|
||||
|
||||
/**
|
||||
* @module PkiPageHeader
|
||||
|
|
@ -25,7 +21,7 @@ import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
|||
*/
|
||||
|
||||
interface Args {
|
||||
backend: { id: string };
|
||||
backend: SecretsEngineResource;
|
||||
}
|
||||
|
||||
const ROUTE_PATH_MAP = {
|
||||
|
|
@ -36,12 +32,8 @@ const ROUTE_PATH_MAP = {
|
|||
|
||||
export default class PkiPageHeader extends Component<Args> {
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly capabilities: CapabilitiesService;
|
||||
|
||||
@tracked engineToDisable = undefined;
|
||||
|
||||
get breadcrumbs() {
|
||||
return [
|
||||
{ label: 'Vault', route: 'vault', icon: 'vault', linkExternal: true },
|
||||
|
|
@ -61,22 +53,4 @@ export default class PkiPageHeader extends Component<Args> {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@task
|
||||
*disableEngine(engine: SecretsEngineResource) {
|
||||
const { engineType, id, path } = engine;
|
||||
|
||||
try {
|
||||
yield this.api.sys.mountsDisableSecretsEngine(id);
|
||||
this.flashMessages.success(`The ${engineType} Secrets Engine at ${path} has been disabled.`);
|
||||
this.router.transitionTo('vault.cluster.secrets.backends');
|
||||
} catch (err) {
|
||||
const { message } = yield this.api.parseError(err);
|
||||
this.flashMessages.danger(
|
||||
`There was an error disabling the ${engineType} Secrets Engine at ${path}: ${message}.`
|
||||
);
|
||||
} finally {
|
||||
this.engineToDisable = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
518
ui/tests/acceptance/secrets/manage-dropdown-routing-test.js
Normal file
518
ui/tests/acceptance/secrets/manage-dropdown-routing-test.js
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { click, currentRouteName, fillIn, findAll, settled, visit } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { module, test } from 'qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import engineDisplayData from 'vault/helpers/engines-display-data';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { runCmd } from 'vault/tests/helpers/commands';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
|
||||
const SECRET_ENGINE_MANAGE_DROPDOWN_ROUTING_CASES = [
|
||||
{
|
||||
key: 'alicloud',
|
||||
type: 'alicloud',
|
||||
isEnginePathClickable: false,
|
||||
showManageDropdown: false,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'azure',
|
||||
type: 'azure',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: false,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'gcp',
|
||||
type: 'gcp',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: false,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'gcpkms',
|
||||
type: 'gcpkms',
|
||||
isEnginePathClickable: false,
|
||||
showManageDropdown: false,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'keymgmt',
|
||||
type: 'keymgmt',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'kubernetes',
|
||||
type: 'kubernetes',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
expectedActionConfigureRoutesOverride: [
|
||||
// if the engine is configured
|
||||
'vault.cluster.secrets.backend.kubernetes.configuration',
|
||||
// if the engine is not configured
|
||||
'vault.cluster.secrets.backend.kubernetes.configure',
|
||||
],
|
||||
expectedLandingConfigureRoutesOverride: [
|
||||
// if the engine is configured
|
||||
'vault.cluster.secrets.backend.kubernetes.configuration',
|
||||
// if the engine is not configured
|
||||
'vault.cluster.secrets.backend.kubernetes.configure',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'kvv1',
|
||||
type: 'kv',
|
||||
version: 1,
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'kvv2',
|
||||
type: 'kv',
|
||||
version: 2,
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: true,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
expectedLandingConfigureRoutesOverride: ['vault.cluster.secrets.backend.kv.configuration'],
|
||||
},
|
||||
{
|
||||
key: 'transform',
|
||||
type: 'transform',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'transit',
|
||||
type: 'transit',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'kmip',
|
||||
type: 'kmip',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
expectedActionConfigureRoutesOverride: [
|
||||
// if the engine is configured
|
||||
'vault.cluster.secrets.backend.kmip.configuration',
|
||||
// if the engine is not configured
|
||||
'vault.cluster.secrets.backend.kmip.configure',
|
||||
],
|
||||
expectedLandingConfigureRoutesOverride: [
|
||||
// if the engine is configured
|
||||
'vault.cluster.secrets.backend.kmip.configuration',
|
||||
// if the engine is not configured
|
||||
'vault.cluster.secrets.backend.kmip.configure',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'ldap',
|
||||
type: 'ldap',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
expectedActionConfigureRoutesOverride: [
|
||||
// if the engine is configured
|
||||
'vault.cluster.secrets.backend.ldap.configuration',
|
||||
// if the engine is not configured
|
||||
'vault.cluster.secrets.backend.ldap.configure',
|
||||
],
|
||||
expectedLandingConfigureRoutesOverride: [
|
||||
// if the engine is configured
|
||||
'vault.cluster.secrets.backend.ldap.configuration',
|
||||
// if the engine is not configured
|
||||
'vault.cluster.secrets.backend.ldap.configure',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'pki',
|
||||
type: 'pki',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
expectedActionConfigureRoutesOverride: [
|
||||
// if the engine is configured
|
||||
'vault.cluster.secrets.backend.pki.configuration',
|
||||
// if the engine is not configured
|
||||
'vault.cluster.secrets.backend.pki.configuration.create',
|
||||
],
|
||||
expectedLandingConfigureRoutesOverride: [
|
||||
// if the engine is configured
|
||||
'vault.cluster.secrets.backend.pki.configuration',
|
||||
// if the engine is not configured
|
||||
'vault.cluster.secrets.backend.pki.configuration.create',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'ssh',
|
||||
type: 'ssh',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'totp',
|
||||
type: 'totp',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'aws',
|
||||
type: 'aws',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: true,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'consul',
|
||||
type: 'consul',
|
||||
isEnginePathClickable: false,
|
||||
showManageDropdown: false,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'nomad',
|
||||
type: 'nomad',
|
||||
isEnginePathClickable: false,
|
||||
showManageDropdown: false,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'rabbitmq',
|
||||
type: 'rabbitmq',
|
||||
isEnginePathClickable: false,
|
||||
showManageDropdown: false,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
},
|
||||
{
|
||||
key: 'database',
|
||||
type: 'database',
|
||||
isEnginePathClickable: true,
|
||||
showManageDropdown: false,
|
||||
showGeneratePolicy: false,
|
||||
showConfigure: true,
|
||||
showDelete: true,
|
||||
expectedLandingRouteOverride: 'vault.cluster.secrets.backend.overview',
|
||||
expectedLandingConfigureRoutesOverride: [],
|
||||
},
|
||||
];
|
||||
|
||||
const secretsEngineListRoute = '/vault/secrets-engines';
|
||||
|
||||
const mountEngine = async ({ type, version }, path) => {
|
||||
await mountSecrets.visit();
|
||||
await click(GENERAL.cardContainer(type));
|
||||
await fillIn(GENERAL.inputByAttr('path'), path);
|
||||
if (type === 'kv' && version === 1) {
|
||||
await click(GENERAL.button('Method Options'));
|
||||
await mountSecrets.version(1);
|
||||
}
|
||||
await click(GENERAL.submitButton);
|
||||
};
|
||||
|
||||
const filterEngineRowByPath = async (path) => {
|
||||
await visit(secretsEngineListRoute);
|
||||
const searchInputSelector = GENERAL.inputSearch('secret-engine-path');
|
||||
if (findAll(searchInputSelector).length) {
|
||||
await fillIn(searchInputSelector, path);
|
||||
}
|
||||
};
|
||||
|
||||
const clickVisibleMenuItem = async (name) => {
|
||||
const visibleItem = findAll(GENERAL.menuItem(name)).find((el) => el.offsetParent !== null);
|
||||
if (!visibleItem) {
|
||||
throw new Error(`No visible menu item found for: ${name}`);
|
||||
}
|
||||
await click(visibleItem);
|
||||
};
|
||||
|
||||
const assertMenuOptionVisibility = (assert, visibilityByOption, contextLabel, engineKey) => {
|
||||
for (const [option, isVisible] of Object.entries(visibilityByOption)) {
|
||||
if (isVisible) {
|
||||
assert.dom(GENERAL.menuItem(option)).exists(`${contextLabel} shows ${option} for ${engineKey}`);
|
||||
} else {
|
||||
assert
|
||||
.dom(GENERAL.menuItem(option))
|
||||
.doesNotExist(`${contextLabel} does not show ${option} for ${engineKey}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clickVisibleConfirmButton = async () => {
|
||||
const visibleConfirmButton = findAll(GENERAL.confirmButton).find((el) => el.offsetParent !== null);
|
||||
if (!visibleConfirmButton) {
|
||||
return false;
|
||||
}
|
||||
await click(visibleConfirmButton);
|
||||
return true;
|
||||
};
|
||||
|
||||
const expectedActionConfigureRoutes = (engineType) => {
|
||||
const { isConfigurable, configRoute } = engineDisplayData(engineType);
|
||||
if (!isConfigurable) {
|
||||
return ['vault.cluster.secrets.backend.configuration.general-settings'];
|
||||
}
|
||||
|
||||
if (configRoute) {
|
||||
return [`vault.cluster.secrets.backend.${configRoute}`];
|
||||
}
|
||||
|
||||
return [
|
||||
// if the engine is configured
|
||||
'vault.cluster.secrets.backend.configuration.plugin-settings',
|
||||
// if the engine is not configured
|
||||
'vault.cluster.secrets.backend.configuration.edit',
|
||||
];
|
||||
};
|
||||
|
||||
const expectedLandingRoute = ({ type, version = 1 }) => {
|
||||
const engineData = engineDisplayData(type);
|
||||
const isKvV1 = type === 'kv' && version === 1;
|
||||
|
||||
if (engineData.isOnlyMountable) {
|
||||
return 'vault.cluster.secrets.backend.configuration.general-settings';
|
||||
}
|
||||
if (engineData.engineRoute && !isKvV1) {
|
||||
return `vault.cluster.secrets.backend.${engineData.engineRoute}`;
|
||||
}
|
||||
return 'vault.cluster.secrets.backend.list-root';
|
||||
};
|
||||
|
||||
const expectedLandingConfigureRoutes = ({ type, version = 1 }) => {
|
||||
const engineData = engineDisplayData(type);
|
||||
const isKvV1 = type === 'kv' && version === 1;
|
||||
|
||||
if (engineData.engineRoute && !isKvV1) {
|
||||
if (engineData.configRoute) {
|
||||
return [`vault.cluster.secrets.backend.${engineData.configRoute}`];
|
||||
}
|
||||
}
|
||||
|
||||
if (engineData.isConfigurable) {
|
||||
return [
|
||||
// if the engine is configured
|
||||
'vault.cluster.secrets.backend.configuration.plugin-settings',
|
||||
// if the engine is not configured
|
||||
'vault.cluster.secrets.backend.configuration.edit',
|
||||
];
|
||||
}
|
||||
|
||||
return ['vault.cluster.secrets.backend.configuration.general-settings'];
|
||||
};
|
||||
|
||||
const runEngineCase = async (assert, engine, uid, isEnterprise = false) => {
|
||||
const mountPath = `manage-${engine.key}-${uid}`;
|
||||
const actionConfigureRoutes =
|
||||
engine.expectedActionConfigureRoutesOverride || expectedActionConfigureRoutes(engine.type);
|
||||
const expectedManage = {
|
||||
showManageDropdown: engine.showManageDropdown ?? false,
|
||||
showGeneratePolicy: (engine.showGeneratePolicy ?? false) && isEnterprise,
|
||||
showConfigure: engine.showConfigure ?? true,
|
||||
showDelete: engine.showDelete ?? true,
|
||||
};
|
||||
|
||||
// if engine path already exists, delete it before starting the test
|
||||
await runCmd(`delete sys/mounts/${mountPath}`);
|
||||
|
||||
// mount the engine
|
||||
await mountEngine(engine, mountPath);
|
||||
|
||||
// verify the engine shows in the list
|
||||
await filterEngineRowByPath(mountPath);
|
||||
assert.dom(GENERAL.tableRow()).exists(`row renders for ${engine.key}`);
|
||||
|
||||
assert.dom(GENERAL.menuTrigger).exists(`Action menu is shown for ${engine.key}`);
|
||||
await click(GENERAL.menuTrigger);
|
||||
|
||||
assertMenuOptionVisibility(
|
||||
assert,
|
||||
{
|
||||
Configure: expectedManage.showConfigure,
|
||||
Delete: expectedManage.showDelete,
|
||||
},
|
||||
'Action menu',
|
||||
engine.key
|
||||
);
|
||||
|
||||
if (expectedManage.showConfigure) {
|
||||
// click configure and verify route
|
||||
await clickVisibleMenuItem('Configure');
|
||||
await settled();
|
||||
assert.true(
|
||||
actionConfigureRoutes.includes(currentRouteName()),
|
||||
`Action: Configure routes correctly for ${engine.key}`
|
||||
);
|
||||
await filterEngineRowByPath(mountPath);
|
||||
}
|
||||
|
||||
if (expectedManage.showDelete) {
|
||||
// click delete and verify the engine is removed from the list
|
||||
await filterEngineRowByPath(mountPath);
|
||||
await click(GENERAL.menuTrigger);
|
||||
await clickVisibleMenuItem('Delete');
|
||||
const didConfirmActionDelete = await clickVisibleConfirmButton();
|
||||
assert.true(didConfirmActionDelete, `Action: Delete shows confirm button for ${engine.key}`);
|
||||
await settled();
|
||||
|
||||
await filterEngineRowByPath(mountPath);
|
||||
assert.dom(GENERAL.tableRow()).doesNotExist(`Action: Delete removes ${engine.key} mount`);
|
||||
|
||||
// remount the engine for manage dropdown testing
|
||||
await mountEngine(engine, mountPath);
|
||||
await filterEngineRowByPath(mountPath);
|
||||
}
|
||||
|
||||
const isEnginePathClickable = engine.isEnginePathClickable ?? false;
|
||||
const backendLinkSelector = `a[href*="/vault/secrets-engines/${mountPath}"]`;
|
||||
|
||||
if (!isEnginePathClickable) {
|
||||
// if the engine path is not expected to be clickable, verify it's not a link and skip the rest of the test
|
||||
assert.dom(backendLinkSelector).doesNotExist(`EnginePath is not a clickable link for ${engine.key}`);
|
||||
await runCmd(`delete sys/mounts/${mountPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.dom(backendLinkSelector).exists(`EnginePath is a clickable link for ${engine.key}`);
|
||||
await click(backendLinkSelector);
|
||||
|
||||
const routeAfterPathClick = engine.expectedLandingRouteOverride || expectedLandingRoute(engine);
|
||||
assert.strictEqual(
|
||||
currentRouteName(),
|
||||
routeAfterPathClick,
|
||||
`Engine path click redirects to ${routeAfterPathClick} for ${engine.key}`
|
||||
);
|
||||
|
||||
const shouldShowManageDropdown = expectedManage.showManageDropdown;
|
||||
|
||||
if (!shouldShowManageDropdown) {
|
||||
// if manage dropdown is not expected to show on the landing page, verify it's not shown and skip the rest of the test
|
||||
assert
|
||||
.dom(GENERAL.dropdownToggle('Manage'))
|
||||
.doesNotExist(`Manage dropdown is not shown on landing page for ${engine.key}`);
|
||||
await runCmd(`delete sys/mounts/${mountPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
assert
|
||||
.dom(GENERAL.dropdownToggle('Manage'))
|
||||
.exists(`Manage dropdown shows on landing page for ${engine.key}`);
|
||||
await click(GENERAL.dropdownToggle('Manage'));
|
||||
assertMenuOptionVisibility(
|
||||
assert,
|
||||
{
|
||||
'Generate policy': expectedManage.showGeneratePolicy,
|
||||
Configure: expectedManage.showConfigure,
|
||||
Delete: expectedManage.showDelete,
|
||||
},
|
||||
'Manage dropdown',
|
||||
engine.key
|
||||
);
|
||||
|
||||
if (expectedManage.showConfigure) {
|
||||
// click configure and verify route
|
||||
await clickVisibleMenuItem('Configure');
|
||||
await settled();
|
||||
const allowedConfigureRoutes =
|
||||
engine.expectedLandingConfigureRoutesOverride || expectedLandingConfigureRoutes(engine);
|
||||
assert.true(
|
||||
allowedConfigureRoutes.includes(currentRouteName()),
|
||||
`Manage Configure routes correctly for ${engine.key}`
|
||||
);
|
||||
|
||||
await filterEngineRowByPath(mountPath);
|
||||
await click(backendLinkSelector);
|
||||
await click(GENERAL.dropdownToggle('Manage'));
|
||||
}
|
||||
|
||||
if (expectedManage.showDelete) {
|
||||
// click delete and verify the engine is removed from the list
|
||||
await clickVisibleMenuItem('Delete');
|
||||
const didConfirmManageDelete = await clickVisibleConfirmButton();
|
||||
if (!didConfirmManageDelete) {
|
||||
assert.true(
|
||||
engine.type === 'kubernetes',
|
||||
`Manage Delete missing confirm is only expected for kubernetes; got ${engine.key}`
|
||||
);
|
||||
await runCmd(`delete sys/mounts/${mountPath}`);
|
||||
await filterEngineRowByPath(mountPath);
|
||||
assert.dom(GENERAL.tableRow()).doesNotExist(`Manage Delete removes ${engine.key} mount`);
|
||||
return;
|
||||
}
|
||||
await settled();
|
||||
|
||||
await filterEngineRowByPath(mountPath);
|
||||
assert.dom(GENERAL.tableRow()).doesNotExist(`Manage Delete removes ${engine.key} mount`);
|
||||
return; // if the delete action is confirmed, the engine should be removed and we can end the test here without needing to clean up again
|
||||
}
|
||||
};
|
||||
|
||||
module('Acceptance | secrets-engines/manage-dropdown routing', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.uid = uuidv4();
|
||||
return login();
|
||||
});
|
||||
|
||||
for (const engine of SECRET_ENGINE_MANAGE_DROPDOWN_ROUTING_CASES) {
|
||||
const isEnterpriseOnly = !!engineDisplayData(engine.type).requiresEnterprise;
|
||||
const engineLabel = isEnterpriseOnly ? `${engine.key} (enterprise only)` : engine.key;
|
||||
|
||||
test(`manage dropdown coverage | ${engineLabel}`, async function (assert) {
|
||||
const isEnterprise = this.owner.lookup('service:version').isEnterprise;
|
||||
await runEngineCase(assert, engine, this.uid, isEnterprise);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -4,35 +4,34 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
click,
|
||||
currentRouteName,
|
||||
currentURL,
|
||||
settled,
|
||||
click,
|
||||
findAll,
|
||||
fillIn,
|
||||
visit,
|
||||
findAll,
|
||||
settled,
|
||||
typeIn,
|
||||
visit,
|
||||
waitFor,
|
||||
} from '@ember/test-helpers';
|
||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import { module, test, skip } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { module, skip, test } from 'qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
|
||||
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import page from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
|
||||
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
|
||||
import { SELECTORS as OIDC } from 'vault/tests/helpers/oidc-config';
|
||||
import { adminOidcCreateRead, adminOidcCreate } from 'vault/tests/helpers/secret-engine/policy-generator';
|
||||
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
|
||||
import engineDisplayData from 'vault/helpers/engines-display-data';
|
||||
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { SELECTORS as OIDC } from 'vault/tests/helpers/oidc-config';
|
||||
import { adminOidcCreate, adminOidcCreateRead } from 'vault/tests/helpers/secret-engine/policy-generator';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
|
||||
import { default as mountSecrets, default as page } from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
|
||||
|
||||
const consoleComponent = create(consoleClass);
|
||||
|
||||
|
|
@ -152,6 +151,7 @@ module('Acceptance | secrets-engines/enable', function (hooks) {
|
|||
|
||||
await page.secretList();
|
||||
await settled();
|
||||
await fillIn(GENERAL.inputSearch('secret-engine-path'), path);
|
||||
assert
|
||||
.dom(GENERAL.tableData(`${path}/`, 'path'))
|
||||
.exists({ count: 1 }, 'renders only one instance of the engine');
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default (test, type) => {
|
|||
|
||||
await fillIn(GENERAL.inputSearch('secret-engine-path'), backend);
|
||||
await click(GENERAL.menuTrigger);
|
||||
await click(GENERAL.menuItem('View configuration'));
|
||||
await click(GENERAL.menuItem('Configure'));
|
||||
assert.strictEqual(
|
||||
currentRouteName(),
|
||||
`${BASE_ROUTE}.${this.expectedConfigEditRoute}`,
|
||||
|
|
@ -117,7 +117,7 @@ export default (test, type) => {
|
|||
await visit(`/vault/secrets-engines`);
|
||||
await fillIn(GENERAL.inputSearch('secret-engine-path'), backend);
|
||||
await click(GENERAL.menuTrigger);
|
||||
await click(GENERAL.menuItem('View configuration'));
|
||||
await click(GENERAL.menuItem('Configure'));
|
||||
|
||||
// For configurable engines, clicking "View configuration" will direct to its plugin settings route
|
||||
await waitUntil(() => currentRouteName() === `${BASE_ROUTE}.${configRoute}`);
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { currentURL, visit, click, fillIn, currentRouteName, waitUntil } from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { click, currentRouteName, currentURL, fillIn, visit, waitUntil } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { module, test } from 'qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
|
||||
module('Acceptance | secret engine mount settings', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
|
@ -63,7 +63,7 @@ module('Acceptance | secret engine mount settings', function (hooks) {
|
|||
await visit('/vault/secrets-engines');
|
||||
await fillIn(GENERAL.inputSearch('secret-engine-path'), path);
|
||||
await click(GENERAL.menuTrigger);
|
||||
await click(GENERAL.menuItem('View configuration'));
|
||||
await click(GENERAL.menuItem('Configure'));
|
||||
// since ldap hasn't been configured yet, it should redirect to configure page
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
|
|
@ -93,7 +93,7 @@ module('Acceptance | secret engine mount settings', function (hooks) {
|
|||
await visit('/vault/secrets-engines');
|
||||
await fillIn(GENERAL.inputSearch('secret-engine-path'), path);
|
||||
await click(GENERAL.menuTrigger);
|
||||
await click(GENERAL.menuItem('View configuration'));
|
||||
await click(GENERAL.menuItem('Configure'));
|
||||
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.configuration.general-settings');
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
|
|
@ -125,7 +125,7 @@ module('Acceptance | secret engine mount settings', function (hooks) {
|
|||
await visit('/vault/secrets-engines');
|
||||
await fillIn(GENERAL.inputSearch('secret-engine-path'), path);
|
||||
await click(GENERAL.menuTrigger);
|
||||
await click(GENERAL.menuItem('View configuration'));
|
||||
await click(GENERAL.menuItem('Configure'));
|
||||
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.configuration.edit');
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
|
|
|
|||
|
|
@ -3,15 +3,16 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { click, fillIn, render } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { module, test } from 'qunit';
|
||||
import sinon from 'sinon';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import { getErrorResponse } from 'vault/tests/helpers/api/error-response';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
module('Integration | Component | kmip | Page::Scopes', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
|
@ -20,6 +21,7 @@ module('Integration | Component | kmip | Page::Scopes', function (hooks) {
|
|||
|
||||
hooks.beforeEach(function () {
|
||||
this.backend = 'kmip-test';
|
||||
this.secretsEngine = new SecretsEngineResource({ path: this.backend, type: 'kmip' });
|
||||
this.owner.lookup('service:secret-mount-path').update(this.backend);
|
||||
|
||||
const { secrets } = this.owner.lookup('service:api');
|
||||
|
|
@ -51,7 +53,7 @@ module('Integration | Component | kmip | Page::Scopes', function (hooks) {
|
|||
|
||||
this.renderComponent = () =>
|
||||
render(
|
||||
hbs`<Page::Scopes @scopes={{this.scopes}} @capabilities={{this.capabilities}} @filterValue={{this.filterValue}} />`,
|
||||
hbs`<Page::Scopes @secretsEngine={{this.secretsEngine}} @scopes={{this.scopes}} @capabilities={{this.capabilities}} @filterValue={{this.filterValue}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
});
|
||||
|
|
|
|||
161
ui/tests/integration/components/manage-dropdown-test.js
Normal file
161
ui/tests/integration/components/manage-dropdown-test.js
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { click, render } from '@ember/test-helpers';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { module, test } from 'qunit';
|
||||
import sinon from 'sinon';
|
||||
import SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
const DEFAULT_MOUNT_DATA = {
|
||||
accessor: 'test_accessor',
|
||||
config: {},
|
||||
description: '',
|
||||
external_entropy_access: false,
|
||||
local: false,
|
||||
plugin_version: '',
|
||||
running_plugin_version: '',
|
||||
running_sha256: '',
|
||||
seal_wrap: false,
|
||||
uuid: 'test-uuid',
|
||||
};
|
||||
|
||||
const TEST_CASES = [
|
||||
{
|
||||
label: 'alicloud',
|
||||
type: 'alicloud',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
{
|
||||
label: 'azure',
|
||||
type: 'azure',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings',
|
||||
},
|
||||
{
|
||||
label: 'gcp',
|
||||
type: 'gcp',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings',
|
||||
},
|
||||
{
|
||||
label: 'gcpkms',
|
||||
type: 'gcpkms',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
{
|
||||
label: 'keymgmt',
|
||||
type: 'keymgmt',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
{
|
||||
label: 'kubernetes',
|
||||
type: 'kubernetes',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.kubernetes.configuration',
|
||||
},
|
||||
{
|
||||
label: 'kvv1',
|
||||
type: 'kv',
|
||||
version: 1,
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
{
|
||||
label: 'kvv2',
|
||||
type: 'kv',
|
||||
version: 2,
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
{
|
||||
label: 'transform',
|
||||
type: 'transform',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
{
|
||||
label: 'transit',
|
||||
type: 'transit',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
{ label: 'kmip', type: 'kmip', expectedRoute: 'vault.cluster.secrets.backend.kmip.configuration' },
|
||||
{ label: 'ldap', type: 'ldap', expectedRoute: 'vault.cluster.secrets.backend.ldap.configuration' },
|
||||
{ label: 'pki', type: 'pki', expectedRoute: 'vault.cluster.secrets.backend.pki.configuration' },
|
||||
{
|
||||
label: 'ssh',
|
||||
type: 'ssh',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings',
|
||||
},
|
||||
{
|
||||
label: 'totp',
|
||||
type: 'totp',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
{
|
||||
label: 'aws',
|
||||
type: 'aws',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.plugin-settings',
|
||||
},
|
||||
{
|
||||
label: 'consul',
|
||||
type: 'consul',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
{
|
||||
label: 'nomad',
|
||||
type: 'nomad',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
{
|
||||
label: 'rabbitmq',
|
||||
type: 'rabbitmq',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
{
|
||||
label: 'database',
|
||||
type: 'database',
|
||||
expectedRoute: 'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
},
|
||||
];
|
||||
|
||||
module('Integration | Component | manage-dropdown | Configure link', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
const makeModel = ({ type, version, id }) => {
|
||||
const options = version ? { version } : undefined;
|
||||
return new SecretsEngineResource({
|
||||
...DEFAULT_MOUNT_DATA,
|
||||
path: `${id}/`,
|
||||
type,
|
||||
options,
|
||||
});
|
||||
};
|
||||
|
||||
TEST_CASES.forEach(({ label, type, version, expectedRoute }) => {
|
||||
test(`Configure link routes correctly for ${label}`, async function (assert) {
|
||||
const routing = this.owner.lookup('service:-routing');
|
||||
const transitionSpy = sinon.stub(routing, 'transitionTo');
|
||||
const id = `${label}-integration-test`;
|
||||
this.model = makeModel({ type, version, id });
|
||||
|
||||
await render(
|
||||
hbs`<ManageDropdown @model={{this.model}} @variant="icon" @configRoute={{this.model.backendConfigurationLink}} />`
|
||||
);
|
||||
|
||||
await click(GENERAL.menuTrigger);
|
||||
await click(GENERAL.menuItem('Configure'));
|
||||
|
||||
assert.true(transitionSpy.called, `Configure action for ${label} triggers a route transition`);
|
||||
assert.strictEqual(
|
||||
transitionSpy.firstCall.args[0],
|
||||
expectedRoute,
|
||||
`Configure action for ${label} transitions to ${expectedRoute}`
|
||||
);
|
||||
assert.true(
|
||||
JSON.stringify(transitionSpy.firstCall.args).includes(id),
|
||||
`Configure action for ${label} includes model id ${id}`
|
||||
);
|
||||
|
||||
transitionSpy.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
88
ui/tests/unit/components/manage-dropdown-test.js
Normal file
88
ui/tests/unit/components/manage-dropdown-test.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { module, test } from 'qunit';
|
||||
import SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
|
||||
const makeResource = ({ type, version }) => {
|
||||
const options = version ? { version } : undefined;
|
||||
return new SecretsEngineResource({
|
||||
accessor: 'test_accessor',
|
||||
config: {},
|
||||
description: '',
|
||||
external_entropy_access: false,
|
||||
local: false,
|
||||
options,
|
||||
path: `${type}-test/`,
|
||||
plugin_version: '',
|
||||
running_plugin_version: '',
|
||||
running_sha256: '',
|
||||
seal_wrap: false,
|
||||
type,
|
||||
uuid: 'test-uuid',
|
||||
});
|
||||
};
|
||||
|
||||
module('Unit | Component | manage-dropdown', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('backendConfigurationLink: addon engines with configRoute always use their config route', function (assert) {
|
||||
const kubernetes = makeResource({ type: 'kubernetes' });
|
||||
assert.strictEqual(
|
||||
kubernetes.backendConfigurationLink,
|
||||
'vault.cluster.secrets.backend.kubernetes.configuration',
|
||||
'kubernetes always routes to its configuration page'
|
||||
);
|
||||
|
||||
const ldap = makeResource({ type: 'ldap' });
|
||||
assert.strictEqual(
|
||||
ldap.backendConfigurationLink,
|
||||
'vault.cluster.secrets.backend.ldap.configuration',
|
||||
'ldap always routes to its configuration page'
|
||||
);
|
||||
|
||||
const pki = makeResource({ type: 'pki' });
|
||||
assert.strictEqual(
|
||||
pki.backendConfigurationLink,
|
||||
'vault.cluster.secrets.backend.pki.configuration',
|
||||
'pki always routes to its configuration page'
|
||||
);
|
||||
});
|
||||
|
||||
test('backendConfigurationLink: configurable engines without configRoute route to plugin-settings', function (assert) {
|
||||
const ssh = makeResource({ type: 'ssh' });
|
||||
assert.strictEqual(
|
||||
ssh.backendConfigurationLink,
|
||||
'vault.cluster.secrets.backend.configuration.plugin-settings',
|
||||
'configurable engine routes to the plugin-settings view'
|
||||
);
|
||||
});
|
||||
|
||||
test('backendConfigurationLink: non-configurable engines always route to general-settings', function (assert) {
|
||||
const alicloud = makeResource({ type: 'alicloud' });
|
||||
assert.strictEqual(
|
||||
alicloud.backendConfigurationLink,
|
||||
'vault.cluster.secrets.backend.configuration.general-settings'
|
||||
);
|
||||
});
|
||||
|
||||
test('backendConfigurationLink: KV v1 routes to general-settings', function (assert) {
|
||||
const kvV1 = makeResource({ type: 'kv', version: 1 });
|
||||
assert.strictEqual(
|
||||
kvV1.backendConfigurationLink,
|
||||
'vault.cluster.secrets.backend.configuration.general-settings'
|
||||
);
|
||||
});
|
||||
|
||||
test('backendConfigurationLink: KV v2 routes to general-settings (configRoute is display-only)', function (assert) {
|
||||
const kvV2 = makeResource({ type: 'kv', version: 2 });
|
||||
assert.strictEqual(
|
||||
kvV2.backendConfigurationLink,
|
||||
'vault.cluster.secrets.backend.configuration.general-settings',
|
||||
"kv's configRoute is skipped because it's for display only"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import engineDisplayData, { unknownEngineMetadata } from 'core/helpers/engines-display-data';
|
||||
import { module, test } from 'qunit';
|
||||
import engineDisplayData, { unknownEngineMetadata } from 'vault/helpers/engines-display-data';
|
||||
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
|
||||
|
||||
module('Unit | Helper | engines-display-data', function () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue