Skip to content

Commit

Permalink
Backport 1.15.x: For showing JSON editor for complex secret in KV det…
Browse files Browse the repository at this point in the history
…ails view (#24308)

* changelog

* kv-data-fields.hbs

* kv-data-fields.js

* details

* details.js

* type

* kv-data-fields-test

* acceptance test
  • Loading branch information
Monkeychip authored Dec 4, 2023
1 parent cb87bc1 commit f42d508
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 26 deletions.
3 changes: 3 additions & 0 deletions changelog/24290.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: When Kv v2 secret is an object, fix so details view defaults to readOnly JSON editor.
```
22 changes: 18 additions & 4 deletions ui/lib/kv/addon/components/kv-data-fields.hbs
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

{{#let (find-by "name" "path" @secret.allFields) as |attr|}}
{{#if @isEdit}}
{{#if (eq @type "edit")}}
<ReadonlyFormField @attr={{attr}} @value={{get @secret attr.name}} />
{{else}}
{{else if (eq @type "create")}}
<FormField @attr={{attr}} @model={{@secret}} @modelValidations={{@modelValidations}} @onKeyUp={{@pathValidations}} />
{{/if}}
{{/let}}

<hr class="is-marginless has-background-gray-200" />
{{#if @showJson}}
<JsonEditor
@title="{{if @isEdit 'Version' 'Secret'}} data"
@title="{{if (eq @type 'create') 'Secret' 'Version'}} data"
@value={{this.codeMirrorString}}
@valueUpdated={{this.handleJson}}
@readOnly={{eq @type "details"}}
/>
{{#if (or @modelValidations.secretData.errors this.lintingErrors)}}
<AlertInline @type={{if this.lintingErrors "warning" "danger"}} @paddingTop={{true}}>
Expand All @@ -22,10 +28,18 @@
{{/if}}
</AlertInline>
{{/if}}
{{else if (eq @type "details")}}
{{#each-in @secret.secretData as |key value|}}
<InfoTableRow @label={{key}} @value={{value}} @alwaysRender={{true}}>
<MaskedInput @name={{key}} @value={{value}} @displayOnly={{true}} @allowCopy={{true}} @allowDownload={{true}} />
</InfoTableRow>
{{else}}
<InfoTableRow @label="" @value="" @alwaysRender={{true}} />
{{/each-in}}
{{else}}
<KvObjectEditor
class="has-top-margin-m"
@label="{{if @isEdit 'Version' 'Secret'}} data"
@label="{{if (eq @type 'create') 'Secret' 'Version'}} data"
@value={{@secret.secretData}}
@onChange={{fn (mut @secret.secretData)}}
@isMasked={{true}}
Expand Down
4 changes: 2 additions & 2 deletions ui/lib/kv/addon/components/kv-data-fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { stringify } from 'core/helpers/stringify';
* <KvDataFields
* @showJson={{true}}
* @secret={{@secret}}
* @isEdit={{true}}
* @type="edit"
* @modelValidations={{this.modelValidations}}
* @pathValidations={{this.pathValidations}}
* />
Expand All @@ -23,7 +23,7 @@ import { stringify } from 'core/helpers/stringify';
* @param {boolean} showJson - boolean passed from parent to hide/show json editor
* @param {object} [modelValidations] - object of errors. If attr.name is in object and has error message display in AlertInline.
* @param {callback} [pathValidations] - callback function fired for the path input on key up
* @param {boolean} [isEdit=false] - if true, this is a new secret version rather than a new secret. Used to change text for some form labels
* @param {string} [type] - can be edit, create, or details. Used to change text for some form labels
*/

export default class KvDataFields extends Component {
Expand Down
25 changes: 13 additions & 12 deletions ui/lib/kv/addon/components/page/secret/details.hbs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}}>
<:tabLinks>
<LinkTo @route="secret.details" data-test-secrets-tab="Secret">Secret</LinkTo>
Expand All @@ -14,8 +19,9 @@
@name="json"
@status="success"
@size="small"
@checked={{this.showJsonView}}
@checked={{or this.showJsonView this.secretDataIsAdvanced}}
@onChange={{fn (mut this.showJsonView)}}
@disabled={{this.secretDataIsAdvanced}}
>
<span class="has-text-grey">JSON</span>
</Toggle>
Expand Down Expand Up @@ -94,15 +100,10 @@
{{/if}}
</EmptyState>
{{else}}
{{#if this.showJsonView}}
<JsonEditor @title="Version data" @value={{stringify @secret.secretData}} @readOnly={{true}} />
{{else}}
{{#each-in @secret.secretData as |key value|}}
<InfoTableRow @label={{key}} @value={{value}} @alwaysRender={{true}}>
<MaskedInput @name={{key}} @value={{value}} @displayOnly={{true}} @allowCopy={{true}} @allowDownload={{true}} />
</InfoTableRow>
{{else}}
<InfoTableRow @label="" @value="" @alwaysRender={{true}} />
{{/each-in}}
{{/if}}
<KvDataFields
@showJson={{or this.showJsonView this.secretDataIsAdvanced}}
@secret={{@secret}}
@modelValidations={{this.modelValidations}}
@type="details"
/>
{{/if}}
10 changes: 10 additions & 0 deletions ui/lib/kv/addon/components/page/secret/details.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ export default class KvSecretDetails extends Component {

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

constructor() {
super(...arguments);
this.originalSecret = JSON.stringify(this.args.secret.secretData || {});
if (this.originalSecret.lastIndexOf('{') > 0) {
// Dumb way to check if there's a nested object in the secret
this.secretDataIsAdvanced = true;
}
}

@action
closeVersionMenu(dropdown) {
Expand Down
7 changes: 6 additions & 1 deletion ui/lib/kv/addon/components/page/secret/edit.hbs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Create New Version">
<:toolbarFilters>
<Toggle
Expand Down Expand Up @@ -43,7 +48,7 @@
@showJson={{or this.showJsonView this.secretDataIsAdvanced}}
@secret={{@secret}}
@modelValidations={{this.modelValidations}}
@isEdit={{true}}
@type="edit"
/>

<div class="has-top-margin-m">
Expand Down
6 changes: 6 additions & 0 deletions ui/lib/kv/addon/components/page/secrets/create.hbs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Create Secret">
<:toolbarFilters>
<Toggle
Expand All @@ -22,6 +27,7 @@
@secret={{@secret}}
@modelValidations={{this.modelValidations}}
@pathValidations={{this.pathValidations}}
@type="create"
/>

<ToggleButton
Expand Down
49 changes: 42 additions & 7 deletions ui/tests/integration/components/kv/kv-data-fields-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { setupRenderingTest } from 'vault/tests/helpers';
import { setupEngine } from 'ember-engines/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { hbs } from 'ember-cli-htmlbars';
import { fillIn, render } from '@ember/test-helpers';
import { fillIn, render, click } from '@ember/test-helpers';
import codemirror from 'vault/tests/helpers/codemirror';
import { FORM } from 'vault/tests/helpers/kv/kv-selectors';
import { PAGE, FORM } from 'vault/tests/helpers/kv/kv-selectors';

module('Integration | Component | kv-v2 | KvDataFields', function (hooks) {
setupRenderingTest(hooks);
Expand All @@ -26,8 +26,9 @@ module('Integration | Component | kv-v2 | KvDataFields', function (hooks) {

test('it updates the secret model', async function (assert) {
assert.expect(2);

await render(hbs`<KvDataFields @showJson={{false}} @secret={{this.secret}} />`, { owner: this.engine });
await render(hbs`<KvDataFields @showJson={{false}} @secret={{this.secret}} @type="create" />`, {
owner: this.engine,
});

await fillIn(FORM.inputByAttr('path'), this.path);
await fillIn(FORM.keyInput(), 'foo');
Expand Down Expand Up @@ -63,14 +64,48 @@ module('Integration | Component | kv-v2 | KvDataFields', function (hooks) {
secretData: this.secret.secretData,
});

await render(hbs`<KvDataFields @showJson={{false}} @isEdit={{true}} @secret={{this.secret}} />`, {
owner: this.engine,
});
await render(
hbs`<KvDataFields @showJson={{false}} @isEdit={{true}} @secret={{this.secret}} @type="edit"/>`,
{
owner: this.engine,
}
);

assert.dom(FORM.inputByAttr('path')).isDisabled();
assert.dom(FORM.inputByAttr('path')).hasValue(this.path);
assert.dom(FORM.keyInput()).hasValue('foo');
assert.dom(FORM.maskedValueInput()).hasValue('bar');
assert.dom(FORM.dataInputLabel({ isJson: false })).hasText('Version data');
});

test('it shows readonly info rows when viewing secret details of simple secret', async function (assert) {
assert.expect(3);
this.secret.secretData = { foo: 'bar' };
this.secret.path = this.path;

await render(hbs`<KvDataFields @showJson={{false}} @secret={{this.secret}} @type="details" />`, {
owner: this.engine,
});
assert.dom(PAGE.infoRow).exists({ count: 1 }, '1 row of data shows');
assert.dom(PAGE.infoRowValue('foo')).hasText('***********');
await click(PAGE.infoRowToggleMasked('foo'));
assert.dom(PAGE.infoRowValue('foo')).hasText('bar', 'secret value shows after toggle');
});

test('it shows readonly json editor when viewing secret details of complex secret', async function (assert) {
assert.expect(3);
this.secret.secretData = {
foo: {
bar: 'baz',
},
};
this.secret.path = this.path;

await render(hbs`<KvDataFields @showJson={{true}} @secret={{this.secret}} @type="details" />`, {
owner: this.engine,
});
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": "baz" }}`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
this.backend = 'kv-engine';
this.path = 'my-secret';
this.pathComplex = 'my-secret-object';
this.version = 2;
this.dataId = kvDataPath(this.backend, this.path);
this.dataIdComplex = kvDataPath(this.backend, this.pathComplex);
this.metadataId = kvMetadataPath(this.backend, this.path);

this.secretData = { foo: 'bar' };
Expand All @@ -38,6 +40,22 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
destroyed: false,
version: this.version,
});
// nested secret
this.secretDataComplex = {
foo: {
bar: 'baz',
},
};
this.store.pushPayload('kv/data', {
modelName: 'kv/data',
id: this.dataIdComplex,
secret_data: this.secretDataComplex,
created_time: '2023-08-20T02:12:17.379762Z',
custom_metadata: null,
deletion_time: '',
destroyed: false,
version: this.version,
});

