mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 12:26:34 +02:00
UI: glimmerize mount backend form (#18335)
This commit is contained in:
parent
83b45be7bc
commit
ea400bb64f
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
ui/app/components/mount-backend/type-form.hbs
Normal file
37
ui/app/components/mount-backend/type-form.hbs
Normal 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>
|
||||
32
ui/app/components/mount-backend/type-form.js
Normal file
32
ui/app/components/mount-backend/type-form.js
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 () {
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
17
ui/app/routes/vault/cluster/settings/auth/enable.js
Normal file
17
ui/app/routes/vault/cluster/settings/auth/enable.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -1 +1 @@
|
||||
<MountBackendForm @onMountSuccess={{action "onMountSuccess"}} />
|
||||
<MountBackendForm @mountModel={{this.model}} @onMountSuccess={{action "onMountSuccess"}} />
|
||||
@ -1 +1 @@
|
||||
<MountBackendForm @mountType="secret" @onMountSuccess={{action "onMountSuccess"}} />
|
||||
<MountBackendForm @mountModel={{this.model}} @mountType="secret" @onMountSuccess={{action "onMountSuccess"}} />
|
||||
@ -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?'
|
||||
)
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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';
|
||||
|
||||
@ -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}',
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user