Skip to content

Commit

Permalink
UI: KV version 2 advanced secret updates (#25235)
Browse files Browse the repository at this point in the history
* Add obfuscation for JSON values on KV data view

* Show JSON editor by default if secret data is complex, but do not disable JSON toggle

* Add text object warning to kv object editor

* a11y fix: do not disable show diff toggle

* test and language cleanup

* update language

* Update tests for expected behavior

* language!

* Add changelog
  • Loading branch information
hashishaw committed Feb 6, 2024
1 parent 8db75cd commit 08eaac1
Show file tree
Hide file tree
Showing 19 changed files with 227 additions and 63 deletions.
3 changes: 3 additions & 0 deletions changelog/25235.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: Do not disable JSON display toggle for KV version 2 secrets
```
8 changes: 8 additions & 0 deletions ui/lib/core/addon/components/json-editor.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
<Icon @name="reload" />
</button>
{{/if}}
{{#if (and @allowObscure @readOnly)}}
{{! For safety we only use obscured values in readonly mode }}
<div>
<Toggle @name="revealValues" @checked={{this.revealValues}} @onChange={{fn (mut this.revealValues)}}>
<span class="has-text-grey">Reveal values</span>
</Toggle>
</div>
{{/if}}
<div class="toolbar-separator"></div>
<CopyButton
class="button is-transparent"
Expand Down
11 changes: 11 additions & 0 deletions ui/lib/core/addon/components/json-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { stringify } from 'core/helpers/stringify';
import { obfuscateData } from 'core/utils/advanced-secret';

/**
* @module JsonEditor
Expand All @@ -30,10 +33,18 @@ import { action } from '@ember/object';
*/

export default class JsonEditorComponent extends Component {
@tracked revealValues = false;
get getShowToolbar() {
return this.args.showToolbar === false ? false : true;
}

get showObfuscatedData() {
return this.args.readOnly && this.args.allowObscure && !this.revealValues;
}
get obfuscatedData() {
return stringify([obfuscateData(JSON.parse(this.args.value))], {});
}

@action
onSetup(editor) {
// store reference to codemirror editor so that it can be passed to valueUpdated when restoring example
Expand Down
12 changes: 10 additions & 2 deletions ui/lib/core/addon/components/kv-object-editor.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
@value={{row.name}}
placeholder={{this.placeholders.key}}
{{on "change" (fn this.updateRow row index)}}
{{on "input" (fn this.validateKey index)}}
class="input"
/>
</div>
Expand Down Expand Up @@ -71,7 +70,7 @@
{{/if}}
</div>
</div>
{{#if (includes index this.whitespaceWarningRows)}}
{{#if (this.showWhitespaceWarning row.name)}}
<div class="has-bottom-margin-s">
<AlertInline
@type="warning"
Expand All @@ -80,6 +79,15 @@
/>
</div>
{{/if}}
{{#if (this.showNonStringWarning row.value)}}
<div class="has-bottom-margin-s">
<AlertInline
@type="warning"
@message="This value will be saved as a string. If you need to save a non-string value, please use the JSON editor."
data-test-kv-object-warning={{index}}
/>
</div>
{{/if}}
{{/each}}
{{#if this.hasDuplicateKeys}}
<Hds::Alert data-test-duplicate-keys-warning @type="inline" @color="warning" as |A|>
Expand Down
30 changes: 15 additions & 15 deletions ui/lib/core/addon/components/kv-object-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { isNone } from '@ember/utils';
import { assert } from '@ember/debug';
import { action } from '@ember/object';
import { guidFor } from '@ember/object/internals';
import { A } from '@ember/array';
import KVObject from 'vault/lib/kv-object';

/**
Expand All @@ -26,7 +25,7 @@ import KVObject from 'vault/lib/kv-object';
* ```
* @param {string} value - the value is captured from the model.
* @param {function} onChange - function that captures the value on change
* @param {boolean} [isMasked = false] - when true the <MaskedInput> renders instead of the default <textarea> to input the value portion of the key/value object
* @param {boolean} [isMasked = false] - when true the <MaskedInput> renders instead of the default <textarea> to input the value portion of the key/value object
* @param {function} [onKeyUp] - function passed in that handles the dom keyup event. Used for validation on the kv custom metadata.
* @param {string} [label] - label displayed over key value inputs
* @param {string} [labelClass] - override default label class in FormFieldLabel component
Expand All @@ -35,11 +34,12 @@ import KVObject from 'vault/lib/kv-object';
* @param {string} [subText] - placed under label.
* @param {string} [keyPlaceholder] - placeholder for key input
* @param {string} [valuePlaceholder] - placeholder for value input
* @param {boolean} [allowWhiteSpace = false] - when true, allows whitespace in the key input
* @param {boolean} [warnNonStringValues = false] - when true, shows a warning if the value is a non-string
*/

export default class KvObjectEditor extends Component {
@tracked kvData;
whitespaceWarningRows = A();

get placeholders() {
return {
Expand Down Expand Up @@ -75,7 +75,6 @@ export default class KvObjectEditor extends Component {
const oldObj = this.kvData.objectAt(index);
assert('object guids match', guidFor(oldObj) === guidFor(object));
this.kvData.removeAt(index);
this.whitespaceWarningRows.removeObject(index);
this.args.onChange(this.kvData.toJSON());
}
@action
Expand All @@ -84,16 +83,17 @@ export default class KvObjectEditor extends Component {
this.args.onKeyUp(event.target.value);
}
}
@action
validateKey(rowIndex, event) {
const { value } = event.target;
const keyHasWhitespace = new RegExp('\\s', 'g').test(value);
const rows = [...this.whitespaceWarningRows];
const rowHasWarning = rows.includes(rowIndex);
if (!keyHasWhitespace && rowHasWarning) {
this.whitespaceWarningRows.removeObject(rowIndex);
} else if (keyHasWhitespace && !rowHasWarning) {
this.whitespaceWarningRows.addObject(rowIndex);
showWhitespaceWarning = (name) => {
if (this.args.allowWhiteSpace) return false;
return new RegExp('\\s', 'g').test(name);
};
showNonStringWarning = (value) => {
if (!this.args.warnNonStringValues) return false;
try {
JSON.parse(value);
return true;
} catch (e) {
return false;
}
}
};
}
20 changes: 20 additions & 0 deletions ui/lib/core/addon/utils/advanced-secret.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,23 @@ export function isAdvancedSecret(value) {
return false;
}
}

/**
* Method to obfuscate all values in a map, including nested values and arrays
* @param obj object
* @returns object
*/
export function obfuscateData(obj) {
if (typeof obj !== 'object' || Array.isArray(obj)) return obj;
const newObj = {};
for (const key of Object.keys(obj)) {
if (Array.isArray(obj[key])) {
newObj[key] = obj[key].map(() => '********');
} else if (typeof obj[key] === 'object') {
newObj[key] = obfuscateData(obj[key]);
} else {
newObj[key] = '********';
}
}
return newObj;
}
2 changes: 2 additions & 0 deletions ui/lib/kv/addon/components/kv-data-fields.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<JsonEditor
@title="{{if (eq @type 'create') 'Secret' 'Version'}} data"
@value={{this.stringifiedSecretData}}
@allowObscure={{true}}
@valueUpdated={{this.handleJson}}
@readOnly={{eq @type "details"}}
/>
Expand Down Expand Up @@ -43,5 +44,6 @@
@value={{@secret.secretData}}
@onChange={{fn (mut @secret.secretData)}}
@isMasked={{true}}
@warnNonStringValues={{true}}
/>
{{/if}}
5 changes: 2 additions & 3 deletions ui/lib/kv/addon/components/page/secret/details.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@
@name="json"
@status="success"
@size="small"
@checked={{or this.showJsonView this.secretDataIsAdvanced}}
@checked={{this.showJsonView}}
@onChange={{fn (mut this.showJsonView)}}
@disabled={{this.secretDataIsAdvanced}}
>
<span class="has-text-grey">JSON</span>
</Toggle>
Expand Down Expand Up @@ -101,7 +100,7 @@
</EmptyState>
{{else}}
<KvDataFields
@showJson={{or this.showJsonView this.secretDataIsAdvanced}}
@showJson={{this.showJsonView}}
@secret={{@secret}}
@modelValidations={{this.modelValidations}}
@type="details"
Expand Down
6 changes: 4 additions & 2 deletions ui/lib/kv/addon/components/page/secret/details.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ export default class KvSecretDetails extends Component {

@tracked showJsonView = false;
@tracked wrappedData = null;
secretDataIsAdvanced;

constructor() {
super(...arguments);
this.originalSecret = JSON.stringify(this.args.secret.secretData || {});
this.secretDataIsAdvanced = isAdvancedSecret(this.originalSecret);
if (isAdvancedSecret(this.originalSecret)) {
// Default to JSON view if advanced
this.showJsonView = true;
}
}

@action
Expand Down
12 changes: 2 additions & 10 deletions ui/lib/kv/addon/components/page/secret/edit.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@

<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Create New Version">
<:toolbarFilters>
<Toggle
@name="json"
@status="success"
@size="small"
@checked={{or this.showJsonView this.secretDataIsAdvanced}}
@onChange={{fn (mut this.showJsonView)}}
@disabled={{this.secretDataIsAdvanced}}
>
<Toggle @name="json" @checked={{this.showJsonView}} @onChange={{fn (mut this.showJsonView)}}>
<span class="has-text-grey">JSON</span>
</Toggle>
</:toolbarFilters>
Expand Down Expand Up @@ -45,7 +38,7 @@
<MessageError @model={{@secret}} @errorMessage={{this.errorMessage}} />

<KvDataFields
@showJson={{or this.showJsonView this.secretDataIsAdvanced}}
@showJson={{this.showJsonView}}
@secret={{@secret}}
@modelValidations={{this.modelValidations}}
@type="edit"
Expand All @@ -58,7 +51,6 @@
@size="small"
@onChange={{fn (mut this.showDiff)}}
@checked={{this.showDiff}}
@disabled={{not this.diffDelta}}
>
<span class="ttl-picker-label is-large">Show diff</span><br />
<div class="description has-text-grey" data-test-diff-description>{{if
Expand Down
6 changes: 4 additions & 2 deletions ui/lib/kv/addon/components/page/secret/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ export default class KvSecretEdit extends Component {
@tracked modelValidations;
@tracked invalidFormAlert;
originalSecret;
secretDataIsAdvanced;

constructor() {
super(...arguments);
this.originalSecret = JSON.stringify(this.args.secret.secretData || {});
this.secretDataIsAdvanced = isAdvancedSecret(this.originalSecret);
if (isAdvancedSecret(this.originalSecret)) {
// Default to JSON view if advanced
this.showJsonView = true;
}
}

get showOldVersionAlert() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) {
await click(FORM.saveBtn);
// Future: test that json is automatic on details too
await click(PAGE.detail.createNewVersion);
assert.dom(FORM.toggleJson).isDisabled();
assert.dom(FORM.toggleJson).isNotDisabled();
assert.dom(FORM.toggleJson).isChecked();
});

Expand Down
1 change: 1 addition & 0 deletions ui/tests/helpers/kv/kv-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const FORM = {
inputByAttr: (attr) => `[data-test-input="${attr}"]`,
fieldByAttr: (attr) => `[data=test=field="${attr}"]`, // formfield
toggleJson: '[data-test-toggle-input="json"]',
toggleJsonValues: '[data-test-toggle-input="revealValues"]',
toggleMasked: '[data-test-button="toggle-masked"]',
toggleMetadata: '[data-test-metadata-toggle]',
jsonEditor: '[data-test-component="code-mirror-modifier"]',
Expand Down
21 changes: 21 additions & 0 deletions ui/tests/integration/components/json-editor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,25 @@ module('Integration | Component | json-editor', function (hooks) {
assert.dom('.CodeMirror-code').hasText(`1${this.example}`, 'Example is restored');
assert.strictEqual(this.value, null, 'Value is cleared on restore example');
});

test('obscure option works correctly', async function (assert) {
this.set('readOnly', true);
await render(hbs`<JsonEditor
@value={{this.json_blob}}
@allowObscure={{true}}
@readOnly={{this.readOnly}}
@valueUpdated={{this.valueUpdated}}
@onFocusOut={{this.onFocusOut}}
/>`);
assert.dom('.CodeMirror-code').hasText(`{ "test": "********"}`, 'shows data with obscured values');
assert.dom('[data-test-toggle-input="revealValues"]').isNotChecked('reveal values toggle is unchecked');
await click('[data-test-toggle-input="revealValues"]');
assert.dom('.CodeMirror-code').hasText(JSON_BLOB, 'shows data with real values');
assert.dom('[data-test-toggle-input="revealValues"]').isChecked('reveal values toggle is checked');
// turn obscure back on to ensure readonly overrides reveal setting
await click('[data-test-toggle-input="revealValues"]');
this.set('readOnly', false);
assert.dom('[data-test-toggle-input="revealValues"]').doesNotExist('reveal values toggle is hidden');
assert.dom('.CodeMirror-code').hasText(JSON_BLOB, 'shows data with real values on edit mode');
});
});
13 changes: 13 additions & 0 deletions ui/tests/integration/components/kv-object-editor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,17 @@ module('Integration | Component | kv-object-editor', function (hooks) {
await click('[data-test-kv-delete-row="0"]');
assert.dom('[data-test-kv-whitespace-warning="0"]').doesNotExist();
});

test('it should display object warning for values when warnNonStringValues true', async function (assert) {
const objValue = `{
"a": "b"
}`;
await render(hbs`<KvObjectEditor @onChange={{this.spy}} @warnNonStringValues={{true}} />`);
await fillIn('[data-test-kv-value="0"]', objValue);
assert.dom('[data-test-kv-object-warning="0"]').exists();
await fillIn('[data-test-kv-value="0"]', 'test ');
assert.dom('[data-test-kv-object-warning="0"]').doesNotExist();
await fillIn('[data-test-kv-value="0"]', 7);
assert.dom('[data-test-kv-object-warning="0"]').exists();
});
});
6 changes: 5 additions & 1 deletion ui/tests/integration/components/kv/kv-data-fields-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ module('Integration | Component | kv-v2 | KvDataFields', function (hooks) {
});

test('it shows readonly json editor when viewing secret details of complex secret', async function (assert) {
assert.expect(3);
assert.expect(4);
this.secret.secretData = {
foo: {
bar: 'baz',
Expand All @@ -106,6 +106,10 @@ module('Integration | Component | kv-v2 | KvDataFields', function (hooks) {
});
assert.dom(PAGE.infoRowValue('foo')).doesNotExist('does not render rows of secret data');
assert.dom('[data-test-component="code-mirror-modifier"]').hasClass('readonly-codemirror');
assert
.dom('[data-test-component="code-mirror-modifier"]')
.includesText(`{ "foo": { "bar": "********" }}`);
await click(FORM.toggleJsonValues);
assert.dom('[data-test-component="code-mirror-modifier"]').includesText(`{ "foo": { "bar": "baz" }}`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,15 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
await click(FORM.toggleMasked);
assert.dom(PAGE.infoRowValue('foo')).hasText('bar', 'renders secret value');
await click(FORM.toggleJson);
await click(FORM.toggleJsonValues);
assert.propEqual(parseJsonEditor(find), this.secretData, 'json editor renders secret data');
assert
.dom(PAGE.detail.versionTimestamp)
.includesText(`Version ${this.version} created`, 'renders version and time created');
});

test('it renders json view when secret is complex', async function (assert) {
assert.expect(3);
assert.expect(4);
await render(
hbs`
<Page::Secret::Details
Expand All @@ -128,7 +129,8 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
{ owner: this.engine }
);
assert.dom(PAGE.infoRowValue('foo')).doesNotExist('does not render rows of secret data');
assert.dom(FORM.toggleJson).isDisabled();
assert.dom(FORM.toggleJson).isChecked();
assert.dom(FORM.toggleJson).isNotDisabled();
assert.dom('[data-test-component="code-mirror-modifier"]').includesText(`{ "foo": { "bar": "baz" }}`);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Edit', function (hooks)
{ owner: this.engine }
);

assert.dom(PAGE.edit.toggleDiff).isDisabled('Diff toggle is disabled');
assert.dom(PAGE.edit.toggleDiff).isNotDisabled('Diff toggle is not disabled');
assert.dom(PAGE.edit.toggleDiffDescription).hasText('No changes to show. Update secret to view diff');
assert.dom(PAGE.diff.visualDiff).doesNotExist('Does not show visual diff');

Expand Down

0 comments on commit 08eaac1

Please sign in to comment.