backport of UI: sanitize namespace input (#23507)

Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
This commit is contained in:
hc-github-team-secure-vault-core
2023-10-04 16:54:56 -04:00
committed by GitHub
parent ddd4a36d83
commit 6c341d14c8
10 changed files with 72 additions and 87 deletions

View File

@@ -7,6 +7,7 @@ import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import Controller, { inject as controller } from '@ember/controller';
import { task, timeout } from 'ember-concurrency';
import { sanitizePath } from 'core/utils/sanitize-path';
export default Controller.extend({
flashMessages: service(),
@@ -24,30 +25,34 @@ export default Controller.extend({
authMethod: '',
oidcProvider: '',
get managedNamespaceChild() {
const fullParam = this.namespaceQueryParam;
const split = fullParam.split('/');
if (split.length > 1) {
split.shift();
return `/${split.join('/')}`;
get namespaceInput() {
const namespaceQP = this.clusterController.namespaceQueryParam;
if (this.managedNamespaceRoot) {
// When managed, the user isn't allowed to edit the prefix `admin/` for their nested namespace
const split = namespaceQP.split('/');
if (split.length > 1) {
split.shift();
return `/${split.join('/')}`;
}
return '';
}
return '';
return namespaceQP;
},
updateManagedNamespace: task(function* (value) {
// debounce
yield timeout(500);
// TODO: Move this to shared fn
const newNamespace = `${this.managedNamespaceRoot}${value}`;
this.namespaceService.setNamespace(newNamespace, true);
this.set('namespaceQueryParam', newNamespace);
}).restartable(),
fullNamespaceFromInput(value) {
const strippedNs = sanitizePath(value);
if (this.managedNamespaceRoot) {
return `${this.managedNamespaceRoot}/${strippedNs}`;
}
return strippedNs;
},
updateNamespace: task(function* (value) {
// debounce
yield timeout(500);
this.namespaceService.setNamespace(value, true);
this.set('namespaceQueryParam', value);
const ns = this.fullNamespaceFromInput(value);
this.namespaceService.setNamespace(ns, true);
this.set('namespaceQueryParam', ns);
}).restartable(),
authSuccess({ isRoot, namespace }) {

View File

@@ -13,14 +13,7 @@ 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
return path.trim().replace(/^\/+|\/+$/g, '');
}
export function ensureTrailingSlash(path) {
return path.replace(/(\w+[^/]$)/g, '$1/');
}
import { sanitizePath, ensureTrailingSlash } from 'core/utils/sanitize-path';
const VERBS = {
read: 'GET',

View File

@@ -23,10 +23,7 @@ import { singularize } from 'ember-inflector';
import { withModelValidations } from 'vault/decorators/model-validations';
import generatedItemAdapter from 'vault/adapters/generated-item-list';
export function sanitizePath(path) {
// remove whitespace + remove trailing and leading slashes
return path.trim().replace(/^\/+|\/+$/g, '');
}
import { sanitizePath } from 'core/utils/sanitize-path';
export default Service.extend({
attrs: null,

View File

@@ -49,52 +49,25 @@
{{/if}}
</Page.header>
{{#unless this.mfaAuthData}}
{{#if this.managedNamespaceRoot}}
<Page.sub-header>
<Toolbar>
<div class="toolbar-namespace-picker" data-test-managed-namespace-toolbar>
<div class="field is-horizontal">
<div class="field-label">
<label class="is-label" for="namespace">Namespace</label>
</div>
<div class="field-label">
<span class="has-text-grey" data-test-managed-namespace-root>/{{this.managedNamespaceRoot}}</span>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input
value={{this.managedNamespaceChild}}
placeholder="/ (Default)"
oninput={{perform this.updateManagedNamespace value="target.value"}}
autocomplete="off"
spellcheck="false"
name="namespace"
id="namespace"
class="input"
type="text"
/>
</div>
</div>
</div>
</div>
</div>
</Toolbar>
</Page.sub-header>
{{else if (has-feature "Namespaces")}}
{{#if (has-feature "Namespaces")}}
<Page.sub-header>
<Toolbar class="toolbar-namespace-picker">
<div class="field is-horizontal" data-test-namespace-toolbar>
<div class="field-label is-normal">
<label class="is-label" for="namespace">Namespace</label>
</div>
{{#if this.managedNamespaceRoot}}
<div class="field-label">
<span class="has-text-grey" data-test-managed-namespace-root>/{{this.managedNamespaceRoot}}</span>
</div>
{{/if}}
<div class="field-body">
<div class="field">
<div class="control">
<input
data-test-auth-form-ns-input
value={{this.namespaceQueryParam}}
placeholder="/ (Root)"
value={{this.namespaceInput}}
placeholder={{if this.managedNamespaceRoot "/ (Default)" "/ (Root)"}}
oninput={{perform this.updateNamespace value="target.value"}}
autocomplete="off"
spellcheck="false"

View File

@@ -0,0 +1,8 @@
export function sanitizePath(path) {
//remove whitespace + remove trailing and leading slashes
return path.trim().replace(/^\/+|\/+$/g, '');
}
export function ensureTrailingSlash(path) {
return path.replace(/(\w+[^/]$)/g, '$1/');
}

View File

@@ -0,0 +1 @@
export { ensureTrailingSlash, sanitizePath } from 'core/utils/sanitize-path';

View File

@@ -76,15 +76,15 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
assert.strictEqual(currentURL(), '/vault/auth?with=token', 'Does not redirect');
assert.dom('[data-test-namespace-toolbar]').exists('Normal namespace toolbar exists');
assert
.dom('[data-test-managed-namespace-toolbar]')
.doesNotExist('Managed namespace toolbar does not exist');
.dom('[data-test-managed-namespace-root]')
.doesNotExist('Managed namespace indicator does not exist');
assert.dom('input#namespace').hasAttribute('placeholder', '/ (Root)');
await fillIn('input#namespace', '/foo');
const encodedNamespace = encodeURIComponent('/foo');
await fillIn('input#namespace', '/foo/bar ');
const encodedNamespace = encodeURIComponent('foo/bar');
assert.strictEqual(
currentURL(),
`/vault/auth?namespace=${encodedNamespace}&with=token`,
'Does not prepend root to namespace'
'correctly sanitizes namespace'
);
});
});

View File

@@ -41,8 +41,7 @@ module('Acceptance | Enterprise | Managed namespace root', function (hooks) {
await visit('/vault/auth');
assert.ok(currentURL().startsWith('/vault/auth'), 'Redirected to auth');
assert.ok(currentURL().includes('?namespace=admin'), 'with base namespace');
assert.dom('[data-test-namespace-toolbar]').doesNotExist('Normal namespace toolbar does not exist');
assert.dom('[data-test-managed-namespace-toolbar]').exists('Managed namespace toolbar exists');
assert.dom('[data-test-namespace-toolbar]').exists('Namespace toolbar exists');
assert.dom('[data-test-managed-namespace-root]').hasText('/admin', 'Shows /admin namespace prefix');
assert.dom('input#namespace').hasAttribute('placeholder', '/ (Default)');
await fillIn('input#namespace', '/foo');
@@ -50,7 +49,13 @@ module('Acceptance | Enterprise | Managed namespace root', function (hooks) {
assert.strictEqual(
currentURL(),
`/vault/auth?namespace=${encodedNamespace}&with=token`,
'Correctly prepends root to namespace'
'Correctly prepends root to namespace when input starts with /'
);
await fillIn('input#namespace', 'foo');
assert.strictEqual(
currentURL(),
`/vault/auth?namespace=${encodedNamespace}&with=token`,
'Correctly prepends root to namespace when input does not start with /'
);
});

View File

@@ -5,7 +5,6 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { sanitizePath, ensureTrailingSlash } from 'vault/services/console';
import sinon from 'sinon';
module('Unit | Service | console', function (hooks) {
@@ -13,20 +12,6 @@ module('Unit | Service | console', function (hooks) {
hooks.beforeEach(function () {});
hooks.afterEach(function () {});
test('#sanitizePath', function (assert) {
assert.strictEqual(
sanitizePath(' /foo/bar/baz/ '),
'foo/bar/baz',
'removes spaces and slashs on either side'
);
assert.strictEqual(sanitizePath('//foo/bar/baz/'), 'foo/bar/baz', 'removes more than one slash');
});
test('#ensureTrailingSlash', function (assert) {
assert.strictEqual(ensureTrailingSlash('foo/bar'), 'foo/bar/', 'adds trailing slash');
assert.strictEqual(ensureTrailingSlash('baz/'), 'baz/', 'keeps trailing slash if there is one');
});
const testCases = [
{
method: 'read',

View File

@@ -0,0 +1,18 @@
import { module, test } from 'qunit';
import { ensureTrailingSlash, sanitizePath } from 'core/utils/sanitize-path';
module('Unit | Utility | sanitize-path', function () {
test('it removes spaces and slashes from either side', function (assert) {
assert.strictEqual(
sanitizePath(' /foo/bar/baz/ '),
'foo/bar/baz',
'removes spaces and slashes on either side'
);
assert.strictEqual(sanitizePath('//foo/bar/baz/'), 'foo/bar/baz', 'removes more than one slash');
});
test('#ensureTrailingSlash', function (assert) {
assert.strictEqual(ensureTrailingSlash('foo/bar'), 'foo/bar/', 'adds trailing slash');
assert.strictEqual(ensureTrailingSlash('baz/'), 'baz/', 'keeps trailing slash if there is one');
});
});