mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 11:38:02 +00:00
UI - fix encoding for user-entered paths (#6294)
* directly depend on route-recognizer * add path encode helper using route-recognizer normalizer methods * encode user-entered paths/ids for places we're not using the built-in ember data buildUrl method * encode secret link params * decode params from the url, and encode for linked-block and navigate-input components * add escape-string-regexp * use list-controller mixin and escape the string when contructing new Regex objects * encode paths in the console service * add acceptance tests for kv secrets * make encoding in linked-block an attribute, and use it on secret lists * egp endpoints are enterprise-only, so include 'enterprise' text in the test * fix routing test and exclude single quote from encoding tests * encode cli string before tokenizing * encode auth_path for use with urlFor * add test for single quote via UI input instead of web cli
This commit is contained in:
@@ -2,11 +2,12 @@ import { assign } from '@ember/polyfills';
|
||||
import { get, set } from '@ember/object';
|
||||
import ApplicationAdapter from './application';
|
||||
import DS from 'ember-data';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
url(path) {
|
||||
const url = `${this.buildURL()}/auth`;
|
||||
return path ? url + '/' + path : url;
|
||||
return path ? url + '/' + encodePath(path) : url;
|
||||
},
|
||||
|
||||
// used in updateRecord on the model#tune action
|
||||
@@ -58,6 +59,6 @@ export default ApplicationAdapter.extend({
|
||||
},
|
||||
|
||||
exchangeOIDC(path, state, code) {
|
||||
return this.ajax(`/v1/auth/${path}/oidc/callback`, 'GET', { data: { state, code } });
|
||||
return this.ajax(`/v1/auth/${encodePath(path)}/oidc/callback`, 'GET', { data: { state, code } });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
revokePrefix(prefix) {
|
||||
let url = this.buildURL() + '/leases/revoke-prefix/' + prefix;
|
||||
let url = this.buildURL() + '/leases/revoke-prefix/' + encodePath(prefix);
|
||||
url = url.replace(/\/$/, '');
|
||||
return this.ajax(url, 'PUT');
|
||||
},
|
||||
|
||||
forceRevokePrefix(prefix) {
|
||||
let url = this.buildURL() + '/leases/revoke-prefix/' + prefix;
|
||||
let url = this.buildURL() + '/leases/revoke-prefix/' + encodePath(prefix);
|
||||
url = url.replace(/\/$/, '');
|
||||
return this.ajax(url, 'PUT');
|
||||
},
|
||||
@@ -43,7 +44,7 @@ export default ApplicationAdapter.extend({
|
||||
|
||||
query(store, type, query) {
|
||||
const prefix = query.prefix || '';
|
||||
return this.ajax(this.buildURL() + '/leases/lookup/' + prefix, 'GET', {
|
||||
return this.ajax(this.buildURL() + '/leases/lookup/' + encodePath(prefix), 'GET', {
|
||||
data: {
|
||||
list: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { assign } from '@ember/polyfills';
|
||||
import ApplicationAdapter from './application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
@@ -31,9 +32,9 @@ export default ApplicationAdapter.extend({
|
||||
},
|
||||
|
||||
urlForRole(backend, id) {
|
||||
let url = `${this.buildURL()}/${backend}/roles`;
|
||||
let url = `${this.buildURL()}/${encodePath(backend)}/roles`;
|
||||
if (id) {
|
||||
url = url + '/' + id;
|
||||
url = url + '/' + encodePath(id);
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { get } from '@ember/object';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
router: service(),
|
||||
|
||||
findRecord(store, type, id, snapshot) {
|
||||
let [path, role] = JSON.parse(id);
|
||||
path = encodePath(path);
|
||||
|
||||
let namespace = get(snapshot, 'adapterOptions.namespace');
|
||||
let url = `/v1/auth/${path}/oidc/auth_url`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { assign } from '@ember/polyfills';
|
||||
import ApplicationAdapter from './application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
@@ -31,9 +32,9 @@ export default ApplicationAdapter.extend({
|
||||
},
|
||||
|
||||
urlForRole(backend, id) {
|
||||
let url = `${this.buildURL()}/${backend}/roles`;
|
||||
let url = `${this.buildURL()}/${encodePath(backend)}/roles`;
|
||||
if (id) {
|
||||
url = url + '/' + id;
|
||||
url = url + '/' + encodePath(id);
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { assign } from '@ember/polyfills';
|
||||
import { resolve, allSettled } from 'rsvp';
|
||||
import ApplicationAdapter from './application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
@@ -34,9 +35,9 @@ export default ApplicationAdapter.extend({
|
||||
},
|
||||
|
||||
urlForRole(backend, id) {
|
||||
let url = `${this.buildURL()}/${backend}/roles`;
|
||||
let url = `${this.buildURL()}/${encodePath(backend)}/roles`;
|
||||
if (id) {
|
||||
url = url + '/' + id;
|
||||
url = url + '/' + encodePath(id);
|
||||
}
|
||||
return url;
|
||||
},
|
||||
@@ -84,7 +85,7 @@ export default ApplicationAdapter.extend({
|
||||
|
||||
findAllZeroAddress(store, query) {
|
||||
const { backend } = query;
|
||||
const url = `/v1/${backend}/config/zeroaddress`;
|
||||
const url = `/v1/${encodePath(backend)}/config/zeroaddress`;
|
||||
return this.ajax(url, 'GET');
|
||||
},
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { assign } from '@ember/polyfills';
|
||||
import ApplicationAdapter from './application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
url(path) {
|
||||
const url = `${this.buildURL()}/mounts`;
|
||||
return path ? url + '/' + path : url;
|
||||
return path ? url + '/' + encodePath(path) : url;
|
||||
},
|
||||
|
||||
internalURL(path) {
|
||||
let url = `/${this.urlPrefix()}/internal/ui/mounts`;
|
||||
if (path) {
|
||||
url = `${url}/${path}`;
|
||||
url = `${url}/${encodePath(path)}`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
@@ -38,14 +39,14 @@ export default ApplicationAdapter.extend({
|
||||
|
||||
findRecord(store, type, path, snapshot) {
|
||||
if (snapshot.attr('type') === 'ssh') {
|
||||
return this.ajax(`/v1/${path}/config/ca`, 'GET');
|
||||
return this.ajax(`/v1/${encodePath(path)}/config/ca`, 'GET');
|
||||
}
|
||||
return;
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
if (query.type === 'aws') {
|
||||
return this.ajax(`/v1/${query.backend}/config/lease`, 'GET').then(resp => {
|
||||
return this.ajax(`/v1/${encodePath(query.backend)}/config/lease`, 'GET').then(resp => {
|
||||
resp.path = query.backend + '/';
|
||||
return resp;
|
||||
});
|
||||
@@ -61,25 +62,25 @@ export default ApplicationAdapter.extend({
|
||||
if (apiPath) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot);
|
||||
const path = snapshot.id;
|
||||
const path = encodePath(snapshot.id);
|
||||
return this.ajax(`/v1/${path}/${apiPath}`, options.isDelete ? 'DELETE' : 'POST', { data });
|
||||
}
|
||||
},
|
||||
|
||||
saveAWSRoot(store, type, snapshot) {
|
||||
let { data } = snapshot.adapterOptions;
|
||||
const path = snapshot.id;
|
||||
const path = encodePath(snapshot.id);
|
||||
return this.ajax(`/v1/${path}/config/root`, 'POST', { data });
|
||||
},
|
||||
|
||||
saveAWSLease(store, type, snapshot) {
|
||||
let { data } = snapshot.adapterOptions;
|
||||
const path = snapshot.id;
|
||||
const path = encodePath(snapshot.id);
|
||||
return this.ajax(`/v1/${path}/config/lease`, 'POST', { data });
|
||||
},
|
||||
|
||||
saveZeroAddressConfig(store, type, snapshot) {
|
||||
const path = snapshot.id;
|
||||
const path = encodePath(snapshot.id);
|
||||
const roles = store
|
||||
.peekAll('role-ssh')
|
||||
.filterBy('zeroAddress')
|
||||
|
||||
@@ -3,13 +3,14 @@ import { isEmpty } from '@ember/utils';
|
||||
import { get } from '@ember/object';
|
||||
import ApplicationAdapter from './application';
|
||||
import DS from 'ember-data';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
_url(backend, id, infix = 'data') {
|
||||
let url = `${this.buildURL()}/${backend}/${infix}/`;
|
||||
let url = `${this.buildURL()}/${encodePath(backend)}/${infix}/`;
|
||||
if (!isEmpty(id)) {
|
||||
url = url + id;
|
||||
url = url + encodePath(id);
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/* eslint-disable */
|
||||
import { isEmpty } from '@ember/utils';
|
||||
import ApplicationAdapter from './application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
_url(backend, id) {
|
||||
let url = `${this.buildURL()}/${backend}/metadata/`;
|
||||
let url = `${this.buildURL()}/${encodePath(backend)}/metadata/`;
|
||||
if (!isEmpty(id)) {
|
||||
url = url + id;
|
||||
url = url + encodePath(id);
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isEmpty } from '@ember/utils';
|
||||
import ApplicationAdapter from './application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
@@ -26,9 +27,9 @@ export default ApplicationAdapter.extend({
|
||||
},
|
||||
|
||||
urlForSecret(backend, id) {
|
||||
let url = `${this.buildURL()}/${backend}/`;
|
||||
let url = `${this.buildURL()}/${encodePath(backend)}/`;
|
||||
if (!isEmpty(id)) {
|
||||
url = url + id;
|
||||
url = url + encodePath(id);
|
||||
}
|
||||
|
||||
return url;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
@@ -47,29 +48,29 @@ export default ApplicationAdapter.extend({
|
||||
},
|
||||
|
||||
urlForSecret(backend, id) {
|
||||
let url = `${this.buildURL()}/${backend}/keys/`;
|
||||
let url = `${this.buildURL()}/${encodePath(backend)}/keys/`;
|
||||
if (id) {
|
||||
url += id;
|
||||
url += encodePath(id);
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
urlForAction(action, backend, id, param) {
|
||||
let urlBase = `${this.buildURL()}/${backend}/${action}`;
|
||||
let urlBase = `${this.buildURL()}/${encodePath(backend)}/${action}`;
|
||||
// these aren't key-specific
|
||||
if (action === 'hash' || action === 'random') {
|
||||
return urlBase;
|
||||
}
|
||||
if (action === 'datakey' && param) {
|
||||
// datakey action has `wrapped` or `plaintext` as part of the url
|
||||
return `${urlBase}/${param}/${id}`;
|
||||
return `${urlBase}/${param}/${encodePath(id)}`;
|
||||
}
|
||||
if (action === 'export' && param) {
|
||||
let [type, version] = param;
|
||||
const exportBase = `${urlBase}/${type}-key/${id}`;
|
||||
const exportBase = `${urlBase}/${type}-key/${encodePath(id)}`;
|
||||
return version ? `${exportBase}/${version}` : exportBase;
|
||||
}
|
||||
return `${urlBase}/${id}`;
|
||||
return `${urlBase}/${encodePath(id)}`;
|
||||
},
|
||||
|
||||
optionsForQuery(id) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed } from '@ember/object';
|
||||
import Component from '@ember/component';
|
||||
import utils from 'vault/lib/key-utils';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'nav',
|
||||
@@ -31,7 +32,7 @@ export default Component.extend({
|
||||
let crumbs = [];
|
||||
const root = this.get('root');
|
||||
const baseKey = this.get('baseKey.display') || this.get('baseKey.id');
|
||||
const baseKeyModel = this.get('baseKey.id');
|
||||
const baseKeyModel = encodePath(this.get('baseKey.id'));
|
||||
|
||||
if (root) {
|
||||
crumbs.push(root);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { inject as service } from '@ember/service';
|
||||
import Component from '@ember/component';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
let LinkedBlockComponent = Component.extend({
|
||||
router: service(),
|
||||
@@ -11,6 +12,8 @@ let LinkedBlockComponent = Component.extend({
|
||||
|
||||
queryParams: null,
|
||||
|
||||
encode: false,
|
||||
|
||||
click(event) {
|
||||
const $target = this.$(event.target);
|
||||
const isAnchorOrButton =
|
||||
@@ -19,7 +22,15 @@ let LinkedBlockComponent = Component.extend({
|
||||
$target.closest('button', event.currentTarget).length > 0 ||
|
||||
$target.closest('a', event.currentTarget).length > 0;
|
||||
if (!isAnchorOrButton) {
|
||||
const params = this.get('params');
|
||||
let params = this.get('params');
|
||||
if (this.encode) {
|
||||
params = params.map((param, index) => {
|
||||
if (index === 0 || typeof param !== 'string') {
|
||||
return param;
|
||||
}
|
||||
return encodePath(param);
|
||||
});
|
||||
}
|
||||
const queryParams = this.get('queryParams');
|
||||
if (queryParams) {
|
||||
params.push({ queryParams });
|
||||
|
||||
@@ -5,6 +5,7 @@ import Component from '@ember/component';
|
||||
import utils from 'vault/lib/key-utils';
|
||||
import keys from 'vault/lib/keycodes';
|
||||
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
const routeFor = function(type, mode) {
|
||||
const MODES = {
|
||||
@@ -43,8 +44,15 @@ export default Component.extend(FocusOnInsertMixin, {
|
||||
filterMatchesKey: null,
|
||||
firstPartialMatch: null,
|
||||
|
||||
transitionToRoute: function() {
|
||||
this.get('router').transitionTo(...arguments);
|
||||
transitionToRoute(...args) {
|
||||
let params = args.map((param, index) => {
|
||||
if (index === 0 || typeof param !== 'string') {
|
||||
return param;
|
||||
}
|
||||
return encodePath(param);
|
||||
});
|
||||
|
||||
this.get('router').transitionTo(...params);
|
||||
},
|
||||
|
||||
shouldFocus: false,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { computed } from '@ember/object';
|
||||
import Component from '@ember/component';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export function linkParams({ mode, secret, queryParams }) {
|
||||
let params;
|
||||
@@ -8,7 +9,7 @@ export function linkParams({ mode, secret, queryParams }) {
|
||||
if (!secret || secret === ' ') {
|
||||
params = [route + '-root'];
|
||||
} else {
|
||||
params = [route, secret];
|
||||
params = [route, encodePath(secret)];
|
||||
}
|
||||
|
||||
if (queryParams) {
|
||||
|
||||
@@ -2,19 +2,12 @@ import { inject as service } from '@ember/service';
|
||||
import { computed } from '@ember/object';
|
||||
import Controller, { inject as controller } from '@ember/controller';
|
||||
import utils from 'vault/lib/key-utils';
|
||||
import ListController from 'vault/mixins/list-controller';
|
||||
|
||||
export default Controller.extend({
|
||||
export default Controller.extend(ListController, {
|
||||
flashMessages: service(),
|
||||
store: service(),
|
||||
clusterController: controller('vault.cluster'),
|
||||
queryParams: {
|
||||
page: 'page',
|
||||
pageFilter: 'pageFilter',
|
||||
},
|
||||
|
||||
page: 1,
|
||||
pageFilter: null,
|
||||
filter: null,
|
||||
|
||||
backendCrumb: computed(function() {
|
||||
return {
|
||||
@@ -27,24 +20,6 @@ export default Controller.extend({
|
||||
|
||||
isLoading: false,
|
||||
|
||||
filterMatchesKey: computed('filter', 'model', 'model.[]', function() {
|
||||
var filter = this.get('filter');
|
||||
var content = this.get('model');
|
||||
return !!(content.length && content.findBy('id', filter));
|
||||
}),
|
||||
|
||||
firstPartialMatch: computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() {
|
||||
var filter = this.get('filter');
|
||||
var content = this.get('model');
|
||||
var filterMatchesKey = this.get('filterMatchesKey');
|
||||
var re = new RegExp('^' + filter);
|
||||
return filterMatchesKey
|
||||
? null
|
||||
: content.find(function(key) {
|
||||
return re.test(key.get('id'));
|
||||
});
|
||||
}),
|
||||
|
||||
filterIsFolder: computed('filter', function() {
|
||||
return !!utils.keyIsFolder(this.get('filter'));
|
||||
}),
|
||||
@@ -65,14 +40,6 @@ export default Controller.extend({
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setFilter(val) {
|
||||
this.set('filter', val);
|
||||
},
|
||||
|
||||
setFilterFocus(bool) {
|
||||
this.set('filterFocused', bool);
|
||||
},
|
||||
|
||||
revokePrefix(prefix, isForce) {
|
||||
const adapter = this.get('store').adapterFor('lease');
|
||||
const method = isForce ? 'forceRevokePrefix' : 'revokePrefix';
|
||||
|
||||
@@ -4,50 +4,19 @@ import Controller from '@ember/controller';
|
||||
import utils from 'vault/lib/key-utils';
|
||||
import BackendCrumbMixin from 'vault/mixins/backend-crumb';
|
||||
import WithNavToNearestAncestor from 'vault/mixins/with-nav-to-nearest-ancestor';
|
||||
import ListController from 'vault/mixins/list-controller';
|
||||
|
||||
export default Controller.extend(BackendCrumbMixin, WithNavToNearestAncestor, {
|
||||
export default Controller.extend(ListController, BackendCrumbMixin, WithNavToNearestAncestor, {
|
||||
flashMessages: service(),
|
||||
queryParams: ['page', 'pageFilter', 'tab'],
|
||||
|
||||
tab: '',
|
||||
page: 1,
|
||||
pageFilter: null,
|
||||
filterFocused: false,
|
||||
|
||||
// set via the route `loading` action
|
||||
isLoading: false,
|
||||
|
||||
filterMatchesKey: computed('filter', 'model', 'model.[]', function() {
|
||||
var filter = this.get('filter');
|
||||
var content = this.get('model');
|
||||
return !!(content.length && content.findBy('id', filter));
|
||||
}),
|
||||
|
||||
firstPartialMatch: computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() {
|
||||
var filter = this.get('filter');
|
||||
var content = this.get('model');
|
||||
var filterMatchesKey = this.get('filterMatchesKey');
|
||||
var re = new RegExp('^' + filter);
|
||||
return filterMatchesKey
|
||||
? null
|
||||
: content.find(function(key) {
|
||||
return re.test(key.get('id'));
|
||||
});
|
||||
}),
|
||||
|
||||
filterIsFolder: computed('filter', function() {
|
||||
return !!utils.keyIsFolder(this.get('filter'));
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setFilter(val) {
|
||||
this.set('filter', val);
|
||||
},
|
||||
|
||||
setFilterFocus(bool) {
|
||||
this.set('filterFocused', bool);
|
||||
},
|
||||
|
||||
chooseAction(action) {
|
||||
this.set('selectedAction', action);
|
||||
},
|
||||
|
||||
@@ -56,11 +56,14 @@ export function executeUICommand(command, logAndOutput, clearLog, toggleFullscre
|
||||
}
|
||||
|
||||
export function parseCommand(command, shouldThrow) {
|
||||
let args = argTokenizer(command);
|
||||
// encode everything but spaces
|
||||
let cmd = encodeURIComponent(command).replace(/%20/g, decodeURIComponent);
|
||||
let args = argTokenizer(cmd);
|
||||
if (args[0] === 'vault') {
|
||||
args.shift();
|
||||
}
|
||||
|
||||
args = args.map(decodeURIComponent);
|
||||
let [method, ...rest] = args;
|
||||
let path;
|
||||
let flags = [];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { computed } from '@ember/object';
|
||||
import Mixin from '@ember/object/mixin';
|
||||
import escapeStringRegexp from 'escape-string-regexp';
|
||||
|
||||
export default Mixin.create({
|
||||
queryParams: {
|
||||
@@ -10,6 +11,7 @@ export default Mixin.create({
|
||||
page: 1,
|
||||
pageFilter: null,
|
||||
filter: null,
|
||||
filterFocused: false,
|
||||
|
||||
isLoading: false,
|
||||
|
||||
@@ -23,7 +25,7 @@ export default Mixin.create({
|
||||
var filter = this.get('filter');
|
||||
var content = this.get('model');
|
||||
var filterMatchesKey = this.get('filterMatchesKey');
|
||||
var re = new RegExp('^' + filter);
|
||||
var re = new RegExp('^' + escapeStringRegexp(filter));
|
||||
return filterMatchesKey
|
||||
? null
|
||||
: content.find(function(key) {
|
||||
|
||||
@@ -4,10 +4,13 @@ import Route from '@ember/routing/route';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { normalizePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
const SUPPORTED_BACKENDS = supportedSecretBackends();
|
||||
|
||||
export default Route.extend({
|
||||
templateName: 'vault/cluster/secrets/backend/list',
|
||||
pathHelp: service('path-help'),
|
||||
queryParams: {
|
||||
page: {
|
||||
refreshModel: true,
|
||||
@@ -20,13 +23,21 @@ export default Route.extend({
|
||||
},
|
||||
},
|
||||
|
||||
templateName: 'vault/cluster/secrets/backend/list',
|
||||
pathHelp: service('path-help'),
|
||||
secretParam() {
|
||||
let { secret } = this.paramsFor(this.routeName);
|
||||
return secret ? normalizePath(secret) : '';
|
||||
},
|
||||
|
||||
enginePathParam() {
|
||||
let { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
return backend;
|
||||
},
|
||||
|
||||
beforeModel() {
|
||||
let owner = getOwner(this);
|
||||
let { secret } = this.paramsFor(this.routeName);
|
||||
let { backend, tab } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
let secret = this.secretParam();
|
||||
let backend = this.enginePathParam();
|
||||
let { tab } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
let secretEngine = this.store.peekRecord('secret-engine', backend);
|
||||
let type = secretEngine && secretEngine.get('engineType');
|
||||
if (!type || !SUPPORTED_BACKENDS.includes(type)) {
|
||||
@@ -58,8 +69,8 @@ export default Route.extend({
|
||||
},
|
||||
|
||||
model(params) {
|
||||
const secret = params.secret ? params.secret : '';
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
const secret = this.secretParam() || '';
|
||||
const backend = this.enginePathParam();
|
||||
const backendModel = this.modelFor('vault.cluster.secrets.backend');
|
||||
return hash({
|
||||
secret,
|
||||
@@ -89,7 +100,7 @@ export default Route.extend({
|
||||
|
||||
afterModel(model) {
|
||||
const { tab } = this.paramsFor(this.routeName);
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
const backend = this.enginePathParam();
|
||||
if (!tab || tab !== 'certs') {
|
||||
return;
|
||||
}
|
||||
@@ -114,7 +125,7 @@ export default Route.extend({
|
||||
let secretParams = this.paramsFor(this.routeName);
|
||||
let secret = resolvedModel.secret;
|
||||
let model = resolvedModel.secrets;
|
||||
let { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
let backend = this.enginePathParam();
|
||||
let backendModel = this.store.peekRecord('secret-engine', backend);
|
||||
let has404 = this.get('has404');
|
||||
// only clear store cache if this is a new model
|
||||
@@ -155,8 +166,8 @@ export default Route.extend({
|
||||
|
||||
actions: {
|
||||
error(error, transition) {
|
||||
let { secret } = this.paramsFor(this.routeName);
|
||||
let { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
let secret = this.secretParam();
|
||||
let backend = this.enginePathParam();
|
||||
let is404 = error.httpStatus === 404;
|
||||
let hasModel = this.controllerFor(this.routeName).get('hasModel');
|
||||
|
||||
|
||||
@@ -6,11 +6,20 @@ import Route from '@ember/routing/route';
|
||||
import utils from 'vault/lib/key-utils';
|
||||
import { getOwner } from '@ember/application';
|
||||
import UnloadModelRoute from 'vault/mixins/unload-model-route';
|
||||
import { encodePath, normalizePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default Route.extend(UnloadModelRoute, {
|
||||
pathHelp: service('path-help'),
|
||||
secretParam() {
|
||||
let { secret } = this.paramsFor(this.routeName);
|
||||
return secret ? normalizePath(secret) : '';
|
||||
},
|
||||
enginePathParam() {
|
||||
let { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
return backend;
|
||||
},
|
||||
capabilities(secret) {
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
const backend = this.enginePathParam();
|
||||
let backendModel = this.modelFor('vault.cluster.secrets.backend');
|
||||
let backendType = backendModel.get('engineType');
|
||||
if (backendType === 'kv' || backendType === 'cubbyhole' || backendType === 'generic') {
|
||||
@@ -37,13 +46,13 @@ export default Route.extend(UnloadModelRoute, {
|
||||
// currently there is no recursive delete for folders in vault, so there's no need to 'edit folders'
|
||||
// perhaps in the future we could recurse _for_ users, but for now, just kick them
|
||||
// back to the list
|
||||
const { secret } = this.paramsFor(this.routeName);
|
||||
let secret = this.secretParam();
|
||||
return this.buildModel(secret).then(() => {
|
||||
const parentKey = utils.parentKeyForKey(secret);
|
||||
const mode = this.routeName.split('.').pop();
|
||||
if (mode === 'edit' && utils.keyIsFolder(secret)) {
|
||||
if (parentKey) {
|
||||
return this.transitionTo('vault.cluster.secrets.backend.list', parentKey);
|
||||
return this.transitionTo('vault.cluster.secrets.backend.list', encodePath(parentKey));
|
||||
} else {
|
||||
return this.transitionTo('vault.cluster.secrets.backend.list-root');
|
||||
}
|
||||
@@ -52,7 +61,8 @@ export default Route.extend(UnloadModelRoute, {
|
||||
},
|
||||
|
||||
buildModel(secret) {
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
const backend = this.enginePathParam();
|
||||
|
||||
let modelType = this.modelType(backend, secret);
|
||||
if (['secret', 'secret-v2'].includes(modelType)) {
|
||||
return resolve();
|
||||
@@ -77,10 +87,10 @@ export default Route.extend(UnloadModelRoute, {
|
||||
},
|
||||
|
||||
model(params) {
|
||||
let { secret } = params;
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
let secret = this.secretParam();
|
||||
let backend = this.enginePathParam();
|
||||
let backendModel = this.modelFor('vault.cluster.secrets.backend', backend);
|
||||
const modelType = this.modelType(backend, secret);
|
||||
let modelType = this.modelType(backend, secret);
|
||||
|
||||
if (!secret) {
|
||||
secret = '\u0020';
|
||||
@@ -139,8 +149,8 @@ export default Route.extend(UnloadModelRoute, {
|
||||
|
||||
setupController(controller, model) {
|
||||
this._super(...arguments);
|
||||
const { secret } = this.paramsFor(this.routeName);
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
let secret = this.secretParam();
|
||||
let backend = this.enginePathParam();
|
||||
const preferAdvancedEdit =
|
||||
this.controllerFor('vault.cluster.secrets.backend').get('preferAdvancedEdit') || false;
|
||||
const backendType = this.backendType();
|
||||
@@ -168,8 +178,8 @@ export default Route.extend(UnloadModelRoute, {
|
||||
|
||||
actions: {
|
||||
error(error) {
|
||||
const { secret } = this.paramsFor(this.routeName);
|
||||
const { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
let secret = this.secretParam();
|
||||
let backend = this.enginePathParam();
|
||||
set(error, 'keyId', backend + '/' + secret);
|
||||
set(error, 'backend', backend);
|
||||
return true;
|
||||
|
||||
@@ -5,6 +5,7 @@ import Service from '@ember/service';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { computed } from '@ember/object';
|
||||
import { shiftCommandIndex } from 'vault/lib/console-helpers';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export function sanitizePath(path) {
|
||||
//remove whitespace + remove trailing and leading slashes
|
||||
@@ -74,7 +75,7 @@ export default Service.extend({
|
||||
ajax(operation, path, options = {}) {
|
||||
let verb = VERBS[operation];
|
||||
let adapter = this.adapter();
|
||||
let url = adapter.buildURL(path);
|
||||
let url = adapter.buildURL(encodePath(path));
|
||||
let { data, wrapTTL } = options;
|
||||
return adapter.ajax(url, verb, {
|
||||
data,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
item.id
|
||||
class="list-item-row"
|
||||
data-test-secret-link=item.id
|
||||
encode=true
|
||||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
item.id
|
||||
class="list-item-row"
|
||||
data-test-secret-link=item.id
|
||||
encode=true
|
||||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
class="list-item-row"
|
||||
data-test-secret-link=item.id
|
||||
tagName="div"
|
||||
encode=true
|
||||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
class="list-item-row"
|
||||
data-test-secret-link=item.id
|
||||
tagName="div"
|
||||
encode=true
|
||||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
item.id
|
||||
class="list-item-row"
|
||||
data-test-secret-link=item.id
|
||||
encode=true
|
||||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
|
||||
38
ui/app/utils/args-tokenizer.js
Normal file
38
ui/app/utils/args-tokenizer.js
Normal file
@@ -0,0 +1,38 @@
|
||||
export default function(argString) {
|
||||
if (Array.isArray(argString)) return argString;
|
||||
|
||||
argString = argString.trim();
|
||||
|
||||
var i = 0;
|
||||
var prevC = null;
|
||||
var c = null;
|
||||
var opening = null;
|
||||
var args = [];
|
||||
|
||||
for (var ii = 0; ii < argString.length; ii++) {
|
||||
prevC = c;
|
||||
c = argString.charAt(ii);
|
||||
|
||||
// split on spaces unless we're in quotes.
|
||||
if (c === ' ' && !opening) {
|
||||
if (!(prevC === ' ')) {
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// don't split the string if we're in matching
|
||||
// opening or closing single and double quotes.
|
||||
if (c === opening) {
|
||||
if (!args[i]) args[i] = '';
|
||||
opening = null;
|
||||
} else if ((c === "'" || c === '"') && argString.indexOf(c, ii + 1) > 0 && !opening) {
|
||||
opening = c;
|
||||
}
|
||||
|
||||
if (!args[i]) args[i] = '';
|
||||
args[i] += c;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
16
ui/app/utils/path-encoding-helpers.js
Normal file
16
ui/app/utils/path-encoding-helpers.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import RouteRecognizer from 'route-recognizer';
|
||||
|
||||
const {
|
||||
Normalizer: { normalizePath, encodePathSegment },
|
||||
} = RouteRecognizer;
|
||||
|
||||
export function encodePath(path) {
|
||||
return path
|
||||
? path
|
||||
.split('/')
|
||||
.map(encodePathSegment)
|
||||
.join('/')
|
||||
: path;
|
||||
}
|
||||
|
||||
export { normalizePath, encodePathSegment };
|
||||
@@ -94,6 +94,7 @@
|
||||
"ember-source": "~3.4.0",
|
||||
"ember-test-selectors": "^1.0.0",
|
||||
"ember-truth-helpers": "^2.1.0",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"eslint-config-prettier": "^3.1.0",
|
||||
"eslint-plugin-ember": "^5.2.0",
|
||||
"eslint-plugin-prettier": "^3.0.0",
|
||||
@@ -106,6 +107,7 @@
|
||||
"prettier": "^1.14.3",
|
||||
"prettier-eslint-cli": "^4.7.1",
|
||||
"qunit-dom": "^0.7.1",
|
||||
"route-recognizer": "^0.3.4",
|
||||
"sass-svg-uri": "^1.0.0",
|
||||
"string.prototype.endswith": "^0.2.0",
|
||||
"string.prototype.startswith": "^0.2.0",
|
||||
|
||||
@@ -54,7 +54,7 @@ module('Acceptance | cluster', function(hooks) {
|
||||
await logout.visit();
|
||||
});
|
||||
|
||||
test('nav item links to first route that user has access to', async function(assert) {
|
||||
test('enterprise nav item links to first route that user has access to', async function(assert) {
|
||||
const read_rgp_policy = `'
|
||||
path "sys/policies/rgp" {
|
||||
capabilities = ["read"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { settled, currentURL, currentRouteName } from '@ember/test-helpers';
|
||||
import { visit, settled, currentURL, currentRouteName } from '@ember/test-helpers';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
@@ -177,7 +177,8 @@ module('Acceptance | secrets/secret/create', function(hooks) {
|
||||
await listPage.visitRoot({ backend: 'secret' });
|
||||
await listPage.create();
|
||||
await editPage.createSecret(path, 'foo', 'bar');
|
||||
await listPage.visit({ backend: 'secret', id: 'foo/bar' });
|
||||
// use visit helper here because ids with / in them get encoded
|
||||
await visit('/vault/secrets/secret/list/foo/bar');
|
||||
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.list');
|
||||
assert.ok(currentURL().endsWith('/'), 'redirects to the path ending in a slash');
|
||||
});
|
||||
@@ -259,4 +260,61 @@ module('Acceptance | secrets/secret/create', function(hooks) {
|
||||
assert.ok(showPage.editIsPresent, 'shows the edit button');
|
||||
await logout.visit();
|
||||
});
|
||||
|
||||
test('paths are properly encoded', async function(assert) {
|
||||
let backend = 'kv';
|
||||
let paths = [
|
||||
'(',
|
||||
')',
|
||||
'"',
|
||||
//"'",
|
||||
'!',
|
||||
'#',
|
||||
'$',
|
||||
'&',
|
||||
'*',
|
||||
'+',
|
||||
'@',
|
||||
'{',
|
||||
'|',
|
||||
'}',
|
||||
'~',
|
||||
'[',
|
||||
'\\',
|
||||
']',
|
||||
'^',
|
||||
'_',
|
||||
].map(char => `${char}some`);
|
||||
assert.expect(paths.length * 2);
|
||||
let secretName = '2';
|
||||
let commands = paths.map(path => `write ${backend}/${path}/${secretName} 3=4`);
|
||||
await consoleComponent.runCommands(['write sys/mounts/kv type=kv', ...commands]);
|
||||
for (let path of paths) {
|
||||
await listPage.visit({ backend, id: path });
|
||||
assert.ok(listPage.secrets.filterBy('text', '2')[0], `${path}: secret is displayed properly`);
|
||||
await listPage.secrets.filterBy('text', '2')[0].click();
|
||||
assert.equal(
|
||||
currentRouteName(),
|
||||
'vault.cluster.secrets.backend.show',
|
||||
`${path}: show page renders correctly`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// the web cli does not handle a single quote in a path, so we test it here via the UI
|
||||
test('creating a secret with a single quote works properly', async function(assert) {
|
||||
await consoleComponent.runCommands('write sys/mounts/kv type=kv');
|
||||
let path = "'some";
|
||||
await listPage.visitRoot({ backend: 'kv' });
|
||||
await listPage.create();
|
||||
await editPage.createSecret(`${path}/2`, 'foo', 'bar');
|
||||
await listPage.visit({ backend: 'kv', id: path });
|
||||
assert.ok(listPage.secrets.filterBy('text', '2')[0], `${path}: secret is displayed properly`);
|
||||
await listPage.secrets.filterBy('text', '2')[0].click();
|
||||
assert.equal(
|
||||
currentRouteName(),
|
||||
'vault.cluster.secrets.backend.show',
|
||||
`${path}: show page renders correctly`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11452,7 +11452,7 @@ rollup@^0.57.1:
|
||||
signal-exit "^3.0.2"
|
||||
sourcemap-codec "^1.4.1"
|
||||
|
||||
route-recognizer@^0.3.3:
|
||||
route-recognizer@^0.3.3, route-recognizer@^0.3.4:
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3"
|
||||
integrity sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==
|
||||
|
||||
Reference in New Issue
Block a user