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
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 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 } });
},
});

View File

@@ -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,
},

View File

@@ -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;
},

View File

@@ -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`;

View File

@@ -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;
},

View File

@@ -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');
},

View File

@@ -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')

View File

@@ -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;
},

View File

@@ -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;
},

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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 });

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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';

View File

@@ -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);
},

View File

@@ -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 = [];

View File

@@ -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) {

View File

@@ -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');

View File

@@ -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;

View File

@@ -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,

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

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-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",

View File

@@ -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"]

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 { 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`
);
});
});

View File

@@ -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==