const metadata = this.server.create('kv-metadatum');
metadata.id = this.metadataId;
Expand All @@ -48,6 +66,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook

this.metadata = this.store.peekRecord('kv/metadata', this.metadataId);
this.secret = this.store.peekRecord('kv/data', this.dataId);
this.secretComplex = this.store.peekRecord('kv/data', this.dataIdComplex);

// this is the route model, not an ember data model
this.model = {
Expand All @@ -56,6 +75,12 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
secret: this.secret,
metadata: this.metadata,
};
this.modelComplex = {
backend: this.backend,
path: this.pathComplex,
secret: this.secretComplex,
metadata: this.metadata,
};
this.breadcrumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: this.model.backend, route: 'list' },
Expand Down Expand Up @@ -90,6 +115,23 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
.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);
await render(
hbs`
<Page::Secret::Details
@path={{this.modelComplex.path}}
@secret={{this.modelComplex.secret}}
@breadcrumbs={{this.breadcrumbs}}
/>
`,
{ owner: this.engine }
);
assert.dom(PAGE.infoRowValue('foo')).doesNotExist('does not render rows of secret data');
assert.dom(FORM.toggleJson).isDisabled();
assert.dom('[data-test-component="code-mirror-modifier"]').includesText(`{ "foo": { "bar": "baz" }}`);
});

test('it renders deleted empty state', async function (assert) {
assert.expect(3);
this.secret.deletionTime = '2023-07-23T02:12:17.379762Z';
Expand Down

0 comments on commit f42d508

Please sign in to comment.