UI: glimmerize mount backend form (#18335)

This commit is contained in:
Chelsea Shaw 2022-12-16 15:26:43 -06:00 committed by GitHub
parent 83b45be7bc
commit ea400bb64f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 772 additions and 414 deletions

View File

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

View File

@ -0,0 +1,37 @@
{{#each (array "generic" "cloud" "infra") as |category|}}
<h3 class="title box-radio-header">
{{capitalize category}}
</h3>
<div class="box-radio-container">
{{#each (filter-by "category" category this.mountTypes) as |type|}}
<BoxRadio
@displayName={{type.displayName}}
@type={{type.type}}
@glyph={{or type.glyph type.type}}
@groupValue={{this.selection}}
@groupName="mount-type"
@onRadioChange={{mut this.selection}}
@disabled={{if type.requiredFeature (not (has-feature type.requiredFeature)) false}}
@tooltipMessage={{if
(or (eq type.type "transform") (eq type.type "kmip") (eq type.type "keymgmt"))
(concat
type.displayName
" is part of the Advanced Data Protection module, which is not included in your enterprise license."
)
"This secret engine is not included in your license."
}}
/>
{{/each}}
</div>
{{/each}}
<div class="field is-grouped box is-fullwidth is-bottomless">
<button
data-test-mount-next
type="button"
class="button is-primary"
{{on "click" (fn @setMountType this.selection)}}
disabled={{not this.selection}}
>
Next
</button>
</div>

View File

@ -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
* <MountBackend::TypeForm @setMountType={{this.setMountType}} @mountType="secret" />
* ```
* @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();
}
}

View File

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

View File

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

View File

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

View File

@ -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 () {

View File

@ -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(() => {

View File

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

View File

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

View File

@ -2,100 +2,66 @@
<p.levelLeft>
<h1 class="title is-3 title-with-icon" data-test-mount-form-header="true">
{{#if this.showEnable}}
{{#let (find-by "type" this.mountModel.type this.mountTypes) as |typeInfo|}}
{{#let (find-by "type" @mountModel.type @mountTypes) as |typeInfo|}}
<Icon @name={{or typeInfo.glyph typeInfo.type}} @size="24" class="has-text-grey-light" />
{{#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}}
</h1>
</p.levelLeft>
</PageHeader>
<form {{on "submit" (perform this.mountBackend)}}>
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="enable" @noun={{if (eq this.mountType "auth") "Auth Method" "Secret Engine"}} />
<MessageError @model={{this.model}} @errorMessage={{this.errorMessage}} @errors={{this.errors}} />
{{#if this.showEnable}}
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="enable" @noun={{if (eq @mountType "secret") "Secret Engine" "Auth Method"}} />
<MessageError @errorMessage={{this.errorMessage}} />
{{#if @mountModel.type}}
<form {{on "submit" (perform this.mountBackend)}}>
<FormFieldGroups
@model={{this.mountModel}}
@onChange={{this.onTypeChange}}
@model={{@mountModel}}
@renderGroup="default"
@onChange={{this.onTypeChange}}
@modelValidations={{this.modelValidations}}
@onKeyUp={{this.onKeyUp}}
/>
<FormFieldGroups @model={{this.mountModel}} @onChange={{this.onTypeChange}} @renderGroup="Method Options" />
{{else}}
{{#each (array "generic" "cloud" "infra") as |category|}}
<h3 class="title box-radio-header">
{{capitalize category}}
</h3>
<div class="box-radio-container">
{{#each (filter-by "category" category this.mountTypes) as |type|}}
<BoxRadio
@displayName={{type.displayName}}
@type={{type.type}}
@glyph={{or type.glyph type.type}}
@groupValue={{this.mountModel.type}}
@groupName="mount-type"
@onRadioChange={{queue (fn (mut this.mountModel.type)) (fn this.onTypeChange "type")}}
@disabled={{if type.requiredFeature (not (has-feature type.requiredFeature)) false}}
@tooltipMessage={{if
(or (eq type.type "transform") (eq type.type "kmip") (eq type.type "keymgmt"))
(concat
type.displayName
" is part of the Advanced Data Protection module, which is not included in your enterprise license."
)
"This secret engine is not included in your license."
}}
/>
{{/each}}
</div>
{{/each}}
{{/if}}
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
{{#if this.showEnable}}
<div class="control">
<button
type="submit"
data-test-mount-submit="true"
class="button is-primary {{if this.mountBackend.isRunning 'loading'}}"
disabled={{this.mountBackend.isRunning}}
>
{{#if (eq this.mountType "auth")}}
Enable Method
{{else}}
Enable Engine
{{/if}}
</button>
</div>
<div class="control">
<button data-test-mount-back type="button" class="button" {{on "click" (fn this.toggleShowEnable false)}}>
Back
</button>
</div>
{{#if this.invalidFormAlert}}
<FormFieldGroups @model={{@mountModel}} @renderGroup="Method Options" />
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
<button
type="submit"
data-test-mount-submit="true"
class="button is-primary {{if this.mountBackend.isRunning 'loading'}}"
disabled={{this.mountBackend.isRunning}}
>
{{#if (eq @mountType "secret")}}
Enable Engine
{{else}}
Enable Method
{{/if}}
</button>
</div>
{{/if}}
{{else}}
<button
data-test-mount-next
type="button"
class="button is-primary"
{{on "click" (fn this.toggleShowEnable true)}}
disabled={{not this.mountModel.type}}
>
Next
</button>
{{/if}}
</div>
</form>
<div class="control">
<button data-test-mount-back type="button" class="button" {{on "click" (fn this.setMountType "")}}>
Back
</button>
</div>
{{#if this.invalidFormAlert}}
<div class="control">
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
</div>
{{/if}}
</div>
</form>
{{else}}
{{! Type not yet set, show type options }}
<MountBackend::TypeForm @setMountType={{this.setMountType}} @mountType={{@mountType}} />
{{/if}}
</div>

View File

@ -1 +1 @@
<MountBackendForm @onMountSuccess={{action "onMountSuccess"}} />
<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{action "onMountSuccess"}} />

View File

@ -1 +1 @@
<MountBackendForm @mountType="secret" @onMountSuccess={{action "onMountSuccess"}} />
<MountBackendForm @mountModel={{this.model}} @mountType="secret" @onMountSuccess={{action "onMountSuccess"}} />

View File

@ -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?'
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
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`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
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`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
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`<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
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`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
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`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
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`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
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`<MountBackendForm @mountType="secret" @mountModel={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
await component.mount('ssh', 'foo');
later(() => cancelTimers(), 50);
await settled();
assert.ok(spy.calledOnce, 'calls the passed success method');
});
});
});

View File

@ -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`<MountBackend::TypeForm @mountType="secret" @setMountType={{this.setType}} />`);
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`<MountBackend::TypeForm @setMountType={{this.setType}} />`);
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`<MountBackend::TypeForm @setMountType={{this.setType}} />`);
assert
.dom('[data-test-mount-type]')
.exists({ count: allSecretTypes.length }, 'Renders all secret engines');
});
});
});

View File

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

View File

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