diff --git a/ui/app/components/mount-backend-form.js b/ui/app/components/mount-backend-form.js index 89bf91ecf3..fafd5a4a61 100644 --- a/ui/app/components/mount-backend-form.js +++ b/ui/app/components/mount-backend-form.js @@ -1,12 +1,11 @@ -import Ember from 'ember'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; -import { action, setProperties } from '@ember/object'; +import { action } from '@ember/object'; import { task } from 'ember-concurrency'; -import { methods } from 'vault/helpers/mountable-auth-methods'; -import { engines, KMIP, TRANSFORM, KEYMGMT } from 'vault/helpers/mountable-secret-engines'; import { waitFor } from '@ember/test-waiters'; +import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; +import { methods } from 'vault/helpers/mountable-auth-methods'; /** * @module MountBackendForm @@ -20,105 +19,77 @@ import { waitFor } from '@ember/test-waiters'; * */ -const METHODS = methods(); -const ENGINES = engines(); - export default class MountBackendForm extends Component { @service store; @service wizard; @service flashMessages; - @service version; - - get mountType() { - return this.args.mountType || 'auth'; - } - - @tracked mountModel = null; - @tracked showEnable = false; // validation related properties @tracked modelValidations = null; @tracked invalidFormAlert = null; - @tracked mountIssue = false; - - @tracked errors = ''; @tracked errorMessage = ''; - constructor() { - super(...arguments); - const type = this.args.mountType || 'auth'; - const modelType = type === 'secret' ? 'secret-engine' : 'auth-method'; - const model = this.store.createRecord(modelType); - model.set('config', this.store.createRecord('mount-config')); - this.mountModel = model; - } - - get mountTypes() { - return this.mountType === 'secret' ? this.engines : METHODS; - } - - get engines() { - if (this.version.isEnterprise) { - return ENGINES.concat([KMIP, TRANSFORM, KEYMGMT]); - } - return ENGINES; - } - willDestroy() { // if unsaved, we want to unload so it doesn't show up in the auth mount list super.willDestroy(...arguments); - this.mountModel.rollbackAttributes(); + this.args.mountModel.rollbackAttributes(); } checkPathChange(type) { - const mount = this.mountModel; + if (!type) return; + const mount = this.args.mountModel; const currentPath = mount.path; - const list = this.mountTypes; - // if the current path matches a type (meaning the user hasn't altered it), + const mountTypes = + this.args.mountType === 'secret' ? supportedSecretBackends() : methods().map((auth) => auth.type); + // if the current path has not been altered by user, // change it here to match the new type - const isUnchanged = list.findBy('type', currentPath); - if (!currentPath || isUnchanged) { + if (!currentPath || mountTypes.includes(currentPath)) { mount.path = type; } } checkModelValidity(model) { const { isValid, state, invalidFormMessage } = model.validate(); - setProperties(this, { - modelValidations: state, - invalidFormAlert: invalidFormMessage, - }); - + this.modelValidations = state; + this.invalidFormAlert = invalidFormMessage; return isValid; } + async showWarningsForKvv2() { + try { + const capabilities = await this.store.findRecord('capabilities', `${this.args.mountModel.path}/config`); + if (!capabilities?.canUpdate) { + // config error is not thrown from secret-engine adapter, so handling here + this.flashMessages.warning( + 'You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.' + ); + // remove the config data from the model otherwise it will persist in the store even though network request failed. + [ + this.args.mountModel.maxVersions, + this.args.mountModel.casRequired, + this.args.mountModel.deleteVersionAfter, + ] = [0, false, 0]; + } + } catch (e) { + // Show different warning if we're not sure the config saved + this.flashMessages.warning( + 'You may not have access to the config endpoint. The secret engine was mounted, but the configuration settings may not be saved.' + ); + } + return; + } + @task @waitFor *mountBackend(event) { event.preventDefault(); - const mountModel = this.mountModel; + const mountModel = this.args.mountModel; const { type, path } = mountModel; // only submit form if validations pass if (!this.checkModelValidity(mountModel)) { return; } - let capabilities = null; - try { - capabilities = yield this.store.findRecord('capabilities', `${path}/config`); - } catch (err) { - if (Ember.testing) { - //captures mount-backend-form component test - yield mountModel.save(); - let mountType = this.mountType; - mountType = mountType === 'secret' ? `${mountType}s engine` : `${mountType} method`; - this.flashMessages.success(`Successfully mounted the ${type} ${mountType} at ${path}.`); - yield this.args.onMountSuccess(type, path); - return; - } else { - throw err; - } - } const changedAttrKeys = Object.keys(mountModel.changedAttributes()); const updatesConfig = @@ -130,7 +101,6 @@ export default class MountBackendForm extends Component { yield mountModel.save(); } catch (err) { if (err.httpStatus === 403) { - this.mountIssue = true; this.flashMessages.danger( 'You do not have access to the sys/mounts endpoint. The secret engine was not mounted.' ); @@ -141,7 +111,7 @@ export default class MountBackendForm extends Component { if (typeof e === 'object') return e.title || e.message || JSON.stringify(e); return e; }); - this.errors = errors; + this.errorMessage = errors; } else if (err.message) { this.errorMessage = err.message; } else { @@ -149,46 +119,39 @@ export default class MountBackendForm extends Component { } return; } - // mountModel must be after the save - if (mountModel.isV2KV && updatesConfig && !capabilities.get('canUpdate')) { - // config error is not thrown from secret-engine adapter, so handling here - this.flashMessages.warning( - 'You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.' - ); - // remove the config data from the model otherwise it will save it even if the network request failed. - [this.mountModel.maxVersions, this.mountModel.casRequired, this.mountModel.deleteVersionAfter] = [ - 0, - false, - 0, - ]; + if (mountModel.isV2KV && updatesConfig) { + yield this.showWarningsForKvv2(); } - let mountType = this.mountType; - mountType = mountType === 'secret' ? `${mountType}s engine` : `${mountType} method`; - this.flashMessages.success(`Successfully mounted the ${type} ${mountType} at ${path}.`); + this.flashMessages.success( + `Successfully mounted the ${type} ${ + this.mountType === 'secret' ? 'secrets engine' : 'auth method' + } at ${path}.` + ); yield this.args.onMountSuccess(type, path); return; } @action onKeyUp(name, value) { - this.mountModel.set(name, value); + this.args.mountModel[name] = value; } @action onTypeChange(path, value) { if (path === 'type') { this.wizard.set('componentState', value); - this.checkPathChange(value); } } @action - toggleShowEnable(value) { - this.showEnable = value; - if (value === true && this.wizard.featureState === 'idle') { - this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE', this.mountModel.type); - } else { - this.wizard.transitionFeatureMachine(this.wizard.featureState, 'RESET', this.mountModel.type); + setMountType(value) { + this.args.mountModel.type = value; + this.checkPathChange(value); + if (value) { + this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE', this.args.mountModel.type); + } else if (this.wizard.featureState === 'idle') { + // resets wizard + this.wizard.transitionFeatureMachine(this.wizard.featureState, 'RESET', this.args.mountModel.type); } } } diff --git a/ui/app/components/mount-backend/type-form.hbs b/ui/app/components/mount-backend/type-form.hbs new file mode 100644 index 0000000000..758ec8d2e4 --- /dev/null +++ b/ui/app/components/mount-backend/type-form.hbs @@ -0,0 +1,37 @@ +{{#each (array "generic" "cloud" "infra") as |category|}} +

+ {{capitalize category}} +

+
+ {{#each (filter-by "category" category this.mountTypes) as |type|}} + + {{/each}} +
+{{/each}} +
+ +
\ No newline at end of file diff --git a/ui/app/components/mount-backend/type-form.js b/ui/app/components/mount-backend/type-form.js new file mode 100644 index 0000000000..d66d31ebc3 --- /dev/null +++ b/ui/app/components/mount-backend/type-form.js @@ -0,0 +1,32 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { methods } from 'vault/helpers/mountable-auth-methods'; +import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-engines'; +import { tracked } from '@glimmer/tracking'; + +/** + * + * @module MountBackendTypeForm + * MountBackendTypeForm components are used to display type options for + * mounting either an auth method or secret engine. + * + * @example + * ```js + * + * ``` + * @param {CallableFunction} setMountType - function will recieve the mount type string. Should update the model type value + * @param {string} [mountType=auth] - mount type can be `auth` or `secret` + */ + +export default class MountBackendTypeForm extends Component { + @service version; + @tracked selection; + + get secretEngines() { + return this.version.isEnterprise ? allEngines() : mountableEngines(); + } + + get mountTypes() { + return this.args.mountType === 'secret' ? this.secretEngines : methods(); + } +} diff --git a/ui/app/components/wizard/mounts-wizard.js b/ui/app/components/wizard/mounts-wizard.js index 3113d86db0..afb8d8de2c 100644 --- a/ui/app/components/wizard/mounts-wizard.js +++ b/ui/app/components/wizard/mounts-wizard.js @@ -2,7 +2,7 @@ import { inject as service } from '@ember/service'; import { alias, equal } from '@ember/object/computed'; import Component from '@ember/component'; import { computed } from '@ember/object'; -import { engines } from 'vault/helpers/mountable-secret-engines'; +import { mountableEngines } from 'vault/helpers/mountable-secret-engines'; import { methods } from 'vault/helpers/mountable-auth-methods'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; const supportedSecrets = supportedSecretBackends(); @@ -36,7 +36,7 @@ export default Component.extend({ }), mountName: computed('currentMachine', 'mountSubtype', function () { if (this.currentMachine === 'secrets') { - var secret = engines().find((engine) => { + const secret = mountableEngines().find((engine) => { return engine.type === this.mountSubtype; }); if (secret) { diff --git a/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js b/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js index cf18cd432b..4b2c306c36 100644 --- a/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js +++ b/ui/app/controllers/vault/cluster/settings/mount-secret-backend.js @@ -1,30 +1,34 @@ import { inject as service } from '@ember/service'; import Controller from '@ember/controller'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; +import { allEngines } from 'vault/helpers/mountable-secret-engines'; +import { action } from '@ember/object'; const SUPPORTED_BACKENDS = supportedSecretBackends(); -export default Controller.extend({ - wizard: service(), - actions: { - onMountSuccess: function (type, path) { - let transition; - if (SUPPORTED_BACKENDS.includes(type)) { - if (type === 'kmip') { - transition = this.transitionToRoute('vault.cluster.secrets.backend.kmip.scopes', path); - } else if (type === 'keymgmt') { - transition = this.transitionToRoute('vault.cluster.secrets.backend.index', path, { - queryParams: { tab: 'provider' }, - }); - } else { - transition = this.transitionToRoute('vault.cluster.secrets.backend.index', path); - } +export default class MountSecretBackendController extends Controller { + @service wizard; + @service router; + + @action + onMountSuccess(type, path) { + let transition; + if (SUPPORTED_BACKENDS.includes(type)) { + const engineInfo = allEngines().findBy('type', type); + if (engineInfo?.engineRoute) { + transition = this.router.transitionTo( + `vault.cluster.secrets.backend.${engineInfo.engineRoute}`, + path + ); } else { - transition = this.transitionToRoute('vault.cluster.secrets.backends'); + const queryParams = engineInfo?.routeQueryParams || {}; + transition = this.router.transitionTo('vault.cluster.secrets.backend.index', path, { queryParams }); } - return transition.followRedirects().then(() => { - this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE', type); - }); - }, - }, -}); + } else { + transition = this.router.transitionTo('vault.cluster.secrets.backends'); + } + return transition.followRedirects().then(() => { + this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE', type); + }); + } +} diff --git a/ui/app/helpers/mountable-secret-engines.js b/ui/app/helpers/mountable-secret-engines.js index f05c1e47ba..20e1692055 100644 --- a/ui/app/helpers/mountable-secret-engines.js +++ b/ui/app/helpers/mountable-secret-engines.js @@ -1,129 +1,117 @@ import { helper as buildHelper } from '@ember/component/helper'; -export const KMIP = { - displayName: 'KMIP', - value: 'kmip', - type: 'kmip', - category: 'generic', - requiredFeature: 'KMIP', -}; - -export const TRANSFORM = { - displayName: 'Transform', - value: 'transform', - type: 'transform', - category: 'generic', - requiredFeature: 'Transform Secrets Engine', -}; - -export const KEYMGMT = { - displayName: 'Key Management', - value: 'keymgmt', - type: 'keymgmt', - glyph: 'key', - category: 'cloud', - requiredFeature: 'Key Management Secrets Engine', -}; +const ENTERPRISE_SECRET_ENGINES = [ + { + displayName: 'KMIP', + type: 'kmip', + engineRoute: 'kmip.scopes', + category: 'generic', + requiredFeature: 'KMIP', + }, + { + displayName: 'Transform', + type: 'transform', + category: 'generic', + requiredFeature: 'Transform Secrets Engine', + }, + { + displayName: 'Key Management', + type: 'keymgmt', + glyph: 'key', + category: 'cloud', + requiredFeature: 'Key Management Secrets Engine', + routeQueryParams: { tab: 'provider' }, + }, +]; const MOUNTABLE_SECRET_ENGINES = [ { displayName: 'Active Directory', - value: 'ad', type: 'ad', category: 'cloud', }, { displayName: 'AliCloud', - value: 'alicloud', type: 'alicloud', category: 'cloud', }, { displayName: 'AWS', - value: 'aws', type: 'aws', category: 'cloud', glyph: 'aws-color', }, { displayName: 'Azure', - value: 'azure', type: 'azure', category: 'cloud', glyph: 'azure-color', }, { displayName: 'Consul', - value: 'consul', type: 'consul', category: 'infra', }, { displayName: 'Databases', - value: 'database', type: 'database', category: 'infra', }, { displayName: 'Google Cloud', - value: 'gcp', type: 'gcp', category: 'cloud', glyph: 'gcp-color', }, { displayName: 'Google Cloud KMS', - value: 'gcpkms', type: 'gcpkms', category: 'cloud', glyph: 'gcp-color', }, { displayName: 'KV', - value: 'kv', type: 'kv', category: 'generic', }, { displayName: 'Nomad', - value: 'nomad', type: 'nomad', category: 'infra', }, { displayName: 'PKI Certificates', - value: 'pki', type: 'pki', category: 'generic', }, { displayName: 'RabbitMQ', - value: 'rabbitmq', type: 'rabbitmq', category: 'infra', }, { displayName: 'SSH', - value: 'ssh', type: 'ssh', category: 'generic', }, { displayName: 'Transit', - value: 'transit', type: 'transit', category: 'generic', }, { displayName: 'TOTP', - value: 'totp', type: 'totp', category: 'generic', }, ]; -export function engines() { +export function mountableEngines() { return MOUNTABLE_SECRET_ENGINES.slice(); } -export default buildHelper(engines); +export function allEngines() { + return [...MOUNTABLE_SECRET_ENGINES, ...ENTERPRISE_SECRET_ENGINES]; +} + +export default buildHelper(mountableEngines); diff --git a/ui/app/models/secret-engine.js b/ui/app/models/secret-engine.js index 66bb3e5b2b..640296c8f8 100644 --- a/ui/app/models/secret-engine.js +++ b/ui/app/models/secret-engine.js @@ -95,43 +95,56 @@ export default SecretEngineModel.extend({ }), formFieldGroups: computed('engineType', function () { - const type = this.engineType; - let defaultGroup; - // KV has specific config options it adds on the enable engine. https://www.vaultproject.io/api/secret/kv/kv-v2#configure-the-kv-engine - if (type === 'kv') { - defaultGroup = { default: ['path', 'maxVersions', 'casRequired', 'deleteVersionAfter'] }; - } else { - defaultGroup = { default: ['path'] }; - } - const optionsGroup = { - 'Method Options': ['description', 'config.listingVisibility', 'local', 'sealWrap'], - }; - // no ttl options for keymgmt - const ttl = type !== 'keymgmt' ? 'defaultLeaseTtl,maxLeaseTtl,' : ''; - optionsGroup['Method Options'].push( - `config.{${ttl}auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}` - ); + let defaultFields = ['path']; + let optionFields; + const CORE_OPTIONS = ['description', 'config.listingVisibility', 'local', 'sealWrap']; - if (type === 'kv' || type === 'generic') { - optionsGroup['Method Options'].unshift('version'); + switch (this.engineType) { + case 'kv': + defaultFields = ['path', 'maxVersions', 'casRequired', 'deleteVersionAfter']; + optionFields = [ + 'version', + ...CORE_OPTIONS, + `config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}`, + ]; + break; + case 'generic': + optionFields = [ + 'version', + ...CORE_OPTIONS, + `config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}`, + ]; + break; + case 'database': + // Highlight TTLs in default + defaultFields = ['path', 'config.{defaultLeaseTtl}', 'config.{maxLeaseTtl}']; + optionFields = [ + ...CORE_OPTIONS, + 'config.{auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}', + ]; + break; + case 'keymgmt': + // no ttl options for keymgmt + optionFields = [ + ...CORE_OPTIONS, + 'config.{auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}', + ]; + break; + default: + defaultFields = ['path']; + optionFields = [ + ...CORE_OPTIONS, + `config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}`, + ]; + break; } - if (type === 'database') { - // For the Database Secret Engine we want to highlight the defaultLeaseTtl and maxLeaseTtl, removing them from the options object - defaultGroup.default.push('config.{defaultLeaseTtl}', 'config.{maxLeaseTtl}'); - return [ - defaultGroup, - { - 'Method Options': [ - 'description', - 'config.listingVisibility', - 'local', - 'sealWrap', - 'config.{auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}', - ], - }, - ]; - } - return [defaultGroup, optionsGroup]; + + return [ + { default: defaultFields }, + { + 'Method Options': optionFields, + }, + ]; }), attrs: computed('formFields', function () { diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index 952e11a996..b753d7d56b 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -2,6 +2,7 @@ import { set } from '@ember/object'; import { hash, all } from 'rsvp'; import Route from '@ember/routing/route'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; +import { allEngines } from 'vault/helpers/mountable-secret-engines'; import { inject as service } from '@ember/service'; import { normalizePath } from 'vault/utils/path-encoding-helpers'; @@ -11,6 +12,7 @@ export default Route.extend({ store: service(), templateName: 'vault/cluster/secrets/backend/list', pathHelp: service('path-help'), + router: service(), // By default assume user doesn't have permissions noMetadataPermissions: true, @@ -62,11 +64,16 @@ export default Route.extend({ const { tab } = this.paramsFor('vault.cluster.secrets.backend.list-root'); const secretEngine = this.store.peekRecord('secret-engine', backend); const type = secretEngine && secretEngine.get('engineType'); + const engineRoute = allEngines().findBy('type', type)?.engineRoute; + if (!type || !SUPPORTED_BACKENDS.includes(type)) { - return this.transitionTo('vault.cluster.secrets'); + return this.router.transitionTo('vault.cluster.secrets'); } if (this.routeName === 'vault.cluster.secrets.backend.list' && !secret.endsWith('/')) { - return this.replaceWith('vault.cluster.secrets.backend.list', secret + '/'); + return this.router.replaceWith('vault.cluster.secrets.backend.list', secret + '/'); + } + if (engineRoute) { + return this.router.transitionTo(`vault.cluster.secrets.backend.${engineRoute}`, backend); } const modelType = this.getModelType(backend, tab); return this.pathHelp.getNewModel(modelType, backend).then(() => { diff --git a/ui/app/routes/vault/cluster/settings/auth/enable.js b/ui/app/routes/vault/cluster/settings/auth/enable.js new file mode 100644 index 0000000000..dd04991e86 --- /dev/null +++ b/ui/app/routes/vault/cluster/settings/auth/enable.js @@ -0,0 +1,17 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class VaultClusterSettingsAuthEnableRoute extends Route { + @service store; + + beforeModel() { + // Unload to prevent naming collisions when we mount a new engine + this.store.unloadAll('auth-method'); + } + + model() { + const authMethod = this.store.createRecord('auth-method'); + authMethod.set('config', this.store.createRecord('mount-config')); + return authMethod; + } +} diff --git a/ui/app/routes/vault/cluster/settings/mount-secret-backend.js b/ui/app/routes/vault/cluster/settings/mount-secret-backend.js index e711b4b140..1cb8188a2d 100644 --- a/ui/app/routes/vault/cluster/settings/mount-secret-backend.js +++ b/ui/app/routes/vault/cluster/settings/mount-secret-backend.js @@ -1,16 +1,17 @@ import Route from '@ember/routing/route'; -import UnloadModelRoute from 'vault/mixins/unload-model-route'; -import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; import { inject as service } from '@ember/service'; -export default Route.extend(UnloadModelRoute, UnsavedModelRoute, { - store: service(), - // intentionally blank - we don't want a model until one is - // created via the form in the controller - model() { - return {}; - }, - activate() { +export default class VaultClusterSettingsMountSecretBackendRoute extends Route { + @service store; + + beforeModel() { + // Unload to prevent naming collisions when we mount a new engine this.store.unloadAll('secret-engine'); - }, -}); + } + + model() { + const secretEngine = this.store.createRecord('secret-engine'); + secretEngine.set('config', this.store.createRecord('mount-config')); + return secretEngine; + } +} diff --git a/ui/app/templates/components/mount-backend-form.hbs b/ui/app/templates/components/mount-backend-form.hbs index 9f9ce2bff7..8c7a1adb67 100644 --- a/ui/app/templates/components/mount-backend-form.hbs +++ b/ui/app/templates/components/mount-backend-form.hbs @@ -2,100 +2,66 @@

{{#if this.showEnable}} - {{#let (find-by "type" this.mountModel.type this.mountTypes) as |typeInfo|}} + {{#let (find-by "type" @mountModel.type @mountTypes) as |typeInfo|}} - {{#if (eq this.mountType "auth")}} - {{concat "Enable " typeInfo.displayName " Authentication Method"}} - {{else}} + {{#if (eq @mountType "secret")}} {{concat "Enable " typeInfo.displayName " Secrets Engine"}} + {{else}} + {{concat "Enable " typeInfo.displayName " Authentication Method"}} {{/if}} {{/let}} - {{else if (eq this.mountType "auth")}} - Enable an Authentication Method - {{else}} + {{else if (eq @mountType "secret")}} Enable a Secrets Engine + {{else}} + Enable an Authentication Method {{/if}}

-
-
- - - {{#if this.showEnable}} + +
+ + + {{#if @mountModel.type}} + - - {{else}} - {{#each (array "generic" "cloud" "infra") as |category|}} -

- {{capitalize category}} -

-
- {{#each (filter-by "category" category this.mountTypes) as |type|}} - - {{/each}} -
- {{/each}} - {{/if}} -
-
- {{#if this.showEnable}} -
- -
-
- -
- {{#if this.invalidFormAlert}} + + +
- +
- {{/if}} - {{else}} - - {{/if}} -
- \ No newline at end of file +
+ +
+ {{#if this.invalidFormAlert}} +
+ +
+ {{/if}} +
+ + {{else}} + {{! Type not yet set, show type options }} + + {{/if}} +
\ No newline at end of file diff --git a/ui/app/templates/vault/cluster/settings/auth/enable.hbs b/ui/app/templates/vault/cluster/settings/auth/enable.hbs index 086bf983f8..b472892b16 100644 --- a/ui/app/templates/vault/cluster/settings/auth/enable.hbs +++ b/ui/app/templates/vault/cluster/settings/auth/enable.hbs @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/settings/mount-secret-backend.hbs b/ui/app/templates/vault/cluster/settings/mount-secret-backend.hbs index 1a241c5438..ab699d7625 100644 --- a/ui/app/templates/vault/cluster/settings/mount-secret-backend.hbs +++ b/ui/app/templates/vault/cluster/settings/mount-secret-backend.hbs @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/core/addon/decorators/confirm-leave.js b/ui/lib/core/addon/decorators/confirm-leave.js index 7e8e142822..d41b997a82 100644 --- a/ui/lib/core/addon/decorators/confirm-leave.js +++ b/ui/lib/core/addon/decorators/confirm-leave.js @@ -1,6 +1,7 @@ import { action } from '@ember/object'; import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; +import Ember from 'ember'; /** * Confirm that the user wants to discard unsaved changes before leaving the page. @@ -30,6 +31,7 @@ export function withConfirmLeave() { const model = this.controller.get('model'); if (model && model.hasDirtyAttributes) { if ( + Ember.testing || window.confirm( 'You have unsaved changes. Navigating away will discard these changes. Are you sure you want to discard your changes?' ) diff --git a/ui/lib/core/addon/helpers/options-for-backend.js b/ui/lib/core/addon/helpers/options-for-backend.js index f6fe2bed90..7b75a3050a 100644 --- a/ui/lib/core/addon/helpers/options-for-backend.js +++ b/ui/lib/core/addon/helpers/options-for-backend.js @@ -1,6 +1,5 @@ import { helper as buildHelper } from '@ember/component/helper'; import { capitalize } from '@ember/string'; -import { assign } from '@ember/polyfills'; const DEFAULT_DISPLAY = { searchPlaceholder: 'Filter secrets', @@ -10,37 +9,35 @@ const DEFAULT_DISPLAY = { editComponent: 'secret-edit', listItemPartial: 'secret-list/item', }; -const ENGINE_SECRET_BACKENDS = { - pki: { - displayName: 'PKI', - navigateTree: false, - tabs: [ - { - label: 'Overview', - link: 'overview', - }, - { - label: 'Roles', - link: 'roles', - }, - { - label: 'Issuers', - link: 'issuers', - }, - { - label: 'Certificates', - link: 'certificates', - }, - { - label: 'Keys', - link: 'keys', - }, - { - label: 'Configuration', - link: 'configuration', - }, - ], - }, +const PKI_ENGINE_BACKEND = { + displayName: 'PKI', + navigateTree: false, + tabs: [ + { + label: 'Overview', + link: 'overview', + }, + { + label: 'Roles', + link: 'roles', + }, + { + label: 'Issuers', + link: 'issuers', + }, + { + label: 'Certificates', + link: 'certificates', + }, + { + label: 'Keys', + link: 'keys', + }, + { + label: 'Configuration', + link: 'configuration', + }, + ], }; const SECRET_BACKENDS = { aws: { @@ -198,22 +195,25 @@ const SECRET_BACKENDS = { }, }; -export function optionsForBackend([backend, tab, isEngine]) { - const selected = isEngine ? ENGINE_SECRET_BACKENDS[backend] : SECRET_BACKENDS[backend]; - let backendOptions; +export function optionsForBackend(backend, tab, isEngine) { + let selected = SECRET_BACKENDS[backend]; + if (backend === 'pki' && isEngine) { + selected = PKI_ENGINE_BACKEND; + } + let backendOptions; if (selected && selected.tabs) { const tabData = selected.tabs.findBy('name', tab) || selected.tabs.findBy('modelPrefix', tab) || selected.tabs[0]; - backendOptions = assign({}, selected, tabData); + backendOptions = { ...selected, ...tabData }; } else if (selected) { backendOptions = selected; } else { - backendOptions = assign({}, DEFAULT_DISPLAY, { - displayName: backend === 'kv' ? 'KV' : capitalize(backend), - }); + backendOptions = { ...DEFAULT_DISPLAY, displayName: backend === 'kv' ? 'KV' : capitalize(backend) }; } return backendOptions; } -export default buildHelper(optionsForBackend); +export default buildHelper(function ([backend, tab, isEngine]) { + return optionsForBackend(backend, tab, isEngine); +}); diff --git a/ui/tests/acceptance/auth-list-test.js b/ui/tests/acceptance/auth-list-test.js index e2cddb3863..42e646a7f9 100644 --- a/ui/tests/acceptance/auth-list-test.js +++ b/ui/tests/acceptance/auth-list-test.js @@ -1,7 +1,6 @@ /* eslint qunit/no-conditional-assertions: "warn" */ import { click, - findAll, fillIn, settled, visit, @@ -17,6 +16,10 @@ import logout from 'vault/tests/pages/logout'; import enablePage from 'vault/tests/pages/settings/auth/enable'; import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends'; import { supportedManagedAuthBackends } from 'vault/helpers/supported-managed-auth-backends'; +import { create } from 'ember-cli-page-object'; +import consoleClass from 'vault/tests/pages/components/console/ui-panel'; + +const consoleComponent = create(consoleClass); module('Acceptance | auth backend list', function (hooks) { setupApplicationTest(hooks); @@ -99,24 +102,22 @@ module('Acceptance | auth backend list', function (hooks) { test('auth methods are linkable and link to correct view', async function (assert) { assert.expect(16); - + const timestamp = new Date().getTime(); await visit('/vault/access'); const supportManaged = supportedManagedAuthBackends(); const backends = supportedAuthBackends(); - for (const backend of backends) { const { type } = backend; - + const path = `${type}-${timestamp}`; if (type !== 'token') { - await enablePage.enable(type, type); + await enablePage.enable(type, path); } await settled(); await visit('/vault/access'); // all auth methods should be linkable - await click(`[data-test-auth-backend-link="${type}"]`); - + await click(`[data-test-auth-backend-link="${type === 'token' ? type : path}"]`); if (!supportManaged.includes(type)) { assert.dom('[data-test-auth-section-tab]').exists({ count: 1 }); assert @@ -124,12 +125,15 @@ module('Acceptance | auth backend list', function (hooks) { .hasText('Configuration', `only shows configuration tab for ${type} auth method`); assert.dom('[data-test-doc-link] .doc-link').exists(`includes doc link for ${type} auth method`); } else { - // managed auth methods should have more than 1 tab - assert.notEqual( - findAll('[data-test-auth-section-tab]').length, - 1, - `has management tabs for ${type} auth method` - ); + let expectedTabs = 2; + if (type == 'ldap' || type === 'okta') { + expectedTabs = 3; + } + assert + .dom('[data-test-auth-section-tab]') + .exists({ count: expectedTabs }, `has management tabs for ${type} auth method`); + // cleanup method + await consoleComponent.runCommands(`delete sys/auth/${path}`); } } }); diff --git a/ui/tests/acceptance/settings/mount-secret-backend-test.js b/ui/tests/acceptance/settings/mount-secret-backend-test.js index 48dd73fd55..2edb8860b6 100644 --- a/ui/tests/acceptance/settings/mount-secret-backend-test.js +++ b/ui/tests/acceptance/settings/mount-secret-backend-test.js @@ -1,5 +1,5 @@ -import { currentRouteName, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; +import { currentRouteName, currentURL, settled } from '@ember/test-helpers'; +import { module, test, skip } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { create } from 'ember-cli-page-object'; import page from 'vault/tests/pages/settings/mount-secret-backend'; @@ -8,6 +8,7 @@ import authPage from 'vault/tests/pages/auth'; import consoleClass from 'vault/tests/pages/components/console/ui-panel'; import logout from 'vault/tests/pages/logout'; import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; +import { allEngines } from 'vault/helpers/mountable-secret-engines'; const consoleComponent = create(consoleClass); @@ -115,6 +116,10 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { { capabilities = ["read"] } + # Allow page to load after mount + path "sys/internal/ui/mounts/${enginePath}" { + capabilities = ["read"] + } `; await consoleComponent.runCommands([ // delete any previous mount with same name @@ -136,8 +141,29 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { .containsText( `You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.` ); + assert.strictEqual( + currentURL(), + `/vault/secrets/${enginePath}/list`, + 'After mounting, redirects to secrets list page' + ); await configPage.visit({ backend: enginePath }); await settled(); assert.dom('[data-test-row-value="Maximum number of versions"]').hasText('Not set'); }); + // TODO JR: enable once kubernetes routes are defined + skip('it should transition to engine route on success if defined in mount config', async function (assert) { + await consoleComponent.runCommands([ + // delete any previous mount with same name + `delete sys/mounts/kmip`, + ]); + await mountSecrets.visit(); + await mountSecrets.selectType('kubernetes'); + await mountSecrets.next().path('kubernetes').submit(); + const { engineRoute } = allEngines().findBy('type', 'kubernetes'); + assert.strictEqual( + currentRouteName(), + `vault.cluster.secrets.backend.${engineRoute}`, + 'Transitions to engine route on mount success' + ); + }); }); diff --git a/ui/tests/helpers/noop-all-api-requests.js b/ui/tests/helpers/noop-all-api-requests.js index ba93c0dd70..9cc33c0ac9 100644 --- a/ui/tests/helpers/noop-all-api-requests.js +++ b/ui/tests/helpers/noop-all-api-requests.js @@ -1,20 +1,18 @@ import Pretender from 'pretender'; +import { noopStub } from './stubs'; -const noop = (response) => { - return function () { - return [response, { 'Content-Type': 'application/json' }, JSON.stringify({})]; - }; -}; - +/** + * DEPRECATED prefer to use `setupMirage` along with stubs in vault/tests/helpers/stubs + */ export default function (options = { usePassthrough: false }) { return new Pretender(function () { - let fn = noop(); + let fn = noopStub(); if (options.usePassthrough) { fn = this.passthrough; } this.post('/v1/**', fn); this.put('/v1/**', fn); this.get('/v1/**', fn); - this.delete('/v1/**', fn || noop(204)); + this.delete('/v1/**', fn || noopStub(204)); }); } diff --git a/ui/tests/helpers/stubs.js b/ui/tests/helpers/stubs.js index 6bc752a29d..a3dccfb463 100644 --- a/ui/tests/helpers/stubs.js +++ b/ui/tests/helpers/stubs.js @@ -1,13 +1,15 @@ export function capabilitiesStub(requestPath, capabilitiesArray) { // sample of capabilitiesArray: ['read', 'update'] return { + [requestPath]: capabilitiesArray, + capabilities: capabilitiesArray, request_id: '40f7e44d-af5c-9b60-bd20-df72eb17e294', lease_id: '', renewable: false, lease_duration: 0, data: { - capabilities: capabilitiesArray, [requestPath]: capabilitiesArray, + capabilities: capabilitiesArray, }, wrap_info: null, warnings: null, @@ -15,23 +17,38 @@ export function capabilitiesStub(requestPath, capabilitiesArray) { }; } +export const noopStub = (response) => { + return function () { + return [response, { 'Content-Type': 'application/json' }, JSON.stringify({})]; + }; +}; + /** * allowAllCapabilitiesStub mocks the response from capabilities-self * that allows the user to do any action (root user) - * EXAMPLE USAGE: - * this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub); + * Example usage assuming setupMirage(hooks) was called: + * this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub(['read'])); */ -export function allowAllCapabilitiesStub() { - return { - request_id: '40f7e44d-af5c-9b60-bd20-df72eb17e294', - lease_id: '', - renewable: false, - lease_duration: 0, - data: { - capabilities: ['root'], - }, - wrap_info: null, - warnings: null, - auth: null, +export function allowAllCapabilitiesStub(capabilitiesList = ['root']) { + return function (_, { requestBody }) { + const { paths } = JSON.parse(requestBody); + const specificCapabilities = paths.reduce((obj, path) => { + return { + ...obj, + [path]: capabilitiesList, + }; + }, {}); + return { + ...specificCapabilities, + capabilities: capabilitiesList, + request_id: 'mirage-795dc9e1-0321-9ac6-71fc', + lease_id: '', + renewable: false, + lease_duration: 0, + data: { ...specificCapabilities, capabilities: capabilitiesList }, + wrap_info: null, + warnings: null, + auth: null, + }; }; } diff --git a/ui/tests/integration/components/mount-backend-form-test.js b/ui/tests/integration/components/mount-backend-form-test.js index 3b227971ee..a78a2bbdbb 100644 --- a/ui/tests/integration/components/mount-backend-form-test.js +++ b/ui/tests/integration/components/mount-backend-form-test.js @@ -2,7 +2,8 @@ import { later, _cancelTimers as cancelTimers } from '@ember/runloop'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, settled } from '@ember/test-helpers'; -import apiStub from 'vault/tests/helpers/noop-all-api-requests'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { allowAllCapabilitiesStub, noopStub } from 'vault/tests/helpers/stubs'; import hbs from 'htmlbars-inline-precompile'; import { create } from 'ember-cli-page-object'; @@ -14,63 +15,153 @@ const component = create(mountBackendForm); module('Integration | Component | mount backend form', function (hooks) { setupRenderingTest(hooks); + setupMirage(hooks); hooks.beforeEach(function () { this.owner.lookup('service:flash-messages').registerTypes(['success', 'danger']); - this.server = apiStub(); + this.store = this.owner.lookup('service:store'); + this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub()); + this.server.post('/sys/auth/foo', noopStub()); + this.server.post('/sys/mounts/foo', noopStub()); + this.onMountSuccess = sinon.spy(); }); hooks.afterEach(function () { this.server.shutdown(); }); - test('it renders', async function (assert) { - await render(hbs`{{mount-backend-form}}`); - assert.strictEqual( - component.header, - 'Enable an Authentication Method', - 'renders auth header in default state' - ); - assert.ok(component.types.length > 0, 'renders type picker'); - }); - - test('it changes path when type is changed', async function (assert) { - await render(hbs`{{mount-backend-form}}`); - await component.selectType('aws'); - await component.next(); - assert.strictEqual(component.pathValue, 'aws', 'sets the value of the type'); - await component.back(); - await component.selectType('approle'); - await component.next(); - assert.strictEqual(component.pathValue, 'approle', 'updates the value of the type'); - }); - - test('it keeps path value if the user has changed it', async function (assert) { - await render(hbs`{{mount-backend-form}}`); - await component.selectType('approle'); - await component.next(); - assert.strictEqual(component.pathValue, 'approle', 'defaults to approle (first in the list)'); - await component.path('newpath'); - await component.back(); - await component.selectType('aws'); - await component.next(); - assert.strictEqual(component.pathValue, 'newpath', 'updates to the value of the type'); - }); - - test('it calls mount success', async function (assert) { - this.server.post('/v1/sys/auth/foo', () => { - return [204, { 'Content-Type': 'application/json' }]; + module('auth method', function (hooks) { + hooks.beforeEach(function () { + this.model = this.store.createRecord('auth-method'); + this.model.set('config', this.store.createRecord('mount-config')); }); - const spy = sinon.spy(); - this.set('onMountSuccess', spy); - await render(hbs`{{mount-backend-form onMountSuccess=this.onMountSuccess}}`); - await component.mount('approle', 'foo'); + test('it renders default state', async function (assert) { + await render( + hbs`` + ); + assert.strictEqual( + component.header, + 'Enable an Authentication Method', + 'renders auth header in default state' + ); + assert.ok(component.types.length > 0, 'renders type picker'); + }); - later(() => cancelTimers(), 50); - await settled(); - const enableRequest = this.server.handledRequests.findBy('url', '/v1/sys/auth/foo'); - assert.ok(enableRequest, 'it calls enable on an auth method'); - assert.ok(spy.calledOnce, 'calls the passed success method'); + test('it changes path when type is changed', async function (assert) { + await render( + hbs`` + ); + await component.selectType('aws'); + await component.next(); + assert.strictEqual(component.pathValue, 'aws', 'sets the value of the type'); + await component.back(); + await component.selectType('approle'); + await component.next(); + assert.strictEqual(component.pathValue, 'approle', 'updates the value of the type'); + }); + + test('it keeps path value if the user has changed it', async function (assert) { + await render( + hbs`` + ); + await component.selectType('approle'); + await component.next(); + assert.strictEqual(this.model.type, 'approle', 'Updates type on model'); + assert.strictEqual(component.pathValue, 'approle', 'defaults to approle (first in the list)'); + await component.path('newpath'); + assert.strictEqual(this.model.path, 'newpath', 'Updates path on model'); + await component.back(); + assert.strictEqual(this.model.type, '', 'Clears type on back'); + assert.strictEqual(this.model.path, 'newpath', 'Path is still newPath'); + await component.selectType('aws'); + await component.next(); + assert.strictEqual(this.model.type, 'aws', 'Updates type on model'); + assert.strictEqual(component.pathValue, 'newpath', 'keeps custom path value'); + }); + + test('it calls mount success', async function (assert) { + assert.expect(2); + this.server.post('/sys/auth/foo', () => { + assert.ok(true, 'it calls enable on an auth method'); + return [204, { 'Content-Type': 'application/json' }]; + }); + const spy = sinon.spy(); + this.set('onMountSuccess', spy); + await render( + hbs`` + ); + await component.mount('approle', 'foo'); + + later(() => cancelTimers(), 50); + await settled(); + assert.ok(spy.calledOnce, 'calls the passed success method'); + }); + }); + + module('secrets engine', function (hooks) { + hooks.beforeEach(function () { + this.model = this.store.createRecord('secret-engine'); + this.model.set('config', this.store.createRecord('mount-config')); + }); + + test('it renders secret specific headers', async function (assert) { + await render( + hbs`` + ); + assert.strictEqual(component.header, 'Enable a Secrets Engine', 'renders secrets header'); + assert.ok(component.types.length > 0, 'renders type picker'); + }); + + test('it changes path when type is changed', async function (assert) { + await render( + hbs`` + ); + await component.selectType('kv'); + await component.next(); + assert.strictEqual(component.pathValue, 'kv', 'sets the value of the type'); + await component.back(); + await component.selectType('ssh'); + await component.next(); + assert.strictEqual(component.pathValue, 'ssh', 'updates the value of the type'); + }); + + test('it keeps path value if the user has changed it', async function (assert) { + await render( + hbs`` + ); + await component.selectType('kv'); + await component.next(); + assert.strictEqual(this.model.type, 'kv', 'Updates type on model'); + assert.strictEqual(component.pathValue, 'kv', 'path matches mount type'); + await component.path('newpath'); + assert.strictEqual(this.model.path, 'newpath', 'Updates path on model'); + await component.back(); + assert.strictEqual(this.model.type, '', 'Clears type on back'); + assert.strictEqual(this.model.path, 'newpath', 'path is still newpath'); + await component.selectType('ssh'); + await component.next(); + assert.strictEqual(this.model.type, 'ssh', 'Updates type on model'); + assert.strictEqual(component.pathValue, 'newpath', 'path stays the same'); + }); + + test('it calls mount success', async function (assert) { + assert.expect(2); + this.server.post('/sys/mounts/foo', () => { + assert.ok(true, 'it calls enable on an secrets engine'); + return [204, { 'Content-Type': 'application/json' }]; + }); + const spy = sinon.spy(); + this.set('onMountSuccess', spy); + await render( + hbs`` + ); + + await component.mount('ssh', 'foo'); + + later(() => cancelTimers(), 50); + await settled(); + assert.ok(spy.calledOnce, 'calls the passed success method'); + }); }); }); diff --git a/ui/tests/integration/components/mount-backend/type-form-test.js b/ui/tests/integration/components/mount-backend/type-form-test.js new file mode 100644 index 0000000000..a709a79d48 --- /dev/null +++ b/ui/tests/integration/components/mount-backend/type-form-test.js @@ -0,0 +1,70 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; +import { click, render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import sinon from 'sinon'; +import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-engines'; +import { methods } from 'vault/helpers/mountable-auth-methods'; + +const secretTypes = mountableEngines().map((engine) => engine.type); +const allSecretTypes = allEngines().map((engine) => engine.type); +const authTypes = methods().map((auth) => auth.type); + +module('Integration | Component | mount-backend/type-form', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.setType = sinon.spy(); + }); + + test('it calls secrets setMountType only on next click', async function (assert) { + const spy = sinon.spy(); + this.set('setType', spy); + await render(hbs``); + + assert + .dom('[data-test-mount-type]') + .exists({ count: secretTypes.length }, 'Renders all mountable engines'); + await click(`[data-test-mount-type="nomad"]`); + assert.dom(`[data-test-mount-type="nomad"] input`).isChecked(`ssh is checked`); + assert.ok(spy.notCalled, 'callback not called'); + await click(`[data-test-mount-type="ssh"]`); + assert.dom(`[data-test-mount-type="ssh"] input`).isChecked(`ssh is checked`); + assert.ok(spy.notCalled, 'callback not called'); + await click('[data-test-mount-next]'); + assert.ok(spy.calledOnceWith('ssh')); + }); + + test('it calls auth setMountType only on next click', async function (assert) { + const spy = sinon.spy(); + this.set('setType', spy); + await render(hbs``); + + assert + .dom('[data-test-mount-type]') + .exists({ count: authTypes.length }, 'Renders all mountable auth methods'); + await click(`[data-test-mount-type="okta"]`); + assert.dom(`[data-test-mount-type="okta"] input`).isChecked(`ssh is checked`); + assert.ok(spy.notCalled, 'callback not called'); + await click(`[data-test-mount-type="github"]`); + assert.dom(`[data-test-mount-type="github"] input`).isChecked(`ssh is checked`); + assert.ok(spy.notCalled, 'callback not called'); + await click('[data-test-mount-next]'); + assert.ok(spy.calledOnceWith('github')); + }); + + module('Enterprise', function (hooks) { + hooks.beforeEach(function () { + this.version = this.owner.lookup('service:version'); + this.version.version = '1.12.1+ent'; + }); + + test('it renders correct items for enterprise secrets', async function (assert) { + await render(hbs``); + + assert + .dom('[data-test-mount-type]') + .exists({ count: allSecretTypes.length }, 'Renders all secret engines'); + }); + }); +}); diff --git a/ui/tests/integration/components/pki-role-generate-test.js b/ui/tests/integration/components/pki-role-generate-test.js index 8a294a9819..216a0b275e 100644 --- a/ui/tests/integration/components/pki-role-generate-test.js +++ b/ui/tests/integration/components/pki-role-generate-test.js @@ -14,7 +14,7 @@ module('Integration | Component | pki-role-generate', function (hooks) { setupEngine(hooks, 'pki'); hooks.beforeEach(async function () { - this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub); + this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub()); this.store = this.owner.lookup('service:store'); this.secretMountPath = this.owner.lookup('service:secret-mount-path'); this.secretMountPath.currentPath = 'pki-test'; diff --git a/ui/tests/unit/models/secret-engine-test.js b/ui/tests/unit/models/secret-engine-test.js index 7d07b53ced..78f0b7d978 100644 --- a/ui/tests/unit/models/secret-engine-test.js +++ b/ui/tests/unit/models/secret-engine-test.js @@ -41,4 +41,126 @@ module('Unit | Model | secret-engine', function (hooks) { assert.strictEqual(model.get('modelTypeForKV'), 'secret-v2'); }); }); + + test('formFieldGroups returns correct values by default', function (assert) { + assert.expect(1); + let model; + run(() => { + model = run(() => + this.owner.lookup('service:store').createRecord('secret-engine', { + type: 'aws', + }) + ); + assert.deepEqual(model.get('formFieldGroups'), [ + { default: ['path'] }, + { + 'Method Options': [ + 'description', + 'config.listingVisibility', + 'local', + 'sealWrap', + 'config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}', + ], + }, + ]); + }); + }); + + test('formFieldGroups returns correct values for KV', function (assert) { + assert.expect(1); + let model; + run(() => { + model = run(() => + this.owner.lookup('service:store').createRecord('secret-engine', { + type: 'kv', + }) + ); + assert.deepEqual(model.get('formFieldGroups'), [ + { default: ['path', 'maxVersions', 'casRequired', 'deleteVersionAfter'] }, + { + 'Method Options': [ + 'version', + 'description', + 'config.listingVisibility', + 'local', + 'sealWrap', + 'config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}', + ], + }, + ]); + }); + }); + + test('formFieldGroups returns correct values for generic', function (assert) { + assert.expect(1); + let model; + run(() => { + model = run(() => + this.owner.lookup('service:store').createRecord('secret-engine', { + type: 'generic', + }) + ); + assert.deepEqual(model.get('formFieldGroups'), [ + { default: ['path'] }, + { + 'Method Options': [ + 'version', + 'description', + 'config.listingVisibility', + 'local', + 'sealWrap', + 'config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}', + ], + }, + ]); + }); + }); + + test('formFieldGroups returns correct values for database', function (assert) { + assert.expect(1); + let model; + run(() => { + model = run(() => + this.owner.lookup('service:store').createRecord('secret-engine', { + type: 'database', + }) + ); + assert.deepEqual(model.get('formFieldGroups'), [ + { default: ['path', 'config.{defaultLeaseTtl}', 'config.{maxLeaseTtl}'] }, + { + 'Method Options': [ + 'description', + 'config.listingVisibility', + 'local', + 'sealWrap', + 'config.{auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}', + ], + }, + ]); + }); + }); + + test('formFieldGroups returns correct values for keymgmt', function (assert) { + assert.expect(1); + let model; + run(() => { + model = run(() => + this.owner.lookup('service:store').createRecord('secret-engine', { + type: 'keymgmt', + }) + ); + assert.deepEqual(model.get('formFieldGroups'), [ + { default: ['path'] }, + { + 'Method Options': [ + 'description', + 'config.listingVisibility', + 'local', + 'sealWrap', + 'config.{auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}', + ], + }, + ]); + }); + }); });