Merge branch 'master-oss' into 1.0-beta-oss

This commit is contained in:
Jeff Mitchell 2018-10-19 09:25:17 -04:00
commit db2bdbbebd
94 changed files with 1946 additions and 707 deletions

View file

@ -14,15 +14,22 @@ IMPROVEMENTS:
* auth/token: New tokens are salted using SHA2-256 HMAC instead of SHA1 hash
* secret/database: Allow Cassandra user to be non-superuser so long as it has
role creation permissions [GH-5402]
* secret/radius: Allow setting the NAS Identifier value in the generated
packet [GH-5465]
* ui: Allow viewing and updating Vault license via the UI
* ui: Onboarding will now display your progress through the chosen tutorials
BUG FIXES:
* agent: Fix potential hang during agent shutdown [GH-5026]
* core: Fix generate-root operations requiring empty `otp` to be provided
instead of an empty body [GH-5495]
* identity: Remove lookup check during alias removal from entity [GH-5524]
* secret/pki: Fix regression in 0.11.2+ causing the NotBefore value of
generated certificates to be set to the Unix epoch if the role value was not
set, instead of using the default of 30 seconds [GH-5481]
* storage/mysql: Use `varbinary` instead of `varchar` when creating HA tables
[GH-5529]
## 0.11.3 (October 8th, 2018)

View file

