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:
Matthew Irish 2019-03-01 10:08:30 -06:00 committed by GitHub
parent 9617832784
commit b585c20d06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 248 additions and 133 deletions

View File

@ -2,11 +2,12 @@ import { assign } from '@ember/polyfills';
import { get, set } from '@ember/object'; import { get, set } from '@ember/object';
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
import DS from 'ember-data'; import DS from 'ember-data';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
url(path) { url(path) {
const url = `${this.buildURL()}/auth`; const url = `${this.buildURL()}/auth`;
return path ? url + '/' + path : url; return path ? url + '/' + encodePath(path) : url;
}, },
// used in updateRecord on the model#tune action // used in updateRecord on the model#tune action
@ -58,6 +59,6 @@ export default ApplicationAdapter.extend({
}, },
exchangeOIDC(path, state, code) { 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 } });
}, },
}); });

View File

@ -1,14 +1,15 @@
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
revokePrefix(prefix) { revokePrefix(prefix) {
let url = this.buildURL() + '/leases/revoke-prefix/' + prefix; let url = this.buildURL() + '/leases/revoke-prefix/' + encodePath(prefix);
url = url.replace(/\/$/, ''); url = url.replace(/\/$/, '');
return this.ajax(url, 'PUT'); return this.ajax(url, 'PUT');
}, },
forceRevokePrefix(prefix) { forceRevokePrefix(prefix) {
let url = this.buildURL() + '/leases/revoke-prefix/' + prefix; let url = this.buildURL() + '/leases/revoke-prefix/' + encodePath(prefix);
url = url.replace(/\/$/, ''); url = url.replace(/\/$/, '');
return this.ajax(url, 'PUT'); return this.ajax(url, 'PUT');
}, },
@ -43,7 +44,7 @@ export default ApplicationAdapter.extend({
query(store, type, query) { query(store, type, query) {
const prefix = query.prefix || ''; const prefix = query.prefix || '';
return this.ajax(this.buildURL() + '/leases/lookup/' + prefix, 'GET', { return this.ajax(this.buildURL() + '/leases/lookup/' + encodePath(prefix), 'GET', {
data: { data: {
list: true, list: true,
}, },

View File

@ -1,5 +1,6 @@
import { assign } from '@ember/polyfills'; import { assign } from '@ember/polyfills';
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
namespace: 'v1', namespace: 'v1',
@ -31,9 +32,9 @@ export default ApplicationAdapter.extend({
}, },
urlForRole(backend, id) { urlForRole(backend, id) {
let url = `${this.buildURL()}/${backend}/roles`; let url = `${this.buildURL()}/${encodePath(backend)}/roles`;
if (id) { if (id) {
url = url + '/' + id; url = url + '/' + encodePath(id);
} }
return url; return url;
}, },

View File

@ -1,12 +1,14 @@
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { get } from '@ember/object'; import { get } from '@ember/object';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
router: service(), router: service(),
findRecord(store, type, id, snapshot) { findRecord(store, type, id, snapshot) {
let [path, role] = JSON.parse(id); let [path, role] = JSON.parse(id);
path = encodePath(path);
let namespace = get(snapshot, 'adapterOptions.namespace'); let namespace = get(snapshot, 'adapterOptions.namespace');
let url = `/v1/auth/${path}/oidc/auth_url`; let url = `/v1/auth/${path}/oidc/auth_url`;

View File

@ -1,5 +1,6 @@
import { assign } from '@ember/polyfills'; import { assign } from '@ember/polyfills';
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
namespace: 'v1', namespace: 'v1',
@ -31,9 +32,9 @@ export default ApplicationAdapter.extend({
}, },
urlForRole(backend, id) { urlForRole(backend, id) {
let url = `${this.buildURL()}/${backend}/roles`; let url = `${this.buildURL()}/${encodePath(backend)}/roles`;
if (id) { if (id) {
url = url + '/' + id; url = url + '/' + encodePath(id);
} }
return url; return url;
}, },

View File

@ -1,6 +1,7 @@
import { assign } from '@ember/polyfills'; import { assign } from '@ember/polyfills';
import { resolve, allSettled } from 'rsvp'; import { resolve, allSettled } from 'rsvp';
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
namespace: 'v1', namespace: 'v1',
@ -34,9 +35,9 @@ export default ApplicationAdapter.extend({
}, },
urlForRole(backend, id) { urlForRole(backend, id) {
let url = `${this.buildURL()}/${backend}/roles`; let url = `${this.buildURL()}/${encodePath(backend)}/roles`;
if (id) { if (id) {
url = url + '/' + id; url = url + '/' + encodePath(id);
} }
return url; return url;
}, },
@ -84,7 +85,7 @@ export default ApplicationAdapter.extend({
findAllZeroAddress(store, query) { findAllZeroAddress(store, query) {
const { backend } = query; const { backend } = query;
const url = `/v1/${backend}/config/zeroaddress`; const url = `/v1/${encodePath(backend)}/config/zeroaddress`;
return this.ajax(url, 'GET'); return this.ajax(url, 'GET');
}, },

View File

@ -1,16 +1,17 @@
import { assign } from '@ember/polyfills'; import { assign } from '@ember/polyfills';
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
url(path) { url(path) {
const url = `${this.buildURL()}/mounts`; const url = `${this.buildURL()}/mounts`;
return path ? url + '/' + path : url; return path ? url + '/' + encodePath(path) : url;
}, },
internalURL(path) { internalURL(path) {
let url = `/${this.urlPrefix()}/internal/ui/mounts`; let url = `/${this.urlPrefix()}/internal/ui/mounts`;
if (path) { if (path) {
url = `${url}/${path}`; url = `${url}/${encodePath(path)}`;
} }
return url; return url;
}, },
@ -38,14 +39,14 @@ export default ApplicationAdapter.extend({
findRecord(store, type, path, snapshot) { findRecord(store, type, path, snapshot) {
if (snapshot.attr('type') === 'ssh') { if (snapshot.attr('type') === 'ssh') {
return this.ajax(`/v1/${path}/config/ca`, 'GET'); return this.ajax(`/v1/${encodePath(path)}/config/ca`, 'GET');
} }
return; return;
}, },
queryRecord(store, type, query) { queryRecord(store, type, query) {
if (query.type === 'aws') { 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 + '/'; resp.path = query.backend + '/';
return resp; return resp;
}); });
@ -61,25 +62,25 @@ export default ApplicationAdapter.extend({
if (apiPath) { if (apiPath) {
const serializer = store.serializerFor(type.modelName); const serializer = store.serializerFor(type.modelName);
const data = serializer.serialize(snapshot); 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 }); return this.ajax(`/v1/${path}/${apiPath}`, options.isDelete ? 'DELETE' : 'POST', { data });
} }
}, },
saveAWSRoot(store, type, snapshot) { saveAWSRoot(store, type, snapshot) {
let { data } = snapshot.adapterOptions; let { data } = snapshot.adapterOptions;
const path = snapshot.id; const path = encodePath(snapshot.id);
return this.ajax(`/v1/${path}/config/root`, 'POST', { data }); return this.ajax(`/v1/${path}/config/root`, 'POST', { data });
}, },
saveAWSLease(store, type, snapshot) { saveAWSLease(store, type, snapshot) {
let { data } = snapshot.adapterOptions; let { data } = snapshot.adapterOptions;
const path = snapshot.id; const path = encodePath(snapshot.id);
return this.ajax(`/v1/${path}/config/lease`, 'POST', { data }); return this.ajax(`/v1/${path}/config/lease`, 'POST', { data });
}, },
saveZeroAddressConfig(store, type, snapshot) { saveZeroAddressConfig(store, type, snapshot) {
const path = snapshot.id; const path = encodePath(snapshot.id);
const roles = store const roles = store
.peekAll('role-ssh') .peekAll('role-ssh')
.filterBy('zeroAddress') .filterBy('zeroAddress')

View File

@ -3,13 +3,14 @@ import { isEmpty } from '@ember/utils';
import { get } from '@ember/object'; import { get } from '@ember/object';
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
import DS from 'ember-data'; import DS from 'ember-data';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
namespace: 'v1', namespace: 'v1',
_url(backend, id, infix = 'data') { _url(backend, id, infix = 'data') {
let url = `${this.buildURL()}/${backend}/${infix}/`; let url = `${this.buildURL()}/${encodePath(backend)}/${infix}/`;
if (!isEmpty(id)) { if (!isEmpty(id)) {
url = url + id; url = url + encodePath(id);
} }
return url; return url;
}, },

View File

@ -1,13 +1,14 @@
/* eslint-disable */ /* eslint-disable */
import { isEmpty } from '@ember/utils'; import { isEmpty } from '@ember/utils';
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
namespace: 'v1', namespace: 'v1',
_url(backend, id) { _url(backend, id) {
let url = `${this.buildURL()}/${backend}/metadata/`; let url = `${this.buildURL()}/${encodePath(backend)}/metadata/`;
if (!isEmpty(id)) { if (!isEmpty(id)) {
url = url + id; url = url + encodePath(id);
} }
return url; return url;
}, },

View File

@ -1,5 +1,6 @@
import { isEmpty } from '@ember/utils'; import { isEmpty } from '@ember/utils';
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
namespace: 'v1', namespace: 'v1',
@ -26,9 +27,9 @@ export default ApplicationAdapter.extend({
}, },
urlForSecret(backend, id) { urlForSecret(backend, id) {
let url = `${this.buildURL()}/${backend}/`; let url = `${this.buildURL()}/${encodePath(backend)}/`;
if (!isEmpty(id)) { if (!isEmpty(id)) {
url = url + id; url = url + encodePath(id);
} }
return url; return url;

View File

@ -1,5 +1,6 @@
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
import { pluralize } from 'ember-inflector'; import { pluralize } from 'ember-inflector';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
namespace: 'v1', namespace: 'v1',
@ -47,29 +48,29 @@ export default ApplicationAdapter.extend({
}, },
urlForSecret(backend, id) { urlForSecret(backend, id) {
let url = `${this.buildURL()}/${backend}/keys/`; let url = `${this.buildURL()}/${encodePath(backend)}/keys/`;
if (id) { if (id) {
url += id; url += encodePath(id);
} }
return url; return url;
}, },
urlForAction(action, backend, id, param) { urlForAction(action, backend, id, param) {
let urlBase = `${this.buildURL()}/${backend}/${action}`; let urlBase = `${this.buildURL()}/${encodePath(backend)}/${action}`;
// these aren't key-specific // these aren't key-specific
if (action === 'hash' || action === 'random') { if (action === 'hash' || action === 'random') {
return urlBase; return urlBase;
} }
if (action === 'datakey' && param) { if (action === 'datakey' && param) {
// datakey action has `wrapped` or `plaintext` as part of the url // datakey action has `wrapped` or `plaintext` as part of the url
return `${urlBase}/${param}/${id}`; return `${urlBase}/${param}/${encodePath(id)}`;
} }
if (action === 'export' && param) { if (action === 'export' && param) {
let [type, version] = param; let [type, version] = param;
const exportBase = `${urlBase}/${type}-key/${id}`; const exportBase = `${urlBase}/${type}-key/${encodePath(id)}`;
return version ? `${exportBase}/${version}` : exportBase; return version ? `${exportBase}/${version}` : exportBase;
} }
return `${urlBase}/${id}`; return `${urlBase}/${encodePath(id)}`;
}, },
optionsForQuery(id) { optionsForQuery(id) {

View File

@ -1,6 +1,7 @@
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import Component from '@ember/component'; import Component from '@ember/component';
import utils from 'vault/lib/key-utils'; import utils from 'vault/lib/key-utils';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default Component.extend({ export default Component.extend({
tagName: 'nav', tagName: 'nav',
@ -31,7 +32,7 @@ export default Component.extend({
let crumbs = []; let crumbs = [];
const root = this.get('root'); const root = this.get('root');
const baseKey = this.get('baseKey.display') || this.get('baseKey.id'); 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) { if (root) {
crumbs.push(root); crumbs.push(root);

View File

@ -1,6 +1,7 @@
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Component from '@ember/component'; import Component from '@ember/component';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import { encodePath } from 'vault/utils/path-encoding-helpers';
let LinkedBlockComponent = Component.extend({ let LinkedBlockComponent = Component.extend({
router: service(), router: service(),
@ -11,6 +12,8 @@ let LinkedBlockComponent = Component.extend({
queryParams: null, queryParams: null,
encode: false,
click(event) { click(event) {
const $target = this.$(event.target); const $target = this.$(event.target);
const isAnchorOrButton = const isAnchorOrButton =
@ -19,7 +22,15 @@ let LinkedBlockComponent = Component.extend({
$target.closest('button', event.currentTarget).length > 0 || $target.closest('button', event.currentTarget).length > 0 ||
$target.closest('a', event.currentTarget).length > 0; $target.closest('a', event.currentTarget).length > 0;
if (!isAnchorOrButton) { 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'); const queryParams = this.get('queryParams');
if (queryParams) { if (queryParams) {
params.push({ queryParams }); params.push({ queryParams });

View File

@ -5,6 +5,7 @@ import Component from '@ember/component';
import utils from 'vault/lib/key-utils'; import utils from 'vault/lib/key-utils';
import keys from 'vault/lib/keycodes'; import keys from 'vault/lib/keycodes';
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert'; import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
import { encodePath } from 'vault/utils/path-encoding-helpers';
const routeFor = function(type, mode) { const routeFor = function(type, mode) {
const MODES = { const MODES = {
@ -43,8 +44,15 @@ export default Component.extend(FocusOnInsertMixin, {
filterMatchesKey: null, filterMatchesKey: null,
firstPartialMatch: null, firstPartialMatch: null,
transitionToRoute: function() { transitionToRoute(...args) {
this.get('router').transitionTo(...arguments); let params = args.map((param, index) => {
if (index === 0 || typeof param !== 'string') {
return param;
}
return encodePath(param);
});
this.get('router').transitionTo(...params);
}, },
shouldFocus: false, shouldFocus: false,

View File

@ -1,5 +1,6 @@
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import Component from '@ember/component'; import Component from '@ember/component';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export function linkParams({ mode, secret, queryParams }) { export function linkParams({ mode, secret, queryParams }) {
let params; let params;
@ -8,7 +9,7 @@ export function linkParams({ mode, secret, queryParams }) {
if (!secret || secret === ' ') { if (!secret || secret === ' ') {
params = [route + '-root']; params = [route + '-root'];
} else { } else {
params = [route, secret]; params = [route, encodePath(secret)];
} }
if (queryParams) { if (queryParams) {

View File

@ -2,19 +2,12 @@ import { inject as service } from '@ember/service';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import Controller, { inject as controller } from '@ember/controller'; import Controller, { inject as controller } from '@ember/controller';
import utils from 'vault/lib/key-utils'; 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(), flashMessages: service(),
store: service(), store: service(),
clusterController: controller('vault.cluster'), clusterController: controller('vault.cluster'),
queryParams: {
page: 'page',
pageFilter: 'pageFilter',
},
page: 1,
pageFilter: null,
filter: null,
backendCrumb: computed(function() { backendCrumb: computed(function() {
return { return {
@ -27,24 +20,6 @@ export default Controller.extend({
isLoading: false, 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() { filterIsFolder: computed('filter', function() {
return !!utils.keyIsFolder(this.get('filter')); return !!utils.keyIsFolder(this.get('filter'));
}), }),
@ -65,14 +40,6 @@ export default Controller.extend({
}), }),
actions: { actions: {
setFilter(val) {
this.set('filter', val);
},
setFilterFocus(bool) {
this.set('filterFocused', bool);
},
revokePrefix(prefix, isForce) { revokePrefix(prefix, isForce) {
const adapter = this.get('store').adapterFor('lease'); const adapter = this.get('store').adapterFor('lease');
const method = isForce ? 'forceRevokePrefix' : 'revokePrefix'; const method = isForce ? 'forceRevokePrefix' : 'revokePrefix';

View File

@ -4,50 +4,19 @@ import Controller from '@ember/controller';
import utils from 'vault/lib/key-utils'; import utils from 'vault/lib/key-utils';
import BackendCrumbMixin from 'vault/mixins/backend-crumb'; import BackendCrumbMixin from 'vault/mixins/backend-crumb';
import WithNavToNearestAncestor from 'vault/mixins/with-nav-to-nearest-ancestor'; 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(), flashMessages: service(),
queryParams: ['page', 'pageFilter', 'tab'], queryParams: ['page', 'pageFilter', 'tab'],
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() { filterIsFolder: computed('filter', function() {
return !!utils.keyIsFolder(this.get('filter')); return !!utils.keyIsFolder(this.get('filter'));
}), }),
actions: { actions: {
setFilter(val) {
this.set('filter', val);
},
setFilterFocus(bool) {
this.set('filterFocused', bool);
},
chooseAction(action) { chooseAction(action) {
this.set('selectedAction', action); this.set('selectedAction', action);
}, },

View File

@ -56,11 +56,14 @@ export function executeUICommand(command, logAndOutput, clearLog, toggleFullscre
} }
export function parseCommand(command, shouldThrow) { 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') { if (args[0] === 'vault') {
args.shift(); args.shift();
} }
args = args.map(decodeURIComponent);
let [method, ...rest] = args; let [method, ...rest] = args;
let path; let path;
let flags = []; let flags = [];

View File

@ -1,5 +1,6 @@
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import Mixin from '@ember/object/mixin'; import Mixin from '@ember/object/mixin';
import escapeStringRegexp from 'escape-string-regexp';
export default Mixin.create({ export default Mixin.create({
queryParams: { queryParams: {
@ -10,6 +11,7 @@ export default Mixin.create({
page: 1, page: 1,
pageFilter: null, pageFilter: null,
filter: null, filter: null,
filterFocused: false,
isLoading: false, isLoading: false,
@ -23,7 +25,7 @@ export default Mixin.create({
var filter = this.get('filter'); var filter = this.get('filter');
var content = this.get('model'); var content = this.get('model');
var filterMatchesKey = this.get('filterMatchesKey'); var filterMatchesKey = this.get('filterMatchesKey');
var re = new RegExp('^' + filter); var re = new RegExp('^' + escapeStringRegexp(filter));
return filterMatchesKey return filterMatchesKey
? null ? null
: content.find(function(key) { : content.find(function(key) {

View File

@ -4,10 +4,13 @@ import Route from '@ember/routing/route';
import { getOwner } from '@ember/application'; import { getOwner } from '@ember/application';
import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends'; import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { normalizePath } from 'vault/utils/path-encoding-helpers';
const SUPPORTED_BACKENDS = supportedSecretBackends(); const SUPPORTED_BACKENDS = supportedSecretBackends();
export default Route.extend({ export default Route.extend({
templateName: 'vault/cluster/secrets/backend/list',
pathHelp: service('path-help'),
queryParams: { queryParams: {
page: { page: {
refreshModel: true, refreshModel: true,
@ -20,13 +23,21 @@ export default Route.extend({
}, },
}, },
templateName: 'vault/cluster/secrets/backend/list', secretParam() {
pathHelp: service('path-help'), let { secret } = this.paramsFor(this.routeName);
return secret ? normalizePath(secret) : '';
},
enginePathParam() {
let { backend } = this.paramsFor('vault.cluster.secrets.backend');
return backend;
},
beforeModel() { beforeModel() {
let owner = getOwner(this); let owner = getOwner(this);
let { secret } = this.paramsFor(this.routeName); let secret = this.secretParam();
let { backend, tab } = this.paramsFor('vault.cluster.secrets.backend'); let backend = this.enginePathParam();
let { tab } = this.paramsFor('vault.cluster.secrets.backend');
let secretEngine = this.store.peekRecord('secret-engine', backend); let secretEngine = this.store.peekRecord('secret-engine', backend);
let type = secretEngine && secretEngine.get('engineType'); let type = secretEngine && secretEngine.get('engineType');
if (!type || !SUPPORTED_BACKENDS.includes(type)) { if (!type || !SUPPORTED_BACKENDS.includes(type)) {
@ -58,8 +69,8 @@ export default Route.extend({
}, },
model(params) { model(params) {
const secret = params.secret ? params.secret : ''; const secret = this.secretParam() || '';
const { backend } = this.paramsFor('vault.cluster.secrets.backend'); const backend = this.enginePathParam();
const backendModel = this.modelFor('vault.cluster.secrets.backend'); const backendModel = this.modelFor('vault.cluster.secrets.backend');
return hash({ return hash({
secret, secret,
@ -89,7 +100,7 @@ export default Route.extend({
afterModel(model) { afterModel(model) {
const { tab } = this.paramsFor(this.routeName); const { tab } = this.paramsFor(this.routeName);
const { backend } = this.paramsFor('vault.cluster.secrets.backend'); const backend = this.enginePathParam();
if (!tab || tab !== 'certs') { if (!tab || tab !== 'certs') {
return; return;
} }
@ -114,7 +125,7 @@ export default Route.extend({
let secretParams = this.paramsFor(this.routeName); let secretParams = this.paramsFor(this.routeName);
let secret = resolvedModel.secret; let secret = resolvedModel.secret;
let model = resolvedModel.secrets; 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 backendModel = this.store.peekRecord('secret-engine', backend);
let has404 = this.get('has404'); let has404 = this.get('has404');
// only clear store cache if this is a new model // only clear store cache if this is a new model
@ -155,8 +166,8 @@ export default Route.extend({
actions: { actions: {
error(error, transition) { error(error, transition) {
let { secret } = this.paramsFor(this.routeName); let secret = this.secretParam();
let { backend } = this.paramsFor('vault.cluster.secrets.backend'); let backend = this.enginePathParam();
let is404 = error.httpStatus === 404; let is404 = error.httpStatus === 404;
let hasModel = this.controllerFor(this.routeName).get('hasModel'); let hasModel = this.controllerFor(this.routeName).get('hasModel');

View File

@ -6,11 +6,20 @@ import Route from '@ember/routing/route';
import utils from 'vault/lib/key-utils'; import utils from 'vault/lib/key-utils';
import { getOwner } from '@ember/application'; import { getOwner } from '@ember/application';
import UnloadModelRoute from 'vault/mixins/unload-model-route'; import UnloadModelRoute from 'vault/mixins/unload-model-route';
import { encodePath, normalizePath } from 'vault/utils/path-encoding-helpers';
export default Route.extend(UnloadModelRoute, { export default Route.extend(UnloadModelRoute, {
pathHelp: service('path-help'), 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) { capabilities(secret) {
const { backend } = this.paramsFor('vault.cluster.secrets.backend'); const backend = this.enginePathParam();
let backendModel = this.modelFor('vault.cluster.secrets.backend'); let backendModel = this.modelFor('vault.cluster.secrets.backend');
let backendType = backendModel.get('engineType'); let backendType = backendModel.get('engineType');
if (backendType === 'kv' || backendType === 'cubbyhole' || backendType === 'generic') { 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' // 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 // perhaps in the future we could recurse _for_ users, but for now, just kick them
// back to the list // back to the list
const { secret } = this.paramsFor(this.routeName); let secret = this.secretParam();
return this.buildModel(secret).then(() => { return this.buildModel(secret).then(() => {
const parentKey = utils.parentKeyForKey(secret); const parentKey = utils.parentKeyForKey(secret);
const mode = this.routeName.split('.').pop(); const mode = this.routeName.split('.').pop();
if (mode === 'edit' && utils.keyIsFolder(secret)) { if (mode === 'edit' && utils.keyIsFolder(secret)) {
if (parentKey) { if (parentKey) {
return this.transitionTo('vault.cluster.secrets.backend.list', parentKey); return this.transitionTo('vault.cluster.secrets.backend.list', encodePath(parentKey));
} else { } else {
return this.transitionTo('vault.cluster.secrets.backend.list-root'); return this.transitionTo('vault.cluster.secrets.backend.list-root');
} }
@ -52,7 +61,8 @@ export default Route.extend(UnloadModelRoute, {
}, },
buildModel(secret) { buildModel(secret) {
const { backend } = this.paramsFor('vault.cluster.secrets.backend'); const backend = this.enginePathParam();
let modelType = this.modelType(backend, secret); let modelType = this.modelType(backend, secret);
if (['secret', 'secret-v2'].includes(modelType)) { if (['secret', 'secret-v2'].includes(modelType)) {
return resolve(); return resolve();
@ -77,10 +87,10 @@ export default Route.extend(UnloadModelRoute, {
}, },
model(params) { model(params) {
let { secret } = params; let secret = this.secretParam();
const { backend } = this.paramsFor('vault.cluster.secrets.backend'); let backend = this.enginePathParam();
let backendModel = this.modelFor('vault.cluster.secrets.backend', backend); let backendModel = this.modelFor('vault.cluster.secrets.backend', backend);
const modelType = this.modelType(backend, secret); let modelType = this.modelType(backend, secret);
if (!secret) { if (!secret) {
secret = '\u0020'; secret = '\u0020';
@ -139,8 +149,8 @@ export default Route.extend(UnloadModelRoute, {
setupController(controller, model) { setupController(controller, model) {
this._super(...arguments); this._super(...arguments);
const { secret } = this.paramsFor(this.routeName); let secret = this.secretParam();
const { backend } = this.paramsFor('vault.cluster.secrets.backend'); let backend = this.enginePathParam();
const preferAdvancedEdit = const preferAdvancedEdit =
this.controllerFor('vault.cluster.secrets.backend').get('preferAdvancedEdit') || false; this.controllerFor('vault.cluster.secrets.backend').get('preferAdvancedEdit') || false;
const backendType = this.backendType(); const backendType = this.backendType();
@ -168,8 +178,8 @@ export default Route.extend(UnloadModelRoute, {
actions: { actions: {
error(error) { error(error) {
const { secret } = this.paramsFor(this.routeName); let secret = this.secretParam();
const { backend } = this.paramsFor('vault.cluster.secrets.backend'); let backend = this.enginePathParam();
set(error, 'keyId', backend + '/' + secret); set(error, 'keyId', backend + '/' + secret);
set(error, 'backend', backend); set(error, 'backend', backend);
return true; return true;

View File

@ -5,6 +5,7 @@ import Service from '@ember/service';
import { getOwner } from '@ember/application'; import { getOwner } from '@ember/application';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { shiftCommandIndex } from 'vault/lib/console-helpers'; import { shiftCommandIndex } from 'vault/lib/console-helpers';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export function sanitizePath(path) { export function sanitizePath(path) {
//remove whitespace + remove trailing and leading slashes //remove whitespace + remove trailing and leading slashes
@ -74,7 +75,7 @@ export default Service.extend({
ajax(operation, path, options = {}) { ajax(operation, path, options = {}) {
let verb = VERBS[operation]; let verb = VERBS[operation];
let adapter = this.adapter(); let adapter = this.adapter();
let url = adapter.buildURL(path); let url = adapter.buildURL(encodePath(path));
let { data, wrapTTL } = options; let { data, wrapTTL } = options;
return adapter.ajax(url, verb, { return adapter.ajax(url, verb, {
data, data,

View File

@ -7,6 +7,7 @@
item.id item.id
class="list-item-row" class="list-item-row"
data-test-secret-link=item.id data-test-secret-link=item.id
encode=true
}} }}
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column is-10"> <div class="column is-10">

View File

@ -7,6 +7,7 @@
item.id item.id
class="list-item-row" class="list-item-row"
data-test-secret-link=item.id data-test-secret-link=item.id
encode=true
}} }}
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column is-10"> <div class="column is-10">

View File

@ -8,6 +8,7 @@
class="list-item-row" class="list-item-row"
data-test-secret-link=item.id data-test-secret-link=item.id
tagName="div" tagName="div"
encode=true
}} }}
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column is-10"> <div class="column is-10">

View File

@ -10,6 +10,7 @@
class="list-item-row" class="list-item-row"
data-test-secret-link=item.id data-test-secret-link=item.id
tagName="div" tagName="div"
encode=true
}} }}
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column is-10"> <div class="column is-10">

View File

@ -7,6 +7,7 @@
item.id item.id
class="list-item-row" class="list-item-row"
data-test-secret-link=item.id data-test-secret-link=item.id
encode=true
}} }}
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column is-10"> <div class="column is-10">

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

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

View File

@ -94,6 +94,7 @@
"ember-source": "~3.4.0", "ember-source": "~3.4.0",
"ember-test-selectors": "^1.0.0", "ember-test-selectors": "^1.0.0",
"ember-truth-helpers": "^2.1.0", "ember-truth-helpers": "^2.1.0",
"escape-string-regexp": "^1.0.5",
"eslint-config-prettier": "^3.1.0", "eslint-config-prettier": "^3.1.0",
"eslint-plugin-ember": "^5.2.0", "eslint-plugin-ember": "^5.2.0",
"eslint-plugin-prettier": "^3.0.0", "eslint-plugin-prettier": "^3.0.0",
@ -106,6 +107,7 @@
"prettier": "^1.14.3", "prettier": "^1.14.3",
"prettier-eslint-cli": "^4.7.1", "prettier-eslint-cli": "^4.7.1",
"qunit-dom": "^0.7.1", "qunit-dom": "^0.7.1",
"route-recognizer": "^0.3.4",
"sass-svg-uri": "^1.0.0", "sass-svg-uri": "^1.0.0",
"string.prototype.endswith": "^0.2.0", "string.prototype.endswith": "^0.2.0",
"string.prototype.startswith": "^0.2.0", "string.prototype.startswith": "^0.2.0",

View File

@ -54,7 +54,7 @@ module('Acceptance | cluster', function(hooks) {
await logout.visit(); 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 = `' const read_rgp_policy = `'
path "sys/policies/rgp" { path "sys/policies/rgp" {
capabilities = ["read"] capabilities = ["read"]

View File

@ -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 { create } from 'ember-cli-page-object';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit'; import { setupApplicationTest } from 'ember-qunit';
@ -177,7 +177,8 @@ module('Acceptance | secrets/secret/create', function(hooks) {
await listPage.visitRoot({ backend: 'secret' }); await listPage.visitRoot({ backend: 'secret' });
await listPage.create(); await listPage.create();
await editPage.createSecret(path, 'foo', 'bar'); 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.equal(currentRouteName(), 'vault.cluster.secrets.backend.list');
assert.ok(currentURL().endsWith('/'), 'redirects to the path ending in a slash'); 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'); assert.ok(showPage.editIsPresent, 'shows the edit button');
await logout.visit(); 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`
);
});
}); });

View File

@ -11452,7 +11452,7 @@ rollup@^0.57.1:
signal-exit "^3.0.2" signal-exit "^3.0.2"
sourcemap-codec "^1.4.1" sourcemap-codec "^1.4.1"
route-recognizer@^0.3.3: route-recognizer@^0.3.3, route-recognizer@^0.3.4:
version "0.3.4" version "0.3.4"
resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3" resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3"
integrity sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g== integrity sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==