UI Onboarding Wizards (#5196)

This commit is contained in:
madalynrose 2018-08-28 01:03:55 -04:00 committed by Matthew Irish
parent 9f99ac44f4
commit bd34be9144
183 changed files with 3007 additions and 530 deletions

View File

@ -1,9 +1,10 @@
import Ember from 'ember';
export default Ember.Component.extend({
auth: Ember.inject.service(),
routing: Ember.inject.service('-routing'),
const { Component, inject, computed, run } = Ember;
export default Component.extend({
auth: inject.service(),
wizard: inject.service(),
routing: inject.service('-routing'),
transitionToRoute: function() {
var router = this.get('routing.router');
@ -12,12 +13,15 @@ export default Ember.Component.extend({
classNames: 'user-menu auth-info',
isRenewing: Ember.computed.or('fakeRenew', 'auth.isRenewing'),
isRenewing: computed.or('fakeRenew', 'auth.isRenewing'),
actions: {
restartGuide() {
this.get('wizard').restartGuide();
},
renewToken() {
this.set('fakeRenew', true);
Ember.run.later(() => {
run.later(() => {
this.set('fakeRenew', false);
this.get('auth').renew();
}, 200);

View File

@ -5,6 +5,7 @@ const { Component, computed } = Ember;
export default Component.extend({
tagName: 'a',
classNames: ['doc-link'],
attributeBindings: ['target', 'rel', 'href'],
layout: hbs`{{yield}}`,
@ -14,6 +15,6 @@ export default Component.extend({
path: '/',
href: computed('path', function() {
return `https://www.vaultproject.io/docs${this.get('path')}`;
return `https://www.vaultproject.io${this.get('path')}`;
}),
});

View File

@ -26,6 +26,7 @@ const MODEL_TYPES = {
};
export default Component.extend({
wizard: inject.service(),
store: inject.service(),
routing: inject.service('-routing'),
// set on the component
@ -57,6 +58,16 @@ export default Component.extend({
this.createOrReplaceModel();
},
didReceiveAttrs() {
if (this.get('wizard.featureState') === 'displayRole') {
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'CONTINUE',
this.get('backend.type')
);
}
},
willDestroy() {
this.get('model').unloadRecord();
this._super(...arguments);
@ -84,10 +95,21 @@ export default Component.extend({
create() {
let model = this.get('model');
this.set('loading', true);
model.save().finally(() => {
model.set('hasGenerated', true);
this.set('loading', false);
});
this.model
.save()
.catch(() => {
if (this.get('wizard.featureState') === 'credentials') {
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'ERROR',
this.get('backend.type')
);
}
})
.finally(() => {
model.set('hasGenerated', true);
this.set('loading', false);
});
},
codemirrorUpdated(attr, val, codemirror) {

View File

@ -3,6 +3,10 @@ import hbs from 'htmlbars-inline-precompile';
const { computed } = Ember;
const GLYPHS_WITH_SVG_TAG = [
'learn',
'video',
'tour',
'stopwatch',
'download',
'folder',
'file',
@ -16,7 +20,7 @@ const GLYPHS_WITH_SVG_TAG = [
'upload',
'control-lock',
'edition-enterprise',
'edition-oss'
'edition-oss',
];
export default Ember.Component.extend({
@ -40,11 +44,12 @@ export default Ember.Component.extend({
glyph: null,
excludeSVG: computed('glyph', function() {
return GLYPHS_WITH_SVG_TAG.includes(this.get('glyph'));
let glyph = this.get('glyph');
return glyph.startsWith('enable/') || GLYPHS_WITH_SVG_TAG.includes(glyph);
}),
size: computed(function() {
return 12;
size: computed('glyph', function() {
return this.get('glyph').startsWith('enable/') ? 48 : 12;
}),
partialName: computed('glyph', function() {

View File

@ -1,12 +1,15 @@
import Ember from 'ember';
import { task } from 'ember-concurrency';
import { methods } from 'vault/helpers/mountable-auth-methods';
import { engines } from 'vault/helpers/mountable-secret-engines';
const { inject } = Ember;
const { inject, computed, Component } = Ember;
const METHODS = methods();
const ENGINES = engines();
export default Ember.Component.extend({
export default Component.extend({
store: inject.service(),
wizard: inject.service(),
flashMessages: inject.service(),
routing: inject.service('-routing'),
@ -38,23 +41,29 @@ export default Ember.Component.extend({
*/
mountModel: null,
showConfig: false,
init() {
this._super(...arguments);
const type = this.get('mountType');
const modelType = type === 'secret' ? 'secret-engine' : 'auth-method';
const model = this.get('store').createRecord(modelType);
this.set('mountModel', model);
this.changeConfigModel(model.get('type'));
},
mountTypes: computed('mountType', function() {
return this.get('mountType') === 'secret' ? ENGINES : METHODS;
}),
willDestroy() {
// if unsaved, we want to unload so it doesn't show up in the auth mount list
this.get('mountModel').rollbackAttributes();
},
getConfigModelType(methodType) {
let mountType = this.get('mountType');
let noConfig = ['approle'];
if (noConfig.includes(methodType)) {
if (mountType === 'secret' || noConfig.includes(methodType)) {
return;
}
if (methodType === 'aws') {
@ -64,27 +73,32 @@ export default Ember.Component.extend({
},
changeConfigModel(methodType) {
const mount = this.get('mountModel');
const configRef = mount.hasMany('authConfigs').value();
const currentConfig = configRef.get('firstObject');
let mount = this.get('mountModel');
if (this.get('mountType') === 'secret') {
return;
}
let configRef = mount.hasMany('authConfigs').value();
let currentConfig = configRef.get('firstObject');
if (currentConfig) {
// rollbackAttributes here will remove the the config model from the store
// because `isNew` will be true
currentConfig.rollbackAttributes();
currentConfig.unloadRecord();
}
const configType = this.getConfigModelType(methodType);
let configType = this.getConfigModelType(methodType);
if (!configType) return;
const config = this.get('store').createRecord(configType);
let config = this.get('store').createRecord(configType);
config.set('backend', mount);
},
checkPathChange(type) {
const mount = this.get('mountModel');
const currentPath = mount.get('path');
let mount = this.get('mountModel');
let currentPath = mount.get('path');
let list = this.get('mountTypes');
// if the current path matches a type (meaning the user hasn't altered it),
// change it here to match the new type
const isUnchanged = METHODS.findBy('type', currentPath);
if (isUnchanged) {
let isUnchanged = list.findBy('type', currentPath);
if (!currentPath || isUnchanged) {
mount.set('path', type);
}
},
@ -101,6 +115,10 @@ export default Ember.Component.extend({
this.get('flashMessages').success(
`Successfully mounted ${type} ${this.get('mountType')} method at ${path}.`
);
if (this.get('mountType') === 'secret') {
yield this.get('onMountSuccess')(type, path);
return;
}
yield this.get('saveConfig').perform(mountModel);
}).drop(),
@ -111,11 +129,16 @@ export default Ember.Component.extend({
try {
if (config && Object.keys(config.changedAttributes()).length) {
yield config.save();
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'CONTINUE',
this.get('mountModel').get('type')
);
this.get('flashMessages').success(
`The config for ${type} ${this.get('mountType')} method at ${path} was saved successfully.`
);
}
yield this.get('onMountSuccess')();
yield this.get('onMountSuccess')(type, path);
} catch (err) {
this.get('flashMessages').danger(
`There was an error saving the configuration for ${type} ${this.get(
@ -129,9 +152,27 @@ export default Ember.Component.extend({
actions: {
onTypeChange(path, value) {
if (path === 'type') {
this.get('wizard').set('componentState', value);
this.changeConfigModel(value);
this.checkPathChange(value);
}
},
toggleShowConfig(value) {
this.set('showConfig', value);
if (value === true && this.get('wizard.featureState') === 'idle') {
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'CONTINUE',
this.get('mountModel').get('type')
);
} else {
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'RESET',
this.get('mountModel').get('type')
);
}
},
},
});

View File

@ -0,0 +1,11 @@
// THIS COMPONENT IS ONLY FOR EXTENDING
// You should use this component if you want to use outerHTML symantics
// in your components - this is the default for upcoming Glimmer components
import Ember from 'ember';
export default Ember.Component.extend({
tagName: '',
});
// yep! that's it, it's more of a way to keep track of what components
// use tagless semantics to make the upgrade to glimmer components easier

View File

@ -1,5 +1,5 @@
import Ember from 'ember';
export default Ember.Component.extend({
tagName: '',
tagName: 'span',
});

View File

@ -2,7 +2,7 @@ import Ember from 'ember';
import decodeConfigFromJWT from 'vault/utils/decode-config-from-jwt';
import ReplicationActions from 'vault/mixins/replication-actions';
const { computed, get } = Ember;
const { computed, get, Component, inject } = Ember;
const DEFAULTS = {
mode: 'primary',
@ -17,7 +17,9 @@ const DEFAULTS = {
replicationMode: 'dr',
};
export default Ember.Component.extend(ReplicationActions, DEFAULTS, {
export default Component.extend(ReplicationActions, DEFAULTS, {
wizard: inject.service(),
version: inject.service(),
didReceiveAttrs() {
this._super(...arguments);
const initialReplicationMode = this.get('initialReplicationMode');
@ -28,7 +30,6 @@ export default Ember.Component.extend(ReplicationActions, DEFAULTS, {
showModeSummary: false,
initialReplicationMode: null,
cluster: null,
version: Ember.inject.service(),
replicationAttrs: computed.alias('cluster.replicationAttrs'),
@ -55,7 +56,6 @@ export default Ember.Component.extend(ReplicationActions, DEFAULTS, {
) {
return false;
}
return true;
}
),
@ -66,7 +66,12 @@ export default Ember.Component.extend(ReplicationActions, DEFAULTS, {
actions: {
onSubmit(/*action, mode, data, event*/) {
return this.submitHandler(...arguments);
let promise = this.submitHandler(...arguments);
let wizard = this.get('wizard');
promise.then(() => {
wizard.transitionFeatureMachine(wizard.get('featureState'), 'ENABLEREPLICATION');
});
return promise;
},
clear() {

View File

@ -5,10 +5,6 @@ const { get, set } = Ember;
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
export default RoleEdit.extend({
init() {
this._super(...arguments);
},
actions: {
createOrUpdate(type, event) {
event.preventDefault();
@ -21,13 +17,13 @@ export default RoleEdit.extend({
}
var credential_type = get(this, 'model.credential_type');
if (credential_type == "iam_user") {
if (credential_type == 'iam_user') {
set(this, 'model.role_arns', []);
}
if (credential_type == "assumed_role") {
if (credential_type == 'assumed_role') {
set(this, 'model.policy_arns', []);
}
if (credential_type == "federation_token") {
if (credential_type == 'federation_token') {
set(this, 'model.role_arns', []);
set(this, 'model.policy_arns', []);
}

View File

@ -2,7 +2,7 @@ import Ember from 'ember';
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
import keys from 'vault/lib/keycodes';
const { get, set, computed } = Ember;
const { get, set, computed, inject } = Ember;
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
@ -12,8 +12,31 @@ export default Ember.Component.extend(FocusOnInsertMixin, {
onDataChange: () => {},
refresh: 'refresh',
model: null,
routing: Ember.inject.service('-routing'),
routing: inject.service('-routing'),
wizard: inject.service(),
requestInFlight: computed.or('model.isLoading', 'model.isReloading', 'model.isSaving'),
didReceiveAttrs() {
this._super(...arguments);
if (
(this.get('wizard.featureState') === 'details' && this.get('mode') === 'create') ||
(this.get('wizard.featureState') === 'role' && this.get('mode') === 'show')
) {
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'CONTINUE',
this.get('backendType')
);
}
if (this.get('wizard.featureState') === 'displayRole') {
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'NOOP',
this.get('backendType')
);
}
},
willDestroyElement() {
const model = this.get('model');
if (get(model, 'isError')) {
@ -49,6 +72,9 @@ export default Ember.Component.extend(FocusOnInsertMixin, {
const model = get(this, 'model');
return model[method]().then(() => {
if (!Ember.get(model, 'isError')) {
if (this.get('wizard.featureState') === 'role') {
this.get('wizard').transitionFeatureMachine('role', 'CONTINUE', this.get('backendType'));
}
successCallback(model);
}
});

View File

@ -1,3 +1,8 @@
import RoleEdit from './role-edit';
export default RoleEdit.extend({});
export default RoleEdit.extend({
init() {
this._super(...arguments);
this.set('backendType', 'pki');
},
});

View File

@ -1,3 +1,8 @@
import RoleEdit from './role-edit';
export default RoleEdit.extend({});
export default RoleEdit.extend({
init() {
this._super(...arguments);
this.set('backendType', 'ssh');
},
});

View File

@ -6,7 +6,7 @@ import KVObject from 'vault/lib/kv-object';
const LIST_ROUTE = 'vault.cluster.secrets.backend.list';
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
const { get, computed } = Ember;
const { get, computed, inject } = Ember;
export default Ember.Component.extend(FocusOnInsertMixin, {
// a key model
@ -35,6 +35,8 @@ export default Ember.Component.extend(FocusOnInsertMixin, {
hasLintError: false,
wizard: inject.service(),
init() {
this._super(...arguments);
const secrets = this.get('key.secretData');
@ -45,6 +47,11 @@ export default Ember.Component.extend(FocusOnInsertMixin, {
this.set('preferAdvancedEdit', true);
}
this.checkRows();
if (this.get('wizard.featureState') === 'details' && this.get('mode') === 'create') {
let engine = this.get('key').backend.includes('kv') ? 'kv' : this.get('key').backend;
this.get('wizard').transitionFeatureMachine('details', 'CONTINUE', engine);
}
if (this.get('mode') === 'edit') {
this.send('addRow');
}
@ -144,6 +151,9 @@ export default Ember.Component.extend(FocusOnInsertMixin, {
return model[method]().then(() => {
if (!Ember.get(model, 'isError')) {
if (this.get('wizard.featureState') === 'secret') {
this.get('wizard').transitionFeatureMachine('secret', 'CONTINUE');
}
successCallback(key);
}
});

View File

@ -26,15 +26,20 @@ export default Component.extend(DEFAULTS, {
buttonText: 'Submit',
thresholdPath: 'required',
generateAction: false,
encoded_token: null,
init() {
this._super(...arguments);
if (this.get('fetchOnInit')) {
this.attemptProgress();
}
return this._super(...arguments);
},
didInsertElement() {
this._super(...arguments);
this.onUpdate(this.getProperties(Object.keys(DEFAULTS)));
},
onUpdate() {},
onShamirSuccess() {},
// can be overridden w/an attr
isComplete(data) {
@ -56,17 +61,23 @@ export default Component.extend(DEFAULTS, {
hasProgress: computed.gt('progress', 0),
actionSuccess(resp) {
const { isComplete, onShamirSuccess, thresholdPath } = this.getProperties(
let { onUpdate, isComplete, onShamirSuccess, thresholdPath } = this.getProperties(
'onUpdate',
'isComplete',
'onShamirSuccess',
'thresholdPath'
);
let threshold = get(resp, thresholdPath);
let props = {
...resp,
threshold,
};
this.stopLoading();
this.set('threshold', get(resp, thresholdPath));
this.setProperties(resp);
if (isComplete(resp)) {
this.setProperties(props);
onUpdate(props);
if (isComplete(props)) {
this.reset();
onShamirSuccess(resp);
onShamirSuccess(props);
}
},

View File

@ -22,6 +22,7 @@ const WRAPPING_ENDPOINTS = ['lookup', 'wrap', 'unwrap', 'rewrap'];
export default Ember.Component.extend(DEFAULTS, {
store: Ember.inject.service(),
wizard: Ember.inject.service(),
// putting these attrs here so they don't get reset when you click back
//random
bytes: 32,
@ -76,11 +77,13 @@ export default Ember.Component.extend(DEFAULTS, {
props = Ember.assign({}, props, { unwrap_data: secret });
}
props = Ember.assign({}, props, secret);
if (resp && resp.wrap_info) {
const keyName = action === 'rewrap' ? 'rewrap_token' : 'token';
props = Ember.assign({}, props, { [keyName]: resp.wrap_info.token });
}
if (props.token || props.rewrap_token || props.unwrap_data || action === 'lookup') {
this.get('wizard').transitionFeatureMachine(this.get('wizard.featureState'), 'CONTINUE');
}
setProperties(this, props);
},

View File

@ -2,7 +2,7 @@ import Ember from 'ember';
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
import keys from 'vault/lib/keycodes';
const { get, set, computed } = Ember;
const { get, set, computed, inject } = Ember;
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
@ -11,8 +11,14 @@ export default Ember.Component.extend(FocusOnInsertMixin, {
onDataChange: null,
refresh: 'refresh',
key: null,
routing: Ember.inject.service('-routing'),
routing: inject.service('-routing'),
requestInFlight: computed.or('key.isLoading', 'key.isReloading', 'key.isSaving'),
wizard: inject.service(),
init() {
this._super(...arguments);
},
willDestroyElement() {
const key = this.get('key');
if (get(key, 'isError')) {
@ -48,6 +54,13 @@ export default Ember.Component.extend(FocusOnInsertMixin, {
const key = get(this, 'key');
return key[method]().then(() => {
if (!Ember.get(key, 'isError')) {
if (this.get('wizard.featureState') === 'secret') {
this.get('wizard').transitionFeatureMachine('secret', 'CONTINUE');
} else {
if (this.get('wizard.featureState') === 'encryption') {
this.get('wizard').transitionFeatureMachine('encryption', 'CONTINUE', 'transit');
}
}
successCallback(key);
}
});

View File

@ -0,0 +1,61 @@
import Ember from 'ember';
import { matchesState } from 'xstate';
const { inject, computed } = Ember;
export default Ember.Component.extend({
classNames: ['ui-wizard-container'],
wizard: inject.service(),
auth: inject.service(),
shouldRender: computed('wizard.showWhenUnauthenticated', 'auth.currentToken', function() {
return this.get('auth.currentToken') || this.get('wizard.showWhenUnauthenticated');
}),
currentState: computed.alias('wizard.currentState'),
featureState: computed.alias('wizard.featureState'),
featureComponent: computed.alias('wizard.featureComponent'),
tutorialComponent: computed.alias('wizard.tutorialComponent'),
componentState: computed.alias('wizard.componentState'),
nextFeature: computed.alias('wizard.nextFeature'),
nextStep: computed.alias('wizard.nextStep'),
actions: {
dismissWizard() {
this.get('wizard').transitionTutorialMachine(this.get('currentState'), 'DISMISS');
},
advanceWizard() {
let inInit = matchesState('init', this.get('wizard.currentState'));
let event = inInit ? this.get('wizard.initEvent') || 'CONTINUE' : 'CONTINUE';
this.get('wizard').transitionTutorialMachine(this.get('currentState'), event);
},
advanceFeature() {
this.get('wizard').transitionFeatureMachine(this.get('featureState'), 'CONTINUE');
},
finishFeature() {
this.get('wizard').transitionFeatureMachine(this.get('featureState'), 'DONE');
},
repeatStep() {
this.get('wizard').transitionFeatureMachine(
this.get('featureState'),
'REPEAT',
this.get('componentState')
);
},
resetFeature() {
this.get('wizard').transitionFeatureMachine(
this.get('featureState'),
'RESET',
this.get('componentState')
);
},
pauseWizard() {
this.get('wizard').transitionTutorialMachine(this.get('currentState'), 'PAUSE');
},
},
});

View File

@ -0,0 +1,14 @@
import Ember from 'ember';
const { Component, inject } = Ember;
export default Component.extend({
wizard: inject.service(),
classNames: ['ui-wizard'],
glyph: null,
headerText: null,
actions: {
dismissWizard() {
this.get('wizard').transitionTutorialMachine(this.get('wizard.currentState'), 'DISMISS');
},
},
});

View File

@ -0,0 +1,8 @@
import outerHTMLComponent from './outer-html';
export default outerHTMLComponent.extend({
headerText: null,
headerIcon: null,
docText: null,
docPath: null,
});

View File

@ -0,0 +1,75 @@
import Ember from 'ember';
const { inject, computed } = Ember;
export default Ember.Component.extend({
wizard: inject.service(),
version: inject.service(),
init() {
this._super(...arguments);
this.maybeHideFeatures();
},
maybeHideFeatures() {
if (this.get('showReplication') === false) {
let feature = this.get('allFeatures').findBy('key', 'replication');
feature.show = false;
}
},
allFeatures: computed(function() {
return [
{
key: 'secrets',
name: 'Secrets',
steps: ['Enabling a secrets engine', 'Adding a secret'],
selected: false,
show: true,
},
{
key: 'authentication',
name: 'Authentication',
steps: ['Enabling an auth method', 'Managing your auth method'],
selected: false,
show: true,
},
{
key: 'policies',
name: 'Policies',
steps: ['Choosing a policy type', 'Creating a policy', 'Deleting your policy', 'Other types of policies'],
selected: false,
show: true,
},
{
key: 'replication',
name: 'Replication',
steps: ['Setting up replication', 'Your cluster information'],
selected: false,
show: true,
},
{
key: 'tools',
name: 'Tools',
steps: ['Wrapping data', 'Lookup wrapped data', 'Rewrapping your data', 'Unwrapping your data'],
selected: false,
show: true,
},
];
}),
showReplication: computed('version.hasPerfReplication', 'version.hasDRReplication', function() {
return this.get('version.hasPerfReplication') || this.get('version.hasDRReplication');
}),
selectedFeatures: computed('allFeatures.@each.selected', function() {
return this.get('allFeatures').filterBy('selected').mapBy('key');
}),
actions: {
saveFeatures() {
let wizard = this.get('wizard');
wizard.saveFeatures(this.get('selectedFeatures'));
wizard.transitionTutorialMachine('active.select', 'CONTINUE');
},
},
});

View File

@ -0,0 +1,71 @@
import Ember from 'ember';
import { engines } 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();
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
const supportedAuth = supportedAuthBackends();
const { inject, computed } = Ember;
export default Ember.Component.extend({
wizard: inject.service(),
featureState: computed.alias('wizard.featureState'),
currentState: computed.alias('wizard.currentState'),
currentMachine: computed.alias('wizard.currentMachine'),
mountSubtype: computed.alias('wizard.componentState'),
fullNextStep: computed.alias('wizard.nextStep'),
nextFeature: computed.alias('wizard.nextFeature'),
nextStep: computed('fullNextStep', function() {
return this.get('fullNextStep').split('.').lastObject;
}),
needsEncryption: computed('mountSubtype', function() {
return this.get('mountSubtype') === 'transit';
}),
stepComponent: computed.alias('wizard.stepComponent'),
detailsComponent: computed('mountSubtype', function() {
let suffix = this.get('currentMachine') === 'secrets' ? 'engine' : 'method';
return this.get('mountSubtype') ? `wizard/${this.get('mountSubtype')}-${suffix}` : null;
}),
isSupported: computed('mountSubtype', function() {
if (this.get('currentMachine') === 'secrets') {
return supportedSecrets.includes(this.get('mountSubtype'));
} else {
return supportedAuth.includes(this.get('mountSubtype'));
}
}),
mountName: computed('mountSubtype', function() {
if (this.get('currentMachine') === 'secrets') {
var secret = engines().find(engine => {
return engine.type === this.get('mountSubtype');
});
if (secret) {
return secret.displayName;
}
} else {
var auth = methods().find(method => {
return method.type === this.get('mountSubtype');
});
if (auth) {
return auth.displayName;
}
}
return null;
}),
actionText: computed('mountSubtype', function() {
switch (this.get('mountSubtype')) {
case 'aws':
return 'Generate Credential';
case 'ssh':
return 'Sign Keys';
case 'pki':
return 'Generate Certificate';
default:
return null;
}
}),
onAdvance() {},
onRepeat() {},
onReset() {},
onDone() {},
});

View File

@ -10,6 +10,8 @@ const DEFAULTS = {
};
export default Ember.Controller.extend(DEFAULTS, {
wizard: Ember.inject.service(),
reset() {
this.setProperties(DEFAULTS);
},
@ -17,6 +19,8 @@ export default Ember.Controller.extend(DEFAULTS, {
initSuccess(resp) {
this.set('loading', false);
this.set('keyData', resp);
this.get('wizard').set('initEvent', 'SAVE');
this.get('wizard').transitionTutorialMachine(this.get('wizard.currentState'), 'TOSAVE');
},
initError(e) {

View File

@ -5,7 +5,6 @@ import PolicyEditController from 'vault/mixins/policy-edit-controller';
export default Ember.Controller.extend(PolicyEditController, {
showFileUpload: false,
file: null,
actions: {
setPolicyFromFile(index, fileInfo) {
let { value, fileName } = fileInfo;

View File

@ -3,6 +3,7 @@ let { inject } = Ember;
export default Ember.Controller.extend({
flashMessages: inject.service(),
wizard: inject.service(),
queryParams: {
page: 'page',
@ -54,6 +55,9 @@ export default Ember.Controller.extend({
.destroyRecord()
.then(() => {
flash.success(`${policyType.toUpperCase()} policy "${name}" was successfully deleted.`);
if (this.get('wizard.featureState') === 'delete') {
this.get('wizard').transitionFeatureMachine('delete', 'CONTINUE', policyType);
}
// this will clear the dataset cache on the store
this.send('willTransition');
})

View File

@ -1,9 +1,13 @@
import Ember from 'ember';
export default Ember.Controller.extend({
wizard: Ember.inject.service(),
actions: {
onMountSuccess: function() {
return this.transitionToRoute('vault.cluster.access.methods');
onMountSuccess: function(type) {
let transition = this.transitionToRoute('vault.cluster.access.methods');
return transition.followRedirects().then(() => {
this.get('wizard').transitionFeatureMachine(this.get('wizard.featureState'), 'CONTINUE', type);
});
},
onConfigError: function(modelId) {
return this.transitionToRoute('vault.cluster.settings.auth.configure', modelId);

View File

@ -3,138 +3,24 @@ import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends
const SUPPORTED_BACKENDS = supportedSecretBackends();
const { computed } = Ember;
export default Ember.Controller.extend({
mountTypes: [
{ label: 'Active Directory', value: 'ad' },
{ label: 'AWS', value: 'aws' },
{ label: 'Consul', value: 'consul' },
{ label: 'Databases', value: 'database' },
{ label: 'Google Cloud', value: 'gcp' },
{ label: 'KV', value: 'kv' },
{ label: 'Nomad', value: 'nomad' },
{ label: 'PKI', value: 'pki' },
{ label: 'RabbitMQ', value: 'rabbitmq' },
{ label: 'SSH', value: 'ssh' },
{ label: 'Transit', value: 'transit' },
{ label: 'TOTP', value: 'totp' },
{ label: 'Cassandra', value: 'cassandra', deprecated: true },
{ label: 'MongoDB', value: 'mongodb', deprecated: true },
{ label: 'MSSQL', value: 'mssql', deprecated: true },
{ label: 'MySQL', value: 'mysql', deprecated: true },
{ label: 'PostgreSQL', value: 'postgresql', deprecated: true },
],
selectedType: null,
selectedPath: null,
description: null,
default_lease_ttl: null,
max_lease_ttl: null,
showConfig: false,
local: false,
sealWrap: false,
version: 2,
selection: computed('selectedType', function() {
return this.get('mountTypes').findBy('value', this.get('selectedType'));
}),
flashMessages: Ember.inject.service(),
reset() {
const defaultBackend = this.get('mountTypes.firstObject.value');
this.setProperties({
selectedPath: defaultBackend,
selectedType: defaultBackend,
description: null,
default_lease_ttl: null,
max_lease_ttl: null,
local: false,
showConfig: false,
sealWrap: false,
version: 2,
});
},
init() {
this._super(...arguments);
this.reset();
},
const { inject, Controller } = Ember;
export default Controller.extend({
wizard: inject.service(),
actions: {
onTypeChange(val) {
const { selectedPath, selectedType } = this.getProperties('selectedPath', 'selectedType');
this.set('selectedType', val);
if (selectedPath === selectedType) {
this.set('selectedPath', val);
onMountSuccess: function(type, path) {
let transition;
if (SUPPORTED_BACKENDS.includes(type)) {
transition = this.transitionToRoute('vault.cluster.secrets.backend.index', path);
} else {
transition = this.transitionToRoute('vault.cluster.secrets.backends');
}
},
toggleShowConfig() {
this.toggleProperty('showConfig');
},
mountBackend() {
const {
selectedPath: path,
selectedType: type,
description,
default_lease_ttl,
local,
max_lease_ttl,
sealWrap,
version,
} = this.getProperties(
'selectedPath',
'selectedType',
'description',
'default_lease_ttl',
'local',
'max_lease_ttl',
'sealWrap',
'version'
);
const currentModel = this.get('model');
if (currentModel && currentModel.rollbackAttributes) {
currentModel.rollbackAttributes();
}
let attrs = {
path,
type,
description,
local,
sealWrap,
};
if (this.get('showConfig')) {
attrs.config = {
defaultLeaseTtl: default_lease_ttl,
maxLeaseTtl: max_lease_ttl,
};
}
if (type === 'kv') {
attrs.options = {
version,
};
}
const model = this.store.createRecord('secret-engine', attrs);
this.set('model', model);
model.save().then(() => {
this.reset();
let transition;
if (SUPPORTED_BACKENDS.includes(type)) {
transition = this.transitionToRoute('vault.cluster.secrets.backend.index', path);
} else {
transition = this.transitionToRoute('vault.cluster.secrets.backends');
}
transition.followRedirects().then(() => {
this.get('flashMessages').success(`Successfully mounted '${type}' at '${path}'!`);
});
return transition.followRedirects().then(() => {
this.get('wizard').transitionFeatureMachine(this.get('wizard.featureState'), 'CONTINUE', type);
});
},
onConfigError: function(modelId) {
return this.transitionToRoute('vault.cluster.settings.configure-secret-backend', modelId);
},
},
});

View File

@ -1,12 +1,20 @@
import Ember from 'ember';
export default Ember.Controller.extend({
wizard: Ember.inject.service(),
actions: {
transitionToCluster() {
transitionToCluster(resp) {
return this.get('model').reload().then(() => {
this.get('wizard').transitionTutorialMachine(this.get('wizard.currentState'), 'CONTINUE', resp);
return this.transitionToRoute('vault.cluster', this.get('model.name'));
});
},
setUnsealState(resp) {
this.get('wizard').set('componentState', resp);
},
isUnsealed(data) {
return data.sealed === false;
},

View File

@ -5,61 +5,76 @@ const MOUNTABLE_AUTH_METHODS = [
displayName: 'AppRole',
value: 'approle',
type: 'approle',
category: 'generic',
},
{
displayName: 'AWS',
value: 'aws',
type: 'aws',
category: 'cloud',
},
{
displayName: 'Azure',
value: 'azure',
type: 'azure',
category: 'cloud',
},
{
displayName: 'Google Cloud',
value: 'gcp',
type: 'gcp',
category: 'cloud',
},
{
displayName: 'GitHub',
value: 'github',
type: 'github',
category: 'cloud',
},
{
displayName: 'JWT/OIDC',
value: 'jwt',
type: 'jwt',
glyph: 'auth',
category: 'generic',
},
{
displayName: 'Kubernetes',
value: 'kubernetes',
type: 'kubernetes',
category: 'infra',
},
{
displayName: 'LDAP',
value: 'ldap',
type: 'ldap',
glyph: 'auth',
category: 'infra',
},
{
displayName: 'Okta',
value: 'okta',
type: 'okta',
category: 'infra',
},
{
displayName: 'RADIUS',
value: 'radius',
type: 'radius',
glyph: 'auth',
category: 'infra',
},
{
displayName: 'TLS Certificates',
value: 'cert',
type: 'cert',
category: 'generic',
},
{
displayName: 'Username & Password',
value: 'userpass',
type: 'userpass',
category: 'generic',
},
];

View File

@ -0,0 +1,83 @@
import Ember from 'ember';
const MOUNTABLE_SECRET_ENGINES = [
{
displayName: 'Active Directory',
value: 'ad',
type: 'ad',
glyph: 'azure',
category: 'cloud',
},
{
displayName: 'AWS',
value: 'aws',
type: 'aws',
category: 'cloud',
},
{
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',
},
{
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() {
return MOUNTABLE_SECRET_ENGINES;
}
export default Ember.Helper.helper(engines);

View File

@ -0,0 +1,50 @@
export default {
key: 'auth',
initial: 'idle',
on: {
RESET: 'idle',
DONE: 'complete',
},
states: {
idle: {
onEntry: [
{ type: 'routeTransition', params: ['vault.cluster.settings.auth.enable'] },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
{ type: 'render', level: 'step', component: 'wizard/auth-idle' },
],
on: {
CONTINUE: 'enable',
},
},
enable: {
onEntry: [
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
{ type: 'render', level: 'step', component: 'wizard/auth-enable' },
],
on: {
CONTINUE: 'list',
},
},
list: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/auth-list' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
DETAILS: 'details',
},
},
details: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/auth-details' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'complete',
},
},
complete: {
onEntry: ['completeFeature'],
},
},
};

View File

@ -0,0 +1,42 @@
export default {
key: 'policies',
initial: 'idle',
states: {
idle: {
onEntry: [
{ type: 'routeTransition', params: ['vault.cluster.policies.index', 'acl'] },
{ type: 'render', level: 'feature', component: 'wizard/policies-intro' },
],
on: {
CONTINUE: 'create',
},
},
create: {
on: {
CONTINUE: 'details',
},
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/policies-create' }],
},
details: {
on: {
CONTINUE: 'delete',
},
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/policies-details' }],
},
delete: {
on: {
CONTINUE: 'others',
},
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/policies-delete' }],
},
others: {
on: {
CONTINUE: 'complete',
},
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/policies-others' }],
},
complete: {
onEntry: ['completeFeature'],
},
},
};

View File

@ -0,0 +1,25 @@
export default {
key: 'replication',
initial: 'setup',
states: {
setup: {
on: {
ENABLEREPLICATION: 'details',
},
onEntry: [
{ type: 'routeTransition', params: ['vault.cluster.replication'] },
{ type: 'render', level: 'feature', component: 'wizard/replication-setup' },
],
},
details: {
on: {
CONTINUE: 'complete',
},
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/replication-details' }],
},
complete: {
onEntry: ['completeFeature'],
on: { RESET: 'idle' },
},
},
};

View File

@ -0,0 +1,143 @@
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
const supportedBackends = supportedSecretBackends();
export default {
key: 'secrets',
initial: 'idle',
on: {
RESET: 'idle',
DONE: 'complete',
ERROR: 'error',
},
states: {
idle: {
onEntry: [
{ type: 'routeTransition', params: ['vault.cluster.settings.mount-secret-backend'] },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
{ type: 'render', level: 'step', component: 'wizard/secrets-idle' },
],
on: {
CONTINUE: 'enable',
},
},
enable: {
onEntry: [
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
{ type: 'render', level: 'step', component: 'wizard/secrets-enable' },
],
on: {
CONTINUE: {
details: { cond: type => supportedBackends.includes(type) },
list: { cond: type => !supportedBackends.includes(type) },
},
},
},
details: {
onEntry: [
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
{ type: 'render', level: 'step', component: 'wizard/secrets-details' },
],
on: {
CONTINUE: {
role: {
cond: type => ['pki', 'aws', 'ssh'].includes(type),
},
secret: {
cond: type => ['cubbyhole', 'database', 'gcp', 'kv', 'nomad', 'rabbitmq', 'totp'].includes(type),
},
encryption: {
cond: type => type === 'transit',
},
},
},
},
encryption: {
onEntry: [
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
{ type: 'render', level: 'step', component: 'wizard/secrets-encryption' },
],
on: {
CONTINUE: 'display',
},
},
credentials: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-credentials' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'display',
},
},
role: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-role' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'displayRole',
},
},
displayRole: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-display-role' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'credentials',
},
},
secret: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-secret' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'display',
},
},
display: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-display' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
REPEAT: {
role: {
cond: type => ['pki', 'aws', 'ssh'].includes(type),
actions: [{ type: 'routeTransition', params: ['vault.cluster.secrets.backend.create-root'] }],
},
secret: {
cond: type => ['cubbyhole', 'database', 'gcp', 'kv', 'nomad', 'rabbitmq', 'totp'].includes(type),
actions: [{ type: 'routeTransition', params: ['vault.cluster.secrets.backend.create-root'] }],
},
encryption: {
cond: type => type === 'transit',
actions: [{ type: 'routeTransition', params: ['vault.cluster.secrets.backend.create-root'] }],
},
},
},
},
list: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-list' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'display',
},
},
error: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/tutorial-error' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'complete',
},
},
complete: {
onEntry: ['completeFeature'],
},
},
};

View File

@ -0,0 +1,61 @@
export default {
key: 'tools',
initial: 'wrap',
states: {
wrap: {
onEntry: [
{ type: 'routeTransition', params: ['vault.cluster.tools'] },
{ type: 'render', level: 'feature', component: 'wizard/tools-wrap' },
],
on: {
CONTINUE: 'wrapped',
},
},
wrapped: {
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/tools-wrapped' }],
on: {
LOOKUP: 'lookup',
},
},
lookup: {
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/tools-lookup' }],
on: {
CONTINUE: 'info',
},
},
info: {
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/tools-info' }],
on: {
REWRAP: 'rewrap',
},
},
rewrap: {
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/tools-rewrap' }],
on: {
CONTINUE: 'rewrapped',
},
},
rewrapped: {
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/tools-rewrapped' }],
on: {
UNWRAP: 'unwrap',
},
},
unwrap: {
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/tools-unwrap' }],
on: {
CONTINUE: 'unwrapped',
},
},
unwrapped: {
onEntry: [{ type: 'render', level: 'feature', component: 'wizard/tools-unwrapped' }],
on: {
CONTINUE: 'complete',
},
},
complete: {
onEntry: ['completeFeature'],
on: { RESET: 'idle' },
},
},
};

View File

@ -0,0 +1,109 @@
export default {
key: 'tutorial',
initial: 'idle',
on: {
DISMISS: 'dismissed',
DONE: 'complete',
PAUSE: 'paused',
},
states: {
init: {
key: 'init',
initial: 'idle',
on: { INITDONE: 'active.select' },
onEntry: [
'showTutorialAlways',
{ type: 'render', level: 'tutorial', component: 'wizard/tutorial-idle' },
{ type: 'render', level: 'feature', component: null },
],
onExit: ['showTutorialWhenAuthenticated'],
states: {
idle: {
on: {
START: 'active.setup',
SAVE: 'active.save',
UNSEAL: 'active.unseal',
LOGIN: 'active.login',
},
},
active: {
onEntry: { type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' },
states: {
setup: {
on: { TOSAVE: 'save' },
onEntry: { type: 'render', level: 'feature', component: 'wizard/init-setup' },
},
save: {
on: { TOUNSEAL: 'unseal' },
onEntry: { type: 'render', level: 'feature', component: 'wizard/init-save-keys' },
},
unseal: {
on: { TOLOGIN: 'login' },
onEntry: { type: 'render', level: 'feature', component: 'wizard/init-unseal' },
},
login: {
onEntry: { type: 'render', level: 'feature', component: 'wizard/init-login' },
},
},
},
},
},
active: {
key: 'feature',
initial: 'select',
onEntry: { type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' },
states: {
select: {
on: {
CONTINUE: 'feature',
},
onEntry: { type: 'render', level: 'feature', component: 'wizard/features-selection' },
},
feature: {},
},
},
idle: {
on: {
INIT: 'init.idle',
AUTH: 'active.select',
CONTINUE: 'active',
},
onEntry: [
{ type: 'render', level: 'feature', component: null },
{ type: 'render', level: 'step', component: null },
{ type: 'render', level: 'detail', component: null },
{ type: 'render', level: 'tutorial', component: 'wizard/tutorial-idle' },
],
},
dismissed: {
onEntry: [
{ type: 'render', level: 'tutorial', component: null },
{ type: 'render', level: 'feature', component: null },
{ type: 'render', level: 'step', component: null },
{ type: 'render', level: 'detail', component: null },
'handleDismissed',
],
},
paused: {
on: {
CONTINUE: 'active.feature',
},
onEntry: [
{ type: 'render', level: 'feature', component: null },
{ type: 'render', level: 'step', component: null },
{ type: 'render', level: 'detail', component: null },
{ type: 'render', level: 'tutorial', component: 'wizard/tutorial-paused' },
'handlePaused',
],
onExit: ['handleResume'],
},
complete: {
onEntry: [
{ type: 'render', level: 'feature', component: null },
{ type: 'render', level: 'step', component: null },
{ type: 'render', level: 'detail', component: null },
{ type: 'render', level: 'tutorial', component: 'wizard/tutorial-complete' },
],
},
},
};

View File

@ -4,6 +4,7 @@ let { inject } = Ember;
export default Ember.Mixin.create({
flashMessages: inject.service(),
wizard: inject.service(),
actions: {
deletePolicy(model) {
let policyType = model.get('policyType');
@ -29,6 +30,9 @@ export default Ember.Mixin.create({
let name = model.get('name');
model.save().then(m => {
flash.success(`${policyType.toUpperCase()} policy "${name}" was successfully saved.`);
if (this.get('wizard.featureState') === 'create') {
this.get('wizard').transitionFeatureMachine('create', 'CONTINUE', policyType);
}
return this.transitionToRoute('vault.cluster.policy.show', m.get('policyType'), m.get('name'));
});
},

View File

@ -2,7 +2,6 @@ import Ember from 'ember';
import DS from 'ember-data';
import { fragment } from 'ember-data-model-fragments/attributes';
import { queryRecord } from 'ember-computed-query';
import { methods } from 'vault/helpers/mountable-auth-methods';
import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import { memberAction } from 'ember-api-actions';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
@ -10,8 +9,6 @@ import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
const { attr, hasMany } = DS;
const { computed } = Ember;
const METHODS = methods();
const configPath = function configPath(strings, key) {
return function(...values) {
return `${strings[0]}${values[key]}${strings[1]}`;
@ -19,15 +16,10 @@ const configPath = function configPath(strings, key) {
};
export default DS.Model.extend({
authConfigs: hasMany('auth-config', { polymorphic: true, inverse: 'backend', async: false }),
path: attr('string', {
defaultValue: METHODS[0].value,
}),
path: attr('string'),
accessor: attr('string'),
name: attr('string'),
type: attr('string', {
defaultValue: METHODS[0].value,
possibleValues: METHODS,
}),
type: attr('string'),
// namespaces introduced types with a `ns_` prefix for built-in engines
// so we need to strip that to normalize the type
methodType: computed('type', function() {
@ -37,8 +29,14 @@ export default DS.Model.extend({
editType: 'textarea',
}),
config: fragment('mount-config', { defaultValue: {} }),
local: attr('boolean'),
sealWrap: attr('boolean'),
local: attr('boolean', {
helpText:
'When replication is enabled, a local mount will not be replicated across clusters. This can only be specified at mount time.',
}),
sealWrap: attr('boolean', {
helpText:
'When enabled - if a seal supporting seal wrapping is specified in the configuration, all critical security parameters (CSPs) in this backend will be seal wrapped. (For K/V mounts, all values will be seal wrapped.) This can only be specified at mount time.',
}),
// used when the `auth` prefix is important,
// currently only when setting perf mount filtering
@ -74,7 +72,7 @@ export default DS.Model.extend({
],
formFieldGroups: [
{ default: ['type', 'path'] },
{ default: ['path'] },
{
'Method Options': [
'description',

View File

@ -4,5 +4,9 @@ import Fragment from 'ember-data-model-fragments/fragment';
export default Fragment.extend({
version: attr('number', {
label: 'Version',
helpText:
'The KV Secrets engine can operate in different modes. Version 1 is the original generic secrets engine the allows for storing of static key/value pairs. Version 2 added more features including data versioning, TTLs, and check and set.',
possibleValues: [2, 1],
defaultFormValue: 2,
}),
});

View File

@ -3,7 +3,7 @@ import DS from 'ember-data';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import { fragment } from 'ember-data-model-fragments/attributes';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
const { attr } = DS;
const { computed } = Ember;
@ -16,12 +16,22 @@ export default DS.Model.extend({
path: attr('string'),
accessor: attr('string'),
name: attr('string'),
type: attr('string'),
description: attr('string'),
type: attr('string', {
label: 'Secret engine type',
}),
description: attr('string', {
editType: 'textarea',
}),
config: fragment('mount-config', { defaultValue: {} }),
options: fragment('mount-options', { defaultValue: {} }),
local: attr('boolean'),
sealWrap: attr('boolean'),
local: attr('boolean', {
helpText:
'When replication is enabled, a local mount will not be replicated across clusters. This can only be specified at mount time.',
}),
sealWrap: attr('boolean', {
helpText:
'When enabled - if a seal supporting seal wrapping is specified in the configuration, all critical security parameters (CSPs) in this backend will be seal wrapped. (For K/V mounts, all values will be seal wrapped.) This can only be specified at mount time.',
}),
modelTypeForKV: computed('engineType', 'options.version', function() {
let type = this.get('engineType');
@ -33,21 +43,51 @@ export default DS.Model.extend({
return modelType;
}),
formFields: [
'type',
'path',
'description',
'accessor',
'local',
'sealWrap',
'config.{defaultLeaseTtl,maxLeaseTtl}',
'options.{version}',
],
formFields: computed('engineType', function() {
let type = this.get('engineType');
let fields = [
'type',
'path',
'description',
'accessor',
'local',
'sealWrap',
'config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}',
];
if (type === 'kv' || type === 'generic') {
fields.push('options.{version}');
}
return fields;
}),
formFieldGroups: computed('engineType', function() {
let type = this.get('engineType');
let defaultGroup = { default: ['path'] };
if (type === 'kv' || type === 'generic') {
defaultGroup.default.push('options.{version}');
}
return [
defaultGroup,
{
'Method Options': [
'description',
'config.listingVisibility',
'local',
'sealWrap',
'config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}',
],
},
];
}),
attrs: computed('formFields', function() {
return expandAttributeMeta(this, this.get('formFields'));
}),
fieldGroups: computed('formFieldGroups', function() {
return fieldToAttrs(this, this.get('formFieldGroups'));
}),
// namespaces introduced types with a `ns_` prefix for built-in engines
// so we need to strip that to normalize the type
engineType: computed('type', function() {

View File

@ -5,6 +5,7 @@ const { inject } = Ember;
export default Ember.Route.extend({
controlGroup: inject.service(),
routing: inject.service('router'),
wizard: inject.service(),
namespaceService: inject.service('namespace'),
actions: {
@ -55,5 +56,27 @@ export default Ember.Route.extend({
return true;
},
didTransition() {
let wizard = this.get('wizard');
if (wizard.get('currentState') !== 'active.feature') {
return true;
}
Ember.run.next(() => {
let applicationURL = this.get('routing.currentURL');
let activeRoute = this.get('routing.currentRouteName');
if (this.get('wizard.setURLAfterTransition')) {
this.set('wizard.setURLAfterTransition', false);
this.set('wizard.expectedURL', applicationURL);
this.set('wizard.expectedRouteName', activeRoute);
}
let expectedRouteName = this.get('wizard.expectedRouteName');
if (this.get('routing').isActive(expectedRouteName) === false) {
wizard.transitionTutorialMachine(wizard.get('currentState'), 'PAUSE');
}
});
return true;
},
},
});

View File

@ -12,7 +12,9 @@ export default Ember.Route.extend(UnloadModel, {
},
model(params) {
return this.get('version.isOSS') ? null : this.store.findRecord('control-group', params.accessor);
return this.get('version').hasFeature('Control Groups')
? this.store.findRecord('control-group', params.accessor)
: null;
},
actions: {

View File

@ -12,6 +12,6 @@ export default Ember.Route.extend(UnloadModel, {
},
model() {
return this.get('version.isOSS') ? null : this.store.createRecord('control-group');
return this.get('version').hasFeature('Control Groups') ? this.store.createRecord('control-group') : null;
},
});

View File

@ -2,6 +2,7 @@ import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Route.extend({
wizard: Ember.inject.service(),
model(params) {
const { section_name: section } = params;
if (section !== 'configuration') {
@ -9,7 +10,13 @@ export default Ember.Route.extend({
Ember.set(error, 'httpStatus', 404);
throw error;
}
return this.modelFor('vault.cluster.access.method');
let backend = this.modelFor('vault.cluster.access.method');
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'DETAILS',
backend.get('type')
);
return backend;
},
setupController(controller) {

View File

@ -7,6 +7,7 @@ const { inject } = Ember;
export default ClusterRouteBase.extend({
flashMessages: inject.service(),
version: inject.service(),
wizard: inject.service(),
beforeModel() {
return this._super().then(() => {
return this.get('version').fetchFeatures();
@ -25,4 +26,15 @@ export default ClusterRouteBase.extend({
this.get('flashMessages').stickyInfo(config.welcomeMessage);
}
},
activate() {
this.get('wizard').set('initEvent', 'LOGIN');
this.get('wizard').transitionTutorialMachine(this.get('wizard.currentState'), 'TOLOGIN');
},
actions: {
willTransition(transition) {
if (transition.targetName !== this.routeName) {
this.get('wizard').transitionTutorialMachine(this.get('wizard.currentState'), 'INITDONE');
}
},
},
});

View File

@ -1 +1,14 @@
export { default } from './cluster-route-base';
import Ember from 'ember';
import ClusterRoute from './cluster-route-base';
const { inject } = Ember;
export default ClusterRoute.extend({
wizard: inject.service(),
activate() {
// always start from idle instead of using the current state
this.get('wizard').transitionTutorialMachine('idle', 'INIT');
this.get('wizard').set('initEvent', 'START');
},
});

View File

@ -5,9 +5,16 @@ import UnsavedModelRoute from 'vault/mixins/unsaved-model-route';
const { inject } = Ember;
export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, {
version: inject.service(),
wizard: inject.service(),
model() {
let policyType = this.policyType();
if (
policyType === 'acl' &&
this.get('wizard.currentMachine') === 'policies' &&
this.get('wizard.featureState') === 'idle'
) {
this.get('wizard').transitionFeatureMachine(this.get('wizard.featureState'), 'CONTINUE');
}
if (!this.get('version.hasSentinel') && policyType !== 'acl') {
return this.transitionTo('vault.cluster.policies', policyType);
}

View File

@ -5,6 +5,7 @@ const { inject } = Ember;
export default Ember.Route.extend(ClusterRoute, {
version: inject.service(),
wizard: inject.service(),
queryParams: {
page: {
refreshModel: true,
@ -14,6 +15,12 @@ export default Ember.Route.extend(ClusterRoute, {
},
},
activate() {
if (this.get('wizard.featureState') === 'details') {
this.get('wizard').transitionFeatureMachine('details', 'CONTINUE', this.policyType());
}
},
shouldReturnEmptyModel(policyType, version) {
return policyType !== 'acl' && (version.get('isOSS') || !version.get('hasSentinel'));
},

View File

@ -1,7 +1,16 @@
import Ember from 'ember';
export default Ember.Route.extend({
wizard: Ember.inject.service(),
model() {
return this.modelFor('vault.cluster.secrets.backend');
let backend = this.modelFor('vault.cluster.secrets.backend');
if (this.get('wizard.featureState') === 'list') {
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'CONTINUE',
backend.get('type')
);
}
return backend;
},
});

View File

@ -20,6 +20,7 @@ var SecretProxy = Ember.Object.extend(KeyMixin, {
});
export default EditBase.extend({
wizard: Ember.inject.service(),
createModel(transition, parentKey) {
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
const modelType = this.modelType(backend);
@ -27,6 +28,9 @@ export default EditBase.extend({
return this.store.createRecord(modelType, { keyType: 'ca' });
}
if (modelType !== 'secret' && modelType !== 'secret-v2') {
if (this.get('wizard.featureState') === 'details' && this.get('wizard.componentState') === 'transit') {
this.get('wizard').transitionFeatureMachine('details', 'CONTINUE', 'transit');
}
return this.store.createRecord(modelType);
}
const key = transition.queryParams.initialKey || '';

View File

@ -2,9 +2,10 @@ import Ember from 'ember';
import DS from 'ember-data';
import UnloadModelRoute from 'vault/mixins/unload-model-route';
const { RSVP } = Ember;
const { RSVP, inject } = Ember;
export default Ember.Route.extend(UnloadModelRoute, {
modelPath: 'model.model',
wizard: inject.service(),
modelType(backendType, section) {
const MODELS = {
'aws-client': 'auth-config/aws/client',
@ -26,6 +27,11 @@ export default Ember.Route.extend(UnloadModelRoute, {
const backend = this.modelFor('vault.cluster.settings.auth.configure');
const { section_name: section } = params;
if (section === 'options') {
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'EDIT',
backend.get('type')
);
return RSVP.hash({
model: backend,
section,
@ -39,6 +45,11 @@ export default Ember.Route.extend(UnloadModelRoute, {
}
const model = this.store.peekRecord(modelType, backend.id);
if (model) {
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'EDIT',
backend.get('type')
);
return RSVP.hash({
model,
section,
@ -47,6 +58,11 @@ export default Ember.Route.extend(UnloadModelRoute, {
return this.store
.findRecord(modelType, backend.id)
.then(config => {
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
'EDIT',
backend.get('type')
);
config.set('backend', backend);
return RSVP.hash({
model: config,

View File

@ -13,9 +13,8 @@ export default Ember.Route.extend(UnloadModel, {
model() {
let type = 'control-group-config';
return this.get('version.isOSS')
? null
: this.store.findRecord(type, 'config').catch(e => {
return this.get('version').hasFeature('Control Groups')
? this.store.findRecord(type, 'config').catch(e => {
// if you haven't saved a config, the API 404s, so create one here to edit and return it
if (e.httpStatus === 404) {
return this.store.createRecord(type, {
@ -23,7 +22,8 @@ export default Ember.Route.extend(UnloadModel, {
});
}
throw e;
});
})
: null;
},
actions: {

View File

@ -2,6 +2,8 @@ import Ember from 'ember';
import { toolsActions } from 'vault/helpers/tools-actions';
export default Ember.Route.extend({
wizard: Ember.inject.service(),
beforeModel(transition) {
const supportedActions = toolsActions();
const { selectedAction } = this.paramsFor(this.routeName);
@ -14,7 +16,14 @@ export default Ember.Route.extend({
actions: {
didTransition() {
const params = this.paramsFor(this.routeName);
if (this.get('wizard.currentMachine') === 'tools') {
this.get('wizard').transitionFeatureMachine(
this.get('wizard.featureState'),
params.selectedAction.toUpperCase()
);
}
this.controller.setProperties(params);
return true;
},
},
});

View File

@ -1 +1,13 @@
export { default } from './cluster-route-base';
import Ember from 'ember';
import ClusterRoute from './cluster-route-base';
const { inject } = Ember;
export default ClusterRoute.extend({
wizard: inject.service(),
activate() {
this.get('wizard').set('initEvent', 'UNSEAL');
this.get('wizard').transitionTutorialMachine(this.get('wizard.currentState'), 'TOUNSEAL');
},
});

View File

@ -1,11 +1,7 @@
import DS from 'ember-data';
import Ember from 'ember';
const { decamelize } = Ember.String;
import ApplicationSerializer from './application';
export default DS.RESTSerializer.extend({
keyForAttribute: function(attr) {
return decamelize(attr);
},
export default ApplicationSerializer.extend({
normalizeBackend(path, backend) {
let struct = {};
for (let attribute in backend) {
@ -51,11 +47,20 @@ export default DS.RESTSerializer.extend({
}
}
const transformedPayload = { [primaryModelClass.modelName]: backends };
return this._super(store, primaryModelClass, transformedPayload, id, requestType);
return this._super(store, primaryModelClass, backends, id, requestType);
},
serialize() {
return this._super(...arguments);
serialize(snapshot) {
let type = snapshot.record.get('engineType');
let data = this._super(...arguments);
// only KV uses options
if (type !== 'kv' && type !== 'generic') {
delete data.options;
} else if (!data.options.version) {
// if options.version isn't set for some reason
// default to 2
data.options.version = 2;
}
return data;
},
});

View File

@ -3,13 +3,16 @@ import { task } from 'ember-concurrency';
const { Service, inject, computed } = Ember;
const hasFeatureMethod = (context, featureKey) => {
const features = context.get('features');
if (!features) {
return false;
}
return features.includes(featureKey);
};
const hasFeature = featureKey => {
return computed('features', 'features.[]', function() {
const features = this.get('features');
if (!features) {
return false;
}
return features.includes(featureKey);
return hasFeatureMethod(this, featureKey);
});
};
export default Service.extend({
@ -33,6 +36,10 @@ export default Service.extend({
this.set('version', resp.version);
},
hasFeature(feature) {
return hasFeatureMethod(this, feature);
},
setFeatures(resp) {
if (!resp.features) {
return;

317
ui/app/services/wizard.js Normal file
View File

@ -0,0 +1,317 @@
import Ember from 'ember';
import { Machine } from 'xstate';
const { Service, inject } = Ember;
import getStorage from 'vault/lib/token-storage';
import TutorialMachineConfig from 'vault/machines/tutorial-machine';
import SecretsMachineConfig from 'vault/machines/secrets-machine';
import PoliciesMachineConfig from 'vault/machines/policies-machine';
import ReplicationMachineConfig from 'vault/machines/replication-machine';
import ToolsMachineConfig from 'vault/machines/tools-machine';
import AuthMachineConfig from 'vault/machines/auth-machine';
const TutorialMachine = Machine(TutorialMachineConfig);
let FeatureMachine = null;
const TUTORIAL_STATE = 'vault:ui-tutorial-state';
const FEATURE_LIST = 'vault:ui-feature-list';
const FEATURE_STATE = 'vault:ui-feature-state';
const COMPLETED_FEATURES = 'vault:ui-completed-list';
const COMPONENT_STATE = 'vault:ui-component-state';
const RESUME_URL = 'vault:ui-tutorial-resume-url';
const RESUME_ROUTE = 'vault:ui-tutorial-resume-route';
const MACHINES = {
secrets: SecretsMachineConfig,
policies: PoliciesMachineConfig,
replication: ReplicationMachineConfig,
tools: ToolsMachineConfig,
authentication: AuthMachineConfig,
};
const DEFAULTS = {
currentState: null,
featureList: null,
featureState: null,
currentMachine: null,
tutorialComponent: null,
featureComponent: null,
stepComponent: null,
detailsComponent: null,
componentState: null,
nextFeature: null,
nextStep: null,
};
export default Service.extend(DEFAULTS, {
router: inject.service(),
showWhenUnauthenticated: false,
init() {
this._super(...arguments);
this.initializeMachines();
},
initializeMachines() {
if (!this.storageHasKey(TUTORIAL_STATE)) {
let state = TutorialMachine.initialState;
this.saveState('currentState', state.value);
this.saveExtState(TUTORIAL_STATE, state.value);
}
this.saveState('currentState', this.getExtState(TUTORIAL_STATE));
if (this.storageHasKey(COMPONENT_STATE)) {
this.set('componentState', this.getExtState(COMPONENT_STATE));
}
let stateNodes = TutorialMachine.getStateNodes(this.get('currentState'));
this.executeActions(stateNodes.reduce((acc, node) => acc.concat(node.onEntry), []), null, 'tutorial');
if (this.storageHasKey(FEATURE_LIST)) {
this.set('featureList', this.getExtState(FEATURE_LIST));
if (this.storageHasKey(FEATURE_STATE)) {
this.saveState('featureState', this.getExtState(FEATURE_STATE));
} else {
if (FeatureMachine != null) {
this.saveState('featureState', FeatureMachine.initialState);
this.saveExtState(FEATURE_STATE, this.get('featureState'));
}
}
this.buildFeatureMachine();
}
},
restartGuide() {
let storage = this.storage();
// empty storage
[
TUTORIAL_STATE,
FEATURE_LIST,
FEATURE_STATE,
COMPLETED_FEATURES,
COMPONENT_STATE,
RESUME_URL,
RESUME_ROUTE,
].forEach(key => storage.removeItem(key));
// reset wizard state
this.setProperties(DEFAULTS);
// restart machines from blank state
this.initializeMachines();
// progress machine to 'active.select'
this.transitionTutorialMachine('idle', 'AUTH');
},
saveState(stateType, state) {
if (state.value) {
state = state.value;
}
let stateKey = '';
while (Ember.typeOf(state) === 'object') {
let newState = Object.keys(state);
stateKey += newState + '.';
state = state[newState];
}
stateKey += state;
this.set(stateType, stateKey);
},
transitionTutorialMachine(currentState, event, extendedState) {
if (extendedState) {
this.set('componentState', extendedState);
this.saveExtState(COMPONENT_STATE, extendedState);
}
let { actions, value } = TutorialMachine.transition(currentState, event);
this.saveState('currentState', value);
this.saveExtState(TUTORIAL_STATE, this.get('currentState'));
this.executeActions(actions, event, 'tutorial');
},
transitionFeatureMachine(currentState, event, extendedState) {
if (!FeatureMachine || !this.get('currentState').includes('active')) {
return;
}
if (extendedState) {
this.set('componentState', extendedState);
this.saveExtState(COMPONENT_STATE, extendedState);
}
let { actions, value } = FeatureMachine.transition(currentState, event, this.get('componentState'));
this.saveState('featureState', value);
this.saveExtState(FEATURE_STATE, value);
this.executeActions(actions, event, 'feature');
// if all features were completed, the FeatureMachine gets nulled
// out and won't exist here as there is no next step
if (FeatureMachine) {
let next;
if (this.get('currentMachine') === 'secrets' && value === 'display') {
next = FeatureMachine.transition(value, 'REPEAT', this.get('componentState'));
} else {
next = FeatureMachine.transition(value, 'CONTINUE', this.get('componentState'));
}
this.saveState('nextStep', next.value);
}
},
saveExtState(key, value) {
this.storage().setItem(key, value);
},
getExtState(key) {
return this.storage().getItem(key);
},
storageHasKey(key) {
return Boolean(this.getExtState(key));
},
executeActions(actions, event, machineType) {
let transitionURL;
let expectedRouteName;
let router = this.get('router');
for (let action of actions) {
let type = action;
if (action.type) {
type = action.type;
}
switch (type) {
case 'render':
this.set(`${action.level}Component`, action.component);
break;
case 'routeTransition':
expectedRouteName = action.params[0];
transitionURL = router.urlFor(...action.params).replace(/^\/ui/, '');
Ember.run.next(() => {
router.transitionTo(...action.params);
});
break;
case 'saveFeatures':
this.saveFeatures(event.features);
break;
case 'completeFeature':
this.completeFeature();
break;
case 'handleDismissed':
this.handleDismissed();
break;
case 'handlePaused':
this.handlePaused();
return;
case 'handleResume':
this.handleResume();
break;
case 'showTutorialWhenAuthenticated':
this.set('showWhenUnauthenticated', false);
break;
case 'showTutorialAlways':
this.set('showWhenUnauthenticated', true);
break;
case 'continueFeature':
this.transitionFeatureMachine(this.get('featureState'), 'CONTINUE', this.get('componentState'));
break;
default:
break;
}
}
if (machineType === 'tutorial') {
return;
}
// if we're transitioning in the actions, we want that url,
// else we want the URL we land on in didTransition in the
// application route - we'll notify the application route to
// update the route
if (transitionURL) {
this.set('expectedURL', transitionURL);
this.set('expectedRouteName', expectedRouteName);
this.set('setURLAfterTransition', false);
} else {
this.set('setURLAfterTransition', true);
}
},
handlePaused() {
let expected = this.get('expectedURL');
if (expected) {
this.saveExtState(RESUME_URL, this.get('expectedURL'));
this.saveExtState(RESUME_ROUTE, this.get('expectedRouteName'));
}
},
handleResume() {
let resumeURL = this.storage().getItem(RESUME_URL);
if (!resumeURL) {
return;
}
this.get('router').transitionTo(resumeURL).followRedirects().then(() => {
this.set('expectedRouteName', this.storage().getItem(RESUME_ROUTE));
this.set('expectedURL', resumeURL);
this.initializeMachines();
this.storage().removeItem(RESUME_URL);
});
},
handleDismissed() {
this.storage().removeItem(FEATURE_STATE);
this.storage().removeItem(FEATURE_LIST);
this.storage().removeItem(COMPONENT_STATE);
},
saveFeatures(features) {
this.set('featureList', features);
this.saveExtState(FEATURE_LIST, this.get('featureList'));
this.buildFeatureMachine();
},
buildFeatureMachine() {
if (this.get('featureList') === null) {
return;
}
this.startFeature();
if (this.storageHasKey(FEATURE_STATE)) {
this.saveState('featureState', this.getExtState(FEATURE_STATE));
}
this.saveExtState(FEATURE_STATE, this.get('featureState'));
let nextFeature =
this.get('featureList').length > 1 ? this.get('featureList').objectAt(1).capitalize() : 'Finish';
this.set('nextFeature', nextFeature);
let next;
if (this.get('currentMachine') === 'secrets' && this.get('featureState') === 'display') {
next = FeatureMachine.transition(this.get('featureState'), 'REPEAT', this.get('componentState'));
} else {
next = FeatureMachine.transition(this.get('featureState'), 'CONTINUE', this.get('componentState'));
}
this.saveState('nextStep', next.value);
let stateNodes = FeatureMachine.getStateNodes(this.get('featureState'));
this.executeActions(stateNodes.reduce((acc, node) => acc.concat(node.onEntry), []), null, 'feature');
},
startFeature() {
const FeatureMachineConfig = MACHINES[this.get('featureList').objectAt(0)];
FeatureMachine = Machine(FeatureMachineConfig);
this.set('currentMachine', this.get('featureList').objectAt(0));
this.saveState('featureState', FeatureMachine.initialState);
},
completeFeature() {
let features = this.get('featureList');
let done = features.shift();
if (!this.getExtState(COMPLETED_FEATURES)) {
let completed = [];
completed.push(done);
this.saveExtState(COMPLETED_FEATURES, completed);
} else {
this.saveExtState(COMPLETED_FEATURES, this.getExtState(COMPLETED_FEATURES).toArray().addObject(done));
}
this.saveExtState(FEATURE_LIST, features.length ? features : null);
this.storage().removeItem(FEATURE_STATE);
if (features.length > 0) {
this.buildFeatureMachine();
} else {
this.storage().removeItem(FEATURE_LIST);
FeatureMachine = null;
this.transitionTutorialMachine(this.get('currentState'), 'DONE');
}
},
storage() {
return getStorage();
},
});

View File

@ -0,0 +1,61 @@
.box-radio-container {
display: flex;
flex-wrap: wrap;
}
.title.box-radio-header {
font-size: $size-6;
color: $grey;
margin: $size-7 0 0 0;
}
.box-radio {
box-sizing: border-box;
flex-basis: 7rem;
width: 7rem;
height: 7.5rem;
padding: $size-10 $size-6 $size-10;
flex-direction: column;
justify-content: space-between;
align-items: center;
display: flex;
border-radius: $radius;
box-shadow: $box-shadow;
text-align: center;
color: $grey;
font-weight: $font-weight-semibold;
line-height: 1;
margin: $size-6 $size-3 $size-6 0;
font-size: 12px;
transition: box-shadow ease-in-out $speed;
will-change: box-shadow;
&.is-selected {
box-shadow: 0 0 0 1px $grey-light, $box-shadow-middle;
}
input[type=radio].radio {
position: absolute;
z-index: 1;
opacity: 0;
}
input[type=radio].radio + label {
border: 1px solid $grey-light;
border-radius: 50%;
cursor: pointer;
display: block;
margin: 1rem auto 0;
height: 1rem;
width: 1rem;
flex-shrink: 0;
flex-grow: 0;
}
input[type=radio].radio:checked + label {
background: $blue;
border: 1px solid $blue;
box-shadow: inset 0 0 0 0.15rem $white;
}
input[type=radio].radio:focus + label {
box-shadow: 0 0 10px 1px rgba($blue, 0.4), inset 0 0 0 0.15rem $white;
}
}

View File

@ -0,0 +1,5 @@
.doc-link {
color: $link;
text-decoration: none;
font-weight: $font-weight-semibold;
}

View File

@ -0,0 +1,44 @@
.feature-header {
font-size: $size-6;
font-weight: $font-weight-semibold;
color: $grey;
}
.feature-box {
box-shadow: $box-shadow;
border-radius: $radius;
padding: $size-8;
margin: $size-8 0;
&.is-active {
box-shadow: 0 0 0 1px $grey-light;
}
}
.feature-box label {
font-weight: $font-weight-semibold;
padding-left: $size-10;
&::before {
top: 3px;
}
&::after {
top: 5px;
}
}
.feature-steps {
font-size: $size-8;
color: $grey;
line-height: 1.5;
margin-left: $size-3;
margin-top: $size-10;
li::before {
// bullet
content: '\2022';
position: relative;
right: $size-11;
}
}

View File

@ -21,4 +21,8 @@
.breadcrumb + .level .title {
margin-top: $size-4;
}
.title .icon {
height: auto;
width: auto;
}
}

View File

@ -0,0 +1,144 @@
.ui-wizard-container {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.ui-wizard-container .app-content {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.ui-wizard-container .app-content.wizard-open {
padding-right: 324px;
@include until($tablet) {
padding-right: 0;
padding-bottom: 50vh;
}
}
.ui-wizard {
z-index: 300;
padding: $size-5;
width: 300px;
background: $white;
box-shadow: $box-shadow, $box-shadow-highest;
position: fixed;
right: $size-8;
bottom: $size-8;
top: calc(3.5rem + #{$size-8});
overflow: auto;
p {
line-height: 1.2;
}
.dismiss-collapsed {
position: absolute;
top: $size-8;
right: $size-8;
color: $grey;
z-index: 30;
}
@include until($tablet) {
box-shadow: $box-shadow, 0 0 20px rgba($black, 0.24);
bottom: 0;
left: 0;
right: 0;
top: 50%;
width: auto;
}
.doc-link {
margin-top: $size-5;
display: block;
}
pre code {
background: $ui-gray-050;
margin: $size-8 0;
}
}
.wizard-header {
margin-bottom: $size-5;
position: relative;
.icon {
margin-right: $size-11;
vertical-align: -0.33rem;
}
}
.wizard-dismiss-menu {
position: absolute;
right: $size-6;
top: $size-6;
z-index: 10;
}
.ui-wizard.collapsed {
color: $white;
background: $black;
bottom: auto;
box-shadow: $box-shadow-middle;
height: auto;
min-height: 0;
padding-bottom: $size-11;
position: fixed;
right: $size-8;
top: calc(3.5rem + #{$size-8});
@include until($tablet) {
box-shadow: $box-shadow, 0 0 20px rgba($black, 0.24);
bottom: 0;
left: 0;
right: 0;
top: auto;
width: auto;
}
.title {
color: $white;
}
.wizard-header {
margin-bottom: $size-10;
}
}
.wizard-divider-box {
background: none;
box-shadow: none;
margin: $size-8 0 0;
padding: 0 $size-8;
border-top: solid 1px $white;
border-image: $dark-vault-gradient 1;
button {
font-size: $size-7;
font-weight: $font-weight-semibold;
}
}
.wizard-section .title .icon {
height: auto;
margin-right: $size-11;
width: auto;
}
.wizard-section:last-of-type {
margin-bottom: $size-5;
}
.wizard-section button:not(:last-of-type) {
margin-bottom: $size-10;
}
.wizard-details {
padding-top: $size-4;
margin-top: $size-4;
border-top: 1px solid $grey-light;
}

View File

@ -44,11 +44,14 @@
@import "./components/auth-form";
@import "./components/b64-toggle";
@import "./components/box-label";
@import "./components/box-radio";
@import "./components/codemirror";
@import "./components/confirm";
@import "./components/console-ui-panel";
@import "./components/control-group";
@import "./components/doc-link";
@import "./components/env-banner";
@import "./components/features-selection";
@import "./components/form-section";
@import "./components/global-flash";
@import "./components/hover-copy-button";
@ -77,4 +80,5 @@
@import "./components/tool-tip";
@import "./components/unseal-warning";
@import "./components/upgrade-overlay";
@import "./components/ui-wizard";
@import "./components/vault-loading";

View File

@ -184,6 +184,7 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
&,
&:first-child:last-child {
margin-left: -$size-10;
margin-right: $size-11;
}
}
}
@ -214,3 +215,16 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
width: auto;
margin: 0 !important;
}
.button.next-feature-step {
width: 100%;
text-align: left;
background: $white;
color: $blue;
box-shadow: none;
display: block;
border: 1px solid $grey-light;
border-radius: $radius;
height: auto;
padding: $size-8;
}

View File

@ -184,7 +184,8 @@ label {
}
}
.select:not(.is-multiple)::after {
.select:not(.is-multiple)::after,
.select:not(.is-multiple)::before {
border-color: $black;
border-width: 2px;
margin-top: 0;
@ -192,7 +193,6 @@ label {
}
.select:not(.is-multiple)::before {
@extend .select:not(.is-multiple)::after;
transform: translateY(-75%) rotate(135deg);
z-index: 5;
}

View File

@ -46,3 +46,7 @@ input::-webkit-inner-spin-button {
-ms-user-select: text; /* IE 10+ */
user-select: text;
}
.link-plain {
text-decoration: none;
}

View File

@ -1,3 +1,4 @@
$dark-vault-gradient: linear-gradient(to right, $vault-gray-dark, $vault-gray);
.has-dark-vault-gradient {
background: linear-gradient(to right, $vault-gray-dark, $vault-gray);
background: $dark-vault-gradient;
}

View File

@ -46,6 +46,11 @@
</li>
{{/if}}
{{/if}}
<li class="action">
<button type="button" class="button link" onclick={{action "restartGuide"}}>
Restart guide
</button>
</li>
<li class="action">
{{#link-to "vault.cluster.logout" activeClusterName id="logout"}}
Sign out

View File

@ -73,6 +73,7 @@
}}
{{else if (eq attr.options.editType 'ttl')}}
{{ttl-picker
data-test-input=attr.name
initialValue=(or (get model valuePath) attr.options.defaultValue)
labelText=labelString
warning=attr.options.warning

View File

@ -1,10 +1,21 @@
<PageHeader as |p|>
<p.levelLeft>
<h1 class="title is-3" data-test-mount-form-header=true>
{{#if (eq mountType "auth")}}
Enable an authentication method
{{#if showConfig}}
{{#with (find-by 'type' mountModel.type mountTypes) as |typeInfo|}}
<ICon @size=28 @glyph={{concat "enable/" (or typeInfo.glyph typeInfo.type)}} />
{{#if (eq mountType "auth")}}
Enable {{typeInfo.displayName}} authentication method
{{else}}
Enable {{typeInfo.displayName}} secrets engine
{{/if}}
{{/with}}
{{else}}
Enable a secrets engine
{{#if (eq mountType "auth")}}
Enable an authentication method
{{else}}
Enable a secrets engine
{{/if}}
{{/if}}
</h1>
</p.levelLeft>
@ -13,19 +24,73 @@
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="enable" @noun={{if (eq mountType "auth") "auth method" "secret engine"}} />
{{message-error model=mountModel}}
{{form-field-groups model=mountModel onChange=(action "onTypeChange") renderGroup="default"}}
{{#if mountModel.authConfigs.firstObject}}
{{form-field-groups model=mountModel.authConfigs.firstObject}}
{{#if showConfig}}
{{form-field-groups model=mountModel onChange=(action "onTypeChange") renderGroup="default"}}
{{#if mountModel.authConfigs.firstObject}}
{{form-field-groups model=mountModel.authConfigs.firstObject}}
{{/if}}
{{form-field-groups model=mountModel onChange=(action "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 mountTypes) as |type|}}
<label
for={{type.type}}
class="box-radio {{if (eq mountModel.type type.type) "is-selected" }}"
data-test-mount-type-radio
data-test-mount-type={{type.type}}
>
<ICon @size=36 @excludeIconClass={{true}} @glyph={{concat "enable/" (or type.glyph type.type)}} />
{{type.displayName}}
<RadioButton
@value={{type.type}}
@radioClass="radio"
@groupValue={{mountModel.type}}
@changed={{queue (action (mut mountModel.type)) (action "onTypeChange" "type")}}
@name="mount-type"
@radioId={{type.type}}
/>
<label for={{type.type}}></label>
</label>
{{/each}}
</div>
{{/each}}
{{/if}}
{{form-field-groups model=mountModel onChange=(action "onTypeChange") renderGroup="Method Options"}}
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<button type="submit" data-test-mount-submit=true class="button is-primary {{if mountBackend.isRunning 'loading'}}" disabled={{mountBackend.isRunning}}>
{{#if (eq mountType "auth")}}
Enable Method
{{else}}
Enable Engine
{{/if}}
</button>
{{#if showConfig}}
<div class="control">
<button type="submit" data-test-mount-submit=true class="button is-primary {{if mountBackend.isRunning 'loading'}}" disabled={{mountBackend.isRunning}}>
{{#if (eq mountType "auth")}}
Enable Method
{{else}}
Enable Engine
{{/if}}
</button>
</div>
<div class="control">
<button
data-test-mount-back
type="button"
class="button"
onclick={{action "toggleShowConfig" false}}
>
Back
</button>
</div>
{{else}}
<button
data-test-mount-next
type="button"
class="button is-primary"
onclick={{action "toggleShowConfig" true}}
disabled={{not mountModel.type}}
>
Next
</button>
{{/if}}
</div>
</form>

View File

@ -12,19 +12,21 @@
{{/if}}
</Nav.items>
</NavHeader>
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
<div class="columns is-centered is-gapless is-fullwidth">
<div class="column is-4-desktop is-6-tablet">
<div class="splash-page-header">
{{yield (hash header=(component 'splash-page/splash-header'))}}
<UiWizard>
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
<div class="columns is-centered is-gapless is-fullwidth">
<div class="column is-4-desktop is-6-tablet">
<div class="splash-page-header">
{{yield (hash header=(component 'splash-page/splash-header'))}}
</div>
<div class="splash-page-sub-header">
{{yield (hash sub-header=(component 'splash-page/splash-header'))}}
</div>
<div class="login-form box is-paddingless is-relative">
{{yield (hash content=(component 'splash-page/splash-content'))}}
</div>
{{yield (hash footer=(component 'splash-page/splash-content')) }}
</div>
<div class="splash-page-sub-header">
{{yield (hash sub-header=(component 'splash-page/splash-header'))}}
</div>
<div class="login-form box is-paddingless is-relative">
{{yield (hash content=(component 'splash-page/splash-content'))}}
</div>
{{yield (hash footer=(component 'splash-page/splash-content')) }}
</div>
</div>
</div>
</UiWizard>

View File

@ -0,0 +1,19 @@
<div class="app-content {{if (and shouldRender featureComponent) "wizard-open"}}">
{{yield}}
</div>
{{#component
(if shouldRender tutorialComponent)
onAdvance=(action "advanceWizard")
onDismiss=(action "dismissWizard")
}}
{{component
featureComponent
componentState=componentState
nextFeature=nextFeature
nextStep=nextStep
onDone=(action "finishFeature")
onRepeat=(action "repeatStep")
onReset=(action "resetFeature")
onAdvance=(action "advanceFeature")
}}
{{/component}}

View File

@ -0,0 +1,17 @@
<PopupMenu @class="wizard-dismiss-menu">
<nav class="menu">
<ul class="menu-list">
<li class="action">
<button type="button" class="button link" onclick={{action "dismissWizard"}}>
Dismiss
</button>
</li>
</ul>
</nav>
</PopupMenu>
<div class="wizard-header">
<h1 class="title is-5">
<ICon @glyph={{glyph}} @size="21" /> {{headerText}}
</h1>
</div>
{{yield}}

View File

@ -0,0 +1,14 @@
<div class="wizard-section {{class}}">
<h2 class="title is-6">
{{#if headerIcon}}
<ICon @glyph={{headerIcon}} @size=24 />
{{/if}}
{{headerText}}
</h2>
{{yield}}
{{#if docText}}
<DocLink @path={{docPath}}>
<ICon @glyph='learn' @size=16 /> {{docText}}
</DocLink>
{{/if}}
</div>

View File

@ -0,0 +1,11 @@
<WizardSection
@headerText="Active Directory"
@headerIcon="enable/azure"
@docText="Docs: Active Directory Secrets"
@docPath="/docs/secrets/ad/index.html"
>
<p>
The AD secrets engine rotates AD passwords dynamically, and is designed for
a high-load environment where many instances may be accessing a shared password simultaneously.
</p>
</WizardSection>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="AppRole"
@headerIcon="enable/approle"
@docText="Docs: AppRole Authentication"
@docPath="/docs/auth/approle.html"
>
<p>
The approle auth method allows machines or apps to authenticate with Vault-defined roles. The open design of AppRole enables a varied set of workflows and configurations to handle large numbers of apps. This auth method is oriented to automated workflows (machines and services), and is less useful for human operators.
</p>
</WizardSection>

View File

@ -0,0 +1,20 @@
<WizardSection
@headerText="Auth Method Details"
@docText="Docs: Authentication Methods"
@docPath="/docs/auth/index.html"
>
<p>
Fantastic! Now you're ready to use your new {{mountName}} auth method!
</p>
</WizardSection>
<WizardSection
@headerText="Want to start again or move on?"
@class="wizard-details"
>
<button type="button" class="button next-feature-step" {{action onReset}}>
Enable another auth method <ICon @glyph="loop" @size=13 @class="is-pulled-right" />
</button>
<button type="button" class="button next-feature-step" {{action onAdvance}}>
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
</button>
</WizardSection>

View File

@ -0,0 +1,9 @@
<WizardSection
@headerText="Editing Your Auth Method"
@docText="Docs: Authentication Methods"
@docPath="/docs/auth/index.html"
>
<p>
You can update your new auth method configuration here. Click the "View method" link to see its details.
</p>
</WizardSection>

View File

@ -0,0 +1,9 @@
<WizardSection
@headerText="Entering Auth Method details"
@docText="Docs: Authentication Methods"
@docPath="/docs/auth/index.html"
>
<p>
Great! Now you can customize this method with a name and description that makes sense for your team, and fill out any options that are specific to this method.
</p>
</WizardSection>

View File

@ -0,0 +1,9 @@
<WizardSection
@headerText="Enabling an Auth Method"
@docText="Docs: Authentication Methods"
@docPath="/docs/auth/index.html"
>
<p>
Controlling who can see your secrets is important. Let's set up a an authentication method for you and your team to use. Don't worry, you can add more methods later. Choose an authentication method to get started.
</p>
</WizardSection>

View File

@ -0,0 +1,9 @@
<WizardSection
@headerText="Auth Method List"
@docText="Docs: Authentication Methods"
@docPath="/docs/auth/index.html"
>
<p>
Awesome! Now you can see your new auth method in the list. Click the ellipsis menu for your method and then click "View Configuration" to see its details.
</p>
</WizardSection>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="AWS"
@headerIcon="enable/aws"
@docText="Docs: AWS Secrets"
@docPath="/docs/secrets/aws/index.html"
>
<p>
The AWS secrets engine generates AWS access credentials dynamically based on IAM policies. This generally makes working with AWS IAM easier, since it does not involve clicking in the web UI. Additionally, the process is codified and mapped to internal auth methods (such as LDAP). The AWS IAM credentials are time-based and are automatically revoked when the Vault lease expires.
</p>
</WizardSection>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="AWS"
@headerIcon="enable/aws"
@docText="Docs: AWS Authentication"
@docPath="/docs/auth/aws.html"
>
<p>
The AWS auth method provides an automated mechanism to retrieve a Vault token for AWS EC2 instances and IAM principals. Unlike most Vault auth methods, this method does not require manual first-deploying, or provisioning security-sensitive credentials (tokens, username/password, client certificates, etc), by operators under many circumstances.
</p>
</WizardSection>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="Azure"
@headerIcon="enable/azure"
@docText="Docs: Azure Authentication"
@docPath="/docs/auth/azure.html"
>
<p>
The Azure auth method allows authentication against Vault using Azure Active Directory credentials.
</p>
</WizardSection>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="TLS Certificates"
@headerIcon="enable/cert"
@docText="Docs: TLS Certificates Authentication"
@docPath="/docs/auth/cert.html"
>
<p>
The TLS Certificates auth method allows authentication using SSL/TLS client certificates which are either signed by a CA or self-signed. CA certificates are associated with a role.
</p>
</WizardSection>

View File

@ -0,0 +1,9 @@
<WizardSection
@headerText="Cubbyhole"
@docText="Docs: Cubbyhole Secrets"
@docPath="/docs/secrets/cubbyhole/index.html"
>
<p>
The cubbyhole secrets engine is used to store arbitrary secrets within the configured physical storage for Vault namespaced to a token. In cubbyhole, paths are scoped per token. No token can access another token's cubbyhole. When the token expires, its cubbyhole is destroyed.
</p>
</WizardSection>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="Consul"
@headerIcon="enable/consul"
@docText="Docs: Consul Secrets"
@docPath="/docs/secrets/consul/index.html"
>
<p>
The Consul secrets engine generates Consul API tokens dynamically based on Consul ACL policies.
</p>
</WizardSection>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="Databases"
@headerIcon="enable/database"
@docText="Docs: Database Secrets"
@docPath="/docs/secrets/databases/index.html"
>
<p>
The database secrets engine generates database credentials dynamically based on configured roles.
</p>
</WizardSection>

View File

@ -0,0 +1,41 @@
<WizardContent @headerText="Vault Web UI" @glyph="tour">
<h2 class="title is-6">
Choosing where to go
</h2>
<p>You did it! You now have access to your Vault and can start entering your data. We can help you get started with any of the options below</p>
{{#if (or (has-feature "Performance Replication") (has-feature "DR Replication")) }}
{{/if}}
<h3 class="feature-header">Walk me through setting up:</h3>
<form id="features-form" class="feature-selection" {{action "saveFeatures" on="submit"}}>
{{#each allFeatures as |feature|}}
{{#if feature.show}}
<div class="feature-box {{if feature.selected 'is-active'}}">
<div class="b-checkbox">
<input
id="feature-{{feature.key}}"
type="checkbox"
class="styled"
checked={{feature.selected}}
onchange={{action (mut feature.selected) value="target.checked"}}
/>
<label for="feature-{{feature.key}}">{{feature.name}}</label>
<button type="button" class="button is-ghost icon is-pulled-right" onclick={{action (toggle (concat feature.key "-isOpen") this)}}>
<ICon
@glyph={{if (get this (concat feature.key "-isOpen")) "chevron-up" "chevron-down"}}
@class="has-text-grey auto-width is-paddingless is-flex-column"
/>
</button>
</div>
{{#if (get this (concat feature.key "-isOpen"))}}
<ul class="feature-steps">
{{#each feature.steps as |step|}}
<li>{{step}}</li>
{{/each}}
</ul>
{{/if}}
</div>
{{/if}}
{{/each}}
<button type="submit" class="button is-primary">Start</button>
</form>
</WizardContent>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="Google Cloud"
@headerIcon="enable/gcp"
@docText="Docs: Google Cloud Secrets"
@docPath="/docs/secrets/gcp/index.html"
>
<p>
The Google Cloud Vault secrets engine dynamically generates Google Cloud service account keys and OAuth tokens based on IAM policies. This enables users to gain access to Google Cloud resources without needing to create or manage a dedicated service account.
</p>
</WizardSection>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="Google Cloud"
@headerIcon="enable/gcp"
@docText="Docs: Google Cloud Authentication"
@docPath="/docs/auth/gcp.html"
>
<p>
The GCP auth method allows authentication against Vault using Google credentials.
</p>
</WizardSection>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="GitHub"
@headerIcon="enable/github"
@docText="Docs: GitHub Authentication"
@docPath="/docs/auth/github.html"
>
<p>
The Github auth method can be used to authenticate with Vault using a GitHub personal access token.
</p>
</WizardSection>

View File

@ -0,0 +1,15 @@
<WizardContent @headerText="Authentication" @glyph="tour">
<WizardSection
@headerText="Authenticate to Vault"
@docText="Learn: Initialization"
@docPath="/docs/concepts/tokens.html"
>
<p>
Vault is unsealed, but we still need to authenticate using the Initial
Root Token that was generated. We recommend setting up an Authentication
Method such as Username & Password for regular use, and only using a root
token for initial setup or for emergencies.
</p>
</WizardSection>
</WizardContent>

View File

@ -0,0 +1,11 @@
<WizardContent @headerText="Initialization" @glyph="tour">
<WizardSection
@headerText="Saving your keys"
@docText="Learn: Initialization"
@docPath="/intro/getting-started/deploy.html#initializing-the-vault">
<p>Now that Vault is initialized, you'll want to save your root token and
master key portions in a safe place. Distribute your keys to responsible
people on your team. If these keys are lost, you may not be able to access
your data again. Keep them safe!</p>
</WizardSection>
</WizardContent>

View File

@ -0,0 +1,18 @@
<WizardContent @headerText="Initialization" @glyph="tour">
<WizardSection
@headerText="Setting up your master keys"
@docText="Learn: Initialization"
@docPath="/intro/getting-started/deploy.html#initializing-the-vault"
>
<p>
This is the very first step of setting
a Vault server. Vault comes with an important
security feature called "seal", which lets you
shut down and secure your Vault installation if
there is a security breach. This is the default
state of Vault, so it is currently sealed since
it was just installed. To unseal the vault, you will
need to provide the key(s) that you generate here.
</p>
</WizardSection>
</WizardContent>

View File

@ -0,0 +1,21 @@
<WizardContent @headerText="Initialization" @glyph="tour">
<WizardSection
@headerText="Unsealing your vault"
@docText="Learn: Initialization"
@docPath="/intro/getting-started/deploy.html#initializing-the-vault"
>
<p>
Now we will provide the {{pluralize componentState.threshold 'key'}} that
you copied or downloaded to unseal the vault so that we can get started
using it. You'll need {{pluralize componentState.threshold 'key'}} total,
and {{#with (pluralize componentState.progress 'key' without-count=true) as |word|}}
{{if (eq word 'key')
(concat componentState.progress " " word " has ")
(concat componentState.progress " " word " have ")
}}
{{/with}} already been provided.
Please provide
{{pluralize (dec componentState.progress componentState.threshold) 'more key'}} to unseal.
</p>
</WizardSection>
</WizardContent>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="Kubernetes"
@headerIcon="enable/kubernetes"
@docText="Docs: Kubernetes Authentication"
@docPath="/docs/auth/kubernetes.html"
>
<p>
The Kubernetes auth method can be used to authenticate with Vault using a Kubernetes Service Account Token. This method of authentication makes it easy to introduce a Vault token into a Kubernetes Pod.
</p>
</WizardSection>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="Key/Value"
@headerIcon="enable/kv"
@docText="Docs: Key/Value Secrets"
@docPath="/docs/secrets/kv/index.html"
>
<p>
The kv secrets engine is used to store arbitrary secrets within the configured physical storage for Vault. This backend can be run in one of two modes. It can be a generic Key-Value store that stores one value for a key. Versioning can be enabled and a configurable number of versions for each key will be stored.
</p>
</WizardSection>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="LDAP"
@headerIcon="enable/ldap"
@docText="Docs: LDAP Authentication"
@docPath="/docs/auth/ldap.html"
>
<p>
The LDAP auth method allows authentication using an existing LDAP server and user/password credentials. This allows Vault to be integrated into environments using LDAP without duplicating the user/pass configuration in multiple places.
</p>
</WizardSection>

View File

@ -0,0 +1,23 @@
<WizardContent @headerText={{capitalize currentMachine}} @glyph="tour">
{{component
stepComponent
mountSubtype=mountSubtype
mountName=mountName
actionText=actionText
nextFeature=nextFeature
nextStep=nextStep
needsEncryption=needsEncryption
isSupported=isSupported
onDone=onDone
onAdvance=onAdvance
onRepeat=onRepeat
onReset=onReset
class="wizard-step"
}}
{{component
detailsComponent
onAdvance=onAdvance
onRepeat=onRepeat
class="wizard-details"
}}
</WizardContent>

View File

@ -0,0 +1,10 @@
<WizardSection
@headerText="Nomad"
@headerIcon="enable/nomad"
@docText="Docs: Nomad Secrets"
@docPath="/docs/secrets/nomad/index.html"
>
<p>
The Nomad secret backend for Vault generates Nomad API tokens dynamically based on pre-existing Nomad ACL policies.
</p>
</WizardSection>

Some files were not shown because too many files have changed in this diff Show More