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
This commit is contained in:
Dan Rivera 2025-06-19 15:10:09 -04:00 committed by GitHub
parent 423d88af67
commit 008835ba36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 594 additions and 483 deletions

3
changelog/31001.txt Normal file
View File

@ -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.
```

View File

@ -49,7 +49,7 @@
<F.Options> <F.Options>
{{#each this.supportedAuthTypes as |type|}} {{#each this.supportedAuthTypes as |type|}}
<option selected={{eq this.selectedAuthMethod type}} value={{type}}> <option selected={{eq this.selectedAuthMethod type}} value={{type}}>
{{auth-display-name type}} {{get (engines-display-data type) "displayName"}}
</option> </option>
{{/each}} {{/each}}
</F.Options> </F.Options>

View File

@ -5,7 +5,7 @@
<Hds::Tabs @onClickTab={{this.onClickTab}} @selectedTabIndex={{this.selectedTabIndex}} as |T|> <Hds::Tabs @onClickTab={{this.onClickTab}} @selectedTabIndex={{this.selectedTabIndex}} as |T|>
{{#each-in @authTabData as |methodType mounts|}} {{#each-in @authTabData as |methodType mounts|}}
<T.Tab data-test-auth-tab={{methodType}}>{{auth-display-name methodType}}</T.Tab> <T.Tab data-test-auth-tab={{methodType}}>{{get (engines-display-data methodType) "displayName"}}</T.Tab>
<T.Panel> <T.Panel>
<div class="has-top-padding-m"> <div class="has-top-padding-m">
{{! Elements "behind" tabs always render on the DOM and are just superficially hidden/shown. {{! Elements "behind" tabs always render on the DOM and are just superficially hidden/shown.

View File

@ -9,13 +9,13 @@
{{#if this.showEnable}} {{#if this.showEnable}}
{{#let (find-by "type" @mountModel.type @mountTypes) as |typeInfo|}} {{#let (find-by "type" @mountModel.type @mountTypes) as |typeInfo|}}
<Icon @name={{typeInfo.glyph}} @size="24" class="has-text-grey-light" /> <Icon @name={{typeInfo.glyph}} @size="24" class="has-text-grey-light" />
{{#if (eq @mountType "secret")}} {{#if (eq @mountCategory "secret")}}
{{concat "Enable " typeInfo.displayName " Secrets Engine"}} {{concat "Enable " typeInfo.displayName " Secrets Engine"}}
{{else}} {{else}}
{{concat "Enable " typeInfo.displayName " Authentication Method"}} {{concat "Enable " typeInfo.displayName " Authentication Method"}}
{{/if}} {{/if}}
{{/let}} {{/let}}
{{else if (eq @mountType "secret")}} {{else if (eq @mountCategory "secret")}}
Enable a Secrets Engine Enable a Secrets Engine
{{else}} {{else}}
Enable an Authentication Method Enable an Authentication Method
@ -25,13 +25,13 @@
</PageHeader> </PageHeader>
<div class="box is-sideless is-bottomless is-fullwidth is-marginless"> <div class="box is-sideless is-bottomless is-fullwidth is-marginless">
<NamespaceReminder @mode="enable" @noun={{if (eq @mountType "secret") "Secret Engine" "Auth Method"}} /> <NamespaceReminder @mode="enable" @noun={{if (eq @mountCategory "secret") "Secret Engine" "Auth Method"}} />
<MessageError @errorMessage={{this.errorMessage}} /> <MessageError @errorMessage={{this.errorMessage}} />
{{#if @mountModel.type}} {{#if @mountModel.type}}
<form {{on "submit" (perform this.mountBackend)}}> <form {{on "submit" (perform this.mountBackend)}}>
<FormFieldGroups <FormFieldGroups
@model={{@mountModel}} @model={{@mountModel}}
@groupName={{if (eq @mountType "secret") "formFieldGroups"}} @groupName={{if (eq @mountCategory "secret") "formFieldGroups"}}
@renderGroup="default" @renderGroup="default"
@modelValidations={{this.modelValidations}} @modelValidations={{this.modelValidations}}
@onKeyUp={{this.onKeyUp}} @onKeyUp={{this.onKeyUp}}
@ -40,7 +40,7 @@
<FormFieldGroups <FormFieldGroups
@model={{@mountModel}} @model={{@mountModel}}
@renderGroup="Method Options" @renderGroup="Method Options"
@groupName={{if (eq @mountType "secret") "formFieldGroups"}} @groupName={{if (eq @mountCategory "secret") "formFieldGroups"}}
> >
<:identityTokenKey> <:identityTokenKey>
<SearchSelectWithModal <SearchSelectWithModal
@ -61,7 +61,7 @@
<div class="field is-grouped box is-fullwidth is-bottomless"> <div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control"> <div class="control">
<Hds::Button <Hds::Button
@text={{if (eq @mountType "secret") "Enable engine" "Enable method"}} @text={{if (eq @mountCategory "secret") "Enable engine" "Enable method"}}
@icon={{if this.mountBackend.isRunning "loading"}} @icon={{if this.mountBackend.isRunning "loading"}}
type="submit" type="submit"
data-test-submit data-test-submit
@ -80,6 +80,6 @@
</form> </form>
{{else}} {{else}}
{{! Type not yet set, show type options }} {{! Type not yet set, show type options }}
<MountBackend::TypeForm @setMountType={{this.setMountType}} @mountType={{@mountType}} /> <MountBackend::TypeForm @setMountType={{this.setMountType}} @mountCategory={{@mountCategory}} />
{{/if}} {{/if}}
</div> </div>

View File

@ -9,13 +9,14 @@ import { service } from '@ember/service';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { task } from 'ember-concurrency'; import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters'; import { waitFor } from '@ember/test-waiters';
import { methods } from 'vault/helpers/mountable-auth-methods'; import { presence } from 'vault/utils/forms/validators';
import { isAddonEngine, allEngines } from 'vault/helpers/mountable-secret-engines'; import { filterEnginesByMountCategory, isAddonEngine } from 'vault/utils/all-engines-metadata';
import { assert } from '@ember/debug';
import { ResponseError } from '@hashicorp/vault-client-typescript'; import { ResponseError } from '@hashicorp/vault-client-typescript';
import AdapterError from '@ember-data/adapter/error';
import type FlashMessageService from 'vault/services/flash-messages'; import type FlashMessageService from 'vault/services/flash-messages';
import type Store from '@ember-data/store'; 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 { AuthEnableModel } from 'vault/routes/vault/cluster/settings/auth/enable';
import type SecretsEngineForm from 'vault/forms/secrets/engine'; import type SecretsEngineForm from 'vault/forms/secrets/engine';
import type CapabilitiesService from 'vault/services/capabilities'; 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. * The `MountBackendForm` is used to mount either a secret or auth backend.
* *
* @example ```js * @example ```js
* <MountBackendForm @mountType="secret" @onMountSuccess={{this.onMountSuccess}} />``` * <MountBackendForm @mountCategory="secret" @onMountSuccess={{this.onMountSuccess}} />```
* *
* @param {function} onMountSuccess - A function that transitions once the Mount has been successfully posted. * @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 { interface Args {
mountModel: MountModel; mountModel: MountModel;
mountType: 'secret' | 'auth'; mountCategory: 'secret' | 'auth';
onMountSuccess: (type: string, path: string, useEngineRoute: boolean) => void; onMountSuccess: (type: string, path: string, useEngineRoute: boolean) => void;
} }
@ -54,41 +55,47 @@ export default class MountBackendForm extends Component<Args> {
@tracked errorMessage: string | string[] = ''; @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() { willDestroy() {
// components are torn down after store is unloaded and will cause an error if attempt to unload record // 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; 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(); this.args.mountModel.unloadRecord();
} }
super.willDestroy(); super.willDestroy();
} }
checkPathChange(type: string) { checkPathChange(backendType: string) {
if (!type) return; if (!backendType) return;
const mount = this.args.mountModel; const mount = this.args.mountModel;
const currentPath = mount.path; const currentPath = mount.path;
const mountTypes = // mountCategory is usually 'secret' or 'auth', but sometimes an empty string is passed in (like when we click the cancel button).
this.args.mountType === 'secret' // In these cases, we should default to returning auth methods.
? allEngines().map((engine) => engine.type) const mountsByType = filterEnginesByMountCategory({
: methods().map((auth) => auth.type); mountCategory: this.args.mountCategory ?? 'auth',
isEnterprise: true,
}).map((engine) => engine.type);
// if the current path has not been altered by user, // if the current path has not been altered by user,
// change it here to match the new type // change it here to match the new type
if (!currentPath || mountTypes.includes(currentPath)) { if (!currentPath || mountsByType.includes(currentPath)) {
mount.path = type; mount.path = backendType;
} }
} }
typeChangeSideEffect(type: string) { typeChangeSideEffect(type: string) {
if (this.args.mountType === 'secret') { if (this.args.mountCategory !== 'secret') return;
// If type PKI, set max lease to ~10years // If type PKI, set max lease to ~10years
this.args.mountModel.config.maxLeaseTtl = type === 'pki' ? '3650d' : 0; this.args.mountModel.config.maxLeaseTtl = type === 'pki' ? '3650d' : 0;
} }
}
checkModelValidity(model: MountModel) { checkModelValidity(model: MountModel) {
const { mountType } = this.args; const { mountCategory } = this.args;
const { isValid, state, invalidFormMessage, data } = const { isValid, state, invalidFormMessage, data } =
mountType === 'secret' ? model.toJSON() : model.validate(); mountCategory === 'secret' ? model.toJSON() : model.validate();
this.modelValidations = state; this.modelValidations = state;
this.invalidFormAlert = invalidFormMessage; this.invalidFormAlert = invalidFormMessage;
return { isValid, data }; return { isValid, data };
@ -97,8 +104,8 @@ export default class MountBackendForm extends Component<Args> {
checkModelWarnings() { checkModelWarnings() {
// check for warnings on change // check for warnings on change
// since we only show errors on submit we need to clear those out and only send warning state // since we only show errors on submit we need to clear those out and only send warning state
const { mountType, mountModel } = this.args; const { mountCategory, mountModel } = this.args;
const { state } = mountType === 'secret' ? mountModel.toJSON() : mountModel.validate(); const { state } = mountCategory === 'secret' ? mountModel.toJSON() : mountModel.validate();
for (const key in state) { for (const key in state) {
state[key].errors = []; state[key].errors = [];
} }
@ -152,7 +159,7 @@ export default class MountBackendForm extends Component<Args> {
@waitFor @waitFor
*mountBackend(event: Event) { *mountBackend(event: Event) {
event.preventDefault(); event.preventDefault();
const { mountModel, mountType } = this.args; const { mountModel, mountCategory } = this.args;
const { type, path } = mountModel; const { type, path } = mountModel;
// only submit form if validations pass // only submit form if validations pass
const { isValid, data: formData } = this.checkModelValidity(mountModel); const { isValid, data: formData } = this.checkModelValidity(mountModel);
@ -161,7 +168,7 @@ export default class MountBackendForm extends Component<Args> {
} }
try { try {
if (mountType === 'secret') { if (mountCategory === 'secret') {
yield this.api.sys.mountsEnableSecretsEngine(path, formData); yield this.api.sys.mountsEnableSecretsEngine(path, formData);
yield this.saveKvConfig(path, formData); yield this.saveKvConfig(path, formData);
} else { } else {
@ -169,7 +176,7 @@ export default class MountBackendForm extends Component<Args> {
} }
this.flashMessages.success( this.flashMessages.success(
`Successfully mounted the ${type} ${ `Successfully mounted the ${type} ${
this.args.mountType === 'secret' ? 'secrets engine' : 'auth method' this.args.mountCategory === 'secret' ? 'secrets engine' : 'auth method'
} at ${path}.` } at ${path}.`
); );
// check whether to use the Ember engine route // check whether to use the Ember engine route

View File

@ -8,7 +8,7 @@
{{capitalize category}} {{capitalize category}}
</Hds::Text::Display> </Hds::Text::Display>
<div class="flex row-wrap row-gap-8 column-gap-16 has-bottom-padding-m"> <div class="flex row-wrap row-gap-8 column-gap-16 has-bottom-padding-m">
{{#each (filter-by "category" category this.mountTypes) as |type|}} {{#each (filter-by "pluginCategory" category this.mountTypes) as |type|}}
<SelectableCard <SelectableCard
id={{type.type}} id={{type.type}}
class="has-top-padding-l has-text-centered small-card" class="has-top-padding-l has-text-centered small-card"
@ -32,7 +32,7 @@
<Hds::Button <Hds::Button
@text="Cancel" @text="Cancel"
@color="secondary" @color="secondary"
@route={{if (eq @mountType "secret") "vault.cluster.secrets.backends" "vault.cluster.access.methods"}} @route={{if (eq @mountCategory "secret") "vault.cluster.secrets.backends" "vault.cluster.access.methods"}}
data-test-cancel data-test-cancel
/> />
</div> </div>

View File

@ -5,8 +5,7 @@
import Component from '@glimmer/component'; import Component from '@glimmer/component';
import { service } from '@ember/service'; import { service } from '@ember/service';
import { allMethods, methods } from 'vault/helpers/mountable-auth-methods'; import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-engines';
/** /**
* *
@ -16,24 +15,28 @@ import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-eng
* *
* @example * @example
* ```js * ```js
* <MountBackend::TypeForm @setMountType={{this.setMountType}} @mountType="secret" /> * <MountBackend::TypeForm @setMountType={{this.setMountType}} @mountCategory="secret" />
* ``` * ```
* @param {CallableFunction} setMountType - function will receive the mount type string. Should update the model type value * @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 { export default class MountBackendTypeForm extends Component {
@service version; @service version;
get secretEngines() { 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() { 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() { get mountTypes() {
return this.args.mountType === 'secret' ? this.secretEngines : this.authMethods; return this.args.mountCategory === 'secret' ? this.secretEngines : this.authMethods;
} }
} }

View File

@ -48,7 +48,21 @@
<div> <div>
<div class="has-text-grey is-grid align-items-center linked-block-title"> <div class="has-text-grey is-grid align-items-center linked-block-title">
{{#if backend.icon}} {{#if backend.icon}}
<Hds::TooltipButton @text={{or backend.engineType backend.path}} aria-label="Type of backend"> <Hds::TooltipButton
@text={{if
backend.isSupportedBackend
(concat
(get (engines-display-data backend.type) "displayName")
(if
(eq backend.version 2)
" version 2"
(if (and (eq backend.version 1) (eq backend.type "kv")) " version 1" "")
)
)
"The UI only supports configuration views for these secret engines. The CLI must be used to manage other engine resources."
}}
aria-label="Type of backend"
>
<Icon @name={{backend.icon}} class="has-text-grey-light" /> <Icon @name={{backend.icon}} class="has-text-grey-light" />
</Hds::TooltipButton> </Hds::TooltipButton>
{{/if}} {{/if}}
@ -67,11 +81,10 @@
{{/if}} {{/if}}
{{/if}} {{/if}}
</div> </div>
{{#if backend.accessor}} <code class="has-text-grey is-size-8" data-test-engine-accessor>
<code class="has-text-grey is-size-8"> {{backend.accessor}}
{{if (eq backend.version 2) (concat "v2 " backend.accessor) backend.accessor}} {{backend.runningPluginVersion}}
</code> </code>
{{/if}}
{{#if backend.description}} {{#if backend.description}}
<ReadMore> <ReadMore>
{{backend.description}} {{backend.description}}

View File

@ -29,7 +29,7 @@ interface Args {
secretEngines: Array<SecretsEngineResource>; secretEngines: Array<SecretsEngineResource>;
} }
export default class SecretListItem extends Component<Args> { export default class SecretEngineList extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService; @service declare readonly flashMessages: FlashMessageService;
@service declare readonly api: ApiService; @service declare readonly api: ApiService;
@service declare readonly router: RouterService; @service declare readonly router: RouterService;

View File

@ -4,13 +4,13 @@
*/ */
import Controller from '@ember/controller'; 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 { export default class SecretsBackendConfigurationEditController extends Controller {
get isWifEngine() { get isWifEngine() {
return WIF_ENGINES.includes(this.model.type); return engineDisplayData(this.model.type)?.isWIF;
} }
get displayName() { get displayName() {
return allEngines().find((engine) => engine.type === this.model.type)?.displayName; return engineDisplayData(this.model.type).displayName;
} }
} }

View File

@ -4,8 +4,8 @@
*/ */
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { WIF_ENGINES } from 'vault/helpers/mountable-secret-engines';
import { toLabel } from 'core/helpers/to-label'; import { toLabel } from 'core/helpers/to-label';
import engineDisplayData from 'vault/helpers/engines-display-data';
export default class SecretsBackendConfigurationController extends Controller { export default class SecretsBackendConfigurationController extends Controller {
get displayFields() { get displayFields() {
@ -26,7 +26,7 @@ export default class SecretsBackendConfigurationController extends Controller {
fields.push('version'); fields.push('version');
} }
// For WIF Secret engines, allow users to set the identity token key when mounting the engine. // 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'); fields.push('config.identityTokenKey');
} }
return fields; return fields;

View File

@ -5,9 +5,9 @@
import { service } from '@ember/service'; import { service } from '@ember/service';
import Controller from '@ember/controller'; 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 { action } from '@ember/object';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import engineDisplayData from 'vault/helpers/engines-display-data';
const SUPPORTED_BACKENDS = supportedSecretBackends(); const SUPPORTED_BACKENDS = supportedSecretBackends();
@ -18,14 +18,15 @@ export default class MountSecretBackendController extends Controller {
onMountSuccess(type, path, useEngineRoute = false) { onMountSuccess(type, path, useEngineRoute = false) {
let transition; let transition;
if (SUPPORTED_BACKENDS.includes(type)) { if (SUPPORTED_BACKENDS.includes(type)) {
const engineInfo = allEngines().find((engine) => engine.type === type); const engineInfo = engineDisplayData(type);
if (useEngineRoute) { if (useEngineRoute) {
transition = this.router.transitionTo( transition = this.router.transitionTo(
`vault.cluster.secrets.backend.${engineInfo.engineRoute}`, `vault.cluster.secrets.backend.${engineInfo.engineRoute}`,
path path
); );
} else { } 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 }); transition = this.router.transitionTo('vault.cluster.secrets.backend.index', path, { queryParams });
} }
} else { } else {

View File

@ -7,8 +7,8 @@ import Form from 'vault/forms/form';
import FormField from 'vault/utils/forms/field'; import FormField from 'vault/utils/forms/field';
import FormFieldGroup from 'vault/utils/forms/field-group'; import FormFieldGroup from 'vault/utils/forms/field-group';
import { WHITESPACE_WARNING } from 'vault/utils/forms/validators'; import { WHITESPACE_WARNING } from 'vault/utils/forms/validators';
import { WIF_ENGINES } from 'vault/helpers/mountable-secret-engines';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
import type { SecretsEngineFormData } from 'vault/secrets/engine'; import type { SecretsEngineFormData } from 'vault/secrets/engine';
import type { Validations } from 'vault/app-types'; import type { Validations } from 'vault/app-types';
@ -134,7 +134,7 @@ export default class SecretsEngineForm extends Form<SecretsEngineFormData> {
if (this.engineType === 'pki') { if (this.engineType === 'pki') {
return [...this.coreOptionFields, ...this.standardConfigFields]; 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 [ return [
...this.coreOptionFields, ...this.coreOptionFields,
defaultTtl, defaultTtl,

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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);

View File

@ -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. * 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 = [ const SUPPORTED_AUTH_BACKENDS = [

View File

@ -8,12 +8,12 @@ import { service } from '@ember/service';
import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs'; import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import apiPath from 'vault/utils/api-path'; import apiPath from 'vault/utils/api-path';
import { withModelValidations } from 'vault/decorators/model-validations'; import { withModelValidations } from 'vault/decorators/model-validations';
import { allMethods } from 'vault/helpers/mountable-auth-methods';
import lazyCapabilities from 'vault/macros/lazy-capabilities'; import lazyCapabilities from 'vault/macros/lazy-capabilities';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { camelize } from '@ember/string'; import { camelize } from '@ember/string';
import { WHITESPACE_WARNING } from 'vault/utils/forms/validators'; import { WHITESPACE_WARNING } from 'vault/utils/forms/validators';
import { supportedTypes } from 'vault/utils/supported-login-methods'; import { supportedTypes } from 'vault/utils/supported-login-methods';
import engineDisplayData from 'vault/helpers/engines-display-data';
const validations = { const validations = {
path: [ path: [
@ -45,9 +45,9 @@ export default class AuthMethodModel extends Model {
} }
get icon() { get icon() {
const authMethods = allMethods().find((backend) => backend.type === this.methodType); // methodType refers to the backend type (e.g., "aws", "azure") and is set on a getter.
const engineData = engineDisplayData(this.methodType);
return authMethods?.glyph || 'users'; return engineData?.glyph || 'users';
} }
get directLoginLink() { get directLoginLink() {

View File

@ -6,11 +6,11 @@
import Model, { attr, hasMany } from '@ember-data/model'; import Model, { attr, hasMany } from '@ember-data/model';
import ArrayProxy from '@ember/array/proxy'; import ArrayProxy from '@ember/array/proxy';
import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; import PromiseProxyMixin from '@ember/object/promise-proxy-mixin';
import { methods } from 'vault/helpers/mountable-auth-methods';
import { withModelValidations } from 'vault/decorators/model-validations'; import { withModelValidations } from 'vault/decorators/model-validations';
import { isPresent } from '@ember/utils'; import { isPresent } from '@ember/utils';
import { service } from '@ember/service'; import { service } from '@ember/service';
import { addManyToArray, addToArray } from 'vault/helpers/add-to-array'; import { addManyToArray, addToArray } from 'vault/helpers/add-to-array';
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
const validations = { const validations = {
name: [{ type: 'presence', message: 'Name is required' }], name: [{ type: 'presence', message: 'Name is required' }],
@ -109,7 +109,7 @@ export default class MfaLoginEnforcementModel extends Model {
} }
iconForMount(type) { iconForMount(type) {
const mountableMethods = methods(); const mountableMethods = filterEnginesByMountCategory({ mountCategory: 'auth', isEnterprise: true });
const mount = mountableMethods.find((method) => method.type === type); const mount = mountableMethods.find((method) => method.type === type);
return mount ? mount.glyph || mount.type : 'token'; return mount ? mount.glyph || mount.type : 'token';
} }

View File

@ -9,8 +9,9 @@ import { equal } from '@ember/object/computed'; // eslint-disable-line
import { withModelValidations } from 'vault/decorators/model-validations'; import { withModelValidations } from 'vault/decorators/model-validations';
import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; 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 { 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(); const LINKED_BACKENDS = supportedSecretBackends();
@ -99,6 +100,14 @@ export default class SecretEngineModel extends Model {
}) })
deleteVersionAfter; 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 */ /* GETTERS */
get isV2KV() { get isV2KV() {
return this.version === 2 && (this.engineType === 'kv' || this.engineType === 'generic'); return this.version === 2 && (this.engineType === 'kv' || this.engineType === 'generic');
@ -115,7 +124,7 @@ export default class SecretEngineModel extends Model {
} }
get icon() { get icon() {
const engineData = allEngines().find((engine) => engine.type === this.engineType); const engineData = engineDisplayData(this.engineType);
return engineData?.glyph || 'lock'; return engineData?.glyph || 'lock';
} }
@ -137,8 +146,7 @@ export default class SecretEngineModel extends Model {
return 'vault.cluster.secrets.backend.overview'; return 'vault.cluster.secrets.backend.overview';
} }
if (isAddonEngine(this.engineType, this.version)) { if (isAddonEngine(this.engineType, this.version)) {
const { engineRoute } = allEngines().find((engine) => engine.type === this.engineType); return `vault.cluster.secrets.backend.${engineDisplayData(this.engineType).engineRoute}`;
return `vault.cluster.secrets.backend.${engineRoute}`;
} }
if (this.isV2KV) { if (this.isV2KV) {
// if it's KV v2 but not registered as an addon, it's type generic // 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() { get formFields() {
const type = this.engineType; 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 // no ttl options for keymgmt
if (type !== 'keymgmt') { if (type !== 'keymgmt') {
fields.push('config.defaultLeaseTtl', 'config.maxLeaseTtl'); fields.push('config.defaultLeaseTtl', 'config.maxLeaseTtl');
@ -180,7 +188,7 @@ export default class SecretEngineModel extends Model {
fields.push('casRequired', 'deleteVersionAfter', 'maxVersions'); fields.push('casRequired', 'deleteVersionAfter', 'maxVersions');
} }
// For WIF Secret engines, allow users to set the identity token key when mounting the engine. // 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'); fields.push('config.identityTokenKey');
} }
return fields; return fields;
@ -232,7 +240,7 @@ export default class SecretEngineModel extends Model {
// no ttl options for keymgmt // no ttl options for keymgmt
optionFields = [...CORE_OPTIONS, 'config.allowedManagedKeys', ...STANDARD_CONFIG]; optionFields = [...CORE_OPTIONS, 'config.allowedManagedKeys', ...STANDARD_CONFIG];
break; break;
case WIF_ENGINES.find((type) => type === this.engineType): case ALL_ENGINES.find((engine) => engine.type === this.engineType && engine.isWIF)?.type:
defaultFields = ['path']; defaultFields = ['path'];
optionFields = [ optionFields = [
...CORE_OPTIONS, ...CORE_OPTIONS,

View File

@ -4,8 +4,9 @@
*/ */
import { baseResourceFactory } from 'vault/resources/base-factory'; 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 { 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'; import type { SecretsEngine } from 'vault/secrets/engine';
@ -30,7 +31,7 @@ export default class SecretsEngineResource extends baseResourceFactory<SecretsEn
} }
get icon() { get icon() {
const engineData = allEngines().find((engine) => engine.type === this.engineType); const engineData = engineDisplayData(this.engineType);
return engineData?.glyph || 'lock'; return engineData?.glyph || 'lock';
} }
@ -52,7 +53,7 @@ export default class SecretsEngineResource extends baseResourceFactory<SecretsEn
return 'vault.cluster.secrets.backend.overview'; return 'vault.cluster.secrets.backend.overview';
} }
if (isAddonEngine(this.engineType, this.version)) { if (isAddonEngine(this.engineType, this.version)) {
const engine = allEngines().find((engine) => engine.type === this.engineType); const engine = engineDisplayData(this.engineType);
if (engine?.engineRoute) { if (engine?.engineRoute) {
return `vault.cluster.secrets.backend.${engine.engineRoute}`; return `vault.cluster.secrets.backend.${engine.engineRoute}`;
} }

View File

@ -5,11 +5,11 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { service } from '@ember/service'; import { service } from '@ember/service';
import { CONFIGURABLE_SECRET_ENGINES } from 'vault/helpers/mountable-secret-engines';
import AwsConfigForm from 'vault/forms/secrets/aws-config'; import AwsConfigForm from 'vault/forms/secrets/aws-config';
import AzureConfigForm from 'vault/forms/secrets/azure-config'; import AzureConfigForm from 'vault/forms/secrets/azure-config';
import GcpConfigForm from 'vault/forms/secrets/gcp-config'; import GcpConfigForm from 'vault/forms/secrets/gcp-config';
import SshConfigForm from 'vault/forms/secrets/ssh-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 SecretsEngineResource from 'vault/resources/secrets/engine';
import type ApiService from 'vault/services/api'; import type ApiService from 'vault/services/api';
@ -47,7 +47,7 @@ export default class SecretsBackendConfigurationEdit extends Route {
}[type] || { issuer: '' }; }[type] || { issuer: '' };
// if the engine type is not configurable or a form class does not exist for the type return a 404. // 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 }; throw { httpStatus: 404, backend };
} }

View File

@ -4,15 +4,14 @@
*/ */
import Route from '@ember/routing/route'; 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 { export default class SecretsBackendConfigurationIndexRoute extends Route {
setupController(controller, resolvedModel) { setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel); super.setupController(controller, resolvedModel);
controller.typeDisplay = allEngines().find( const engine = engineDisplayData(resolvedModel.secretsEngine.type);
(engine) => engine.type === resolvedModel.secretsEngine.type controller.typeDisplay = engine.displayName;
)?.displayName; controller.isConfigurable = engine.isConfigurable ?? false;
controller.isConfigurable = CONFIGURABLE_SECRET_ENGINES.includes(resolvedModel.secretsEngine.type);
controller.modelId = resolvedModel.secretsEngine.id; controller.modelId = resolvedModel.secretsEngine.id;
} }
} }

View File

@ -7,11 +7,12 @@ import { set } from '@ember/object';
import { hash } from 'rsvp'; import { hash } from 'rsvp';
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; 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 { service } from '@ember/service';
import { normalizePath } from 'vault/utils/path-encoding-helpers'; import { normalizePath } from 'vault/utils/path-encoding-helpers';
import { assert } from '@ember/debug'; import { assert } from '@ember/debug';
import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs'; import { pathIsDirectory } from 'kv/utils/kv-breadcrumbs';
import engineDisplayData from 'vault/helpers/engines-display-data';
const SUPPORTED_BACKENDS = supportedSecretBackends(); const SUPPORTED_BACKENDS = supportedSecretBackends();
@ -85,11 +86,13 @@ export default Route.extend({
const type = secretEngine?.engineType; const type = secretEngine?.engineType;
assert('secretEngine.engineType is not defined', !!type); assert('secretEngine.engineType is not defined', !!type);
// if configuration only, redirect to configuration route // 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); 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)) { if (!type || !SUPPORTED_BACKENDS.includes(type)) {
return this.router.transitionTo('vault.cluster.secrets'); return this.router.transitionTo('vault.cluster.secrets');
} }

View File

@ -5,7 +5,7 @@
import ApplicationSerializer from './application'; import ApplicationSerializer from './application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; 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, { export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
attrs: { attrs: {
@ -85,7 +85,7 @@ export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
data.options = data.version ? { version: data.version } : {}; data.options = data.version ? { version: data.version } : {};
delete 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. // 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 // 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. // identity_token_key can accidentally carry over if a user backs out of the form and changes the type from WIF to non-WIF.

View File

@ -14,7 +14,7 @@
<p.levelLeft> <p.levelLeft>
<h1 class="title is-3"> <h1 class="title is-3">
Configure Configure
{{get (find-by "type" this.model.type (mountable-auth-methods)) "displayName"}} {{get (engines-display-data this.model.type) "displayName"}}
</h1> </h1>
</p.levelLeft> </p.levelLeft>
</PageHeader> </PageHeader>

View File

@ -3,4 +3,4 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
}} }}
<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{action "onMountSuccess"}} /> <MountBackendForm @mountModel={{this.model}} @mountCategory="auth" @onMountSuccess={{action "onMountSuccess"}} />

View File

@ -3,4 +3,4 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
}} }}
<MountBackendForm @mountModel={{this.model}} @mountType="secret" @onMountSuccess={{action "onMountSuccess"}} /> <MountBackendForm @mountModel={{this.model}} @mountCategory="secret" @onMountSuccess={{action "onMountSuccess"}} />

View File

@ -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',
},
];

View File

@ -4,8 +4,8 @@
*/ */
import Component from '@glimmer/component'; import Component from '@glimmer/component';
import engineDisplayData from 'vault/helpers/engines-display-data';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { CONFIGURATION_ONLY } from 'vault/helpers/mountable-secret-engines';
/** /**
* @module SecretListHeader * @module SecretListHeader
@ -29,6 +29,6 @@ export default class SecretListHeader extends Component {
get showListTab() { get showListTab() {
// only show the list tab if the engine is not a configuration only engine and the UI supports it // only show the list tab if the engine is not a configuration only engine and the UI supports it
const { engineType } = this.args.model; const { engineType } = this.args.model;
return supportedSecretBackends().includes(engineType) && !CONFIGURATION_ONLY.includes(engineType); return supportedSecretBackends().includes(engineType) && !engineDisplayData(engineType)?.isOnlyMountable;
} }
} }

View File

@ -33,14 +33,14 @@ export default class SecretsEngineMountConfig extends Component<Args> {
get fields(): Array<Field> { get fields(): Array<Field> {
const { secretsEngine } = this.args; const { secretsEngine } = this.args;
return [ return [
{ label: 'Secret Engine Type', value: secretsEngine.engineType }, { label: 'Secret engine type', value: secretsEngine.engineType },
{ label: 'Path', value: secretsEngine.path }, { label: 'Path', value: secretsEngine.path },
{ label: 'Accessor', value: secretsEngine.accessor }, { label: 'Accessor', value: secretsEngine.accessor },
{ label: 'Local', value: secretsEngine.local }, { 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: 'Default Lease TTL', value: duration([secretsEngine.config.defaultLeaseTtl]) },
{ label: 'Max Lease TTL', value: duration([secretsEngine.config.maxLeaseTtl]) }, { 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 },
]; ];
} }
} }

View File

@ -11,9 +11,9 @@ import { v4 as uuidv4 } from 'uuid';
import { login, loginNs } from 'vault/tests/helpers/auth/auth-helpers'; import { login, loginNs } from 'vault/tests/helpers/auth/auth-helpers';
import { MANAGED_AUTH_BACKENDS } from 'vault/helpers/supported-managed-auth-backends'; import { MANAGED_AUTH_BACKENDS } from 'vault/helpers/supported-managed-auth-backends';
import { deleteAuthCmd, mountAuthCmd, runCmd, createNS } from 'vault/tests/helpers/commands'; 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 { GENERAL } from 'vault/tests/helpers/general-selectors';
import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend-form-selectors'; import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend-form-selectors';
import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
const SELECTORS = { const SELECTORS = {
createUser: '[data-test-entity-create-link="user"]', 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 // Test all auth methods, not just those you can log in with
methods() filterEnginesByMountCategory({ mountCategory: 'auth', isEnterprise: false })
.map((backend) => backend.type) .map((backend) => backend.type)
.forEach((type) => { .forEach((type) => {
test(`${type} auth method`, async function (assert) { test(`${type} auth method`, async function (assert) {

View File

@ -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 mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers'; 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 { mountEngineCmd, runCmd } from 'vault/tests/helpers/commands';
import { v4 as uuidv4 } from 'uuid'; 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('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) // 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 mountSecrets.visit();
await mountBackend(engine.type, `${engine.type}-${uuidv4()}`); await mountBackend(engine.type, `${engine.type}-${uuidv4()}`);

View File

@ -9,10 +9,10 @@ import { click, currentRouteName, fillIn } from '@ember/test-helpers';
import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { login } from 'vault/tests/helpers/auth/auth-helpers';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import { setupMirage } from 'ember-cli-mirage/test-support'; 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 { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
import { runCmd } from '../helpers/commands'; import { runCmd } from '../helpers/commands';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; 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) { module('Acceptance | Enterprise | keymgmt', function (hooks) {
setupApplicationTest(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) { test('it transitions to list route after mount success', async function (assert) {
assert.expect(1); assert.expect(1);
const engine = allEngines().find((e) => e.type === 'keymgmt'); const engine = engineDisplayData('keymgmt');
// delete any previous mount with same name // delete any previous mount with same name
await runCmd([`delete sys/mounts/${engine.type}`]); await runCmd([`delete sys/mounts/${engine.type}`]);

View File

@ -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 alphabetsPage from 'vault/tests/pages/secrets/backend/transform/alphabets';
import searchSelect from 'vault/tests/pages/components/search-select'; import searchSelect from 'vault/tests/pages/components/search-select';
import { runCmd } from '../helpers/commands'; 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 { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors';
import engineDisplayData from 'vault/helpers/engines-display-data';
const searchSelectComponent = create(searchSelect); 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) { test('it transitions to list route after mount success', async function (assert) {
assert.expect(1); assert.expect(1);
const engine = allEngines().find((e) => e.type === 'transform'); const engine = engineDisplayData('transform');
// delete any previous mount with same name // delete any previous mount with same name
await runCmd([`delete sys/mounts/${engine.type}`]); await runCmd([`delete sys/mounts/${engine.type}`]);

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1 * 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 { selectChoose } from 'ember-power-select/test-support';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit'; import { setupApplicationTest } from 'ember-qunit';
@ -62,6 +62,44 @@ module('Acceptance | secret-engine list view', function (hooks) {
await runCmd(deleteEngineCmd('aws')); 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) { test('enterprise: cannot view list without permissions inside namespace', async function (assert) {
this.version = 'enterprise'; this.version = 'enterprise';
this.backend = `bk-${this.uid}`; this.backend = `bk-${this.uid}`;

View File

@ -25,7 +25,6 @@ import configPage from 'vault/tests/pages/secrets/backend/configuration';
import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { login } from 'vault/tests/helpers/auth/auth-helpers';
import consoleClass from 'vault/tests/pages/components/console/ui-panel'; import consoleClass from 'vault/tests/pages/components/console/ui-panel';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; 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 { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-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 { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
import { SELECTORS as OIDC } from 'vault/tests/helpers/oidc-config'; import { SELECTORS as OIDC } from 'vault/tests/helpers/oidc-config';
import { adminOidcCreateRead, adminOidcCreate } from 'vault/tests/helpers/secret-engine/policy-generator'; 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); 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('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) // 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); assert.expect(addons.length);
for (const engine of addons) { 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) // test supported backends that are not ember engines (enterprise only engines are tested individually)
const nonEngineBackends = supportedSecretBackends().filter((b) => !BACKENDS_WITH_ENGINES.includes(b)); const nonEngineBackends = supportedSecretBackends().filter((b) => !BACKENDS_WITH_ENGINES.includes(b));
// add back kv because we want to test v1 // 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); assert.expect(engines.length);
for (const engine of engines) { for (const engine of engines) {
@ -247,7 +252,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
} }
await click(GENERAL.submitButton); 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( assert.strictEqual(
currentRouteName(), currentRouteName(),
`vault.cluster.secrets.backend.${route}`, `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) { 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); assert.expect(unsupported.length);
for (const engine of unsupported) { for (const engine of unsupported) {
@ -375,7 +382,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
await visit(`/vault/secrets/${path}/configuration`); await visit(`/vault/secrets/${path}/configuration`);
await click(SES.configurationToggle); await click(SES.configurationToggle);
assert 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}`); .hasText(newKey, `shows identity token key on configuration page for engine: ${engine}`);
// cleanup // cleanup
@ -411,7 +418,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
await click(SES.configurationToggle); await click(SES.configurationToggle);
assert 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}`); .hasText('general-key', `shows identity token key on configuration page for engine: ${engine}`);
// cleanup // cleanup