@ -46,6 +46,11 @@ func pathConfig(b *backend) *framework.Path {
Default: 10,
Description: "RADIUS NAS port field (default: 10)",
},
"nas_identifier": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "RADIUS NAS Identifier field (optional)",
},
},
ExistenceCheck: b.configExistenceCheck,
@ -110,6 +115,7 @@ func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *f
"dial_timeout": cfg.DialTimeout,
"read_timeout": cfg.ReadTimeout,
"nas_port": cfg.NasPort,
"nas_identifier": cfg.NasIdentifier,
},
}
return resp, nil
@ -190,6 +196,13 @@ func (b *backend) pathConfigCreateUpdate(ctx context.Context, req *logical.Reque
cfg.NasPort = d.Get("nas_port").(int)
}
nasIdentifier, ok := d.GetOk("nas_identifier")
if ok {
cfg.NasIdentifier = nasIdentifier.(string)
} else if req.Operation == logical.CreateOperation {
cfg.NasIdentifier = d.Get("nas_identifier").(string)
}
entry, err := logical.StorageEntryJSON("config", cfg)
if err != nil {
return nil, err
@ -209,6 +222,7 @@ type ConfigEntry struct {
DialTimeout int `json:"dial_timeout" structs:"dial_timeout" mapstructure:"dial_timeout"`
ReadTimeout int `json:"read_timeout" structs:"read_timeout" mapstructure:"read_timeout"`
NasPort int `json:"nas_port" structs:"nas_port" mapstructure:"nas_port"`
NasIdentifier string `json:"nas_identifier" structs:"nas_identifier" mapstructure:"nas_identifier"`
}
const pathConfigHelpSyn = `

View file

@ -144,6 +144,9 @@ func (b *backend) RadiusLogin(ctx context.Context, req *logical.Request, usernam
packet := radius.New(radius.CodeAccessRequest, []byte(cfg.Secret))
UserName_SetString(packet, username)
UserPassword_SetString(packet, password)
if cfg.NasIdentifier != "" {
NASIdentifier_AddString(packet, cfg.NasIdentifier)
}
packet.Add(5, radius.NewInteger(uint32(cfg.NasPort)))
client := radius.Client{

View file

@ -150,7 +150,7 @@ func NewMySQLBackend(conf map[string]string, logger log.Logger) (physical.Backen
// Create the required table if it doesn't exists.
if !lockTableExist {
create_query := "CREATE TABLE IF NOT EXISTS " + dbLockTable +
" (node_job varchar(512), current_leader varchar(512), PRIMARY KEY (node_job))"
" (node_job varbinary(512), current_leader varbinary(512), PRIMARY KEY (node_job))"
if _, err := db.Exec(create_query); err != nil {
return nil, errwrap.Wrapf("failed to create mysql table: {{err}}", err)
}

View file

@ -18,6 +18,9 @@ export default ApplicationAdapter.extend({
queryRecord(store, type, query) {
const { id } = query;
if (!id) {
return;
}
return this.findRecord(store, type, id).then(resp => {
resp.path = id;
return resp;

View file

@ -0,0 +1,72 @@
/* eslint-disable */
import { isEmpty } from '@ember/utils';
import { get } from '@ember/object';
import ApplicationAdapter from './application';
import DS from 'ember-data';
export default ApplicationAdapter.extend({
namespace: 'v1',
_url(backend, id, infix = 'data') {
let url = `${this.buildURL()}/${backend}/${infix}/`;
if (!isEmpty(id)) {
url = url + id;
}
return url;
},
urlForFindRecord(id) {
let [backend, path, version] = JSON.parse(id);
return this._url(backend, path) + `?version=${version}`;
},
findRecord() {
return this._super(...arguments).catch(errorOrModel => {
// if it's a real 404, this will be an error, if not
// it will be the body of a deleted / destroyed version
if (errorOrModel instanceof DS.AdapterError) {
throw errorOrModel;
}
return errorOrModel;
});
},
urlForCreateRecord(modelName, snapshot) {
let backend = snapshot.belongsTo('secret').belongsTo('engine').id;
let path = snapshot.attr('path');
return this._url(backend, path);
},
createRecord(store, modelName, snapshot) {
let backend = snapshot.belongsTo('secret').belongsTo('engine').id;
let path = snapshot.attr('path');
return this._super(...arguments).then(resp => {
resp.id = JSON.stringify([backend, path, resp.version]);
return resp;
});
},
urlForUpdateRecord(id) {
let [backend, path] = JSON.parse(id);
return this._url(backend, path);
},
v2DeleteOperation(store, id, deleteType = 'delete') {
let [backend, path, version] = JSON.parse(id);
// deleteType should be 'delete', 'destroy', 'undelete'
return this.ajax(this._url(backend, path, deleteType), 'POST', { data: { versions: [version] } }).then(
() => {
let model = store.peekRecord('secret-v2-version', id);
return model && model.reload();
}
);
},
handleResponse(status, headers, payload, requestData) {
// the body of the 404 will have some relevant information
if (status === 404 && get(payload, 'data.metadata')) {
return this._super(200, headers, payload, requestData);
}
return this._super(...arguments);
},
});

View file

@ -1,34 +1,55 @@
/* eslint-disable */
import { isEmpty } from '@ember/utils';
import SecretAdapter from './secret';
import ApplicationAdapter from './application';
export default SecretAdapter.extend({
createOrUpdate(store, type, snapshot) {
const serializer = store.serializerFor(type.modelName);
const data = serializer.serialize(snapshot);
const { id } = snapshot;
return this.ajax(this.urlForSecret(snapshot.attr('backend'), id), 'POST', {
data: { data },
});
},
urlForSecret(backend, id, infix = 'data') {
let url = `${this.buildURL()}/${backend}/${infix}/`;
export default ApplicationAdapter.extend({
namespace: 'v1',
_url(backend, id) {
let url = `${this.buildURL()}/${backend}/metadata/`;
if (!isEmpty(id)) {
url = url + id;
}
return url;
},
fetchByQuery(query, methodCall) {
let { id, backend } = query;
let args = [backend, id];
if (methodCall === 'query') {
args.push('metadata');
}
return this.ajax(this.urlForSecret(...args), 'GET', this.optionsForQuery(id, methodCall)).then(resp => {
// we override query here because the query object has a bunch of client-side
// concerns and we only want to send "list" to the server
query(store, type, query) {
let { backend, id } = query;
return this.ajax(this._url(backend, id), 'GET', { data: { list: true } }).then(resp => {
resp.id = id;
resp.backend = backend;
return resp;
});
},
urlForQueryRecord(query) {
let { id, backend } = query;
return this._url(backend, id);
},
queryRecord(store, type, query) {
let { backend, id } = query;
return this.ajax(this._url(backend, id), 'GET').then(resp => {
resp.id = id;
resp.backend = backend;
return resp;
});
},
detailURL(snapshot) {
let backend = snapshot.belongsTo('engine', { id: true }) || snapshot.attr('engineId');
let { id } = snapshot;
return this._url(backend, id);
},
urlForUpdateRecord(store, type, snapshot) {
return this.detailURL(snapshot);
},
urlForCreateRecord(modelName, snapshot) {
return this.detailURL(snapshot);
},
urlForDeleteRecord(store, type, snapshot) {
return this.detailURL(snapshot);
},
});

View file

@ -47,6 +47,7 @@ export default ApplicationAdapter.extend({
const { id, backend } = query;
return this.ajax(this.urlForSecret(backend, id), 'GET', this.optionsForQuery(id, action)).then(resp => {
resp.id = id;
resp.backend = backend;
return resp;
});
},

View file

@ -0,0 +1,2 @@
import OuterHTML from './outer-html';
export default OuterHTML.extend();

View file

@ -0,0 +1,2 @@
import OuterHTML from './outer-html';
export default OuterHTML.extend();

View file

@ -22,6 +22,9 @@ const GLYPHS_WITH_SVG_TAG = [
'control-lock',
'edition-enterprise',
'edition-oss',
'check-plain',
'check-circle-fill',
'cancel-square-outline',
];
export default Component.extend({

View file

@ -39,7 +39,8 @@ export default Component.extend({
fetchListCapability: task(function*() {
try {
if (this.listCapability) {
this.listCapability.unloadRecord();
yield this.listCapability.reload();
return;
}
let capability = yield this.store.findRecord('capabilities', 'sys/namespaces/');
this.set('listCapability', capability);

View file

@ -163,8 +163,7 @@ export default Component.extend(FocusOnInsertMixin, {
},
actions: {
handleInput: function(event) {
var filter = event.target.value;
handleInput: function(filter) {
this.get('filterDidChange')(filter);
debounce(this, 'filterUpdated', filter, 200);
},
@ -173,14 +172,15 @@ export default Component.extend(FocusOnInsertMixin, {
this.get('filterFocusDidChange')(isFocused);
},
handleKeyPress: function(val, event) {
handleKeyPress: function(event) {
if (event.keyCode === keys.TAB) {
this.onTab(event);
}
},
handleKeyUp: function(val, event) {
handleKeyUp: function(event) {
var keyCode = event.keyCode;
let val = event.target.value;
if (keyCode === keys.ENTER) {
this.onEnter(val);
}

View file

@ -1,12 +1,14 @@
import { or } from '@ember/object/computed';
import { isBlank, isNone } from '@ember/utils';
import $ from 'jquery';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { computed, get } from '@ember/object';
import { computed, set } from '@ember/object';
import { alias } from '@ember/object/computed';
import { task, waitForEvent } from 'ember-concurrency';
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
import keys from 'vault/lib/keycodes';
import KVObject from 'vault/lib/kv-object';
import { maybeQueryRecord } from 'vault/macros/maybe-query-record';
const LIST_ROUTE = 'vault.cluster.secrets.backend.list';
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
@ -15,9 +17,11 @@ const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
export default Component.extend(FocusOnInsertMixin, {
wizard: service(),
router: service(),
store: service(),
// a key model
key: null,
model: null,
// a value to pre-fill the key input - this is populated by the corresponding
// 'initialKey' queryParam
@ -44,10 +48,15 @@ export default Component.extend(FocusOnInsertMixin, {
codemirrorString: null,
hasLintError: false,
isV2: false,
init() {
this._super(...arguments);
const secrets = this.get('key.secretData');
let secrets = this.model.secretData;
if (!secrets && this.model.selectedVersion) {
this.set('isV2', true);
secrets = this.model.belongsTo('selectedVersion').value().secretData;
}
const data = KVObject.create({ content: [] }).fromJSON(secrets);
this.set('secretData', data);
this.set('codemirrorString', data.toJSONString());
@ -55,82 +64,152 @@ export default 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.wizard.featureState === 'details' && this.mode === 'create') {
let engine = this.model.backend.includes('kv') ? 'kv' : this.model.backend;
this.wizard.transitionFeatureMachine('details', 'CONTINUE', engine);
}
if (this.get('mode') === 'edit') {
if (this.mode === 'edit') {
this.send('addRow');
}
},
didInsertElement() {
this._super(...arguments);
$(document).on('keyup.keyEdit', this.onEscape.bind(this));
},
willDestroyElement() {
this._super(...arguments);
const key = this.get('key');
if (get(key, 'isError') && !key.isDestroyed) {
key.rollbackAttributes();
waitForKeyUp: task(function*() {
while (true) {
let event = yield waitForEvent(document.body, 'keyup');
this.onEscape(event);
}
$(document).off('keyup.keyEdit');
},
})
.on('didInsertElement')
.cancelOn('willDestroyElement'),
partialName: computed('mode', function() {
return `partials/secret-form-${this.get('mode')}`;
return `partials/secret-form-${this.mode}`;
}),
showPrefix: or('key.initialParentKey', 'key.parentKey'),
updatePath: maybeQueryRecord(
'capabilities',
context => {
if (context.mode === 'create') {
return;
}
let backend = context.isV2 ? context.get('model.engine.id') : context.model.backend;
let id = context.model.id;
let path = context.isV2 ? `${backend}/data/${id}` : `${backend}/${id}`;
return {
id: path,
};
},
'isV2',
'model',
'model.id',
'mode'
),
canDelete: alias('updatePath.canDelete'),
canEdit: alias('updatePath.canUpdate'),
requestInFlight: or('key.isLoading', 'key.isReloading', 'key.isSaving'),
v2UpdatePath: maybeQueryRecord(
'capabilities',
context => {
if (context.mode === 'create' || context.isV2 === false) {
return;
}
let backend = context.get('model.engine.id');
let id = context.model.id;
return {
id: `${backend}/metadata/${id}`,
};
},
'isV2',
'model',
'model.id',
'mode'
),
canEditV2Secret: alias('v2UpdatePath.canUpdate'),
deleteVersionPath: maybeQueryRecord(
'capabilities',
context => {
let backend = context.get('model.engine.id');
let id = context.model.id;
return {
id: `${backend}/delete/${id}`,
};
},
'model.id'
),
canDeleteVersion: alias('deleteVersionPath.canUpdate'),
destroyVersionPath: maybeQueryRecord(
'capabilities',
context => {
let backend = context.get('model.engine.id');
let id = context.model.id;
return {
id: `${backend}/destroy/${id}`,
};
},
'model.id'
),
canDestroyVersion: alias('destroyVersionPath.canUpdate'),
undeleteVersionPath: maybeQueryRecord(
'capabilities',
context => {
let backend = context.get('model.engine.id');
let id = context.model.id;
return {
id: `${backend}/undelete/${id}`,
};
},
'model.id'
),
canUndeleteVersion: alias('undeleteVersionPath.canUpdate'),
isFetchingVersionCapabilities: or(
'deleteVersionPath.isPending',
'destroyVersionPath.isPending',
'undeleteVersionPath.isPending'
),
requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'),
buttonDisabled: or(
'requestInFlight',
'key.isFolder',
'key.isError',
'key.flagsIsInvalid',
'model.isFolder',
'model.isError',
'model.flagsIsInvalid',
'hasLintError',
'error'
),
modelForData: computed('isV2', 'model', function() {
return this.isV2 ? this.model.belongsTo('selectedVersion').value() : this.model;
}),
basicModeDisabled: computed('secretDataIsAdvanced', 'showAdvancedMode', function() {
return this.get('secretDataIsAdvanced') || this.get('showAdvancedMode') === false;
return this.secretDataIsAdvanced || this.showAdvancedMode === false;
}),
secretDataAsJSON: computed('secretData', 'secretData.[]', function() {
return this.get('secretData').toJSON();
return this.secretData.toJSON();
}),
secretDataIsAdvanced: computed('secretData', 'secretData.[]', function() {
return this.get('secretData').isAdvanced();
return this.secretData.isAdvanced();
}),
hasDataChanges() {
const keyDataString = this.get('key.dataAsJSONString');
const sameData = this.get('secretData').toJSONString() === keyDataString;
if (sameData === false) {
this.set('lastChange', Date.now());
}
this.get('onDataChange')(!sameData);
},
showAdvancedMode: computed('preferAdvancedEdit', 'secretDataIsAdvanced', 'lastChange', function() {
return this.get('secretDataIsAdvanced') || this.get('preferAdvancedEdit');
return this.secretDataIsAdvanced || this.preferAdvancedEdit;
}),
transitionToRoute() {
this.get('router').transitionTo(...arguments);
this.router.transitionTo(...arguments);
},
onEscape(e) {
if (e.keyCode !== keys.ESC || this.get('mode') !== 'show') {
if (e.keyCode !== keys.ESC || this.mode !== 'show') {
return;
}
const parentKey = this.get('key.parentKey');
const parentKey = this.model.parentKey;
if (parentKey) {
this.transitionToRoute(LIST_ROUTE, parentKey);
} else {
@ -139,111 +218,118 @@ export default Component.extend(FocusOnInsertMixin, {
},
// successCallback is called in the context of the component
persistKey(method, successCallback, isCreate) {
let model = this.get('key');
let key = model.get('id');
persistKey(successCallback) {
let secret = this.model;
let model = this.modelForData;
let isV2 = this.isV2;
let key = model.get('path') || model.id;
if (key.startsWith('/')) {
key = key.replace(/^\/+/g, '');
model.set('id', key);
model.set(model.pathAttr, key);
}
if (isCreate && typeof model.createRecord === 'function') {
// create an ember data model from the proxy
model = model.createRecord(model.get('backend'));
this.set('key', model);
}
return model[method]().then(() => {
if (!get(model, 'isError')) {
if (this.get('wizard.featureState') === 'secret') {
this.get('wizard').transitionFeatureMachine('secret', 'CONTINUE');
return model.save().then(() => {
if (!model.isError) {
if (isV2 && Object.keys(secret.changedAttributes()).length) {
secret.set('id', key);
// save secret metadata
secret
.save()
.then(() => {
this.saveComplete(successCallback, key);
})
.catch(e => {
this.set(e, e.errors.join(' '));
});
} else {
this.saveComplete(successCallback, key);
}
successCallback(key);
}
});
},
saveComplete(callback, key) {
if (this.wizard.featureState === 'secret') {
this.wizard.transitionFeatureMachine('secret', 'CONTINUE');
}
callback(key);
},
checkRows() {
if (this.get('secretData').get('length') === 0) {
if (this.secretData.length === 0) {
this.send('addRow');
}
},
actions: {
//submit on shift + enter
handleKeyDown(e) {
e.stopPropagation();
if (!(e.keyCode === keys.ENTER && e.metaKey)) {
return;
}
let $form = this.$('form');
let $form = this.element.querySelector('form');
if ($form.length) {
$form.submit();
}
$form = null;
},
handleChange() {
this.set('codemirrorString', this.get('secretData').toJSONString(true));
this.hasDataChanges();
this.set('codemirrorString', this.secretData.toJSONString(true));
set(this.modelForData, 'secretData', this.secretData.toJSON());
},
createOrUpdateKey(type, event) {
event.preventDefault();
const newData = this.get('secretData').toJSON();
this.get('key').set('secretData', newData);
let model = this.modelForData;
// prevent from submitting if there's no key
// maybe do something fancier later
if (type === 'create' && isBlank(this.get('key.id'))) {
if (type === 'create' && isBlank(model.get('path') || model.id)) {
return;
}
this.persistKey(
'save',
key => {
this.hasDataChanges();
this.transitionToRoute(SHOW_ROUTE, key);
},
type === 'create'
);
this.persistKey(key => {
this.transitionToRoute(SHOW_ROUTE, key);
});
},
deleteKey() {
this.persistKey('destroyRecord', () => {
this.model.destroyRecord().then(() => {
this.transitionToRoute(LIST_ROOT_ROUTE);
});
},
deleteVersion(deleteType = 'destroy') {
let id = this.modelForData.id;
return this.store.adapterFor('secret-v2-version').v2DeleteOperation(this.store, id, deleteType);
},
refresh() {
this.get('onRefresh')();
this.onRefresh();
},
addRow() {
const data = this.get('secretData');
const data = this.secretData;
if (isNone(data.findBy('name', ''))) {
data.pushObject({ name: '', value: '' });
this.set('codemirrorString', data.toJSONString(true));
this.send('handleChange');
}
this.checkRows();
this.hasDataChanges();
},
deleteRow(name) {
const data = this.get('secretData');
const data = this.secretData;
const item = data.findBy('name', name);
if (isBlank(item.name)) {
return;
}
data.removeObject(item);
this.checkRows();
this.hasDataChanges();
this.set('codemirrorString', data.toJSONString(true));
this.rerender();
this.send('handleChange');
},
toggleAdvanced(bool) {
this.get('onToggleAdvancedEdit')(bool);
this.onToggleAdvancedEdit(bool);
},
codemirrorUpdated(val, codemirror) {
@ -252,7 +338,7 @@ export default Component.extend(FocusOnInsertMixin, {
const noErrors = codemirror.state.lint.marked.length === 0;
if (noErrors) {
try {
this.get('secretData').fromJSONString(val);
this.secretData.fromJSONString(val);
} catch (e) {
this.set('error', e.message);
}
@ -262,7 +348,7 @@ export default Component.extend(FocusOnInsertMixin, {
},
formatJSON() {
this.set('codemirrorString', this.get('secretData').toJSONString(true));
this.set('codemirrorString', this.secretData.toJSONString(true));
},
},
});

View file

@ -1,13 +1,100 @@
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { FEATURE_MACHINE_STEPS, INIT_STEPS } from 'vault/helpers/wizard-constants';
export default Component.extend({
wizard: service(),
classNames: ['ui-wizard'],
glyph: null,
headerText: null,
selectProgress: null,
currentMachine: computed.alias('wizard.currentMachine'),
tutorialState: computed.alias('wizard.currentState'),
tutorialComponent: computed.alias('wizard.tutorialComponent'),
showProgress: computed('wizard.featureComponent', 'tutorialState', function() {
return (
this.tutorialComponent.includes('active') &&
(this.tutorialState.includes('init.active') ||
(this.wizard.featureComponent && this.wizard.featureMachineHistory))
);
}),
featureMachineHistory: computed.alias('wizard.featureMachineHistory'),
totalFeatures: computed('wizard.featureList', function() {
return this.wizard.featureList.length;
}),
completedFeatures: computed('wizard.currentMachine', function() {
return this.wizard.getCompletedFeatures();
}),
currentFeatureProgress: computed('featureMachineHistory.[]', function() {
if (this.tutorialState.includes('active.feature')) {
let totalSteps = FEATURE_MACHINE_STEPS[this.currentMachine];
if (this.currentMachine === 'secrets') {
if (this.featureMachineHistory.includes('secret')) {
totalSteps = totalSteps['secret']['secret'];
}
if (this.featureMachineHistory.includes('list')) {
totalSteps = totalSteps['secret']['list'];
}
if (this.featureMachineHistory.includes('encryption')) {
totalSteps = totalSteps['encryption'];
}
if (this.featureMachineHistory.includes('role') || typeof totalSteps === 'object') {
totalSteps = totalSteps['role'];
}
}
return {
percentage: (this.featureMachineHistory.length / totalSteps) * 100,
feature: this.currentMachine,
text: `Step ${this.featureMachineHistory.length} of ${totalSteps}`,
};
}
return null;
}),
currentTutorialProgress: computed('tutorialState', function() {
if (this.tutorialState.includes('init.active')) {
let currentStepName = this.tutorialState.split('.')[2];
let currentStepNumber = INIT_STEPS.indexOf(currentStepName) + 1;
return {
percentage: (currentStepNumber / INIT_STEPS.length) * 100,
text: `Step ${currentStepNumber} of ${INIT_STEPS.length}`,
};
}
return null;
}),
progressBar: computed('currentFeatureProgress', 'currentFeature', 'currentTutorialProgress', function() {
let bar = [];
if (this.currentTutorialProgress) {
bar.push({
style: `width:${this.currentTutorialProgress.percentage}%;`,
completed: false,
showIcon: true,
});
} else {
if (this.currentFeatureProgress) {
this.completedFeatures.forEach(feature => {
bar.push({ style: 'width:100%;', completed: true, feature: feature, showIcon: true });
});
this.wizard.featureList.forEach(feature => {
if (feature === this.currentMachine) {
bar.push({
style: `width:${this.currentFeatureProgress.percentage}%;`,
completed: this.currentFeatureProgress.percentage == 100 ? true : false,
feature: feature,
showIcon: true,
});
} else {
bar.push({ style: 'width:0%;', completed: false, feature: feature, showIcon: true });
}
});
}
}
return bar;
}),
actions: {
dismissWizard() {
this.get('wizard').transitionTutorialMachine(this.get('wizard.currentState'), 'DISMISS');
this.wizard.transitionTutorialMachine(this.wizard.currentState, 'DISMISS');
},
},
});

View file

@ -1,10 +1,12 @@
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { FEATURE_MACHINE_TIME } from 'vault/helpers/wizard-constants';
export default Component.extend({
wizard: service(),
version: service(),
init() {
this._super(...arguments);
this.maybeHideFeatures();
@ -16,7 +18,24 @@ export default Component.extend({
feature.show = false;
}
},
estimatedTime: computed('selectedFeatures', function() {
let time = 0;
for (let feature of Object.keys(FEATURE_MACHINE_TIME)) {
if (this.selectedFeatures.includes(feature)) {
time += FEATURE_MACHINE_TIME[feature];
}
}
return time;
}),
selectProgress: computed('selectedFeatures', function() {
let bar = this.selectedFeatures.map(feature => {
return { style: 'width:0%;', completed: false, showIcon: true, feature: feature };
});
if (bar.length === 0) {
bar = [{ style: 'width:0%;', showIcon: false }];
}
return bar;
}),
allFeatures: computed(function() {
return [
{

View file

@ -12,9 +12,5 @@ export default Controller.extend(BackendCrumbMixin, {
// so we have to manually bubble here
this.send('refreshModel');
},
hasChanges(hasChanges) {
this.send('hasDataChanges', hasChanges);
},
},
});

View file

@ -11,9 +11,6 @@ export default Controller.extend(BackendCrumbMixin, {
refresh: function() {
this.send('refreshModel');
},
hasChanges(hasChanges) {
this.send('hasDataChanges', hasChanges);
},
toggleAdvancedEdit(bool) {
this.set('preferAdvancedEdit', bool);
this.get('backendController').set('preferAdvancedEdit', bool);

View file

@ -3,6 +3,11 @@ import BackendCrumbMixin from 'vault/mixins/backend-crumb';
export default Controller.extend(BackendCrumbMixin, {
backendController: controller('vault.cluster.secrets.backend'),
queryParams: ['version'],
version: '',
reset() {
this.set('version', '');
},
actions: {
refresh: function() {
// closure actions don't bubble to routes,
@ -10,10 +15,6 @@ export default Controller.extend(BackendCrumbMixin, {
this.send('refreshModel');
},
hasChanges(hasChanges) {
this.send('hasDataChanges', hasChanges);
},
toggleAdvancedEdit(bool) {
this.set('preferAdvancedEdit', bool);
this.get('backendController').set('preferAdvancedEdit', bool);

View file

@ -65,11 +65,14 @@ export default Controller.extend(BackendCrumbMixin, {
});
},
delete(item) {
delete(item, type) {
const name = item.id;
item.destroyRecord().then(() => {
this.send('reload');
this.get('flashMessages').success(`${name} was successfully deleted.`);
this.send('reload');
if (type === 'secret') {
return this.transitionToRoute('vault.cluster.secrets.backend.list-root');
}
});
},
},

View file

@ -3,10 +3,12 @@ import BackendCrumbMixin from 'vault/mixins/backend-crumb';
export default Controller.extend(BackendCrumbMixin, {
backendController: controller('vault.cluster.secrets.backend'),
queryParams: ['tab'],
queryParams: ['tab', 'version'],
version: '',
tab: '',
reset() {
this.set('tab', '');
this.set('version', '');
},
actions: {
refresh: function() {
@ -15,10 +17,6 @@ export default Controller.extend(BackendCrumbMixin, {
this.send('refreshModel');
},
hasChanges(hasChanges) {
this.send('hasDataChanges', hasChanges);
},
toggleAdvancedEdit(bool) {
this.set('preferAdvancedEdit', bool);
this.get('backendController').set('preferAdvancedEdit', bool);

View file

@ -9,6 +9,7 @@ export const STORAGE_KEYS = {
TUTORIAL_STATE: 'vault:ui-tutorial-state',
FEATURE_LIST: 'vault:ui-feature-list',
FEATURE_STATE: 'vault:ui-feature-state',
FEATURE_STATE_HISTORY: 'vault:ui-feature-state-history',
COMPLETED_FEATURES: 'vault:ui-completed-list',
COMPONENT_STATE: 'vault:ui-component-state',
RESUME_URL: 'vault:ui-tutorial-resume-url',
@ -36,4 +37,30 @@ export const DEFAULTS = {
componentState: null,
nextFeature: null,
nextStep: null,
featureMachineHistory: null,
};
export const FEATURE_MACHINE_STEPS = {
secrets: {
encryption: 5,
secret: {
list: 4,
secret: 5,
},
role: 7,
},
policies: 5,
replication: 2,
tools: 8,
authentication: 4,
};
export const INIT_STEPS = ['setup', 'save', 'unseal', 'login'];
export const FEATURE_MACHINE_TIME = {
secrets: 7,
policies: 5,
replication: 5,
tools: 8,
authentication: 5,
};

View file

@ -16,7 +16,7 @@ export default {
{ type: 'render', level: 'tutorial', component: 'wizard/tutorial-idle' },
{ type: 'render', level: 'feature', component: null },
],
onExit: ['showTutorialWhenAuthenticated'],
onExit: ['showTutorialWhenAuthenticated', 'clearFeatureData'],
states: {
idle: {
on: {

View file

@ -11,7 +11,7 @@
// });
//
import { queryRecord } from 'ember-computed-query';
import { maybeQueryRecord } from 'vault/macros/maybe-query-record';
export function apiPath(strings, ...keys) {
return function(data) {
@ -26,11 +26,25 @@ export function apiPath(strings, ...keys) {
export default function() {
let [templateFn, ...keys] = arguments;
return queryRecord(
return maybeQueryRecord(
'capabilities',
context => {
// pull all context attrs
let contextObject = context.getProperties(...keys);
// remove empty ones
let nonEmptyContexts = Object.keys(contextObject).reduce((ret, key) => {
if (contextObject[key] != null) {
ret[key] = contextObject[key];
}
return ret;
}, {});
// if all of them aren't present, cancel the fetch
if (Object.keys(nonEmptyContexts).length !== keys.length) {
return;
}
// otherwise proceed with the capabilities check
return {
id: templateFn(context.getProperties(...keys)),
id: templateFn(nonEmptyContexts),
};
},
...keys

View file

@ -0,0 +1,17 @@
import { computed } from '@ember/object';
import ObjectProxy from '@ember/object/proxy';
import PromiseProxyMixin from '@ember/object/promise-proxy-mixin';
import { resolve } from 'rsvp';
export function maybeQueryRecord(modelName, options = {}, ...keys) {
return computed(...keys, {
get() {
const query = typeof options === 'function' ? options(this) : options;
const PromiseObject = ObjectProxy.extend(PromiseProxyMixin);
return PromiseObject.create({
promise: query ? this.get('store').queryRecord(modelName, query) : resolve({}),
});
},
});
}

View file

@ -22,9 +22,9 @@ export default Mixin.create({
},
forceFocus() {
var $selector = this.$(this.get('focusOnInsertSelector') || 'input').first();
if (!$selector.is(':focus')) {
$selector.focus();
var $targ = this.element.querySelectorAll(this.get('focusOnInsertSelector') || 'input[type="text"]')[0];
if ($targ && $targ !== document.activeElement) {
$targ.focus();
}
},
});

View file

@ -0,0 +1,54 @@
import { computed } from '@ember/object';
import Mixin from '@ember/object/mixin';
import utils from '../lib/key-utils';
export default Mixin.create({
// what attribute has the path for the key
// will.be 'path' for v2 or 'id' v1
pathAttr: 'id',
flags: null,
initialParentKey: null,
isCreating: computed('initialParentKey', function() {
return this.get('initialParentKey') != null;
}),
pathVal() {
return this.get(this.pathAttr);
},
// rather than using defineProperty for all of these,
// we're just going to hardcode the known keys for the path ('id' and 'path')
isFolder: computed('id', 'path', function() {
return utils.keyIsFolder(this.pathVal());
}),
keyParts: computed('id', 'path', function() {
return utils.keyPartsForKey(this.pathVal());
}),
parentKey: computed('id', 'path', 'isCreating', {
get: function() {
return this.isCreating ? this.initialParentKey : utils.parentKeyForKey(this.pathVal());
},
set: function(_, value) {
return value;
},
}),
keyWithoutParent: computed('id', 'path', 'parentKey', {
get: function() {
var key = this.pathVal();
return key ? key.replace(this.parentKey, '') : null;
},
set: function(_, value) {
if (value && value.trim()) {
this.set(this.pathAttr, this.parentKey + value);
} else {
this.set(this.pathAttr, null);
}
return value;
},
}),
});

View file

@ -27,13 +27,17 @@ export default AuthConfig.extend({
label: 'NAS Port',
}),
nasIdentifier: attr('string', {
label: 'NAS Identifier',
}),
fieldGroups: computed(function() {
const groups = [
{
default: ['host', 'secret'],
},
{
'RADIUS Options': ['port', 'nasPort', 'dialTimeout', 'unregisteredUserPolicies'],
'RADIUS Options': ['port', 'nasPort', 'nasIdentifier', 'dialTimeout', 'unregisteredUserPolicies'],
},
];
return fieldToAttrs(this, groups);

View file

@ -1,45 +0,0 @@
import { computed } from '@ember/object';
import Mixin from '@ember/object/mixin';
import utils from '../lib/key-utils';
export default Mixin.create({
flags: null,
initialParentKey: null,
isCreating: computed('initialParentKey', function() {
return this.get('initialParentKey') != null;
}),
isFolder: computed('id', function() {
return utils.keyIsFolder(this.get('id'));
}),
keyParts: computed('id', function() {
return utils.keyPartsForKey(this.get('id'));
}),
parentKey: computed('id', 'isCreating', {
get: function() {
return this.get('isCreating') ? this.get('initialParentKey') : utils.parentKeyForKey(this.get('id'));
},
set: function(_, value) {
return value;
},
}),
keyWithoutParent: computed('id', 'parentKey', {
get: function() {
var key = this.get('id');
return key ? key.replace(this.get('parentKey'), '') : null;
},
set: function(_, value) {
if (value && value.trim()) {
this.set('id', this.get('parentKey') + value);
} else {
this.set('id', null);
}
return value;
},
}),
});

View file

@ -1,6 +1,6 @@
import { match } from '@ember/object/computed';
import DS from 'ember-data';
import KeyMixin from './key-mixin';
import KeyMixin from 'vault/mixins/key-mixin';
const { attr } = DS;
/* sample response

View file

@ -1,7 +1,6 @@
import { computed } from '@ember/object';
import DS from 'ember-data';
import { fragment } from 'ember-data-model-fragments/attributes';
import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
const { attr } = DS;
@ -41,6 +40,10 @@ export default DS.Model.extend({
return modelType;
}),
isV2KV: computed('modelTypeForKV', function() {
return this.modelTypeForKV === 'secret-v2';
}),
formFields: computed('engineType', function() {
let type = this.get('engineType');
let fields = [

View file

@ -0,0 +1,17 @@
import Secret from './secret';
import DS from 'ember-data';
import { bool } from '@ember/object/computed';
const { attr, belongsTo } = DS;
export default Secret.extend({
pathAttr: 'path',
version: attr('number'),
secret: belongsTo('secret-v2'),
path: attr('string'),
deletionTime: attr('string'),
createdTime: attr('string'),
deleted: bool('deletionTime'),
destroyed: attr('boolean'),
currentVersion: attr('number'),
});

View file

@ -1,3 +1,38 @@
import Secret from './secret';
import DS from 'ember-data';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import KeyMixin from 'vault/mixins/key-mixin';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
export default Secret.extend();
const { attr, hasMany, belongsTo, Model } = DS;
export default Model.extend(KeyMixin, {
engine: belongsTo('secret-engine', { async: false }),
engineId: attr('string'),
versions: hasMany('secret-v2-version', { async: false, inverse: null }),
selectedVersion: belongsTo('secret-v2-version', { async: false, inverse: 'secret' }),
createdTime: attr(),
updatedTime: attr(),
currentVersion: attr('number'),
oldestVersion: attr('number'),
maxVersions: attr('number', {
defaultValue: 10,
label: 'Maximum Number of Versions',
}),
casRequired: attr('boolean', {
defaultValue: false,
label: 'Require Check and Set',
helpText:
'Writes will only be allowed if the keys current version matches the version specified in the cas parameter',
}),
fields: computed(function() {
return expandAttributeMeta(this, ['maxVersions', 'casRequired']);
}),
versionPath: lazyCapabilities(apiPath`${'engineId'}/data/${'id'}`, 'engineId', 'id'),
secretPath: lazyCapabilities(apiPath`${'engineId'}/metadata/${'id'}`, 'engineId', 'id'),
canEdit: alias('versionPath.canUpdate'),
canDelete: alias('secretPath.canUpdate'),
canRead: alias('secretPath.canRead'),
});

View file

@ -1,7 +1,9 @@
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import DS from 'ember-data';
import KeyMixin from './key-mixin';
import KeyMixin from 'vault/mixins/key-mixin';
const { attr } = DS;
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
export default DS.Model.extend(KeyMixin, {
auth: attr('string'),
@ -21,5 +23,10 @@ export default DS.Model.extend(KeyMixin, {
}),
helpText: attr('string'),
// TODO this needs to be a relationship like `engine` on kv-v2
backend: attr('string'),
secretPath: lazyCapabilities(apiPath`${'backend'}/${'id'}`, 'backend', 'id'),
canEdit: alias('secretPath.canUpdate'),
canDelete: alias('secretPath.canUpdate'),
canRead: alias('secretPath.canRead'),
});

View file

@ -16,16 +16,8 @@ export default Route.extend({
},
afterModel(model, transition) {
let target = transition.targetName;
let path = model && model.get('path');
let type = model && model.get('type');
if (type === 'kv' && model.get('options.version') === 2) {
this.get('flashMessages').stickyInfo(
`"${path}" is a newer version of the KV backend. The Vault UI does not currently support the additional versioning features. All actions taken through the UI in this engine will operate on the most recent version of a secret.`
);
}
if (target === this.routeName) {
if (transition.targetName === this.routeName) {
return this.replaceWith('vault.cluster.secrets.backend.list-root', path);
}
},

View file

@ -1 +1,45 @@
export { default } from './create';
import { hash } from 'rsvp';
import { inject as service } from '@ember/service';
import EditBase from './secret-edit';
let secretModel = (store, backend, key) => {
let backendModel = store.peekRecord('secret-engine', backend);
let modelType = backendModel.get('modelTypeForKV');
if (modelType !== 'secret-v2') {
return store.createRecord(modelType, {
id: key,
});
}
let secret = store.createRecord(modelType);
secret.set('engine', backendModel);
let version = store.createRecord('secret-v2-version', {
path: key,
});
secret.set('selectedVersion', version);
return secret;
};
export default EditBase.extend({
wizard: service(),
createModel(transition) {
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
const modelType = this.modelType(backend);
if (modelType === 'role-ssh') {
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);
}
return secretModel(this.store, backend, transition.queryParams.initialKey);
},
model(params, transition) {
return hash({
secret: this.createModel(transition),
capabilities: {},
});
},
});

View file

@ -1,59 +1,10 @@
import { hash } from 'rsvp';
import { inject as service } from '@ember/service';
import EmberObject from '@ember/object';
import EditBase from './secret-edit';
import KeyMixin from 'vault/models/key-mixin';
import Route from '@ember/routing/route';
var SecretProxy = EmberObject.extend(KeyMixin, {
store: null,
toModel() {
return this.getProperties('id', 'secretData', 'backend');
},
createRecord(backend) {
let backendModel = this.store.peekRecord('secret-engine', backend);
return this.store.createRecord(backendModel.get('modelTypeForKV'), this.toModel());
},
willDestroy() {
this.store = null;
},
});
export default EditBase.extend({
wizard: service(),
createModel(transition, parentKey) {
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
const modelType = this.modelType(backend);
if (modelType === 'role-ssh') {
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 || '';
const model = SecretProxy.create({
initialParentKey: parentKey,
store: this.store,
});
if (key) {
// have to set this after so that it will be
// computed properly in the template (it's dependent on `initialParentKey`)
model.set('keyWithoutParent', key);
}
return model;
},
model(params, transition) {
const parentKey = params.secret ? params.secret : '';
return hash({
secret: this.createModel(transition, parentKey),
capabilities: {},
export default Route.extend({
beforeModel() {
let { secret } = this.paramsFor(this.routeName);
return this.transitionTo('vault.cluster.secrets.backend.create-root', {
queryParams: { initialKey: secret },
});
},
});

View file

@ -1,3 +1,9 @@
import EditBase from './secret-edit';
export default EditBase.extend();
export default EditBase.extend({
queryParams: {
version: {
refreshModel: true,
},
},
});

View file

@ -166,8 +166,8 @@ export default Route.extend({
return true;
},
reload() {
this.refresh();
this.store.clearAllDatasets();
this.refresh();
},
},
});

View file

@ -1,22 +1,23 @@
import { set } from '@ember/object';
import { hash } from 'rsvp';
import { hash, resolve } from 'rsvp';
import Route from '@ember/routing/route';
import utils from 'vault/lib/key-utils';
import UnloadModelRoute from 'vault/mixins/unload-model-route';
import DS from 'ember-data';
export default Route.extend(UnloadModelRoute, {
capabilities(secret) {
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
let backendModel = this.modelFor('vault.cluster.secrets.backend');
let backendType = backendModel.get('engineType');
let version = backendModel.get('options.version');
if (backendType === 'kv' || backendType === 'cubbyhole' || backendType === 'generic') {
return resolve({});
}
let path;
if (backendType === 'transit') {
path = backend + '/keys/' + secret;
} else if (backendType === 'ssh' || backendType === 'aws') {
path = backend + '/roles/' + secret;
} else if (version && version === 2) {
path = backend + '/data/' + secret;
} else {
path = backend + '/' + secret;
}
@ -72,7 +73,26 @@ export default Route.extend(UnloadModelRoute, {
secret = secret.replace('cert/', '');
}
return hash({
secret: this.store.queryRecord(modelType, { id: secret, backend }),
secret: this.store.queryRecord(modelType, { id: secret, backend }).then(resp => {
if (modelType === 'secret-v2') {
let backendModel = this.modelFor('vault.cluster.secrets.backend', backend);
let targetVersion = parseInt(params.version || resp.currentVersion, 10);
let version = resp.versions.findBy('version', targetVersion);
// 404 if there's no version
if (!version) {
let error = new DS.AdapterError();
set(error, 'httpStatus', 404);
throw error;
}
resp.set('engine', backendModel);
return version.reload().then(() => {
resp.set('selectedVersion', version);
return resp;
});
}
return resp;
}),
capabilities: this.capabilities(secret),
});
},
@ -120,14 +140,24 @@ export default Route.extend(UnloadModelRoute, {
},
willTransition(transition) {
if (this.get('hasChanges')) {
let model = this.controller.model;
let version = model.get('selectedVersion');
let changed = model.changedAttributes();
let changedKeys = Object.keys(changed);
// until we have time to move `backend` on a v1 model to a relationship,
// it's going to dirty the model state, so we need to look for it
// and explicity ignore it here
if (
(changedKeys.length && changedKeys[0] !== 'backend') ||
(version && Object.keys(version.changedAttributes()).length)
) {
if (
window.confirm(
'You have unsaved changes. Navigating away will discard these changes. Are you sure you want to discard your changes?'
)
) {
version && version.rollbackAttributes();
this.unloadModel();
this.set('hasChanges', false);
return true;
} else {
transition.abort();
@ -136,9 +166,5 @@ export default Route.extend(UnloadModelRoute, {
}
return this._super(...arguments);
},
hasDataChanges(hasChanges) {
this.set('hasChanges', hasChanges);
},
},
});

View file

@ -1,3 +1,9 @@
import EditBase from './secret-edit';
export default EditBase.extend();
export default EditBase.extend({
queryParams: {
version: {
refreshModel: true,
},
},
});

View file

@ -26,11 +26,8 @@ export default 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()
);
if (this.wizard.currentMachine === 'tools') {
this.wizard.transitionFeatureMachine(this.wizard.featureState, params.selected_action.toUpperCase());
}
this.controller.setProperties(params);
return true;

View file

@ -0,0 +1,31 @@
import { get } from '@ember/object';
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
secretDataPath: 'data.data',
normalizeItems(payload) {
let path = this.secretDataPath;
// move response that is the contents of the secret from the dataPath
// to `secret_data` so it will be `secretData` in the model
payload.secret_data = get(payload, path);
payload = Object.assign({}, payload, payload.data.metadata);
delete payload.data;
payload.path = payload.id;
// return the payload if it's expecting a single object or wrap
// it as an array if not
return payload;
},
serialize(snapshot) {
let version = 0;
let secret = snapshot.belongsTo('secret');
if (secret) {
version = secret.attr('currentVersion');
}
return {
data: snapshot.attr('secretData'),
options: {
cas: version,
},
};
},
});

View file

@ -1,5 +1,47 @@
import SecretSerializer from './secret';
import ApplicationSerializer from './application';
import DS from 'ember-data';
export default SecretSerializer.extend({
secretDataPath: 'data.data',
export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
versions: { embedded: 'always' },
},
secretDataPath: 'data',
normalizeItems(payload, requestType) {
if (payload.data.keys && Array.isArray(payload.data.keys)) {
// if we have data.keys, it's a list of ids, so we map over that
// and create objects with id's
return payload.data.keys.map(secret => {
// secrets don't have an id in the response, so we need to concat the full
// path of the secret here - the id in the payload is added
// in the adapter after making the request
let fullSecretPath = payload.id ? payload.id + secret : secret;
// if there is no path, it's a "top level" secret, so add
// a unicode space for the id
// https://github.com/hashicorp/vault/issues/3348
if (!fullSecretPath) {
fullSecretPath = '\u0020';
}
return {
id: fullSecretPath,
engine_id: payload.backend,
};
});
}
// transform versions to an array with composite IDs
if (payload.data.versions) {
payload.data.versions = Object.keys(payload.data.versions).map(version => {
let body = payload.data.versions[version];
body.version = version;
body.path = payload.id;
body.id = JSON.stringify([payload.backend, payload.id, version]);
return body;
});
}
payload.data.id = payload.id;
return requestType === 'queryRecord' ? payload.data : [payload.data];
},
serializeHasMany() {
return;
},
});

View file

@ -19,7 +19,7 @@ export default ApplicationSerializer.extend({
if (!fullSecretPath) {
fullSecretPath = '\u0020';
}
return { id: fullSecretPath };
return { id: fullSecretPath, backend: payload.backend };
});
}
let path = this.get('secretDataPath');

View file

@ -12,19 +12,19 @@ export default Service.extend({
connectionViolations: filterBy('events', 'violatedDirective', 'connect-src'),
attach() {
this.get('monitor').perform();
this.monitor.perform();
},
remove() {
this.get('monitor').cancelAll();
this.monitor.cancelAll();
},
monitor: task(function*() {
this.get('events').clear();
this.events.clear();
while (true) {
let event = yield waitForEvent(window.document, 'securitypolicyviolation');
this.get('events').addObject(event);
this.events.addObject(event);
}
}),
});

View file

@ -5,56 +5,73 @@ import { Machine } from 'xstate';
import getStorage from 'vault/lib/token-storage';
import { STORAGE_KEYS, DEFAULTS, MACHINES } from 'vault/helpers/wizard-constants';
const {
TUTORIAL_STATE,
COMPONENT_STATE,
FEATURE_STATE,
FEATURE_LIST,
FEATURE_STATE_HISTORY,
COMPLETED_FEATURES,
RESUME_URL,
RESUME_ROUTE,
} = STORAGE_KEYS;
const TutorialMachine = Machine(MACHINES.tutorial);
let FeatureMachine = null;
export default Service.extend(DEFAULTS, {
router: service(),
showWhenUnauthenticated: false,
featureMachineHistory: null,
init() {
this._super(...arguments);
this.initializeMachines();
},
initializeMachines() {
if (!this.storageHasKey(STORAGE_KEYS.TUTORIAL_STATE)) {
if (!this.storageHasKey(TUTORIAL_STATE)) {
let state = TutorialMachine.initialState;
this.saveState('currentState', state.value);
this.saveExtState(STORAGE_KEYS.TUTORIAL_STATE, state.value);
this.saveExtState(TUTORIAL_STATE, state.value);
}
this.saveState('currentState', this.getExtState(STORAGE_KEYS.TUTORIAL_STATE));
if (this.storageHasKey(STORAGE_KEYS.COMPONENT_STATE)) {
this.set('componentState', this.getExtState(STORAGE_KEYS.COMPONENT_STATE));
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'));
let stateNodes = TutorialMachine.getStateNodes(this.currentState);
this.executeActions(stateNodes.reduce((acc, node) => acc.concat(node.onEntry), []), null, 'tutorial');
if (this.storageHasKey(STORAGE_KEYS.FEATURE_LIST)) {
this.set('featureList', this.getExtState(STORAGE_KEYS.FEATURE_LIST));
if (this.storageHasKey(STORAGE_KEYS.FEATURE_STATE)) {
this.saveState('featureState', this.getExtState(STORAGE_KEYS.FEATURE_STATE));
} else {
if (FeatureMachine != null) {
this.saveState('featureState', FeatureMachine.initialState);
this.saveExtState(STORAGE_KEYS.FEATURE_STATE, this.get('featureState'));
}
if (this.storageHasKey(FEATURE_LIST)) {
this.set('featureList', this.getExtState(FEATURE_LIST));
if (this.storageHasKey(FEATURE_STATE_HISTORY)) {
this.set('featureMachineHistory', this.getExtState(FEATURE_STATE_HISTORY));
}
this.saveState(
'featureState',
this.getExtState(FEATURE_STATE) || (FeatureMachine ? FeatureMachine.initialState : null)
);
this.saveExtState(FEATURE_STATE, this.featureState);
this.buildFeatureMachine();
}
},
restartGuide() {
clearFeatureData() {
let storage = this.storage();
// empty storage
[
STORAGE_KEYS.TUTORIAL_STATE,
STORAGE_KEYS.FEATURE_LIST,
STORAGE_KEYS.FEATURE_STATE,
STORAGE_KEYS.COMPLETED_FEATURES,
STORAGE_KEYS.COMPONENT_STATE,
STORAGE_KEYS.RESUME_URL,
STORAGE_KEYS.RESUME_ROUTE,
].forEach(key => storage.removeItem(key));
[FEATURE_LIST, FEATURE_STATE, FEATURE_STATE_HISTORY, COMPLETED_FEATURES].forEach(key =>
storage.removeItem(key)
);
this.set('currentMachine', null);
this.set('featureMachineHistory', null);
this.set('featureState', null);
this.set('featureList', null);
},
restartGuide() {
this.clearFeatureData();
let storage = this.storage();
// empty storage
[TUTORIAL_STATE, COMPONENT_STATE, RESUME_URL, RESUME_ROUTE].forEach(key => storage.removeItem(key));
// reset wizard state
this.setProperties(DEFAULTS);
// restart machines from blank state
@ -63,6 +80,32 @@ export default Service.extend(DEFAULTS, {
this.transitionTutorialMachine('idle', 'AUTH');
},
saveFeatureHistory(state) {
if (
this.getCompletedFeatures().length === 0 &&
this.featureMachineHistory === null &&
(state === 'idle' || state === 'wrap')
) {
let newHistory = [state];
this.set('featureMachineHistory', newHistory);
} else {
if (this.featureMachineHistory) {
if (!this.featureMachineHistory.includes(state)) {
let newHistory = this.featureMachineHistory.addObject(state);
this.set('featureMachineHistory', newHistory);
} else {
//we're repeating steps
let stepIndex = this.featureMachineHistory.indexOf(state);
let newHistory = this.featureMachineHistory.splice(0, stepIndex + 1);
this.set('featureMachineHistory', newHistory);
}
}
}
if (this.featureMachineHistory) {
this.saveExtState(FEATURE_STATE_HISTORY, this.featureMachineHistory);
}
},
saveState(stateType, state) {
if (state.value) {
state = state.value;
@ -75,40 +118,44 @@ export default Service.extend(DEFAULTS, {
}
stateKey += state;
this.set(stateType, stateKey);
if (stateType === 'featureState') {
//only track progress if we are on the first step of the first feature
this.saveFeatureHistory(state);
}
},
transitionTutorialMachine(currentState, event, extendedState) {
if (extendedState) {
this.set('componentState', extendedState);
this.saveExtState(STORAGE_KEYS.COMPONENT_STATE, extendedState);
this.saveExtState(COMPONENT_STATE, extendedState);
}
let { actions, value } = TutorialMachine.transition(currentState, event);
this.saveState('currentState', value);
this.saveExtState(STORAGE_KEYS.TUTORIAL_STATE, this.get('currentState'));
this.saveExtState(TUTORIAL_STATE, this.currentState);
this.executeActions(actions, event, 'tutorial');
},
transitionFeatureMachine(currentState, event, extendedState) {
if (!FeatureMachine || !this.get('currentState').includes('active')) {
if (!FeatureMachine || !this.currentState.includes('active')) {
return;
}
if (extendedState) {
this.set('componentState', extendedState);
this.saveExtState(STORAGE_KEYS.COMPONENT_STATE, extendedState);
this.saveExtState(COMPONENT_STATE, extendedState);
}
let { actions, value } = FeatureMachine.transition(currentState, event, this.get('componentState'));
this.saveState('featureState', value);
this.saveExtState(STORAGE_KEYS.FEATURE_STATE, 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'));
if (this.currentMachine === 'secrets' && value === 'display') {
next = FeatureMachine.transition(value, 'REPEAT', this.componentState);
} else {
next = FeatureMachine.transition(value, 'CONTINUE', this.get('componentState'));
next = FeatureMachine.transition(value, 'CONTINUE', this.componentState);
}
this.saveState('nextStep', next.value);
}
@ -129,7 +176,7 @@ export default Service.extend(DEFAULTS, {
executeActions(actions, event, machineType) {
let transitionURL;
let expectedRouteName;
let router = this.get('router');
let router = this.router;
for (let action of actions) {
let type = action;
@ -168,8 +215,11 @@ export default Service.extend(DEFAULTS, {
case 'showTutorialAlways':
this.set('showWhenUnauthenticated', true);
break;
case 'clearFeatureData':
this.clearFeatureData();
break;
case 'continueFeature':
this.transitionFeatureMachine(this.get('featureState'), 'CONTINUE', this.get('componentState'));
this.transitionFeatureMachine(this.featureState, 'CONTINUE', this.componentState);
break;
default:
break;
@ -192,94 +242,107 @@ export default Service.extend(DEFAULTS, {
},
handlePaused() {
let expected = this.get('expectedURL');
let expected = this.expectedURL;
if (expected) {
this.saveExtState(STORAGE_KEYS.RESUME_URL, this.get('expectedURL'));
this.saveExtState(STORAGE_KEYS.RESUME_ROUTE, this.get('expectedRouteName'));
this.saveExtState(RESUME_URL, this.expectedURL);
this.saveExtState(RESUME_ROUTE, this.expectedRouteName);
}
},
handleResume() {
let resumeURL = this.storage().getItem(STORAGE_KEYS.RESUME_URL);
let resumeURL = this.storage().getItem(RESUME_URL);
if (!resumeURL) {
return;
}
this.get('router').transitionTo(resumeURL).followRedirects().then(() => {
this.set('expectedRouteName', this.storage().getItem(STORAGE_KEYS.RESUME_ROUTE));
this.set('expectedURL', resumeURL);
this.initializeMachines();
this.storage().removeItem(STORAGE_KEYS.RESUME_URL);
});
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(STORAGE_KEYS.FEATURE_STATE);
this.storage().removeItem(STORAGE_KEYS.FEATURE_LIST);
this.storage().removeItem(STORAGE_KEYS.COMPONENT_STATE);
this.storage().removeItem(FEATURE_STATE);
this.storage().removeItem(FEATURE_LIST);
this.storage().removeItem(FEATURE_STATE_HISTORY);
this.storage().removeItem(COMPONENT_STATE);
},
saveFeatures(features) {
this.set('featureList', features);
this.saveExtState(STORAGE_KEYS.FEATURE_LIST, this.get('featureList'));
this.saveExtState(FEATURE_LIST, this.featureList);
this.buildFeatureMachine();
},
buildFeatureMachine() {
if (this.get('featureList') === null) {
if (this.featureList === null) {
return;
}
this.startFeature();
if (this.storageHasKey(STORAGE_KEYS.FEATURE_STATE)) {
this.saveState('featureState', this.getExtState(STORAGE_KEYS.FEATURE_STATE));
}
this.saveExtState(STORAGE_KEYS.FEATURE_STATE, this.get('featureState'));
let nextFeature =
this.get('featureList').length > 1
? this.get('featureList')
.objectAt(1)
.capitalize()
: 'Finish';
let nextFeature = this.featureList.length > 1 ? this.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'));
if (this.currentMachine === 'secrets' && this.featureState === 'display') {
next = FeatureMachine.transition(this.featureState, 'REPEAT', this.componentState);
} else {
next = FeatureMachine.transition(this.get('featureState'), 'CONTINUE', this.get('componentState'));
next = FeatureMachine.transition(this.featureState, 'CONTINUE', this.componentState);
}
this.saveState('nextStep', next.value);
let stateNodes = FeatureMachine.getStateNodes(this.get('featureState'));
let stateNodes = FeatureMachine.getStateNodes(this.featureState);
this.executeActions(stateNodes.reduce((acc, node) => acc.concat(node.onEntry), []), null, 'feature');
},
startFeature() {
const FeatureMachineConfig = MACHINES[this.get('featureList').objectAt(0)];
const FeatureMachineConfig = MACHINES[this.featureList.objectAt(0)];
FeatureMachine = Machine(FeatureMachineConfig);
this.set('currentMachine', this.get('featureList').objectAt(0));
this.saveState('featureState', FeatureMachine.initialState);
this.set('currentMachine', this.featureList.objectAt(0));
if (this.storageHasKey(FEATURE_STATE)) {
this.saveState('featureState', this.getExtState(FEATURE_STATE));
} else {
this.saveState('featureState', FeatureMachine.initialState);
}
this.saveExtState(FEATURE_STATE, this.featureState);
},
getCompletedFeatures() {
if (this.storageHasKey(COMPLETED_FEATURES)) {
return this.getExtState(COMPLETED_FEATURES).toArray();
}
return [];
},
completeFeature() {
let features = this.get('featureList');
let features = this.featureList;
let done = features.shift();
if (!this.getExtState(STORAGE_KEYS.COMPLETED_FEATURES)) {
if (!this.getExtState(COMPLETED_FEATURES)) {
let completed = [];
completed.push(done);
this.saveExtState(STORAGE_KEYS.COMPLETED_FEATURES, completed);
this.saveExtState(COMPLETED_FEATURES, completed);
} else {
this.saveExtState(
STORAGE_KEYS.COMPLETED_FEATURES,
this.getExtState(STORAGE_KEYS.COMPLETED_FEATURES).toArray().addObject(done)
COMPLETED_FEATURES,
this.getExtState(COMPLETED_FEATURES)
.toArray()
.addObject(done)
);
}
this.saveExtState(STORAGE_KEYS.FEATURE_LIST, features.length ? features : null);
this.storage().removeItem(STORAGE_KEYS.FEATURE_STATE);
this.saveExtState(FEATURE_LIST, features.length ? features : null);
this.storage().removeItem(FEATURE_STATE);
if (this.featureMachineHistory) {
this.set('featureMachineHistory', []);
this.saveExtState(FEATURE_STATE_HISTORY, []);
}
if (features.length > 0) {
this.buildFeatureMachine();
} else {
this.storage().removeItem(STORAGE_KEYS.FEATURE_LIST);
this.storage().removeItem(FEATURE_LIST);
FeatureMachine = null;
this.transitionTutorialMachine(this.get('currentState'), 'DONE');
this.transitionTutorialMachine(this.currentState, 'DONE');
}
},

View file

@ -30,6 +30,7 @@
color: inherit;
font-size: $body-size;
min-height: 2rem;
padding: 0;
&:not(.console-ui-command):not(.CodeMirror-line) {
padding-left: $console-spacing;
@ -152,14 +153,14 @@ header .navbar-sections {
.console-spinner.control {
height: 21px;
width: 21px;
transform: scale(.75);
transform: scale(0.75);
transform-origin: center;
&::after {
height: auto;
width: auto;
right: .25rem;
left: .25rem;
top: .25rem;
bottom: .25rem;
right: 0.25rem;
left: 0.25rem;
top: 0.25rem;
bottom: 0.25rem;
}
}

View file

@ -1,6 +1,6 @@
.info-table-row {
box-shadow: 0 1px 0 $grey-light;
margin: 0 $size-6;
margin: 0;
@include from($tablet) {
display: flex;
@ -60,4 +60,7 @@
@include until($tablet) {
display: none;
}
.info-table-row:not(.is-mobile) .column:last-child {
padding-left: 0;
}
}

View file

@ -3,7 +3,7 @@
}
.masked-input .masked-value {
padding-left: 2.50rem;
padding-left: 2.5rem;
}
// we want to style the boxes the same everywhere so they
@ -25,6 +25,8 @@
.masked-input .display-only {
line-height: 1.5;
font-size: 1rem;
padding-top: 0;
padding-bottom: 0;
}
.masked-input-toggle {

View file

@ -45,13 +45,6 @@
}
}
.namespace-header {
margin: $size-9 $size-9 0;
color: $grey;
font-size: $size-8;
font-weight: $font-weight-semibold;
text-transform: uppercase;
}
.current-namespace {
border-bottom: 1px solid rgba($black, 0.1);
}

View file

@ -0,0 +1,24 @@
.secret-control-bar {
margin: 0;
padding: $size-10 $size-9;
background: $grey-lighter;
box-shadow: 0 1px 0 $grey-light, 0 -1px 0 $grey-light;
display: flex;
justify-content: flex-end;
.control {
flex: 0 1 auto;
padding: 0 $size-10;
font-size: $size-8;
height: 1.8rem;
line-height: 1.8rem;
display: flex;
flex-direction: column;
justify-content: center;
.switch[type="checkbox"].is-small + label::before {
top: 0.5rem;
}
.switch[type="checkbox"].is-small + label::after {
top: 0.6rem;
}
}
}

View file

@ -67,7 +67,7 @@
.wizard-header {
border-bottom: $light-border;
padding: $size-8 $size-4 $size-8 2rem;
padding: 0 $size-4 $size-8 2rem;
margin: $size-4 0;
position: relative;
@ -79,7 +79,7 @@
.title .icon {
left: 0;
position: absolute;
top: 0.7rem;
top: 0;
}
}
@ -173,3 +173,91 @@
.wizard-instructions {
margin: $size-4 0;
}
.selection-summary {
display: flex;
align-items: center;
width: 100%;
justify-content: space-between;
}
.time-estimate {
align-items: center;
color: $grey;
display: flex;
font-size: 12px;
}
.progress-container {
align-items: center;
background: $white;
bottom: 0;
height: $wizard-progress-bar-height;
display: flex;
left: 0;
padding: 0;
position: absolute;
right: 0;
transform: translateY(50%);
width: 100%;
}
.progress-bar {
background: $ui-gray-100;
box-shadow: inset 0 0 0 1px $ui-gray-200;
display: flex;
height: $wizard-progress-bar-height;
position: relative;
width: 100%;
}
.feature-progress-container {
align-items: center;
flex: 1 0 auto;
padding: 0 ($wizard-progress-check-size / 4);
position: relative;
}
.feature-progress {
background: $green;
border-radius: $wizard-progress-bar-height;
height: $wizard-progress-bar-height;
}
.feature-check {
height: $wizard-progress-check-size;
left: $wizard-progress-check-size / 2;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: $wizard-progress-check-size;
z-index: 10;
}
.feature-progress-container .feature-check {
left: 100%;
}
.feature-progress-container:first-child {
padding-left: 0;
.progress-bar,
.feature-progress {
border-radius: $wizard-progress-bar-height 0 0 $wizard-progress-bar-height;
}
}
.feature-progress-container:first-child:last-child {
.progress-bar,
.feature-progress {
border-radius: $wizard-progress-bar-height;
}
}
.incomplete-check svg {
fill: $ui-gray-200;
}
.completed-check svg {
fill: $green;
}

View file

@ -71,6 +71,7 @@
@import "./components/popup-menu";
@import "./components/radial-progress";
@import "./components/role-item";
@import "./components/secret-control-bar";
@import "./components/shamir-progress";
@import "./components/sidebar";
@import "./components/splash-page";

View file

@ -217,14 +217,17 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
}
.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;
box-shadow: none;
color: $blue;
display: flex;
height: auto;
line-height: 1.2;
justify-content: space-between;
text-align: left;
white-space: normal;
padding: $size-8;
width: 100%;
}

View file

@ -4,7 +4,15 @@
&:before {
font-size: $size-5;
color: $white-ter;
content: '|';
content: "|";
position: relative;
}
}
.list-header {
margin: $size-9 $size-9 0;
color: $grey;
font-size: $size-8;
font-weight: $font-weight-semibold;
text-transform: uppercase;
}

View file

@ -1,27 +1,35 @@
.switch[type="checkbox"] {
&.is-small {
+ label {
font-size: $size-9;
font-size: $size-8;
font-weight: bold;
padding-left: $size-9 * 2.5;
padding-left: $size-8 * 2.5;
margin: 0 0.25rem;
&::before {
top: $size-9 / 5;
height: $size-9;
width: $size-9 * 2;
top: $size-8 / 5;
height: $size-8;
width: $size-8 * 2;
}
&::after {
width: $size-9 * 0.68;
height: $size-9 * 0.68;
left: .15rem;
top: $size-9 / 2.5;
width: $size-8 * 0.8;
height: $size-8 * 0.8;
transform: translateX(0.15rem);
left: 0;
top: $size-8/ 4;
}
}
&:checked + label::after {
left: ($size-9 * 2) - ($size-9 * 0.9);
left: 0;
transform: translateX(($size-8 * 2) - ($size-8 * 0.94));
}
}
}
.switch[type="checkbox"].is-small + label::after {
will-change: left;
}
.switch[type="checkbox"]:focus + label {
box-shadow: 0 0 1px $blue;
}
.switch[type="checkbox"].is-success:checked + label::before {
background: $blue;
}

View file

@ -84,3 +84,7 @@ $box-link-hover-shadow: 0 0 0 1px $grey-light;
// animations
$speed: 150ms;
$speed-slow: $speed * 2;
// Wizard
$wizard-progress-bar-height: 6px;
$wizard-progress-check-size: 16px;

View file

@ -0,0 +1,11 @@
<div class="box is-bottomless has-background-white-bis">
<div class="columns is-centered">
<div class="column is-half has-text-centered">
<div class="box is-shadowless has-background-white-bis">
<p class="has-text-grey">
{{yield}}
</p>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,3 @@
<div class="box is-sideless has-background-white-bis has-text-grey has-text-centered">
{{yield}}
</div>

View file

@ -41,7 +41,7 @@
</div>
</div>
<header class="current-namespace">
<h5 class="namespace-header">Current namespace</h5>
<h5 class="list-header">Current namespace</h5>
<div class="level is-mobile namespace-link">
<span class="level-left">{{if namespacePath (concat namespacePath "/") "root"}}</span>
<ICon @glyph="checkmark-circled-outline" @size="16" @class="has-text-success level-right" />

View file

@ -1,16 +1,17 @@
<div class="field">
<p class="control has-icons-left has-icons-right">
{{input
value=filter
placeholder=(or placeholder "Filter keys")
<input
class="filter input"
disabled=disabled
key-up=(action "handleKeyUp")
key-down=(action "handleKeyPress")
input=(action "handleInput")
focus-in=(action "setFilterFocused" true)
focus-out=(action "setFilterFocused" false)
}}
disabled={{disabled}}
value={{@filter}}
placeholder={{ or @placeholder "Filter keys" }}
oninput={{action "handleInput" value="target.value"}}
onkeyup={{action "handleKeyUp" }}
onkeydown={{action "handleKeyPress"}}
onfocus={{action "setFilterFocused" true}}
onblur={{action "setFilterFocused" false}}
/>
{{i-con glyph="ios-search-strong" class="is-left has-text-grey" size=18}}
</p>

View file

@ -0,0 +1,81 @@
{{#if (and (or @model.isNew @canEditV2Secret) @isV2)}}
<div class="form-section box is-shadowless is-fullwidth">
<label class="title is-5">
Secret Metadata
</label>
{{#each @model.fields as |attr|}}
<FormField data-test-field @attr={{attr}} @model={{this.model}} />
{{/each}}
</div>
{{/if}}
{{#if @showAdvancedMode}}
<div class="form-section">
<label class="title is-5">
{{#if isV2}}
Version Data
{{else}}
Secret Data
{{/if}}
</label>
<JsonEditor
@value={{@codemirrorString}}
@valueUpdated={{@editActions.codemirrorUpdated}}
@onFocusOut={{@editActions.formatJSON}}
/>
</div>
{{else}}
<div class="form-section">
<label class="title is-5">
{{#if isV2}}
Version Data
{{else}}
Secret Data
{{/if}}
</label>
{{#each @secretData as |secret index|}}
<div class="info-table-row">
<div class="column is-one-quarter info-table-row-edit">
{{input
data-test-secret-key=true
value=secret.name
placeholder="key"
change=(action @editActions.handleChange)
class="input"
autocomplete="off"
}}
</div>
<div class="column info-table-row-edit">
<MaskedInput
@name={{secret.name}}
@onKeyDown={{@editActions.handleKeyDown}}
@onChange={{@editActions.handleChange}}
@value={{secret.value}}
data-test-secret-value="true"
/>
</div>
<div class="column is-narrow info-table-row-edit">
{{#if (eq @secretData.length (inc index))}}
<button type="button" {{action @editActions.addRow}} class="button is-outlined is-primary" data-test-secret-add-row="true">
Add
</button>
{{else}}
<button
class="button has-text-grey is-expanded is-icon"
type="button"
{{action @editActions.deleteRow secret.name}}
aria-label="Delete row"
>
<ICon
@size="22"
@glyph="trash-a"
@excludeIconClass={{true}}
class="is-large has-text-grey-light"
/>
</button>
{{/if}}
</div>
</div>
{{/each}}
</div>
{{/if}}

View file

@ -12,6 +12,8 @@
<h1 class="title is-3">
{{#if (eq mode "create") }}
Create Secret
{{else if (and isV2 (eq mode 'edit'))}}
Create New Version
{{else if (eq mode 'edit')}}
Edit Secret
{{else}}
@ -19,41 +21,187 @@
{{/if}}
</h1>
</p.levelLeft>
<p.levelRight>
{{#if canDelete}}
<ConfirmAction
@buttonClasses="button is-compact is-ghost has-icon-right"
@onConfirmAction={{action "deleteKey"}}
@confirmMessage={{if isV2
(concat "This will permanently delete " model.id " and all its versions. Are you sure you want to do this?")
(concat "Are you sure you want to delete " model.id "?")
}}
@cancelButtonText="Cancel"
data-test-secret-delete="true"
>
Delete secret <ICon @glyph="chevron-right" @size="11" />
</ConfirmAction>
{{/if}}
</p.levelRight>
</PageHeader>
<div class="box is-sideless has-background-grey-lighter has-slim-padding is-marginless">
<div class="level">
<div class="level-left">
</div>
<div class="level-right is-marginless">
<div class="field is-horizontal is-flex-end is-single-line">
<div class="control is-flex">
<input
data-test-secret-json-toggle=true
id="json"
type="checkbox"
name="json"
class="switch is-rounded is-success is-small"
checked={{showAdvancedMode}}
onchange={{action "toggleAdvanced" value="target.checked"}}
disabled={{and (eq mode 'show') secretDataIsAdvanced}}
/>
<label for="json">JSON</label>
</div>
{{#if (and (not-eq mode 'create') (or capabilities.canUpdate capabilities.canDelete))}}
<div class="control is-flex">
{{input
id="edit"
type="checkbox"
name="navToEdit"
class="switch is-rounded is-success is-small"
checked=(eq mode 'edit')
change=(action (nav-to-route (concat 'vault.cluster.secrets.backend.' (if (eq mode 'show') 'edit' 'show')) key.id replace=true) )
}}
<label for="edit">Edit</label>
</div>
{{/if}}
</div>
</div>
<div class="secret-control-bar">
<div class="control">
<input
data-test-secret-json-toggle=true
id="json"
type="checkbox"
name="json"
class="switch is-rounded is-success is-small"
checked={{showAdvancedMode}}
onchange={{action "toggleAdvanced" value="target.checked"}}
disabled={{and (eq mode 'show') secretDataIsAdvanced}}
/>
<label for="json" class="has-text-grey">JSON</label>
</div>
{{#if (and (eq mode 'show') (or canEditV2Secret canEdit))}}
<div class="control">
{{#let (concat 'vault.cluster.secrets.backend.' (if (eq mode 'show') 'edit' 'show')) as |targetRoute|}}
{{#if isV2}}
<LinkTo
@params={{array targetRoute model.id (query-params version=this.modelForData.version)}}
@replace={{true}}
class="link link-plain has-text-weight-semibold"
>
Create new version
</LinkTo>
{{else}}
<LinkTo
@params={{array targetRoute model.id}}
@replace={{true}}
class="link link-plain has-text-weight-semibold"
>
Edit Secret
</LinkTo>
{{/if}}
{{/let}}
</div>
{{/if}}
{{#if (and (eq @mode "show") this.isV2)}}
<div class="control">
<BasicDropdown
@class="popup-menu"
@horizontalPosition="auto-right"
@verticalPosition="below"
as |D|
>
<D.trigger
data-test-popup-menu-trigger="true"
@class={{concat "popup-menu-trigger button is-ghost has-text-grey" (if D.isOpen " is-active")}}
@tagName="button"
>
Version {{this.modelForData.version}}
<ICon @glyph="chevron-right" @size="11" />
</D.trigger>
<D.content @class="popup-menu-content ">
<nav class="box menu">
<ul class="menu-list">
{{#if this.modelForData.destroyed}}
<li class="action has-text-grey">
<button type="button" class="link" disabled >
The data for {{this.model.id}} version {{this.modelForData.version}} has been destroyed.
</button>
</li>
{{else}}
{{#if isFetchingVersionCapabilities}}
<li class="action">
<button disabled=true type="button" class="link button is-loading is-transparent">
loading
</button>
</li>
{{else}}
<li class="action">
{{#if this.modelForData.deleted}}
{{#if canUndeleteVersion}}
<button type="button" class="link" {{action "deleteVersion" "undelete"}}>
Undelete version
</button>
{{else}}
<button type="button" class="link" disabled >
The data for {{this.model.id}} version {{this.modelForData.version}} has been deleted. You do not have the permisssion to undelete it.
</button>
{{/if}}
{{else if canDeleteVersion}}
<ConfirmAction
@buttonClasses="link has-text-danger"
@containerClasses="message-body is-block"
@messageClasses="is-block"
@onConfirmAction={{action "deleteVersion" "delete"}}
@confirmMessage={{
concat "Are you sure you want to delete " model.id " version " this.modelForData.version "?"
}}
@cancelButtonText="Cancel"
data-test-secret-v2-delete="true"
>
Delete version
</ConfirmAction>
{{else}}
<button type="button" class="link" disabled >
You do not have the permissions to delete the data for this secret.
</button>
{{/if}}
</li>
{{#if canDestroyVersion}}
<li class="action">
<ConfirmAction
@buttonClasses="link has-text-danger"
@containerClasses="message-body is-block"
@messageClasses="is-block"
@onConfirmAction={{action "deleteVersion" "destroy"}}
@confirmMessage={{
concat "This will permanently destroy " model.id " version " this.modelForData.version ". Are you sure you want to do this?"
}}
@cancelButtonText="Cancel"
data-test-secret-v2-destroy="true"
>
Permanently destroy version
</ConfirmAction>
</li>
{{else}}
<button type="button" class="link" disabled >
You do not have the permissions to destroy the data for this secret.
</button>
{{/if}}
{{/if}}
{{/if}}
</ul>
</nav>
</D.content>
</BasicDropdown>
</div>
<div class="control">
<BasicDropdown
@class="popup-menu"
@horizontalPosition="auto-right"
@verticalPosition="below"
as |D|
>
<D.trigger
data-test-popup-menu-trigger="true"
@class={{concat "popup-menu-trigger button is-ghost has-text-grey" (if D.isOpen " is-active")}}
@tagName="button"
>
History <ICon @glyph="chevron-right" @size="11" />
</D.trigger>
<D.content @class="popup-menu-content ">
<nav class="box menu">
<h5 class="list-header">Versions</h5>
<ul class="menu-list">
{{#each (reverse this.model.versions) as |secretVersion|}}
<li class="action">
<LinkTo class="link" @params={{array (query-params version=secretVersion.version)}}>
Version {{secretVersion.version}}
{{#if (eq secretVersion.version this.model.currentVersion)}}
<ICon @glyph="checkmark-circled-outline" @excludeIconClass={{true}} @size="13" @class="has-text-success is-pulled-right" />
{{else if secretVersion.deleted}}
<ICon @glyph="cancel-square-outline" @size="13" @excludeIconClass={{true}} @class="has-text-grey is-pulled-right" />
{{/if}}
</LinkTo>
</li>
{{/each}}
</ul>
</nav>
</D.content>
</BasicDropdown>
</div>
{{/if}}
</div>
{{partial partialName}}

View file

@ -32,8 +32,8 @@
<div class="control">
{{#secret-link
mode="create"
secret=(or baseKey.id '')
queryParams=(query-params initialKey='')
secret=''
queryParams=(query-params initialKey=baseKey.id)
class="button has-icon-right is-ghost is-compact"
data-test-secret-create=true
}}

View file

@ -15,5 +15,26 @@
<h1 class="title is-5">
<ICon @glyph={{glyph}} @size="21" /> {{headerText}}
</h1>
{{#if showProgress}}
<ToolTip @verticalPosition="below" as |T|>
<T.trigger @tabindex=false>
<WizardProgress @currentFeatureProgress={{currentFeatureProgress}} @progressBar={{progressBar}} />
</T.trigger>
<T.content @class="tool-tip">
<div class="box">
{{#if currentTutorialProgress}}
{{currentTutorialProgress.text}}
{{else}}
<p>{{capitalize currentFeatureProgress.feature}}</p>
{{currentFeatureProgress.text}}
{{/if}}
</div>
</T.content>
</ToolTip>
{{else}}
{{#if selectProgress}}
<WizardProgress @noProgress={{true}} @progressBar={{selectProgress}} />
{{/if}}
{{/if}}
</div>
{{yield}}

View file

@ -0,0 +1,12 @@
<div class="progress-container">
{{#each progressBar as |bar|}}
<div class="feature-progress-container">
<span class="progress-bar">
<span class="feature-progress" style={{bar.style}} {{! template-lint-disable }}></span>
</span>
{{#if bar.showIcon}}
<ICon class="feature-check {{if bar.completed 'completed-check' 'incomplete-check'}}" @glyph="check-circle-fill" @size="16" @excludeIconClass={{true}}/>
{{/if}}
</div>
{{/each}}
</div>

View file

@ -12,9 +12,9 @@
@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" />
Enable another auth method <ICon @glyph="loop" @size=13 />
</button>
<button type="button" class="button next-feature-step" {{action onAdvance}}>
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
</button>
</WizardSection>

View file

@ -1,4 +1,4 @@
<WizardContent @headerText="Vault Web UI" @glyph="tour">
<WizardContent @headerText="Vault Web UI" @glyph="tour" @selectProgress={{selectProgress}}>
<h2 class="title is-6">
Choosing where to go
</h2>
@ -36,6 +36,11 @@
</div>
{{/if}}
{{/each}}
<button type="submit" class="button is-primary">Start</button>
<span class="selection-summary">
<button type="submit" class="button is-primary">Start</button>
{{#if selectedFeatures}}
<span class="time-estimate"><ICon @glyph="stopwatch" @class="has-text-grey auto-width is-paddingless is-flex-column"/>About {{estimatedTime}} minutes</span>
{{/if}}
</span>
</form>
</WizardContent>

View file

@ -13,7 +13,7 @@
@class="wizard-details"
>
<button type="button" class="button next-feature-step" {{action onAdvance}}>
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
</button>
</WizardSection>
</WizardContent>

View file

@ -13,7 +13,7 @@
Ready to move on?
</h3>
<button type="button" class="button next-feature-step" {{action onAdvance}}>
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
</button>
</div>
</WizardContent>

View file

@ -30,9 +30,9 @@
</button>
{{/if}}
<button type="button" class="button next-feature-step" {{action onReset}}>
Enable another secrets engine <ICon @glyph="loop" @size=13 @class="is-pulled-right" />
Enable another secrets engine <ICon @glyph="loop" @size=13 />
</button>
<button type="button" class="button next-feature-step" {{action onDone}}>
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
</button>
</WizardSection>

View file

@ -9,6 +9,6 @@
</p>
</WizardSection>
<button type="button" class="button next-feature-step" {{action onAdvance}}>
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
</button>
</WizardContent>

View file

@ -7,9 +7,9 @@
</WizardSection>
<WizardSection>
<button type="button" class="button next-feature-step" {{action onReset}}>
Go Back <ICon @glyph="reply" @size=12 @class="is-pulled-right" />
Go Back <ICon @glyph="reply" @size=12 />
</button>
<button type="button" class="button next-feature-step" {{action onAdvance}}>
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 @class="is-pulled-right" />
{{nextFeature}} <ICon @glyph="chevron-right" @size=10 />
</button>
</WizardSection>

View file

@ -1,48 +0,0 @@
{{#unless key.isFolder}}
{{#if showAdvancedMode}}
{{json-editor
value=codemirrorString
valueUpdated=(action "codemirrorUpdated")
onFocusOut=(action "formatJSON")
}}
{{else}}
{{#each secretData as |secret index|}}
<div class="info-table-row">
<div class="column is-one-quarter info-table-row-edit">
{{input
data-test-secret-key=true
value=secret.name
placeholder="key"
change="handleChange"
class="input"
}}
</div>
<div class="column info-table-row-edit">
{{masked-input
data-test-secret-value=true
name=secret.name
onKeyDown=(action "handleKeyDown")
onChange=(action "handleChange")
value=secret.value
}}
</div>
<div class="column is-narrow info-table-row-edit">
{{#if (eq secretData.length (inc index))}}
<button type="button" {{action "addRow"}} class="button is-outlined is-primary" data-test-secret-add-row=true>
Add
</button>
{{else}}
<button
class="button has-text-grey is-expanded is-icon"
type="button"
{{action "deleteRow" secret.name}}
aria-label="Delete row"
>
{{i-con size=22 glyph='trash-a' excludeIconClass=true class="is-large has-text-grey-light"}}
</button>
{{/if}}
</div>
</div>
{{/each}}
{{/if}}
{{/unless}}

View file

@ -1,38 +1,39 @@
<form class="{{if showAdvancedMode 'advanced-edit' 'simple-edit'}}" onsubmit={{action "createOrUpdateKey" "create"}} onchange={{action "handleChange"}}>
<form class="{{if showAdvancedMode 'advanced-edit' 'simple-edit'}}" onsubmit={{action "createOrUpdateKey" "create"}}>
<div class="field box is-fullwidth is-sideless is-marginless">
<NamespaceReminder @mode="create" @noun="secret" />
{{message-error model=key errorMessage=error}}
<label class="label is-font-weight-normal" for="kv-key">Path for this secret</label>
<div class="field has-addons">
{{#if (not-eq key.initialParentKey '') }}
{{! need this to prevent a shift in the layout before we transition when saving }}
{{#if key.isCreating}}
<p class="control is-no-flex-grow">
<button type="button" class="button is-static has-background-grey-lighter has-text-grey has-default-border">
{{key.initialParentKey}}
</button>
</p>
{{else}}
<p class="control is-no-flex-grow">
<button type="button" class="button is-static">
{{key.parentKey}}
</button>
</p>
{{/if}}
{{/if}}
<p class="control is-expanded">
{{input data-test-secret-path=true id="kv-key" class="input" value=key.keyWithoutParent}}
</p>
</div>
{{#if key.isFolder}}
<MessageError @model={{model}} @errorMessage={{error}} />
<label class="is-label" for="kv-key">Path for this secret</label>
<p class="control is-expanded">
{{input
autocomplete="off"
data-test-secret-path="true"
id="kv-key"
class="input"
value=(get modelForData modelForData.pathAttr)
}}
</p>
{{#if modelForData.isFolder}}
<p class="help is-danger">
The secret path may not end in <code>/</code>
</p>
{{/if}}
</div>
{{partial "partials/secret-edit-display"}}
<SecretEditDisplay
@showAdvancedMode={{showAdvancedMode}}
@codemirrorString={{codemirrorString}}
@secretData={{secretData}}
@isV2={{isV2}}
@model={{model}}
@canEditV2Secret={{canEditV2Secret}}
@editActions={{hash
codemirrorUpdated=(action "codemirrorUpdated")
formatJSON=(action "formatJSON")
handleKeyDown=(action "handleKeyDown")
handleChange=(action "handleChange")
deleteRow=(action "deleteRow")
addRow=(action "addRow")
}}
/>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button
@ -47,7 +48,7 @@
<div class="control">
{{#secret-link
mode="list"
secret=key.initialParentKey
secret=model.parentKey
class="button"
}}
Cancel

View file

@ -1,81 +1,52 @@
<form onsubmit={{action "createOrUpdateKey" "update"}} onchange={{action "handleChange"}}>
<div class="box is-sideless is-fullwidth is-marginless">
{{message-error model=key errorMessage=error}}
<form onsubmit={{action "createOrUpdateKey" "update"}}>
<div class="box is-sideless is-fullwidth is-marginless is-paddingless">
<MessageError @model={{model}} @errorMessage={{error}} />
<NamespaceReminder @mode="edit" @noun="secret" />
{{#unless showAdvancedMode}}
<div class="table info-table-row-header">
<div class="info-table-row thead">
<div class="th column is-one-quarter">
Key
</div>
<div class="th column">
Value
</div>
</div>
{{#if (not-eq model.selectedVersion.version model.currentVersion)}}
<div class="form-section">
<MessageInPage @type="warning" @class="is-marginless">
You are creating a new version based on data from Version {{model.selectedVersion.version}}. The current version for <code>{{model.id}}</code> is Version {{model.currentVersion}}.
</MessageInPage>
</div>
{{/unless}}
{{/if}}
<SecretEditDisplay
@showAdvancedMode={{showAdvancedMode}}
@codemirrorString={{codemirrorString}}
@secretData={{secretData}}
@isV2={{isV2}}
@canEditV2Secret={{canEditV2Secret}}
@model={{model}}
@editActions={{hash
codemirrorUpdated=(action "codemirrorUpdated")
formatJSON=(action "formatJSON")
handleKeyDown=(action "handleKeyDown")
handleChange=(action "handleChange")
deleteRow=(action "deleteRow")
addRow=(action "addRow")
}}
/>
</div>
{{#if capabilities.canUpdate}}
{{partial "partials/secret-edit-display"}}
{{else}}
<div class="field is-horizontal box is-sideless is-marginless">
<div class="content">
<p>Your current token does not have capabilities to update this secret.</p>
<p>
{{#secret-link
mode=(if key.isFolder "list" "show")
secret=key.id
class="button"
}}
Back to <code>{{key.id}}</code>
{{/secret-link}}
</p>
</div>
</div>
{{/if}}
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
<div class="field is-grouped">
{{#unless key.isFolder}}
{{#if capabilities.canUpdate}}
<div class="control">
<button
type="submit"
disabled={{buttonDisabled}}
class="button is-primary"
>
Save
</button>
</div>
{{/if}}
{{/unless}}
<div class="control">
<button
type="submit"
disabled={{buttonDisabled}}
class="button is-primary"
>
Save
</button>
</div>
<div class="control">
{{#secret-link
mode=(if key.isFolder "list" "show")
secret=key.id
mode="show"
secret=model.id
class="button"
queryParams=(query-params version=this.modelForData.version)
}}
Cancel
{{/secret-link}}
</div>
</div>
{{#if capabilities.canDelete}}
{{#confirm-action
buttonClasses="button"
onConfirmAction=(action "deleteKey")
confirmMessage=(if key.isFolder
(concat "Are you sure you want to delete " key.id " and all its contents?")
(concat "Are you sure you want to delete " key.id "?")
)
cancelButtonText="Cancel"
data-test-secret-delete="true"
}}
{{#if key.isFolder}}
Delete folder
{{else}}
Delete secret
{{/if}}
{{/confirm-action}}
{{/if}}
</div>
</form>

View file

@ -1,24 +1,34 @@
{{#if showAdvancedMode}}
{{json-editor
value=key.dataAsJSONString
options=(hash
readOnly=true
)
}}
{{#if (and isV2 modelForData.destroyed)}}
<BlockEmpty>
Version {{modelForData.version}} of this secret has been permanently destroyed.
</BlockEmpty>
{{else if (and isV2 modelForData.deleted)}}
<BlockEmpty>
Version {{modelForData.version}} of this secret has been deleted.
</BlockEmpty>
{{else}}
<div class="table info-table-row-header">
<div class="info-table-row thead">
<div class="th column is-one-quarter">
Key
</div>
<div class="th column">
Value
{{#if showAdvancedMode}}
{{json-editor
value=modelForData.dataAsJSONString
options=(hash
readOnly=true
)
}}
{{else}}
<div class="table info-table-row-header">
<div class="info-table-row thead">
<div class="th column is-one-quarter">
Key
</div>
<div class="th column">
Value
</div>
</div>
</div>
</div>
{{#each-in key.secretData as |key value|}}
{{#info-table-row label=key value=value alwaysRender=true}}
{{masked-input value=value displayOnly=true}}
{{/info-table-row}}
{{/each-in}}
{{#each-in modelForData.secretData as |key value|}}
{{#info-table-row label=key value=value alwaysRender=true}}
{{masked-input value=value displayOnly=true}}
{{/info-table-row}}
{{/each-in}}
{{/if}}
{{/if}}

View file

@ -10,19 +10,95 @@
}}
<div class="columns is-mobile">
<div class="column is-10">
{{#secret-link
mode=(if item.isFolder "list" "show")
secret=item.id
class="has-text-black has-text-weight-semibold"
}}
{{i-con
glyph=(if item.isFolder 'folder' 'file')
size=14
class="has-text-grey-light"
}}{{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
{{/secret-link}}
<SecretLink
@mode={{if item.isFolder "list" "show" }}
@secret={{item.id}}
@class="has-text-black has-text-weight-semibold"
>
<ICon
@glyph={{if item.isFolder 'folder' 'file' }}
@size="14"
@class="has-text-grey-light"
/>{{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
</SecretLink>
</div>
<div class="column has-text-right">
<PopupMenu name="secret-menu" @contentClass="is-wide">
<nav class="menu">
<ul class="menu-list">
{{#if item.isFolder}}
<SecretLink
@mode="list"
@secret={{item.id}}
@class="has-text-black has-text-weight-semibold"
>
Contents
</SecretLink>
{{else}}
{{#if (or item.versionPath.isLoading item.secretPath.isLoading)}}
<li class="action">
<button disabled=true type="button" class="link button is-loading is-transparent">
loading
</button>
</li>
{{else}}
{{#if item.canRead}}
<li class="action">
<SecretLink
@mode="show"
@secret={{item.id}}
@class="has-text-black has-text-weight-semibold"
>
Details
</SecretLink>
</li>
{{!-- // will add a link to the history view once it exists
{{#if backendModel.isV2KV}}
<li class="action">
Verion History
</li>
{{/if}}
--}}
{{/if}}
{{#if item.canEdit}}
<li class="action">
<SecretLink
@mode="edit"
@secret={{item.id}}
@class="has-text-black has-text-weight-semibold"
>
{{if backendModel.isV2KV
"Create New Version"
"Edit"
}}
</SecretLink>
</li>
{{/if}}
{{#if item.canDelete}}
<li class="action">
{{#confirm-action
confirmButtonClasses="button is-primary"
buttonClasses="link is-destroy"
onConfirmAction=(action "delete" item "secret")
confirmMessage=(concat "Are you sure you want to permanently delete " item.id " and all its versions?")
showConfirm=(get this (concat "shouldDelete-" item.id))
class=(if (get this (concat "shouldDelete-" item.id)) "message is-block is-warning is-outline")
containerClasses="message-body is-block"
messageClasses="is-block"
data-test-v2-kv-delete=item.id
}}
{{if backendModel.isV2KV
"Permanently Delete"
"Delete"
}}
{{/confirm-action}}
</li>
{{/if}}
{{/if}}
{{/if}}
</ul>
</nav>
</PopupMenu>
</div>
</div>
{{/linked-block}}

View file

@ -0,0 +1,3 @@
<svg width="{{size}}" height="{{size}}" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M19 5v14H5V5h14zm0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.41 5L12 10.59 9.41 8 8 9.41 10.59 12 8 14.59 9.41 16 12 13.41 14.59 16 16 14.59 13.41 12 16 9.41 14.59 8z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 380 B

View file

@ -0,0 +1,3 @@
<svg width="{{size}}" height="{{size}}" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>

After

Width:  |  Height:  |  Size: 278 B

View file

@ -0,0 +1,3 @@
<svg width="{{size}}" height="{{size}}" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>

After

Width:  |  Height:  |  Size: 210 B

View file

@ -46,27 +46,19 @@
</div>
{{/each}}
{{else}}
<div class="box is-bottomless has-background-white-bis">
<div class="columns is-centered">
<div class="column is-half has-text-centered">
<div class="box is-shadowless has-background-white-bis">
<p class="has-text-grey">
{{#if (eq baseKey.id '')}}
There are currently no {{pluralize options.item}} in this backend.
{{else}}
{{#if filterIsFolder}}
{{#if (eq filter baseKey.id)}}
There are no {{pluralize options.item}} under <code>{{or filter}}</code>.
{{else}}
We couldn't find a folder matching <code>{{filter}}</code>.
{{/if}}
{{/if}}
{{/if}}
</p>
</div>
</div>
</div>
</div>
<BlockEmpty>
{{#if (eq baseKey.id '')}}
There are currently no {{pluralize options.item}} in this backend.
{{else}}
{{#if filterIsFolder}}
{{#if (eq filter baseKey.id)}}
There are no {{pluralize options.item}} under <code>{{or filter}}</code>.
{{else}}
We couldn't find a folder matching <code>{{filter}}</code>.
{{/if}}
{{/if}}
{{/if}}
</BlockEmpty>
{{/if}}
{{/with}}
{{#if (gt model.meta.lastPage 1) }}

View file

@ -5,7 +5,6 @@
mode=mode
root=backendCrumb
capabilities=capabilities
onDataChange=(action "hasChanges")
onRefresh=(action "refresh")
onToggleAdvancedEdit=(action "toggleAdvancedEdit")
initialKey=initialKey

View file

@ -21,14 +21,14 @@
</h1>
</p.levelLeft>
</PageHeader>
<div class="box is-sideless has-background-white-bis has-text-grey has-text-centered">
<BlockError>
{{#if model.message}}
<p>{{model.message}}</p>
{{/if}}
{{#each model.errors as |error|}}
<p>{{error}}</p>
{{/each}}
</div>
</BlockError>
{{/if}}
</div>
</section>

View file

@ -38,12 +38,6 @@ module('Acceptance | secrets/generic/create', function(hooks) {
await listPage.create();
await editPage.createSecret(kvPath, 'foo', 'bar');
let capabilitiesReq = this.server.passthroughRequests.findBy('url', '/v1/sys/capabilities-self');
assert.equal(
JSON.parse(capabilitiesReq.requestBody).paths,
`${path}/${kvPath}`,
'calls capabilites with the correct path'
);
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page');
assert.ok(showPage.editIsPresent, 'shows the edit button');
});
@ -64,12 +58,6 @@ module('Acceptance | secrets/generic/create', function(hooks) {
await listPage.create();
await editPage.createSecret(kvPath, 'foo', 'bar');
let capabilitiesReq = this.server.passthroughRequests.findBy('url', '/v1/sys/capabilities-self');
assert.equal(
JSON.parse(capabilitiesReq.requestBody).paths,
`${path}/data/${kvPath}`,
'calls capabilites with the correct path'
);
await listPage.visitRoot({ backend: path });
assert.equal(listPage.secrets.length, 2, 'lists two secrets in the backend');
});

View file

@ -29,12 +29,7 @@ module('Acceptance | secrets/secret/create', function(hooks) {
await listPage.create();
await editPage.createSecret(path, 'foo', 'bar');
let capabilitiesReq = this.server.passthroughRequests.findBy('url', '/v1/sys/capabilities-self');
assert.equal(
JSON.parse(capabilitiesReq.requestBody).paths,
`secret/data/${path}`,
'calls capabilites with the correct path'
);
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page');
assert.ok(showPage.editIsPresent, 'shows the edit button');
});
@ -55,12 +50,9 @@ module('Acceptance | secrets/secret/create', function(hooks) {
await listPage.create();
await editPage.createSecret(secretPath, 'foo', 'bar');
let capabilitiesReq = this.server.passthroughRequests.findBy('url', '/v1/sys/capabilities-self');
assert.equal(
JSON.parse(capabilitiesReq.requestBody).paths,
`${enginePath}/${secretPath}`,
'calls capabilites with the correct path'
);
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'redirects to the show page');
assert.ok(showPage.editIsPresent, 'shows the edit button');
});
test('it redirects to the path ending in / for list pages', async function(assert) {

View file

@ -1,18 +1,32 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find, settled } from '@ember/test-helpers';
import { resolve } from 'rsvp';
import { run } from '@ember/runloop';
import Service from '@ember/service';
import hbs from 'htmlbars-inline-precompile';
let capabilities;
const storeService = Service.extend({
queryRecord() {
return resolve(capabilities);
},
});
module('Integration | Component | secret edit', function(hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function() {
capabilities = null;
this.codeMirror = this.owner.lookup('service:code-mirror');
run(() => {
this.owner.unregister('service:store');
this.owner.register('service:store', storeService);
});
});
test('it disables JSON toggle in show mode when is an advanced format', async function(assert) {
this.set('mode', 'show');
this.set('key', {
this.set('model', {
secretData: {
int: 2,
null: null,
@ -20,13 +34,13 @@ module('Integration | Component | secret edit', function(hooks) {
},
});
await render(hbs`{{secret-edit mode=mode key=key }}`);
await render(hbs`{{secret-edit mode=mode model=model }}`);
assert.dom('[data-test-secret-json-toggle]').isDisabled();
});
test('it does JSON toggle in show mode when showing string data', async function(assert) {
this.set('mode', 'show');
this.set('key', {
this.set('model', {
secretData: {
int: '2',
null: 'null',
@ -34,13 +48,13 @@ module('Integration | Component | secret edit', function(hooks) {
},
});
await render(hbs`{{secret-edit mode=mode key=key }}`);
await render(hbs`{{secret-edit mode=mode model=model }}`);
assert.dom('[data-test-secret-json-toggle]').isNotDisabled();
});
test('it shows an error when creating and data is not an object', async function(assert) {
this.set('mode', 'create');
this.set('key', {
this.set('model', {
secretData: {
int: '2',
null: 'null',
@ -48,7 +62,7 @@ module('Integration | Component | secret edit', function(hooks) {
},
});
await render(hbs`{{secret-edit mode=mode key=key preferAdvancedEdit=true }}`);
await render(hbs`{{secret-edit mode=mode model=model preferAdvancedEdit=true }}`);
let instance = this.codeMirror.instanceFor(find('[data-test-component=json-editor]').id);
instance.setValue(JSON.stringify([{ foo: 'bar' }]));
await settled();
@ -57,10 +71,10 @@ module('Integration | Component | secret edit', function(hooks) {
test('it shows an error when editing and the data is not an object', async function(assert) {
this.set('mode', 'edit');
this.set('capabilities', {
capabilities = {
canUpdate: true,
});
this.set('key', {
};
this.set('model', {
secretData: {
int: '2',
null: 'null',
@ -68,7 +82,7 @@ module('Integration | Component | secret edit', function(hooks) {
},
});
await render(hbs`{{secret-edit capabilities=capabilities mode=mode key=key preferAdvancedEdit=true }}`);
await render(hbs`{{secret-edit mode=mode model=model preferAdvancedEdit=true }}`);
let instance = this.codeMirror.instanceFor(find('[data-test-component=json-editor]').id);
instance.setValue(JSON.stringify([{ foo: 'bar' }]));
await settled();

View file

@ -1,26 +1,70 @@
import { resolve } from 'rsvp';
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import apiStub from 'vault/tests/helpers/noop-all-api-requests';
module('Unit | Adapter | secret-v2', function(hooks) {
setupTest(hooks);
test('secret api urls', function(assert) {
let url, method, options;
let adapter = this.owner.factoryFor('adapter:secret-v2').create({
ajax: (...args) => {
[url, method, options] = args;
return resolve({});
hooks.beforeEach(function() {
this.server = apiStub();
});
hooks.afterEach(function() {
this.server.shutdown();
});
[
['query', null, {}, { id: '', backend: 'secret' }, 'GET', '/v1/secret/metadata/?list=true'],
['queryRecord', null, {}, { id: 'foo', backend: 'secret' }, 'GET', '/v1/secret/metadata/foo'],
[
'updateRecord',
{
serializerFor() {
return {
serializeIntoHash() {},
};
},
},
{},
{
id: 'foo',
belongsTo() {
return 'secret';
},
},
'PUT',
'/v1/secret/metadata/foo',
],
[
'deleteRecord',
{
serializerFor() {
return {
serializeIntoHash() {},
};
},
},
{},
{
id: 'foo',
belongsTo() {
return 'secret';
},
},
'DELETE',
'/v1/secret/metadata/foo',
],
].forEach(([adapterMethod, store, type, queryOrSnapshot, expectedHttpVerb, expectedURL]) => {
test(`secret-v2: ${adapterMethod}`, function(assert) {
let adapter = this.owner.lookup('adapter:secret-v2');
adapter[adapterMethod](store, type, queryOrSnapshot);
let { url, method } = this.server.handledRequests[0];
assert.equal(url, expectedURL, `${adapterMethod} calls the correct url: ${expectedURL}`);
assert.equal(
method,
expectedHttpVerb,
`${adapterMethod} uses the correct http verb: ${expectedHttpVerb}`
);
});
adapter.query({}, 'secret', { id: '', backend: 'secret' });
assert.equal(url, '/v1/secret/metadata/', 'query generic url OK');
assert.equal('GET', method, 'query generic method OK');
assert.deepEqual(options, { data: { list: true } }, 'query generic url OK');
adapter.queryRecord({}, 'secret', { id: 'foo', backend: 'secret' });
assert.equal(url, '/v1/secret/data/foo', 'queryRecord generic url OK');
assert.equal('GET', method, 'queryRecord generic method OK');
});
});

View file

@ -0,0 +1,86 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import apiStub from 'vault/tests/helpers/noop-all-api-requests';
module('Unit | Adapter | secret-v2-version', function(hooks) {
setupTest(hooks);
hooks.beforeEach(function() {
this.server = apiStub();
});
hooks.afterEach(function() {
this.server.shutdown();
});
let fakeStore = {
peekRecord() {
return {
reload() {},
};
},
};
[
[
'findRecord with version',
'findRecord',
[null, {}, JSON.stringify(['secret', 'foo', '2']), {}],
'GET',
'/v1/secret/data/foo?version=2',
],
[
'v2DeleteOperation with delete',
'v2DeleteOperation',
[fakeStore, JSON.stringify(['secret', 'foo', '2']), 'delete'],
'POST',
'/v1/secret/delete/foo',
{ versions: ['2'] },
],
[
'v2DeleteOperation with destroy',
'v2DeleteOperation',
[fakeStore, JSON.stringify(['secret', 'foo', '2']), 'destroy'],
'POST',
'/v1/secret/destroy/foo',
{ versions: ['2'] },
],
[
'v2DeleteOperation with destroy',
'v2DeleteOperation',
[fakeStore, JSON.stringify(['secret', 'foo', '2']), 'undelete'],
'POST',
'/v1/secret/undelete/foo',
{ versions: ['2'] },
],
[
'updateRecord makes calls to correct url',
'updateRecord',
[
{
serializerFor() {
return { serializeIntoHash() {} };
},
},
{},
{ id: JSON.stringify(['secret', 'foo', '2']) },
],
'PUT',
'/v1/secret/data/foo',
],
].forEach(([testName, adapterMethod, args, expectedHttpVerb, expectedURL, exptectedRequestBody]) => {
test(`${testName}`, function(assert) {
let adapter = this.owner.lookup('adapter:secret-v2-version');
adapter[adapterMethod](...args);
let { url, method, requestBody } = this.server.handledRequests[0];
assert.equal(url, expectedURL, `${adapterMethod} calls the correct url: ${expectedURL}`);
assert.equal(
method,
expectedHttpVerb,
`${adapterMethod} uses the correct http verb: ${expectedHttpVerb}`
);
if (exptectedRequestBody) {
assert.deepEqual(JSON.parse(requestBody), exptectedRequestBody);
}
});
});
});

View file

@ -119,6 +119,7 @@ module('Unit | Machine | tutorial-machine', function() {
},
actions: [
'showTutorialWhenAuthenticated',
'clearFeatureData',
{ type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' },
{ type: 'render', level: 'feature', component: 'wizard/features-selection' },
],

View file

@ -146,6 +146,7 @@ module('Unit | Service | wizard', function(hooks) {
],
storage: [
{ key: STORAGE_KEYS.FEATURE_STATE, value: undefined },
{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: undefined },
{ key: STORAGE_KEYS.FEATURE_LIST, value: undefined },
{ key: STORAGE_KEYS.COMPONENT_STATE, value: undefined },
{ key: STORAGE_KEYS.TUTORIAL_STATE, value: 'active.select' },
@ -155,6 +156,19 @@ module('Unit | Service | wizard', function(hooks) {
],
},
},
{
method: 'clearFeatureData',
args: [],
expectedResults: {
props: [{ prop: 'currentMachine', value: null }, { prop: 'featureMachineHistory', value: null }],
storage: [
{ key: STORAGE_KEYS.FEATURE_STATE, value: undefined },
{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: undefined },
{ key: STORAGE_KEYS.FEATURE_LIST, value: undefined },
{ key: STORAGE_KEYS.COMPONENT_STATE, value: undefined },
],
},
},
{
method: 'saveState',
args: [
@ -194,6 +208,75 @@ module('Unit | Service | wizard', function(hooks) {
props: [{ prop: 'currentState', value: 'login' }],
},
},
{
method: 'saveFeatureHistory',
args: ['idle'],
properties: { featureList: ['policies', 'tools'] },
storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }],
expectedResults: {
props: [{ prop: 'featureMachineHistory', value: null }],
},
},
{
method: 'saveFeatureHistory',
args: ['idle'],
properties: { featureList: ['policies', 'tools'] },
storage: [],
expectedResults: {
props: [{ prop: 'featureMachineHistory', value: ['idle'] }],
},
},
{
method: 'saveFeatureHistory',
args: ['idle'],
properties: { featureList: ['policies', 'tools'] },
storage: [],
expectedResults: {
props: [{ prop: 'featureMachineHistory', value: ['idle'] }],
},
},
{
method: 'saveFeatureHistory',
args: ['idle'],
properties: { featureMachineHistory: [], featureList: ['policies', 'tools'] },
storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }],
expectedResults: {
props: [{ prop: 'featureMachineHistory', value: ['idle'] }],
storage: [{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle'] }],
},
},
{
method: 'saveFeatureHistory',
args: ['idle'],
properties: { featureMachineHistory: null, featureList: ['policies', 'tools'] },
storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }],
expectedResults: {
props: [{ prop: 'featureMachineHistory', value: null }],
},
},
{
method: 'saveFeatureHistory',
args: ['create'],
properties: { featureMachineHistory: ['idle'], featureList: ['policies', 'tools'] },
storage: [{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] }],
expectedResults: {
props: [{ prop: 'featureMachineHistory', value: ['idle', 'create'] }],
storage: [{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle', 'create'] }],
},
},
{
method: 'saveFeatureHistory',
args: ['create'],
properties: { featureMachineHistory: ['idle'], featureList: ['policies', 'tools'] },
storage: [
{ key: STORAGE_KEYS.COMPLETED_FEATURES, value: ['secrets'] },
{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle', 'create'] },
],
expectedResults: {
props: [{ prop: 'featureMachineHistory', value: ['idle', 'create'] }],
storage: [{ key: STORAGE_KEYS.FEATURE_STATE_HISTORY, value: ['idle', 'create'] }],
},
},
{
method: 'startFeature',
args: [],

View file

@ -1072,14 +1072,7 @@ func (i *IdentityStore) deleteAliasesInEntityInTxn(txn *memdb.Txn, entity *ident
// Remove identity indices from aliases table for those that needs to
// be removed
for _, alias := range removeList {
aliasToBeRemoved, err := i.MemDBAliasByIDInTxn(txn, alias.ID, false, false)
if err != nil {
return err
}
if aliasToBeRemoved == nil {
return fmt.Errorf("alias was not indexed")
}
err = i.MemDBDeleteAliasByIDInTxn(txn, aliasToBeRemoved.ID, false)
err := i.MemDBDeleteAliasByIDInTxn(txn, alias.ID, false)
if err != nil {
return err
}

View file

@ -163,7 +163,11 @@ This endpoint creates or updates a named role.
- `allowed_extensions` `(string: "")` Specifies a comma-separated list of
extensions that certificates can have when signed. To allow any critical
options, set this to an empty string. Will default to allowing any extensions.
options, set this to an empty string. Will default to allowing any
extensions. For the list of extensions, take a look at the [sshd
manual's](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT)
`AUTHORIZED_KEYS FILE FORMAT` section. You should add a `permit-` before the
name of extension to allow it.
- `default_critical_options` `(map<string|string>: "")`  Specifies a map of
critical options certificates should have if none are provided when signing.