From 008835ba36151e82d4fa370265bb9c1d644c068a Mon Sep 17 00:00:00 2001 From: Dan Rivera Date: Thu, 19 Jun 2025 15:10:09 -0400 Subject: [PATCH] UI: Surface plugin version & cleanup utils (#31001) * surface plugin version & removing mountable-auth-methods.js * UI: Removing mountable-secret-engines.js (#30950) * first pass, removing all related imports * fix usage * fix category * fix typos * fix more tests * fix more tests pt2 * attempting WIF const removal * fix wif tests, removing config consts * fixing tests * please * removing fallback * cleanup * fix type ent test * remove isaddon * Revert "remove isaddon" This reverts commit ee114197b7299711e35e3c8e5aca9694063726eb. * adding tab click * update case * fix case, rename to isOnlyMountable * fix backend form * more test fix * adding changelog * pr comments * renaming params, adding requiresADP * updates * updates and pr comments * perhaps update the test --- changelog/31001.txt | 3 + ui/app/components/auth/form-template.hbs | 2 +- ui/app/components/auth/tabs.hbs | 2 +- ui/app/components/mount-backend-form.hbs | 14 +- ui/app/components/mount-backend-form.ts | 59 ++-- ui/app/components/mount-backend/type-form.hbs | 4 +- ui/app/components/mount-backend/type-form.js | 17 +- ui/app/components/secret-engine/list.hbs | 25 +- ui/app/components/secret-engine/list.ts | 2 +- .../secrets/backend/configuration/edit.js | 6 +- .../secrets/backend/configuration/index.js | 4 +- .../cluster/settings/mount-secret-backend.js | 9 +- ui/app/forms/secrets/engine.ts | 4 +- ui/app/helpers/auth-display-name.ts | 11 - ui/app/helpers/engines-display-data.ts | 27 ++ ui/app/helpers/mountable-auth-methods.js | 133 -------- ui/app/helpers/mountable-secret-engines.js | 179 ----------- ui/app/helpers/supported-auth-backends.js | 2 - ui/app/models/auth-method.js | 8 +- ui/app/models/mfa-login-enforcement.js | 4 +- ui/app/models/secret-engine.js | 22 +- ui/app/resources/secrets/engine.ts | 7 +- .../secrets/backend/configuration/edit.ts | 4 +- .../secrets/backend/configuration/index.js | 9 +- .../vault/cluster/secrets/backend/list.js | 9 +- ui/app/serializers/secret-engine.js | 4 +- .../vault/cluster/settings/auth/configure.hbs | 2 +- .../vault/cluster/settings/auth/enable.hbs | 2 +- .../cluster/settings/mount-secret-backend.hbs | 2 +- ui/app/utils/all-engines-metadata.ts | 301 ++++++++++++++++++ .../addon/components/secret-list-header.js | 4 +- .../components/secrets-engine-mount-config.ts | 6 +- ui/tests/acceptance/auth/auth-list-test.js | 4 +- ui/tests/acceptance/enterprise-kmip-test.js | 4 +- ui/tests/acceptance/enterprise-kmse-test.js | 4 +- .../acceptance/enterprise-transform-test.js | 4 +- .../secret-engine-list-view-test.js | 40 ++- .../settings/mount-secret-backend-test.js | 21 +- .../auth-config-form/options-test.js | 4 +- .../components/mount-backend-form-test.js | 45 ++- .../mount-backend/type-form-test.js | 25 +- .../configuration-details-test.js | 12 +- .../secret-engine/configure-wif-test.js | 17 +- .../secrets-engine-mount-config-test.js | 4 +- ui/tests/unit/models/secret-engine-test.js | 5 + ui/types/vault/models/secret-engine.d.ts | 1 + 46 files changed, 594 insertions(+), 483 deletions(-) create mode 100644 changelog/31001.txt delete mode 100644 ui/app/helpers/auth-display-name.ts create mode 100644 ui/app/helpers/engines-display-data.ts delete mode 100644 ui/app/helpers/mountable-auth-methods.js delete mode 100644 ui/app/helpers/mountable-secret-engines.js create mode 100644 ui/app/utils/all-engines-metadata.ts diff --git a/changelog/31001.txt b/changelog/31001.txt new file mode 100644 index 0000000000..85cf909ebe --- /dev/null +++ b/changelog/31001.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui/secrets: Display the plugin version on the secret engine list view. Move KV's version to a tooltip that appears when hovering over the engine's name. +``` \ No newline at end of file diff --git a/ui/app/components/auth/form-template.hbs b/ui/app/components/auth/form-template.hbs index 0f52b0ab45..d806553d7f 100644 --- a/ui/app/components/auth/form-template.hbs +++ b/ui/app/components/auth/form-template.hbs @@ -49,7 +49,7 @@ {{#each this.supportedAuthTypes as |type|}} {{/each}} diff --git a/ui/app/components/auth/tabs.hbs b/ui/app/components/auth/tabs.hbs index cfb49cc3f9..52bfca458b 100644 --- a/ui/app/components/auth/tabs.hbs +++ b/ui/app/components/auth/tabs.hbs @@ -5,7 +5,7 @@ {{#each-in @authTabData as |methodType mounts|}} - {{auth-display-name methodType}} + {{get (engines-display-data methodType) "displayName"}}
{{! Elements "behind" tabs always render on the DOM and are just superficially hidden/shown. diff --git a/ui/app/components/mount-backend-form.hbs b/ui/app/components/mount-backend-form.hbs index 264b4b5a87..6f20923e6f 100644 --- a/ui/app/components/mount-backend-form.hbs +++ b/ui/app/components/mount-backend-form.hbs @@ -9,13 +9,13 @@ {{#if this.showEnable}} {{#let (find-by "type" @mountModel.type @mountTypes) as |typeInfo|}} - {{#if (eq @mountType "secret")}} + {{#if (eq @mountCategory "secret")}} {{concat "Enable " typeInfo.displayName " Secrets Engine"}} {{else}} {{concat "Enable " typeInfo.displayName " Authentication Method"}} {{/if}} {{/let}} - {{else if (eq @mountType "secret")}} + {{else if (eq @mountCategory "secret")}} Enable a Secrets Engine {{else}} Enable an Authentication Method @@ -25,13 +25,13 @@
- + {{#if @mountModel.type}}
<:identityTokenKey>
{{else}} {{! Type not yet set, show type options }} - + {{/if}}
\ No newline at end of file diff --git a/ui/app/components/mount-backend-form.ts b/ui/app/components/mount-backend-form.ts index fbfed74fd4..1f37c66245 100644 --- a/ui/app/components/mount-backend-form.ts +++ b/ui/app/components/mount-backend-form.ts @@ -9,13 +9,14 @@ import { service } from '@ember/service'; import { action } from '@ember/object'; import { task } from 'ember-concurrency'; import { waitFor } from '@ember/test-waiters'; -import { methods } from 'vault/helpers/mountable-auth-methods'; -import { isAddonEngine, allEngines } from 'vault/helpers/mountable-secret-engines'; +import { presence } from 'vault/utils/forms/validators'; +import { filterEnginesByMountCategory, isAddonEngine } from 'vault/utils/all-engines-metadata'; +import { assert } from '@ember/debug'; import { ResponseError } from '@hashicorp/vault-client-typescript'; +import AdapterError from '@ember-data/adapter/error'; import type FlashMessageService from 'vault/services/flash-messages'; import type Store from '@ember-data/store'; -import type AdapterError from '@ember-data/adapter/error'; import type { AuthEnableModel } from 'vault/routes/vault/cluster/settings/auth/enable'; import type SecretsEngineForm from 'vault/forms/secrets/engine'; import type CapabilitiesService from 'vault/services/capabilities'; @@ -27,10 +28,10 @@ import type { ApiError } from '@ember-data/adapter/error'; * The `MountBackendForm` is used to mount either a secret or auth backend. * * @example ```js - * ``` + * ``` * * @param {function} onMountSuccess - A function that transitions once the Mount has been successfully posted. - * @param {string} [mountType=auth] - The type of backend we want to mount. + * @param {string} mountCategory - The type of engine to mount, either 'secret' or 'auth'. * */ @@ -38,7 +39,7 @@ type MountModel = SecretsEngineForm | AuthEnableModel; interface Args { mountModel: MountModel; - mountType: 'secret' | 'auth'; + mountCategory: 'secret' | 'auth'; onMountSuccess: (type: string, path: string, useEngineRoute: boolean) => void; } @@ -54,41 +55,47 @@ export default class MountBackendForm extends Component { @tracked errorMessage: string | string[] = ''; + constructor(owner: unknown, args: Args) { + super(owner, args); + assert(`@mountCategory is required. Must be "auth" or "secret".`, presence(this.args.mountCategory)); + } + willDestroy() { // components are torn down after store is unloaded and will cause an error if attempt to unload record const noTeardown = this.store && !this.store.isDestroying; - if (noTeardown && this.args.mountType === 'auth' && this.args?.mountModel?.isNew) { + if (noTeardown && this.args.mountCategory === 'auth' && this.args?.mountModel?.isNew) { this.args.mountModel.unloadRecord(); } super.willDestroy(); } - checkPathChange(type: string) { - if (!type) return; + checkPathChange(backendType: string) { + if (!backendType) return; const mount = this.args.mountModel; const currentPath = mount.path; - const mountTypes = - this.args.mountType === 'secret' - ? allEngines().map((engine) => engine.type) - : methods().map((auth) => auth.type); + // mountCategory is usually 'secret' or 'auth', but sometimes an empty string is passed in (like when we click the cancel button). + // In these cases, we should default to returning auth methods. + const mountsByType = filterEnginesByMountCategory({ + mountCategory: this.args.mountCategory ?? 'auth', + isEnterprise: true, + }).map((engine) => engine.type); // if the current path has not been altered by user, // change it here to match the new type - if (!currentPath || mountTypes.includes(currentPath)) { - mount.path = type; + if (!currentPath || mountsByType.includes(currentPath)) { + mount.path = backendType; } } typeChangeSideEffect(type: string) { - if (this.args.mountType === 'secret') { - // If type PKI, set max lease to ~10years - this.args.mountModel.config.maxLeaseTtl = type === 'pki' ? '3650d' : 0; - } + if (this.args.mountCategory !== 'secret') return; + // If type PKI, set max lease to ~10years + this.args.mountModel.config.maxLeaseTtl = type === 'pki' ? '3650d' : 0; } checkModelValidity(model: MountModel) { - const { mountType } = this.args; + const { mountCategory } = this.args; const { isValid, state, invalidFormMessage, data } = - mountType === 'secret' ? model.toJSON() : model.validate(); + mountCategory === 'secret' ? model.toJSON() : model.validate(); this.modelValidations = state; this.invalidFormAlert = invalidFormMessage; return { isValid, data }; @@ -97,8 +104,8 @@ export default class MountBackendForm extends Component { checkModelWarnings() { // check for warnings on change // since we only show errors on submit we need to clear those out and only send warning state - const { mountType, mountModel } = this.args; - const { state } = mountType === 'secret' ? mountModel.toJSON() : mountModel.validate(); + const { mountCategory, mountModel } = this.args; + const { state } = mountCategory === 'secret' ? mountModel.toJSON() : mountModel.validate(); for (const key in state) { state[key].errors = []; } @@ -152,7 +159,7 @@ export default class MountBackendForm extends Component { @waitFor *mountBackend(event: Event) { event.preventDefault(); - const { mountModel, mountType } = this.args; + const { mountModel, mountCategory } = this.args; const { type, path } = mountModel; // only submit form if validations pass const { isValid, data: formData } = this.checkModelValidity(mountModel); @@ -161,7 +168,7 @@ export default class MountBackendForm extends Component { } try { - if (mountType === 'secret') { + if (mountCategory === 'secret') { yield this.api.sys.mountsEnableSecretsEngine(path, formData); yield this.saveKvConfig(path, formData); } else { @@ -169,7 +176,7 @@ export default class MountBackendForm extends Component { } this.flashMessages.success( `Successfully mounted the ${type} ${ - this.args.mountType === 'secret' ? 'secrets engine' : 'auth method' + this.args.mountCategory === 'secret' ? 'secrets engine' : 'auth method' } at ${path}.` ); // check whether to use the Ember engine route diff --git a/ui/app/components/mount-backend/type-form.hbs b/ui/app/components/mount-backend/type-form.hbs index 3fcaf3824e..34b8e9cef9 100644 --- a/ui/app/components/mount-backend/type-form.hbs +++ b/ui/app/components/mount-backend/type-form.hbs @@ -8,7 +8,7 @@ {{capitalize category}}
- {{#each (filter-by "category" category this.mountTypes) as |type|}} + {{#each (filter-by "pluginCategory" category this.mountTypes) as |type|}}
\ No newline at end of file diff --git a/ui/app/components/mount-backend/type-form.js b/ui/app/components/mount-backend/type-form.js index a04d051e47..3fc40e6464 100644 --- a/ui/app/components/mount-backend/type-form.js +++ b/ui/app/components/mount-backend/type-form.js @@ -5,8 +5,7 @@ import Component from '@glimmer/component'; import { service } from '@ember/service'; -import { allMethods, methods } from 'vault/helpers/mountable-auth-methods'; -import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-engines'; +import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; /** * @@ -16,24 +15,28 @@ import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-eng * * @example * ```js - * + * * ``` * @param {CallableFunction} setMountType - function will receive the mount type string. Should update the model type value - * @param {string} [mountType=auth] - mount type can be `auth` or `secret` + * @param {string} [mountCategory=auth] - mount category can be `auth` or `secret` */ export default class MountBackendTypeForm extends Component { @service version; get secretEngines() { - return this.version.isEnterprise ? allEngines() : mountableEngines(); + // 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 }); } get authMethods() { - return this.version.isEnterprise ? allMethods() : methods(); + // If an enterprise license is present, return all auth methods; + // otherwise, return only the auth methods supported in OSS. + return filterEnginesByMountCategory({ mountCategory: 'auth', isEnterprise: this.version.isEnterprise }); } get mountTypes() { - return this.args.mountType === 'secret' ? this.secretEngines : this.authMethods; + return this.args.mountCategory === 'secret' ? this.secretEngines : this.authMethods; } } diff --git a/ui/app/components/secret-engine/list.hbs b/ui/app/components/secret-engine/list.hbs index 3323919c1d..9ad2f8d83c 100644 --- a/ui/app/components/secret-engine/list.hbs +++ b/ui/app/components/secret-engine/list.hbs @@ -48,7 +48,21 @@
{{#if backend.icon}} - + {{/if}} @@ -67,11 +81,10 @@ {{/if}} {{/if}}
- {{#if backend.accessor}} - - {{if (eq backend.version 2) (concat "v2 " backend.accessor) backend.accessor}} - - {{/if}} + + {{backend.accessor}} + {{backend.runningPluginVersion}} + {{#if backend.description}} {{backend.description}} diff --git a/ui/app/components/secret-engine/list.ts b/ui/app/components/secret-engine/list.ts index e0727deeaf..533477e54d 100644 --- a/ui/app/components/secret-engine/list.ts +++ b/ui/app/components/secret-engine/list.ts @@ -29,7 +29,7 @@ interface Args { secretEngines: Array; } -export default class SecretListItem extends Component { +export default class SecretEngineList extends Component { @service declare readonly flashMessages: FlashMessageService; @service declare readonly api: ApiService; @service declare readonly router: RouterService; diff --git a/ui/app/controllers/vault/cluster/secrets/backend/configuration/edit.js b/ui/app/controllers/vault/cluster/secrets/backend/configuration/edit.js index 63cdca50f8..afd81a57ea 100644 --- a/ui/app/controllers/vault/cluster/secrets/backend/configuration/edit.js +++ b/ui/app/controllers/vault/cluster/secrets/backend/configuration/edit.js @@ -4,13 +4,13 @@ */ import Controller from '@ember/controller'; -import { WIF_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines'; +import engineDisplayData from 'vault/helpers/engines-display-data'; export default class SecretsBackendConfigurationEditController extends Controller { get isWifEngine() { - return WIF_ENGINES.includes(this.model.type); + return engineDisplayData(this.model.type)?.isWIF; } get displayName() { - return allEngines().find((engine) => engine.type === this.model.type)?.displayName; + return engineDisplayData(this.model.type).displayName; } } diff --git a/ui/app/controllers/vault/cluster/secrets/backend/configuration/index.js b/ui/app/controllers/vault/cluster/secrets/backend/configuration/index.js index 2a1f5a2841..4da896091e 100644 --- a/ui/app/controllers/vault/cluster/secrets/backend/configuration/index.js +++ b/ui/app/controllers/vault/cluster/secrets/backend/configuration/index.js @@ -4,8 +4,8 @@ */ import Controller from '@ember/controller'; -import { WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; import { toLabel } from 'core/helpers/to-label'; +import engineDisplayData from 'vault/helpers/engines-display-data'; export default class SecretsBackendConfigurationController extends Controller { get displayFields() { @@ -26,7 +26,7 @@ export default class SecretsBackendConfigurationController extends Controller { fields.push('version'); } // For WIF Secret engines, allow users to set the identity token key when mounting the engine. - if (WIF_ENGINES.includes(engineType)) { + if (engineDisplayData(engineType)?.isWIF) { fields.push('config.identityTokenKey'); } return fields; diff --git a/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js b/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js index 8f0715b2a0..9e97ee479b 100644 --- a/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js +++ b/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js @@ -5,9 +5,9 @@ import { service } from '@ember/service'; import Controller from '@ember/controller'; -import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; -import { allEngines } from 'vault/helpers/mountable-secret-engines'; import { action } from '@ember/object'; +import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; +import engineDisplayData from 'vault/helpers/engines-display-data'; const SUPPORTED_BACKENDS = supportedSecretBackends(); @@ -18,14 +18,15 @@ export default class MountSecretBackendController extends Controller { onMountSuccess(type, path, useEngineRoute = false) { let transition; if (SUPPORTED_BACKENDS.includes(type)) { - const engineInfo = allEngines().find((engine) => engine.type === type); + const engineInfo = engineDisplayData(type); if (useEngineRoute) { transition = this.router.transitionTo( `vault.cluster.secrets.backend.${engineInfo.engineRoute}`, path ); } else { - const queryParams = engineInfo?.routeQueryParams || {}; + // For keymgmt, we need to land on provider tab by default using query params + const queryParams = engineInfo.type === 'keymgmt' ? { tab: 'provider' } : {}; transition = this.router.transitionTo('vault.cluster.secrets.backend.index', path, { queryParams }); } } else { diff --git a/ui/app/forms/secrets/engine.ts b/ui/app/forms/secrets/engine.ts index 9df40d6ab2..9956c6b160 100644 --- a/ui/app/forms/secrets/engine.ts +++ b/ui/app/forms/secrets/engine.ts @@ -7,8 +7,8 @@ import Form from 'vault/forms/form'; import FormField from 'vault/utils/forms/field'; import FormFieldGroup from 'vault/utils/forms/field-group'; import { WHITESPACE_WARNING } from 'vault/utils/forms/validators'; -import { WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; import { tracked } from '@glimmer/tracking'; +import { ALL_ENGINES } from 'vault/utils/all-engines-metadata'; import type { SecretsEngineFormData } from 'vault/secrets/engine'; import type { Validations } from 'vault/app-types'; @@ -134,7 +134,7 @@ export default class SecretsEngineForm extends Form { if (this.engineType === 'pki') { return [...this.coreOptionFields, ...this.standardConfigFields]; } - if (WIF_ENGINES.find((type) => type === this.engineType)) { + if (ALL_ENGINES.find((engine) => engine.type === this.engineType && engine.isWIF)?.type) { return [ ...this.coreOptionFields, defaultTtl, diff --git a/ui/app/helpers/auth-display-name.ts b/ui/app/helpers/auth-display-name.ts deleted file mode 100644 index 6014a0947d..0000000000 --- a/ui/app/helpers/auth-display-name.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { ALL_LOGIN_METHODS } from 'vault/utils/supported-login-methods'; - -export default function authDisplayName(methodType: string) { - const displayName = ALL_LOGIN_METHODS?.find((t) => t.type === methodType)?.displayName; - return displayName || methodType; -} diff --git a/ui/app/helpers/engines-display-data.ts b/ui/app/helpers/engines-display-data.ts new file mode 100644 index 0000000000..2de527e49a --- /dev/null +++ b/ui/app/helpers/engines-display-data.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { ALL_ENGINES } from 'vault/utils/all-engines-metadata'; + +/** + * 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. + * + * Example usage: + * const engineMetadata = engineDisplayData('kmip'); + * if (engineMetadata?.requiresEnterprise) { + * console.log(`This mount: ${engineMetadata.engineType} requires an enterprise license`); + * } + * + * @param {string} methodType - The engine type (sometimes called backend) to look up (e.g., "aws", "azure"). + * @returns {Object|undefined} - The engine metadata, which includes information about its mount type (e.g., secret or auth) + * and whether it requires an enterprise license. Returns undefined if no match is found. + */ +export default function engineDisplayData(methodType: string) { + const engine = ALL_ENGINES?.find((t) => t.type === methodType); + return engine; +} diff --git a/ui/app/helpers/mountable-auth-methods.js b/ui/app/helpers/mountable-auth-methods.js deleted file mode 100644 index 53d2410d6c..0000000000 --- a/ui/app/helpers/mountable-auth-methods.js +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { helper as buildHelper } from '@ember/component/helper'; - -/** - * These are all the auth methods that can be mounted. - * Some methods may not be available for login via the UI, - * which are in the `supported-auth-backends` helper. - */ - -const ENTERPRISE_AUTH_METHODS = [ - { - displayName: 'SAML', - value: 'saml', - type: 'saml', - category: 'generic', - glyph: 'saml-color', - }, -]; - -const MOUNTABLE_AUTH_METHODS = [ - { - displayName: 'AliCloud', - value: 'alicloud', - type: 'alicloud', - category: 'cloud', - glyph: 'alibaba-color', - }, - { - displayName: 'AppRole', - value: 'approle', - type: 'approle', - category: 'generic', - glyph: 'cpu', - }, - { - displayName: 'AWS', - value: 'aws', - type: 'aws', - category: 'cloud', - glyph: 'aws-color', - }, - { - displayName: 'Azure', - value: 'azure', - type: 'azure', - category: 'cloud', - glyph: 'azure-color', - }, - { - displayName: 'Google Cloud', - value: 'gcp', - type: 'gcp', - category: 'cloud', - glyph: 'gcp-color', - }, - { - displayName: 'GitHub', - value: 'github', - type: 'github', - category: 'cloud', - glyph: 'github-color', - }, - { - displayName: 'JWT', - value: 'jwt', - type: 'jwt', - glyph: 'jwt', - category: 'generic', - }, - { - displayName: 'OIDC', - value: 'oidc', - type: 'oidc', - glyph: 'openid-color', - category: 'generic', - }, - { - displayName: 'Kubernetes', - value: 'kubernetes', - type: 'kubernetes', - category: 'infra', - glyph: 'kubernetes-color', - }, - { - displayName: 'LDAP', - value: 'ldap', - type: 'ldap', - glyph: 'folder-users', - category: 'infra', - }, - { - displayName: 'Okta', - value: 'okta', - type: 'okta', - category: 'infra', - glyph: 'okta-color', - }, - { - displayName: 'RADIUS', - value: 'radius', - type: 'radius', - glyph: 'mainframe', - category: 'infra', - }, - { - displayName: 'TLS Certificates', - value: 'cert', - type: 'cert', - category: 'generic', - glyph: 'certificate', - }, - { - displayName: 'Username & Password', - value: 'userpass', - type: 'userpass', - category: 'generic', - glyph: 'users', - }, -]; - -export function methods() { - return MOUNTABLE_AUTH_METHODS.slice(); -} - -export function allMethods() { - return [...MOUNTABLE_AUTH_METHODS, ...ENTERPRISE_AUTH_METHODS]; -} - -export default buildHelper(methods); diff --git a/ui/app/helpers/mountable-secret-engines.js b/ui/app/helpers/mountable-secret-engines.js deleted file mode 100644 index 525d3ee0b7..0000000000 --- a/ui/app/helpers/mountable-secret-engines.js +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { helper as buildHelper } from '@ember/component/helper'; - -const ENTERPRISE_SECRET_ENGINES = [ - { - displayName: 'KMIP', - type: 'kmip', - glyph: 'lock', - engineRoute: 'kmip.scopes.index', - category: 'generic', - requiredFeature: 'KMIP', - }, - { - displayName: 'Transform', - type: 'transform', - category: 'generic', - requiredFeature: 'Transform Secrets Engine', - glyph: 'transform-data', - }, - { - displayName: 'Key Management', - type: 'keymgmt', - glyph: 'key', - category: 'cloud', - requiredFeature: 'Key Management Secrets Engine', - routeQueryParams: { tab: 'provider' }, - }, -]; - -const MOUNTABLE_SECRET_ENGINES = [ - { - displayName: 'AliCloud', - type: 'alicloud', - glyph: 'alibaba-color', - category: 'cloud', - }, - { - displayName: 'AWS', - type: 'aws', - category: 'cloud', - glyph: 'aws-color', - }, - { - displayName: 'Azure', - type: 'azure', - category: 'cloud', - glyph: 'azure-color', - }, - { - displayName: 'Consul', - type: 'consul', - glyph: 'consul-color', - category: 'infra', - }, - { - displayName: 'Databases', - type: 'database', - category: 'infra', - glyph: 'database', - }, - { - displayName: 'Google Cloud', - type: 'gcp', - category: 'cloud', - glyph: 'gcp-color', - }, - { - displayName: 'Google Cloud KMS', - type: 'gcpkms', - category: 'cloud', - glyph: 'gcp-color', - }, - { - displayName: 'KV', - type: 'kv', - glyph: 'key-values', - engineRoute: 'kv.list', - category: 'generic', - }, - { - displayName: 'Nomad', - type: 'nomad', - glyph: 'nomad-color', - category: 'infra', - }, - { - displayName: 'PKI Certificates', - type: 'pki', - glyph: 'certificate', - engineRoute: 'pki.overview', - category: 'generic', - }, - { - displayName: 'RabbitMQ', - type: 'rabbitmq', - glyph: 'rabbitmq-color', - category: 'infra', - }, - { - displayName: 'SSH', - type: 'ssh', - glyph: 'terminal-screen', - category: 'generic', - }, - { - displayName: 'Transit', - type: 'transit', - glyph: 'swap-horizontal', - category: 'generic', - }, - { - displayName: 'TOTP', - type: 'totp', - glyph: 'history', - category: 'generic', - }, - { - displayName: 'LDAP', - type: 'ldap', - engineRoute: 'ldap.overview', - category: 'generic', - glyph: 'folder-users', - }, - { - displayName: 'Kubernetes', - type: 'kubernetes', - engineRoute: 'kubernetes.overview', - category: 'generic', - glyph: 'kubernetes-color', - }, -]; - -// A list of Workload Identity Federation engines. -export const WIF_ENGINES = ['aws', 'azure', 'gcp']; - -export function wifEngines() { - return WIF_ENGINES.slice(); -} - -// The UI only supports configuration views for these secrets engines. The CLI must be used to manage other engine resources (i.e. roles, credentials). -export const CONFIGURATION_ONLY = ['azure', 'gcp']; - -export function configurationOnly() { - return CONFIGURATION_ONLY.slice(); -} - -// Secret engines that have their own configuration page and actions -// These engines do not exist in their own Ember engine. -export const CONFIGURABLE_SECRET_ENGINES = ['aws', 'azure', 'gcp', 'ssh']; - -export function configurableSecretEngines() { - return CONFIGURABLE_SECRET_ENGINES.slice(); -} - -export function mountableEngines() { - return MOUNTABLE_SECRET_ENGINES.slice(); -} -// secret engines that have not other views than the mount view and mount details view -export const UNSUPPORTED_ENGINES = ['alicloud', 'consul', 'gcpkms', 'nomad', 'rabbitmq']; - -export function unsupportedEngines() { - return UNSUPPORTED_ENGINES.slice(); -} - -export function allEngines() { - return [...MOUNTABLE_SECRET_ENGINES, ...ENTERPRISE_SECRET_ENGINES]; -} - -export function isAddonEngine(type, version) { - if (type === 'kv' && version === 1) return false; - const engineRoute = allEngines().find((engine) => engine.type === type)?.engineRoute; - return !!engineRoute; -} - -export default buildHelper(mountableEngines); diff --git a/ui/app/helpers/supported-auth-backends.js b/ui/app/helpers/supported-auth-backends.js index 4c9185cf48..c27f6b9a37 100644 --- a/ui/app/helpers/supported-auth-backends.js +++ b/ui/app/helpers/supported-auth-backends.js @@ -7,8 +7,6 @@ import { helper as buildHelper } from '@ember/component/helper'; /** * These are all the auth methods with which a user can log into the UI. - * This is a subset of the methods found in the `mountable-auth-methods` helper, - * which lists all the methods that can be mounted. */ const SUPPORTED_AUTH_BACKENDS = [ diff --git a/ui/app/models/auth-method.js b/ui/app/models/auth-method.js index 2ea3ef645c..58c82450ef 100644 --- a/ui/app/models/auth-method.js +++ b/ui/app/models/auth-method.js @@ -8,12 +8,12 @@ import { service } from '@ember/service'; import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs'; import apiPath from 'vault/utils/api-path'; import { withModelValidations } from 'vault/decorators/model-validations'; -import { allMethods } from 'vault/helpers/mountable-auth-methods'; import lazyCapabilities from 'vault/macros/lazy-capabilities'; import { action } from '@ember/object'; import { camelize } from '@ember/string'; import { WHITESPACE_WARNING } from 'vault/utils/forms/validators'; import { supportedTypes } from 'vault/utils/supported-login-methods'; +import engineDisplayData from 'vault/helpers/engines-display-data'; const validations = { path: [ @@ -45,9 +45,9 @@ export default class AuthMethodModel extends Model { } get icon() { - const authMethods = allMethods().find((backend) => backend.type === this.methodType); - - return authMethods?.glyph || 'users'; + // methodType refers to the backend type (e.g., "aws", "azure") and is set on a getter. + const engineData = engineDisplayData(this.methodType); + return engineData?.glyph || 'users'; } get directLoginLink() { diff --git a/ui/app/models/mfa-login-enforcement.js b/ui/app/models/mfa-login-enforcement.js index 51c357fe99..d6c909e499 100644 --- a/ui/app/models/mfa-login-enforcement.js +++ b/ui/app/models/mfa-login-enforcement.js @@ -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 { methods } from 'vault/helpers/mountable-auth-methods'; import { withModelValidations } from 'vault/decorators/model-validations'; import { isPresent } from '@ember/utils'; import { service } from '@ember/service'; 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' }], @@ -109,7 +109,7 @@ export default class MfaLoginEnforcementModel extends Model { } iconForMount(type) { - const mountableMethods = methods(); + const mountableMethods = filterEnginesByMountCategory({ mountCategory: 'auth', isEnterprise: true }); const mount = mountableMethods.find((method) => method.type === type); return mount ? mount.glyph || mount.type : 'token'; } diff --git a/ui/app/models/secret-engine.js b/ui/app/models/secret-engine.js index 6dbe684668..0134bbb7cf 100644 --- a/ui/app/models/secret-engine.js +++ b/ui/app/models/secret-engine.js @@ -9,8 +9,9 @@ import { equal } from '@ember/object/computed'; // eslint-disable-line import { withModelValidations } from 'vault/decorators/model-validations'; import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; -import { isAddonEngine, allEngines, WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; import { WHITESPACE_WARNING } from 'vault/utils/forms/validators'; +import { ALL_ENGINES, isAddonEngine } from 'vault/utils/all-engines-metadata'; +import engineDisplayData from 'vault/helpers/engines-display-data'; const LINKED_BACKENDS = supportedSecretBackends(); @@ -99,6 +100,14 @@ export default class SecretEngineModel extends Model { }) deleteVersionAfter; + // `plugin_version` represents the version specified at mount time (if any), and is only used for external plugins. + // For built-in plugins, this field is intentionally left empty to simplify upgrades. + // + // `running_plugin_version` reflects the actual version of the plugin currently running, + // regardless of whether it is built-in or external. This provides a reliable source of truth + // and is why we are surfacing it over plugin_version. + @attr('string') runningPluginVersion; + /* GETTERS */ get isV2KV() { return this.version === 2 && (this.engineType === 'kv' || this.engineType === 'generic'); @@ -115,7 +124,7 @@ export default class SecretEngineModel extends Model { } get icon() { - const engineData = allEngines().find((engine) => engine.type === this.engineType); + const engineData = engineDisplayData(this.engineType); return engineData?.glyph || 'lock'; } @@ -137,8 +146,7 @@ export default class SecretEngineModel extends Model { return 'vault.cluster.secrets.backend.overview'; } if (isAddonEngine(this.engineType, this.version)) { - const { engineRoute } = allEngines().find((engine) => engine.type === this.engineType); - return `vault.cluster.secrets.backend.${engineRoute}`; + return `vault.cluster.secrets.backend.${engineDisplayData(this.engineType).engineRoute}`; } if (this.isV2KV) { // if it's KV v2 but not registered as an addon, it's type generic @@ -160,7 +168,7 @@ export default class SecretEngineModel extends Model { get formFields() { const type = this.engineType; - const fields = ['type', 'path', 'description', 'accessor', 'local', 'sealWrap']; + const fields = ['type', 'path', 'description', 'accessor', 'runningPluginVersion', 'local', 'sealWrap']; // no ttl options for keymgmt if (type !== 'keymgmt') { fields.push('config.defaultLeaseTtl', 'config.maxLeaseTtl'); @@ -180,7 +188,7 @@ export default class SecretEngineModel extends Model { fields.push('casRequired', 'deleteVersionAfter', 'maxVersions'); } // For WIF Secret engines, allow users to set the identity token key when mounting the engine. - if (WIF_ENGINES.includes(type)) { + if (engineDisplayData(type)?.isWIF) { fields.push('config.identityTokenKey'); } return fields; @@ -232,7 +240,7 @@ export default class SecretEngineModel extends Model { // no ttl options for keymgmt optionFields = [...CORE_OPTIONS, 'config.allowedManagedKeys', ...STANDARD_CONFIG]; break; - case WIF_ENGINES.find((type) => type === this.engineType): + case ALL_ENGINES.find((engine) => engine.type === this.engineType && engine.isWIF)?.type: defaultFields = ['path']; optionFields = [ ...CORE_OPTIONS, diff --git a/ui/app/resources/secrets/engine.ts b/ui/app/resources/secrets/engine.ts index c492a70f1d..b975f39d4f 100644 --- a/ui/app/resources/secrets/engine.ts +++ b/ui/app/resources/secrets/engine.ts @@ -4,8 +4,9 @@ */ import { baseResourceFactory } from 'vault/resources/base-factory'; -import { isAddonEngine, allEngines } from 'vault/helpers/mountable-secret-engines'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; +import { isAddonEngine } from 'vault/utils/all-engines-metadata'; +import engineDisplayData from 'vault/helpers/engines-display-data'; import type { SecretsEngine } from 'vault/secrets/engine'; @@ -30,7 +31,7 @@ export default class SecretsEngineResource extends baseResourceFactory engine.type === this.engineType); + const engineData = engineDisplayData(this.engineType); return engineData?.glyph || 'lock'; } @@ -52,7 +53,7 @@ export default class SecretsEngineResource extends baseResourceFactory engine.type === this.engineType); + const engine = engineDisplayData(this.engineType); if (engine?.engineRoute) { return `vault.cluster.secrets.backend.${engine.engineRoute}`; } diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts index afc36d44c4..4c199cc75b 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts +++ b/ui/app/routes/vault/cluster/secrets/backend/configuration/edit.ts @@ -5,11 +5,11 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { CONFIGURABLE_SECRET_ENGINES } from 'vault/helpers/mountable-secret-engines'; import AwsConfigForm from 'vault/forms/secrets/aws-config'; import AzureConfigForm from 'vault/forms/secrets/azure-config'; import GcpConfigForm from 'vault/forms/secrets/gcp-config'; import SshConfigForm from 'vault/forms/secrets/ssh-config'; +import engineDisplayData from 'vault/helpers/engines-display-data'; import type SecretsEngineResource from 'vault/resources/secrets/engine'; import type ApiService from 'vault/services/api'; @@ -47,7 +47,7 @@ export default class SecretsBackendConfigurationEdit extends Route { }[type] || { issuer: '' }; // if the engine type is not configurable or a form class does not exist for the type return a 404. - if (!CONFIGURABLE_SECRET_ENGINES.includes(type) || !formClass) { + if (!engineDisplayData(type)?.isConfigurable || !formClass) { throw { httpStatus: 404, backend }; } diff --git a/ui/app/routes/vault/cluster/secrets/backend/configuration/index.js b/ui/app/routes/vault/cluster/secrets/backend/configuration/index.js index ec20f1dcf9..a40b01ee09 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/configuration/index.js +++ b/ui/app/routes/vault/cluster/secrets/backend/configuration/index.js @@ -4,15 +4,14 @@ */ import Route from '@ember/routing/route'; -import { CONFIGURABLE_SECRET_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines'; +import engineDisplayData from 'vault/helpers/engines-display-data'; export default class SecretsBackendConfigurationIndexRoute extends Route { setupController(controller, resolvedModel) { super.setupController(controller, resolvedModel); - controller.typeDisplay = allEngines().find( - (engine) => engine.type === resolvedModel.secretsEngine.type - )?.displayName; - controller.isConfigurable = CONFIGURABLE_SECRET_ENGINES.includes(resolvedModel.secretsEngine.type); + const engine = engineDisplayData(resolvedModel.secretsEngine.type); + controller.typeDisplay = engine.displayName; + controller.isConfigurable = engine.isConfigurable ?? false; controller.modelId = resolvedModel.secretsEngine.id; } } diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index 5af9436873..0a48022126 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -7,11 +7,12 @@ import { set } from '@ember/object'; import { hash } from 'rsvp'; import Route from '@ember/routing/route'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; -import { allEngines, isAddonEngine, CONFIGURATION_ONLY } from 'vault/helpers/mountable-secret-engines'; +import { isAddonEngine, filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; import { service } from '@ember/service'; import { normalizePath } from 'vault/utils/path-encoding-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(); @@ -85,11 +86,13 @@ export default Route.extend({ const type = secretEngine?.engineType; assert('secretEngine.engineType is not defined', !!type); // if configuration only, redirect to configuration route - if (CONFIGURATION_ONLY.includes(type)) { + if (engineDisplayData(type)?.isOnlyMountable) { return this.router.transitionTo('vault.cluster.secrets.backend.configuration', backend); } - const engineRoute = allEngines().find((engine) => engine.type === type)?.engineRoute; + const engineRoute = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: true }).find( + (engine) => engine.type === type + )?.engineRoute; if (!type || !SUPPORTED_BACKENDS.includes(type)) { return this.router.transitionTo('vault.cluster.secrets'); } diff --git a/ui/app/serializers/secret-engine.js b/ui/app/serializers/secret-engine.js index eff2ebe549..d75d0b9e52 100644 --- a/ui/app/serializers/secret-engine.js +++ b/ui/app/serializers/secret-engine.js @@ -5,7 +5,7 @@ import ApplicationSerializer from './application'; import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; -import { WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; +import engineDisplayData from 'vault/helpers/engines-display-data'; export default ApplicationSerializer.extend(EmbeddedRecordsMixin, { attrs: { @@ -85,7 +85,7 @@ export default ApplicationSerializer.extend(EmbeddedRecordsMixin, { data.options = data.version ? { version: data.version } : {}; delete data.version; - if (!WIF_ENGINES.includes(type)) { + if (!engineDisplayData(type)?.isWIF) { // only send identity_token_key if it's set on a WIF secret engine. // because of issues with the model unloading with a belongsTo relationships // identity_token_key can accidentally carry over if a user backs out of the form and changes the type from WIF to non-WIF. diff --git a/ui/app/templates/vault/cluster/settings/auth/configure.hbs b/ui/app/templates/vault/cluster/settings/auth/configure.hbs index 2ce7b2c73e..ef796a3a2b 100644 --- a/ui/app/templates/vault/cluster/settings/auth/configure.hbs +++ b/ui/app/templates/vault/cluster/settings/auth/configure.hbs @@ -14,7 +14,7 @@

Configure - {{get (find-by "type" this.model.type (mountable-auth-methods)) "displayName"}} + {{get (engines-display-data this.model.type) "displayName"}}

diff --git a/ui/app/templates/vault/cluster/settings/auth/enable.hbs b/ui/app/templates/vault/cluster/settings/auth/enable.hbs index f9189e0a81..5ed4c3436f 100644 --- a/ui/app/templates/vault/cluster/settings/auth/enable.hbs +++ b/ui/app/templates/vault/cluster/settings/auth/enable.hbs @@ -3,4 +3,4 @@ SPDX-License-Identifier: BUSL-1.1 }} - \ No newline at end of file + \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/settings/mount-secret-backend.hbs b/ui/app/templates/vault/cluster/settings/mount-secret-backend.hbs index 31917d239e..078fed4000 100644 --- a/ui/app/templates/vault/cluster/settings/mount-secret-backend.hbs +++ b/ui/app/templates/vault/cluster/settings/mount-secret-backend.hbs @@ -3,4 +3,4 @@ SPDX-License-Identifier: BUSL-1.1 }} - \ No newline at end of file + \ No newline at end of file diff --git a/ui/app/utils/all-engines-metadata.ts b/ui/app/utils/all-engines-metadata.ts new file mode 100644 index 0000000000..fb8df7948c --- /dev/null +++ b/ui/app/utils/all-engines-metadata.ts @@ -0,0 +1,301 @@ +/** + * Copyright (c) HashiCorp, Inc. + * 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; + glyph?: string; + isWIF?: boolean; // flag for 'Workload Identity Federation' engines. + 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 their own configuration page and actions. - These engines do not exist in their own Ember engine. + 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; +} + +/** + * @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; +} + +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', + glyph: 'key-values', + mountCategory: ['secret'], + type: 'kv', + }, + { + pluginCategory: 'generic', + displayName: 'KMIP', + engineRoute: 'kmip.scopes.index', + 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', + glyph: 'kubernetes-color', + mountCategory: ['auth', 'secret'], + type: 'kubernetes', + }, + { + pluginCategory: 'generic', + displayName: 'LDAP', + engineRoute: 'ldap.overview', + 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', + engineRoute: 'pki.overview', + 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', + }, +]; diff --git a/ui/lib/core/addon/components/secret-list-header.js b/ui/lib/core/addon/components/secret-list-header.js index 1237a002f5..b24a163342 100644 --- a/ui/lib/core/addon/components/secret-list-header.js +++ b/ui/lib/core/addon/components/secret-list-header.js @@ -4,8 +4,8 @@ */ import Component from '@glimmer/component'; +import engineDisplayData from 'vault/helpers/engines-display-data'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; -import { CONFIGURATION_ONLY } from 'vault/helpers/mountable-secret-engines'; /** * @module SecretListHeader @@ -29,6 +29,6 @@ export default class SecretListHeader extends Component { get showListTab() { // only show the list tab if the engine is not a configuration only engine and the UI supports it const { engineType } = this.args.model; - return supportedSecretBackends().includes(engineType) && !CONFIGURATION_ONLY.includes(engineType); + return supportedSecretBackends().includes(engineType) && !engineDisplayData(engineType)?.isOnlyMountable; } } diff --git a/ui/lib/core/addon/components/secrets-engine-mount-config.ts b/ui/lib/core/addon/components/secrets-engine-mount-config.ts index 9959f62c98..e5c27478ce 100644 --- a/ui/lib/core/addon/components/secrets-engine-mount-config.ts +++ b/ui/lib/core/addon/components/secrets-engine-mount-config.ts @@ -33,14 +33,14 @@ export default class SecretsEngineMountConfig extends Component { get fields(): Array { const { secretsEngine } = this.args; return [ - { label: 'Secret Engine Type', value: secretsEngine.engineType }, + { label: 'Secret engine type', value: secretsEngine.engineType }, { label: 'Path', value: secretsEngine.path }, { label: 'Accessor', value: secretsEngine.accessor }, { label: 'Local', value: secretsEngine.local }, - { label: 'Seal Wrap', value: secretsEngine.sealWrap }, + { label: 'Seal wrap', value: secretsEngine.sealWrap }, { label: 'Default Lease TTL', value: duration([secretsEngine.config.defaultLeaseTtl]) }, { label: 'Max Lease TTL', value: duration([secretsEngine.config.maxLeaseTtl]) }, - { label: 'Identity Token Key', value: secretsEngine.config.identityTokenKey }, + { label: 'Identity token key', value: secretsEngine.config.identityTokenKey }, ]; } } diff --git a/ui/tests/acceptance/auth/auth-list-test.js b/ui/tests/acceptance/auth/auth-list-test.js index 6898d425d2..0410349bae 100644 --- a/ui/tests/acceptance/auth/auth-list-test.js +++ b/ui/tests/acceptance/auth/auth-list-test.js @@ -11,9 +11,9 @@ import { v4 as uuidv4 } from 'uuid'; import { login, loginNs } from 'vault/tests/helpers/auth/auth-helpers'; import { MANAGED_AUTH_BACKENDS } from 'vault/helpers/supported-managed-auth-backends'; import { deleteAuthCmd, mountAuthCmd, runCmd, createNS } from 'vault/tests/helpers/commands'; -import { methods } from 'vault/helpers/mountable-auth-methods'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend-form-selectors'; +import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; const SELECTORS = { createUser: '[data-test-entity-create-link="user"]', @@ -78,7 +78,7 @@ module('Acceptance | auth backend list', function (hooks) { }); // Test all auth methods, not just those you can log in with - methods() + filterEnginesByMountCategory({ mountCategory: 'auth', isEnterprise: false }) .map((backend) => backend.type) .forEach((type) => { test(`${type} auth method`, async function (assert) { diff --git a/ui/tests/acceptance/enterprise-kmip-test.js b/ui/tests/acceptance/enterprise-kmip-test.js index 9e5f0807a4..be40bd205f 100644 --- a/ui/tests/acceptance/enterprise-kmip-test.js +++ b/ui/tests/acceptance/enterprise-kmip-test.js @@ -23,7 +23,7 @@ import credentialsPage from 'vault/tests/pages/secrets/backend/kmip/credentials' import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers'; -import { allEngines } from 'vault/helpers/mountable-secret-engines'; +import engineDisplayData from 'vault/helpers/engines-display-data'; import { mountEngineCmd, runCmd } from 'vault/tests/helpers/commands'; import { v4 as uuidv4 } from 'uuid'; @@ -94,7 +94,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { test('it should enable KMIP & transitions to addon engine route after mount success', async function (assert) { // test supported backends that ARE ember engines (enterprise only engines are tested individually) - const engine = allEngines().find((e) => e.type === 'kmip'); + const engine = engineDisplayData('kmip'); await mountSecrets.visit(); await mountBackend(engine.type, `${engine.type}-${uuidv4()}`); diff --git a/ui/tests/acceptance/enterprise-kmse-test.js b/ui/tests/acceptance/enterprise-kmse-test.js index 091ae49c18..1e20ea0bcc 100644 --- a/ui/tests/acceptance/enterprise-kmse-test.js +++ b/ui/tests/acceptance/enterprise-kmse-test.js @@ -9,10 +9,10 @@ import { click, currentRouteName, fillIn } from '@ember/test-helpers'; import { login } from 'vault/tests/helpers/auth/auth-helpers'; import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; import { setupMirage } from 'ember-cli-mirage/test-support'; -import { allEngines } from 'vault/helpers/mountable-secret-engines'; import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers'; import { runCmd } from '../helpers/commands'; import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; +import engineDisplayData from 'vault/helpers/engines-display-data'; module('Acceptance | Enterprise | keymgmt', function (hooks) { setupApplicationTest(hooks); @@ -24,7 +24,7 @@ module('Acceptance | Enterprise | keymgmt', function (hooks) { test('it transitions to list route after mount success', async function (assert) { assert.expect(1); - const engine = allEngines().find((e) => e.type === 'keymgmt'); + const engine = engineDisplayData('keymgmt'); // delete any previous mount with same name await runCmd([`delete sys/mounts/${engine.type}`]); diff --git a/ui/tests/acceptance/enterprise-transform-test.js b/ui/tests/acceptance/enterprise-transform-test.js index ffc3a44219..d08f4b337f 100644 --- a/ui/tests/acceptance/enterprise-transform-test.js +++ b/ui/tests/acceptance/enterprise-transform-test.js @@ -17,11 +17,11 @@ import rolesPage from 'vault/tests/pages/secrets/backend/transform/roles'; import alphabetsPage from 'vault/tests/pages/secrets/backend/transform/alphabets'; import searchSelect from 'vault/tests/pages/components/search-select'; import { runCmd } from '../helpers/commands'; -import { allEngines } from 'vault/helpers/mountable-secret-engines'; import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers'; import { v4 as uuidv4 } from 'uuid'; import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import engineDisplayData from 'vault/helpers/engines-display-data'; const searchSelectComponent = create(searchSelect); @@ -64,7 +64,7 @@ module('Acceptance | Enterprise | Transform secrets', function (hooks) { test('it transitions to list route after mount success', async function (assert) { assert.expect(1); - const engine = allEngines().find((e) => e.type === 'transform'); + const engine = engineDisplayData('transform'); // delete any previous mount with same name await runCmd([`delete sys/mounts/${engine.type}`]); diff --git a/ui/tests/acceptance/secret-engine-list-view-test.js b/ui/tests/acceptance/secret-engine-list-view-test.js index 962e1eb8a8..c294ed0116 100644 --- a/ui/tests/acceptance/secret-engine-list-view-test.js +++ b/ui/tests/acceptance/secret-engine-list-view-test.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { click, fillIn, currentRouteName, visit, currentURL } from '@ember/test-helpers'; +import { click, fillIn, currentRouteName, visit, currentURL, triggerEvent } from '@ember/test-helpers'; import { selectChoose } from 'ember-power-select/test-support'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; @@ -62,6 +62,44 @@ module('Acceptance | secret-engine list view', function (hooks) { await runCmd(deleteEngineCmd('aws')); }); + test('hovering over the icon of an unsupported engine shows unsupported tooltip', async function (assert) { + await visit('/vault/secrets'); + await page.enableEngine(); + await click(MOUNT_BACKEND_FORM.mountType('nomad')); + await click(GENERAL.submitButton); + + await selectChoose(GENERAL.searchSelect.trigger('filter-by-engine-type'), 'nomad'); + + await triggerEvent('.hds-tooltip-button', 'mouseenter'); + assert + .dom('.hds-tooltip-container') + .hasText( + 'The UI only supports configuration views for these secret engines. The CLI must be used to manage other engine resources.', + 'shows tooltip text for unsupported engine' + ); + // cleanup + await runCmd(deleteEngineCmd('nomad')); + }); + + test('hovering over the icon of a supported engine shows engine name and version (if applicable)', async function (assert) { + await visit('/vault/secrets'); + await page.enableEngine(); + await click(MOUNT_BACKEND_FORM.mountType('ssh')); + await click(GENERAL.submitButton); + await click(GENERAL.breadcrumbLink('Secrets')); + + await selectChoose(GENERAL.searchSelect.trigger('filter-by-engine-type'), 'kv'); + + await triggerEvent('.hds-tooltip-button', 'mouseenter'); + assert.dom('.hds-tooltip-container').hasText('KV version 2', 'shows tooltip for kv version 2'); + + await click('[data-test-selected-list-button="delete"]'); + + await selectChoose(GENERAL.searchSelect.trigger('filter-by-engine-type'), 'ssh'); + await triggerEvent('.hds-tooltip-button', 'mouseenter'); + assert.dom('.hds-tooltip-container').hasText('SSH', 'shows tooltip for SSH without version'); + }); + test('enterprise: cannot view list without permissions inside namespace', async function (assert) { this.version = 'enterprise'; this.backend = `bk-${this.uid}`; diff --git a/ui/tests/acceptance/settings/mount-secret-backend-test.js b/ui/tests/acceptance/settings/mount-secret-backend-test.js index 4a5b8f9828..95fd7c9e86 100644 --- a/ui/tests/acceptance/settings/mount-secret-backend-test.js +++ b/ui/tests/acceptance/settings/mount-secret-backend-test.js @@ -25,7 +25,6 @@ import configPage from 'vault/tests/pages/secrets/backend/configuration'; 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 { CONFIGURATION_ONLY, mountableEngines } from 'vault/helpers/mountable-secret-engines'; 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'; @@ -33,6 +32,8 @@ import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend 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'; const consoleComponent = create(consoleClass); @@ -203,7 +204,9 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { test('it should transition to mountable addon engine after mount success', async function (assert) { // test supported backends that ARE ember engines (enterprise only engines are tested individually) - const addons = mountableEngines().filter((e) => BACKENDS_WITH_ENGINES.includes(e.type)); + const addons = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: false }).filter( + (e) => BACKENDS_WITH_ENGINES.includes(e.type) + ); assert.expect(addons.length); for (const engine of addons) { @@ -230,7 +233,9 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { // test supported backends that are not ember engines (enterprise only engines are tested individually) const nonEngineBackends = supportedSecretBackends().filter((b) => !BACKENDS_WITH_ENGINES.includes(b)); // add back kv because we want to test v1 - const engines = mountableEngines().filter((e) => nonEngineBackends.includes(e.type) || e.type === 'kv'); + const engines = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: false }).filter( + (e) => (nonEngineBackends.includes(e.type) || e.type === 'kv') && e.type !== 'cubbyhole' + ); assert.expect(engines.length); for (const engine of engines) { @@ -247,7 +252,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { } await click(GENERAL.submitButton); - const route = CONFIGURATION_ONLY.includes(engine.type) ? 'configuration.index' : 'list-root'; + const route = engineDisplayData(engine.type)?.isOnlyMountable ? 'configuration.index' : 'list-root'; assert.strictEqual( currentRouteName(), `vault.cluster.secrets.backend.${route}`, @@ -262,7 +267,9 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { }); test('it should transition back to backend list for unsupported backends', async function (assert) { - const unsupported = mountableEngines().filter((e) => !supportedSecretBackends().includes(e.type)); + const unsupported = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: false }).filter( + (e) => !supportedSecretBackends().includes(e.type) + ); assert.expect(unsupported.length); for (const engine of unsupported) { @@ -375,7 +382,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { await visit(`/vault/secrets/${path}/configuration`); await click(SES.configurationToggle); assert - .dom(GENERAL.infoRowValue('Identity Token Key')) + .dom(GENERAL.infoRowValue('Identity token key')) .hasText(newKey, `shows identity token key on configuration page for engine: ${engine}`); // cleanup @@ -411,7 +418,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { await click(SES.configurationToggle); assert - .dom(GENERAL.infoRowValue('Identity Token Key')) + .dom(GENERAL.infoRowValue('Identity token key')) .hasText('general-key', `shows identity token key on configuration page for engine: ${engine}`); // cleanup diff --git a/ui/tests/integration/components/auth-config-form/options-test.js b/ui/tests/integration/components/auth-config-form/options-test.js index c47413df01..5a2a6d85c3 100644 --- a/ui/tests/integration/components/auth-config-form/options-test.js +++ b/ui/tests/integration/components/auth-config-form/options-test.js @@ -9,10 +9,10 @@ import { setupMirage } from 'ember-cli-mirage/test-support'; import { click, fillIn, render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import { methods } from 'vault/helpers/mountable-auth-methods'; +import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; const userLockoutSupported = ['approle', 'ldap', 'userpass']; -const userLockoutUnsupported = methods() +const userLockoutUnsupported = filterEnginesByMountCategory({ mountCategory: 'auth', isEnterprise: false }) .map((m) => m.type) .filter((m) => !userLockoutSupported.includes(m)); diff --git a/ui/tests/integration/components/mount-backend-form-test.js b/ui/tests/integration/components/mount-backend-form-test.js index cb6b7780d5..bef28705d6 100644 --- a/ui/tests/integration/components/mount-backend-form-test.js +++ b/ui/tests/integration/components/mount-backend-form-test.js @@ -12,12 +12,14 @@ import { allowAllCapabilitiesStub, noopStub } from 'vault/tests/helpers/stubs'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend-form-selectors'; import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers'; -import { methods } from 'vault/helpers/mountable-auth-methods'; -import { mountableEngines, WIF_ENGINES } from 'vault/helpers/mountable-secret-engines'; +import { ALL_ENGINES, filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; + import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; import SecretsEngineForm from 'vault/forms/secrets/engine'; +const WIF_ENGINES = ALL_ENGINES.filter((e) => e.isWIF).map((e) => e.type); + module('Integration | Component | mount backend form', function (hooks) { setupRenderingTest(hooks); setupMirage(hooks); @@ -46,13 +48,16 @@ module('Integration | Component | mount backend form', function (hooks) { test('it renders default state', async function (assert) { assert.expect(15); await render( - hbs`` + hbs`` ); assert .dom(GENERAL.title) .hasText('Enable an Authentication Method', 'renders auth header in default state'); - for (const method of methods()) { + for (const method of filterEnginesByMountCategory({ + mountCategory: 'auth', + isEnterprise: false, + }).filter((engine) => engine.type !== 'token')) { assert .dom(MOUNT_BACKEND_FORM.mountType(method.type)) .hasText(method.displayName, `renders type:${method.displayName} picker`); @@ -61,7 +66,7 @@ module('Integration | Component | mount backend form', function (hooks) { test('it changes path when type is changed', async function (assert) { await render( - hbs`` + hbs`` ); await click(MOUNT_BACKEND_FORM.mountType('aws')); @@ -73,7 +78,7 @@ module('Integration | Component | mount backend form', function (hooks) { test('it keeps path value if the user has changed it', async function (assert) { await render( - hbs`` + hbs`` ); await click(MOUNT_BACKEND_FORM.mountType('approle')); assert.strictEqual(this.model.type, 'approle', 'Updates type on model'); @@ -90,7 +95,7 @@ module('Integration | Component | mount backend form', function (hooks) { test('it does not show a selected token type when first mounting an auth method', async function (assert) { await render( - hbs`` + hbs`` ); await click(MOUNT_BACKEND_FORM.mountType('github')); await click(GENERAL.button('Method Options')); @@ -115,7 +120,7 @@ module('Integration | Component | mount backend form', function (hooks) { this.set('onMountSuccess', spy); await render( - hbs`` + hbs`` ); await mountBackend('approle', 'foo'); later(() => cancelTimers(), 50); @@ -146,10 +151,13 @@ module('Integration | Component | mount backend form', function (hooks) { test('it renders secret engine specific headers', async function (assert) { assert.expect(17); await render( - hbs`` + hbs`` ); assert.dom(GENERAL.title).hasText('Enable a Secrets Engine', 'renders secrets header'); - for (const method of mountableEngines()) { + for (const method of filterEnginesByMountCategory({ + mountCategory: 'secret', + isEnterprise: false, + }).filter((engine) => engine.type !== 'cubbyhole')) { assert .dom(MOUNT_BACKEND_FORM.mountType(method.type)) .hasText(method.displayName, `renders type:${method.displayName} picker`); @@ -158,7 +166,7 @@ module('Integration | Component | mount backend form', function (hooks) { test('it changes path when type is changed', async function (assert) { await render( - hbs`` + hbs`` ); await click(MOUNT_BACKEND_FORM.mountType('azure')); assert.dom(GENERAL.inputByAttr('path')).hasValue('azure', 'sets the value of the type'); @@ -169,7 +177,7 @@ module('Integration | Component | mount backend form', function (hooks) { test('it keeps path value if the user has changed it', async function (assert) { await render( - hbs`` + hbs`` ); await click(MOUNT_BACKEND_FORM.mountType('kv')); assert.strictEqual(this.model.type, 'kv', 'Updates type on model'); @@ -195,7 +203,7 @@ module('Integration | Component | mount backend form', function (hooks) { this.set('onMountSuccess', spy); await render( - hbs`` + hbs`` ); await mountBackend('ssh', 'foo'); @@ -212,7 +220,7 @@ module('Integration | Component | mount backend form', function (hooks) { module('WIF secret engines', function () { test('it shows identityTokenKey when type is a WIF engine and hides when its not', async function (assert) { await render( - hbs`` + hbs`` ); for (const engine of WIF_ENGINES) { await click(MOUNT_BACKEND_FORM.mountType(engine)); @@ -222,7 +230,10 @@ module('Integration | Component | mount backend form', function (hooks) { .exists(`Identity token key field shows when type=${this.model.type}`); await click(GENERAL.backButton); } - for (const engine of mountableEngines().filter((e) => !WIF_ENGINES.includes(e.type))) { + for (const engine of filterEnginesByMountCategory({ + mountCategory: 'secret', + isEnterprise: false, + }).filter((e) => !WIF_ENGINES.includes(e.type) && e.type !== 'cubbyhole')) { // check non-wif engine await click(MOUNT_BACKEND_FORM.mountType(engine.type)); await click(GENERAL.button('Method Options')); @@ -233,9 +244,9 @@ module('Integration | Component | mount backend form', function (hooks) { } }); - test('it updates identityTokeKey if user has changed it', async function (assert) { + test('it updates identityTokenKey if user has changed it', async function (assert) { await render( - hbs`` + hbs`` ); assert.strictEqual( this.model.config.identityTokenKey, diff --git a/ui/tests/integration/components/mount-backend/type-form-test.js b/ui/tests/integration/components/mount-backend/type-form-test.js index e66c6304a2..1f98eab899 100644 --- a/ui/tests/integration/components/mount-backend/type-form-test.js +++ b/ui/tests/integration/components/mount-backend/type-form-test.js @@ -8,15 +8,22 @@ import { setupRenderingTest } from 'vault/tests/helpers'; import { click, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import sinon from 'sinon'; -import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-engines'; -import { allMethods, methods } from 'vault/helpers/mountable-auth-methods'; +import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata'; import { setRunOptions } from 'ember-a11y-testing/test-support'; import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend-form-selectors'; -const secretTypes = mountableEngines().map((engine) => engine.type); -const allSecretTypes = allEngines().map((engine) => engine.type); -const authTypes = methods().map((auth) => auth.type); -const allAuthTypes = allMethods().map((auth) => auth.type); +const secretTypes = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: false }) + .filter((engine) => engine.type !== 'cubbyhole') + .map((engine) => engine.type); +const allSecretTypes = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: true }) + .filter((engine) => engine.type !== 'cubbyhole') + .map((engine) => engine.type); +const authTypes = filterEnginesByMountCategory({ mountCategory: 'auth', isEnterprise: false }) + .filter((engine) => engine.type !== 'token') + .map((auth) => auth.type); +const allAuthTypes = filterEnginesByMountCategory({ mountCategory: 'auth', isEnterprise: true }) + .filter((engine) => engine.type !== 'token') + .map((auth) => auth.type); module('Integration | Component | mount-backend/type-form', function (hooks) { setupRenderingTest(hooks); @@ -29,7 +36,7 @@ module('Integration | Component | mount-backend/type-form', function (hooks) { assert.expect(secretTypes.length + 1, 'renders all mountable engines plus calls a spy'); const spy = sinon.spy(); this.set('setType', spy); - await render(hbs``); + await render(hbs``); for (const type of secretTypes) { assert.dom(MOUNT_BACKEND_FORM.mountType(type)).exists(`Renders ${type} mountable secret engine`); @@ -65,7 +72,7 @@ module('Integration | Component | mount-backend/type-form', function (hooks) { 'color-contrast': { enabled: false }, }, }); - await render(hbs``); + await render(hbs``); for (const type of allSecretTypes) { assert.dom(MOUNT_BACKEND_FORM.mountType(type)).exists(`Renders ${type} secret engine`); } @@ -73,7 +80,7 @@ module('Integration | Component | mount-backend/type-form', function (hooks) { test('it renders correct items for enterprise auth methods', async function (assert) { assert.expect(allAuthTypes.length, 'renders all enterprise auth engines'); - await render(hbs``); + await render(hbs``); for (const type of allAuthTypes) { assert.dom(MOUNT_BACKEND_FORM.mountType(type)).exists(`Renders ${type} auth engine`); } diff --git a/ui/tests/integration/components/secret-engine/configuration-details-test.js b/ui/tests/integration/components/secret-engine/configuration-details-test.js index 865e326764..f323444242 100644 --- a/ui/tests/integration/components/secret-engine/configuration-details-test.js +++ b/ui/tests/integration/components/secret-engine/configuration-details-test.js @@ -6,16 +6,14 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'vault/tests/helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import { allEngines } from 'vault/helpers/mountable-secret-engines'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; -import { CONFIGURABLE_SECRET_ENGINES } from 'vault/helpers/mountable-secret-engines'; import { expectedConfigKeys, expectedValueOfConfigKeys, } from 'vault/tests/helpers/secret-engine/secret-engine-helpers'; - -const allEnginesArray = allEngines(); // saving as const so we don't invoke the method multiple times via the for loop +import { ALL_ENGINES } from 'vault/utils/all-engines-metadata'; +import engineDisplayData from 'vault/helpers/engines-display-data'; module('Integration | Component | SecretEngine::ConfigurationDetails', function (hooks) { setupRenderingTest(hooks); @@ -60,10 +58,12 @@ module('Integration | Component | SecretEngine::ConfigurationDetails', function .hasText(`Get started by configuring your Display Name secrets engine.`); }); - for (const type of CONFIGURABLE_SECRET_ENGINES) { + for (const type of ALL_ENGINES.filter((engine) => engine.isConfigurable ?? false).map( + (engine) => engine.type + )) { test(`${type}: it shows config details if configModel(s) are passed in`, async function (assert) { this.config = this.configs[type]; - this.typeDisplay = allEnginesArray.find((engine) => engine.type === type).displayName; + this.typeDisplay = engineDisplayData(type).displayName; await render( hbs`` diff --git a/ui/tests/integration/components/secret-engine/configure-wif-test.js b/ui/tests/integration/components/secret-engine/configure-wif-test.js index fbe8813337..e915ecca85 100644 --- a/ui/tests/integration/components/secret-engine/configure-wif-test.js +++ b/ui/tests/integration/components/secret-engine/configure-wif-test.js @@ -12,6 +12,7 @@ import { render, click, fillIn } from '@ember/test-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { v4 as uuidv4 } from 'uuid'; import { hbs } from 'ember-cli-htmlbars'; +import { ALL_ENGINES } from 'vault/utils/all-engines-metadata'; import { overrideResponse } from 'vault/tests/helpers/stubs'; import { expectedConfigKeys, @@ -21,14 +22,14 @@ import { fillInAwsConfig, } from 'vault/tests/helpers/secret-engine/secret-engine-helpers'; import { capabilitiesStub } from 'vault/tests/helpers/stubs'; -import { WIF_ENGINES, allEngines } from 'vault/helpers/mountable-secret-engines'; +import engineDisplayData from 'vault/helpers/engines-display-data'; import waitForError from 'vault/tests/helpers/wait-for-error'; import AwsConfigForm from 'vault/forms/secrets/aws-config'; import AzureConfigForm from 'vault/forms/secrets/azure-config'; import GcpConfigForm from 'vault/forms/secrets/gcp-config'; import SshConfigForm from 'vault/forms/secrets/ssh-config'; -const allEnginesArray = allEngines(); // saving as const so we don't invoke the method multiple times in the for loop +const WIF_ENGINES = ALL_ENGINES.filter((e) => e.isWIF).map((e) => e.type); module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) { setupRenderingTest(hooks); @@ -73,7 +74,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) for (const type of WIF_ENGINES) { test(`${type}: it renders default fields`, async function (assert) { this.id = `${type}-${this.uid}`; - this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.displayName = engineDisplayData(type).displayName; this.form = this.getForm(type, {}, { isNew: true }); this.type = type; @@ -114,7 +115,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) for (const type of WIF_ENGINES) { test(`${type}: it renders wif fields when user selects wif access type`, async function (assert) { this.id = `${type}-${this.uid}`; - this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.displayName = engineDisplayData(type).displayName; this.form = this.getForm(type, {}, { isNew: true }); this.type = type; @@ -597,7 +598,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) for (const type of WIF_ENGINES) { test(`${type}: it renders fields`, async function (assert) { this.id = `${type}-${this.uid}`; - this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.displayName = engineDisplayData(type).displayName; this.form = this.getForm(type, {}, { isNew: true }); this.type = type; @@ -637,7 +638,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) for (const type of WIF_ENGINES) { test(`${type}: it defaults to WIF accessType if WIF fields are already set`, async function (assert) { this.id = `${type}-${this.uid}`; - this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.displayName = engineDisplayData(type).displayName; const config = createConfig(`${type}-wif`); this.form = this.getForm(type, config); this.type = type; @@ -658,7 +659,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) for (const type of WIF_ENGINES) { test(`${type}: it renders issuer if global issuer is already set`, async function (assert) { this.id = `${type}-${this.uid}`; - this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.displayName = engineDisplayData(type).displayName; this.issuer = 'https://foo-bar-blah.com'; const config = createConfig(`${type}-wif`); this.form = this.getForm(type, { ...config, issuer: this.issuer }); @@ -885,7 +886,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) this.id = `${type}-${this.uid}`; const config = createConfig(`${type}-generic`); this.form = this.getForm(type, config); - this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; + this.displayName = engineDisplayData(type).displayName; this.type = type; await this.renderComponent(); diff --git a/ui/tests/integration/components/secrets-engine-mount-config-test.js b/ui/tests/integration/components/secrets-engine-mount-config-test.js index 3590310421..9ddcde5fa7 100644 --- a/ui/tests/integration/components/secrets-engine-mount-config-test.js +++ b/ui/tests/integration/components/secrets-engine-mount-config-test.js @@ -52,12 +52,12 @@ module('Integration | Component | secrets-engine-mount-config', function (hooks) await click(selectors.toggle); assert - .dom(GENERAL.infoRowValue('Secret Engine Type')) + .dom(GENERAL.infoRowValue('Secret engine type')) .hasText(this.secretsEngine.engineType, 'Secret engine type renders'); assert.dom(GENERAL.infoRowValue('Path')).hasText(this.secretsEngine.path, 'Path renders'); assert.dom(GENERAL.infoRowValue('Accessor')).hasText(this.secretsEngine.accessor, 'Accessor renders'); assert.dom(GENERAL.infoRowValue('Local')).includesText('No', 'Local renders'); - assert.dom(GENERAL.infoRowValue('Seal Wrap')).includesText('Yes', 'Seal wrap renders'); + assert.dom(GENERAL.infoRowValue('Seal wrap')).includesText('Yes', 'Seal wrap renders'); assert.dom(GENERAL.infoRowValue('Default Lease TTL')).includesText('0', 'Default Lease TTL renders'); assert .dom(GENERAL.infoRowValue('Max Lease TTL')) diff --git a/ui/tests/unit/models/secret-engine-test.js b/ui/tests/unit/models/secret-engine-test.js index faacdbafe0..32bd22e7e5 100644 --- a/ui/tests/unit/models/secret-engine-test.js +++ b/ui/tests/unit/models/secret-engine-test.js @@ -25,6 +25,7 @@ module('Unit | Model | secret-engine', function (hooks) { 'path', 'description', 'accessor', + 'runningPluginVersion', 'local', 'sealWrap', 'config.defaultLeaseTtl', @@ -48,6 +49,7 @@ module('Unit | Model | secret-engine', function (hooks) { 'path', 'description', 'accessor', + 'runningPluginVersion', 'local', 'sealWrap', 'config.defaultLeaseTtl', @@ -73,6 +75,7 @@ module('Unit | Model | secret-engine', function (hooks) { 'path', 'description', 'accessor', + 'runningPluginVersion', 'local', 'sealWrap', 'config.defaultLeaseTtl', @@ -100,6 +103,7 @@ module('Unit | Model | secret-engine', function (hooks) { 'path', 'description', 'accessor', + 'runningPluginVersion', 'local', 'sealWrap', 'config.allowedManagedKeys', @@ -121,6 +125,7 @@ module('Unit | Model | secret-engine', function (hooks) { 'path', 'description', 'accessor', + 'runningPluginVersion', 'local', 'sealWrap', 'config.defaultLeaseTtl', diff --git a/ui/types/vault/models/secret-engine.d.ts b/ui/types/vault/models/secret-engine.d.ts index 0b07a1abdf..682903c192 100644 --- a/ui/types/vault/models/secret-engine.d.ts +++ b/ui/types/vault/models/secret-engine.d.ts @@ -27,6 +27,7 @@ export default class SecretEngineModel extends Model { maxVersions: number; casRequired: boolean; deleteVersionAfter: string; + runningPluginVersion: string; get modelTypeForKV(): string; get isV2KV(): boolean; get attrs(): Array;