View File

@ -9,10 +9,10 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
import { click, fillIn, render } from '@ember/test-helpers'; import { click, fillIn, render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; 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 userLockoutSupported = ['approle', 'ldap', 'userpass'];
const userLockoutUnsupported = methods() const userLockoutUnsupported = filterEnginesByMountCategory({ mountCategory: 'auth', isEnterprise: false })
.map((m) => m.type) .map((m) => m.type)
.filter((m) => !userLockoutSupported.includes(m)); .filter((m) => !userLockoutSupported.includes(m));

View File

@ -12,12 +12,14 @@ import { allowAllCapabilitiesStub, noopStub } from 'vault/tests/helpers/stubs';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend-form-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 { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-helpers';
import { methods } from 'vault/helpers/mountable-auth-methods'; import { ALL_ENGINES, filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
import { mountableEngines, WIF_ENGINES } from 'vault/helpers/mountable-secret-engines';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon'; import sinon from 'sinon';
import SecretsEngineForm from 'vault/forms/secrets/engine'; 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) { module('Integration | Component | mount backend form', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
setupMirage(hooks); setupMirage(hooks);
@ -46,13 +48,16 @@ module('Integration | Component | mount backend form', function (hooks) {
test('it renders default state', async function (assert) { test('it renders default state', async function (assert) {
assert.expect(15); assert.expect(15);
await render( await render(
hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountCategory="auth" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
assert assert
.dom(GENERAL.title) .dom(GENERAL.title)
.hasText('Enable an Authentication Method', 'renders auth header in default state'); .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 assert
.dom(MOUNT_BACKEND_FORM.mountType(method.type)) .dom(MOUNT_BACKEND_FORM.mountType(method.type))
.hasText(method.displayName, `renders type:${method.displayName} picker`); .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) { test('it changes path when type is changed', async function (assert) {
await render( await render(
hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountCategory="auth" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await click(MOUNT_BACKEND_FORM.mountType('aws')); 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) { test('it keeps path value if the user has changed it', async function (assert) {
await render( await render(
hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountCategory="auth" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await click(MOUNT_BACKEND_FORM.mountType('approle')); await click(MOUNT_BACKEND_FORM.mountType('approle'));
assert.strictEqual(this.model.type, 'approle', 'Updates type on model'); 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) { test('it does not show a selected token type when first mounting an auth method', async function (assert) {
await render( await render(
hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountCategory="auth" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await click(MOUNT_BACKEND_FORM.mountType('github')); await click(MOUNT_BACKEND_FORM.mountType('github'));
await click(GENERAL.button('Method Options')); await click(GENERAL.button('Method Options'));
@ -115,7 +120,7 @@ module('Integration | Component | mount backend form', function (hooks) {
this.set('onMountSuccess', spy); this.set('onMountSuccess', spy);
await render( await render(
hbs`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountCategory="auth" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await mountBackend('approle', 'foo'); await mountBackend('approle', 'foo');
later(() => cancelTimers(), 50); 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) { test('it renders secret engine specific headers', async function (assert) {
assert.expect(17); assert.expect(17);
await render( await render(
hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountCategory="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
assert.dom(GENERAL.title).hasText('Enable a Secrets Engine', 'renders secrets header'); 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 assert
.dom(MOUNT_BACKEND_FORM.mountType(method.type)) .dom(MOUNT_BACKEND_FORM.mountType(method.type))
.hasText(method.displayName, `renders type:${method.displayName} picker`); .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) { test('it changes path when type is changed', async function (assert) {
await render( await render(
hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountCategory="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await click(MOUNT_BACKEND_FORM.mountType('azure')); await click(MOUNT_BACKEND_FORM.mountType('azure'));
assert.dom(GENERAL.inputByAttr('path')).hasValue('azure', 'sets the value of the type'); 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) { test('it keeps path value if the user has changed it', async function (assert) {
await render( await render(
hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountCategory="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await click(MOUNT_BACKEND_FORM.mountType('kv')); await click(MOUNT_BACKEND_FORM.mountType('kv'));
assert.strictEqual(this.model.type, 'kv', 'Updates type on model'); 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); this.set('onMountSuccess', spy);
await render( await render(
hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountCategory="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
await mountBackend('ssh', 'foo'); await mountBackend('ssh', 'foo');
@ -212,7 +220,7 @@ module('Integration | Component | mount backend form', function (hooks) {
module('WIF secret engines', function () { module('WIF secret engines', function () {
test('it shows identityTokenKey when type is a WIF engine and hides when its not', async function (assert) { test('it shows identityTokenKey when type is a WIF engine and hides when its not', async function (assert) {
await render( await render(
hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountCategory="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
for (const engine of WIF_ENGINES) { for (const engine of WIF_ENGINES) {
await click(MOUNT_BACKEND_FORM.mountType(engine)); 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}`); .exists(`Identity token key field shows when type=${this.model.type}`);
await click(GENERAL.backButton); 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 // check non-wif engine
await click(MOUNT_BACKEND_FORM.mountType(engine.type)); await click(MOUNT_BACKEND_FORM.mountType(engine.type));
await click(GENERAL.button('Method Options')); 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( await render(
hbs`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />` hbs`<MountBackendForm @mountCategory="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
); );
assert.strictEqual( assert.strictEqual(
this.model.config.identityTokenKey, this.model.config.identityTokenKey,

View File

@ -8,15 +8,22 @@ import { setupRenderingTest } from 'vault/tests/helpers';
import { click, render } from '@ember/test-helpers'; import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars'; import { hbs } from 'ember-cli-htmlbars';
import sinon from 'sinon'; import sinon from 'sinon';
import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-engines'; import { filterEnginesByMountCategory } from 'vault/utils/all-engines-metadata';
import { allMethods, methods } from 'vault/helpers/mountable-auth-methods';
import { setRunOptions } from 'ember-a11y-testing/test-support'; import { setRunOptions } from 'ember-a11y-testing/test-support';
import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend-form-selectors'; import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend-form-selectors';
const secretTypes = mountableEngines().map((engine) => engine.type); const secretTypes = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: false })
const allSecretTypes = allEngines().map((engine) => engine.type); .filter((engine) => engine.type !== 'cubbyhole')
const authTypes = methods().map((auth) => auth.type); .map((engine) => engine.type);
const allAuthTypes = allMethods().map((auth) => auth.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) { module('Integration | Component | mount-backend/type-form', function (hooks) {
setupRenderingTest(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'); assert.expect(secretTypes.length + 1, 'renders all mountable engines plus calls a spy');
const spy = sinon.spy(); const spy = sinon.spy();
this.set('setType', spy); this.set('setType', spy);
await render(hbs`<MountBackend::TypeForm @mountType="secret" @setMountType={{this.setType}} />`); await render(hbs`<MountBackend::TypeForm @mountCategory="secret" @setMountType={{this.setType}} />`);
for (const type of secretTypes) { for (const type of secretTypes) {
assert.dom(MOUNT_BACKEND_FORM.mountType(type)).exists(`Renders ${type} mountable secret engine`); 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 }, 'color-contrast': { enabled: false },
}, },
}); });
await render(hbs`<MountBackend::TypeForm @mountType="secret" @setMountType={{this.setType}} />`); await render(hbs`<MountBackend::TypeForm @mountCategory="secret" @setMountType={{this.setType}} />`);
for (const type of allSecretTypes) { for (const type of allSecretTypes) {
assert.dom(MOUNT_BACKEND_FORM.mountType(type)).exists(`Renders ${type} secret engine`); 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) { test('it renders correct items for enterprise auth methods', async function (assert) {
assert.expect(allAuthTypes.length, 'renders all enterprise auth engines'); assert.expect(allAuthTypes.length, 'renders all enterprise auth engines');
await render(hbs`<MountBackend::TypeForm @mountType="auth" @setMountType={{this.setType}} />`); await render(hbs`<MountBackend::TypeForm @mountCategory="auth" @setMountType={{this.setType}} />`);
for (const type of allAuthTypes) { for (const type of allAuthTypes) {
assert.dom(MOUNT_BACKEND_FORM.mountType(type)).exists(`Renders ${type} auth engine`); assert.dom(MOUNT_BACKEND_FORM.mountType(type)).exists(`Renders ${type} auth engine`);
} }

View File

@ -6,16 +6,14 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers'; import { setupRenderingTest } from 'vault/tests/helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { allEngines } from 'vault/helpers/mountable-secret-engines';
import { render } from '@ember/test-helpers'; import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars'; import { hbs } from 'ember-cli-htmlbars';
import { CONFIGURABLE_SECRET_ENGINES } from 'vault/helpers/mountable-secret-engines';
import { import {
expectedConfigKeys, expectedConfigKeys,
expectedValueOfConfigKeys, expectedValueOfConfigKeys,
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers'; } from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
const allEnginesArray = allEngines(); // saving as const so we don't invoke the method multiple times via the for loop import engineDisplayData from 'vault/helpers/engines-display-data';
module('Integration | Component | SecretEngine::ConfigurationDetails', function (hooks) { module('Integration | Component | SecretEngine::ConfigurationDetails', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@ -60,10 +58,12 @@ module('Integration | Component | SecretEngine::ConfigurationDetails', function
.hasText(`Get started by configuring your Display Name secrets engine.`); .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) { test(`${type}: it shows config details if configModel(s) are passed in`, async function (assert) {
this.config = this.configs[type]; this.config = this.configs[type];
this.typeDisplay = allEnginesArray.find((engine) => engine.type === type).displayName; this.typeDisplay = engineDisplayData(type).displayName;
await render( await render(
hbs`<SecretEngine::ConfigurationDetails @config={{this.config}} @typeDisplay={{this.typeDisplay}}/>` hbs`<SecretEngine::ConfigurationDetails @config={{this.config}} @typeDisplay={{this.typeDisplay}}/>`

View File

@ -12,6 +12,7 @@ import { render, click, fillIn } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { hbs } from 'ember-cli-htmlbars'; import { hbs } from 'ember-cli-htmlbars';
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
import { overrideResponse } from 'vault/tests/helpers/stubs'; import { overrideResponse } from 'vault/tests/helpers/stubs';
import { import {
expectedConfigKeys, expectedConfigKeys,
@ -21,14 +22,14 @@ import {
fillInAwsConfig, fillInAwsConfig,
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers'; } from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
import { capabilitiesStub } from 'vault/tests/helpers/stubs'; 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 waitForError from 'vault/tests/helpers/wait-for-error';
import AwsConfigForm from 'vault/forms/secrets/aws-config'; import AwsConfigForm from 'vault/forms/secrets/aws-config';
import AzureConfigForm from 'vault/forms/secrets/azure-config'; import AzureConfigForm from 'vault/forms/secrets/azure-config';
import GcpConfigForm from 'vault/forms/secrets/gcp-config'; import GcpConfigForm from 'vault/forms/secrets/gcp-config';
import SshConfigForm from 'vault/forms/secrets/ssh-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) { module('Integration | Component | SecretEngine::ConfigureWif', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@ -73,7 +74,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
for (const type of WIF_ENGINES) { for (const type of WIF_ENGINES) {
test(`${type}: it renders default fields`, async function (assert) { test(`${type}: it renders default fields`, async function (assert) {
this.id = `${type}-${this.uid}`; 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.form = this.getForm(type, {}, { isNew: true });
this.type = type; this.type = type;
@ -114,7 +115,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
for (const type of WIF_ENGINES) { for (const type of WIF_ENGINES) {
test(`${type}: it renders wif fields when user selects wif access type`, async function (assert) { test(`${type}: it renders wif fields when user selects wif access type`, async function (assert) {
this.id = `${type}-${this.uid}`; 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.form = this.getForm(type, {}, { isNew: true });
this.type = type; this.type = type;
@ -597,7 +598,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
for (const type of WIF_ENGINES) { for (const type of WIF_ENGINES) {
test(`${type}: it renders fields`, async function (assert) { test(`${type}: it renders fields`, async function (assert) {
this.id = `${type}-${this.uid}`; 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.form = this.getForm(type, {}, { isNew: true });
this.type = type; this.type = type;
@ -637,7 +638,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
for (const type of WIF_ENGINES) { for (const type of WIF_ENGINES) {
test(`${type}: it defaults to WIF accessType if WIF fields are already set`, async function (assert) { test(`${type}: it defaults to WIF accessType if WIF fields are already set`, async function (assert) {
this.id = `${type}-${this.uid}`; this.id = `${type}-${this.uid}`;
this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; this.displayName = engineDisplayData(type).displayName;
const config = createConfig(`${type}-wif`); const config = createConfig(`${type}-wif`);
this.form = this.getForm(type, config); this.form = this.getForm(type, config);
this.type = type; this.type = type;
@ -658,7 +659,7 @@ module('Integration | Component | SecretEngine::ConfigureWif', function (hooks)
for (const type of WIF_ENGINES) { for (const type of WIF_ENGINES) {
test(`${type}: it renders issuer if global issuer is already set`, async function (assert) { test(`${type}: it renders issuer if global issuer is already set`, async function (assert) {
this.id = `${type}-${this.uid}`; 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'; this.issuer = 'https://foo-bar-blah.com';
const config = createConfig(`${type}-wif`); const config = createConfig(`${type}-wif`);
this.form = this.getForm(type, { ...config, issuer: this.issuer }); 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}`; this.id = `${type}-${this.uid}`;
const config = createConfig(`${type}-generic`); const config = createConfig(`${type}-generic`);
this.form = this.getForm(type, config); this.form = this.getForm(type, config);
this.displayName = allEnginesArray.find((engine) => engine.type === type)?.displayName; this.displayName = engineDisplayData(type).displayName;
this.type = type; this.type = type;
await this.renderComponent(); await this.renderComponent();

View File

@ -52,12 +52,12 @@ module('Integration | Component | secrets-engine-mount-config', function (hooks)
await click(selectors.toggle); await click(selectors.toggle);
assert assert
.dom(GENERAL.infoRowValue('Secret Engine Type')) .dom(GENERAL.infoRowValue('Secret engine type'))
.hasText(this.secretsEngine.engineType, 'Secret engine type renders'); .hasText(this.secretsEngine.engineType, 'Secret engine type renders');
assert.dom(GENERAL.infoRowValue('Path')).hasText(this.secretsEngine.path, 'Path 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('Accessor')).hasText(this.secretsEngine.accessor, 'Accessor renders');
assert.dom(GENERAL.infoRowValue('Local')).includesText('No', 'Local 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('Default Lease TTL')).includesText('0', 'Default Lease TTL renders');
assert assert
.dom(GENERAL.infoRowValue('Max Lease TTL')) .dom(GENERAL.infoRowValue('Max Lease TTL'))

View File

@ -25,6 +25,7 @@ module('Unit | Model | secret-engine', function (hooks) {
'path', 'path',
'description', 'description',
'accessor', 'accessor',
'runningPluginVersion',
'local', 'local',
'sealWrap', 'sealWrap',
'config.defaultLeaseTtl', 'config.defaultLeaseTtl',
@ -48,6 +49,7 @@ module('Unit | Model | secret-engine', function (hooks) {
'path', 'path',
'description', 'description',
'accessor', 'accessor',
'runningPluginVersion',
'local', 'local',
'sealWrap', 'sealWrap',
'config.defaultLeaseTtl', 'config.defaultLeaseTtl',
@ -73,6 +75,7 @@ module('Unit | Model | secret-engine', function (hooks) {
'path', 'path',
'description', 'description',
'accessor', 'accessor',
'runningPluginVersion',
'local', 'local',
'sealWrap', 'sealWrap',
'config.defaultLeaseTtl', 'config.defaultLeaseTtl',
@ -100,6 +103,7 @@ module('Unit | Model | secret-engine', function (hooks) {
'path', 'path',
'description', 'description',
'accessor', 'accessor',
'runningPluginVersion',
'local', 'local',
'sealWrap', 'sealWrap',
'config.allowedManagedKeys', 'config.allowedManagedKeys',
@ -121,6 +125,7 @@ module('Unit | Model | secret-engine', function (hooks) {
'path', 'path',
'description', 'description',
'accessor', 'accessor',
'runningPluginVersion',
'local', 'local',
'sealWrap', 'sealWrap',
'config.defaultLeaseTtl', 'config.defaultLeaseTtl',

View File

@ -27,6 +27,7 @@ export default class SecretEngineModel extends Model {
maxVersions: number; maxVersions: number;
casRequired: boolean; casRequired: boolean;
deleteVersionAfter: string; deleteVersionAfter: string;
runningPluginVersion: string;
get modelTypeForKV(): string; get modelTypeForKV(): string;
get isV2KV(): boolean; get isV2KV(): boolean;
get attrs(): Array<FormField>; get attrs(): Array<FormField>;