mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-04 14:25:35 -04:00
wrap refactor
This commit is contained in:
parent
cb234d1f2d
commit
b12e376847
4 changed files with 179 additions and 99 deletions
|
|
@ -4,37 +4,41 @@
|
|||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
/**
|
||||
* @module ToolWrap
|
||||
* ToolWrap components are components that sys/wrapping/wrap functionality. Most of the functionality is passed through as actions from the tool-actions-form and then called back with properties.
|
||||
* @module <ToolWrap
|
||||
* ToolWrap components are components that sys/wrapping/wrap functionality.
|
||||
*
|
||||
* @example
|
||||
* <ToolWrap
|
||||
* @errors={{@errors}}
|
||||
* @onBack={{action "onBack" (array "token")}}
|
||||
* @onChange={{action "onChange"}}
|
||||
* @onClear={{action "onClear"}}
|
||||
* @token={{@token}}
|
||||
* />
|
||||
*
|
||||
* @param {object} errors=null - errors returned if wrap fails
|
||||
* @param {function} onBack - callback that only clears specific values so the action can be repeated. Must be passed as `{{action "onBack"}}`
|
||||
* @param {function} onChange - callback that fires when inputs change and passes value and param name back to the parent
|
||||
* @param {function} onClear - callback that resets all of values to defaults. Must be passed as `{{action "onClear"}}`
|
||||
* @param {string} token=null - returned after user clicks "Wrap data", if there is a token value it displays instead of the JsonEditor
|
||||
* <ToolWrap />
|
||||
*/
|
||||
|
||||
export default class ToolWrap extends Component {
|
||||
@service store;
|
||||
@service flashMessages;
|
||||
|
||||
@tracked buttonDisabled = false;
|
||||
@tracked token = '';
|
||||
@tracked wrapTTL = null;
|
||||
@tracked wrapData = '{\n}';
|
||||
@tracked errorMessage = '';
|
||||
|
||||
@action
|
||||
reset(clearData = true) {
|
||||
this.token = '';
|
||||
this.errorMessage = '';
|
||||
this.wrapTTL = null;
|
||||
if (clearData) this.wrapData = '{\n}';
|
||||
}
|
||||
|
||||
@action
|
||||
updateTtl(evt) {
|
||||
if (!evt) return;
|
||||
const ttl = evt.enabled ? `${evt.seconds}s` : '30m';
|
||||
this.args.onChange('wrapTTL', ttl);
|
||||
this.wrapTTL = evt.enabled ? `${evt.seconds}s` : '30m';
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
@ -42,6 +46,21 @@ export default class ToolWrap extends Component {
|
|||
codemirror.performLint();
|
||||
const hasErrors = codemirror?.state.lint.marked?.length > 0;
|
||||
this.buttonDisabled = hasErrors;
|
||||
this.args.onChange('data', val);
|
||||
if (!hasErrors) this.wrapData = val;
|
||||
}
|
||||
|
||||
@action
|
||||
async handleSubmit(evt) {
|
||||
evt.preventDefault();
|
||||
const data = JSON.parse(this.wrapData);
|
||||
const wrapTTL = this.wrapTTL || null;
|
||||
|
||||
try {
|
||||
const response = await this.store.adapterFor('tools').toolAction('wrap', data, { wrapTTL });
|
||||
this.token = response.wrap_info.token;
|
||||
this.flashMessages.success('Wrap was successful.');
|
||||
} catch (error) {
|
||||
this.errorMessage = errorMessage(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,7 @@
|
|||
{{else if (eq this.selectedAction "lookup")}}
|
||||
<ToolLookup />
|
||||
{{else if (eq this.selectedAction "wrap")}}
|
||||
<ToolWrap
|
||||
@token={{this.token}}
|
||||
@onBack={{action "onBack" (array "token")}}
|
||||
@onClear={{action "onClear"}}
|
||||
@onChange={{action "onChange"}}
|
||||
@errors={{this.errors}}
|
||||
@data={{this.data}}
|
||||
/>
|
||||
<ToolWrap />
|
||||
{{else}}
|
||||
<EmptyState @title="Tool not available" />
|
||||
{{/if}}
|
||||
|
|
|
|||
|
|
@ -11,54 +11,58 @@
|
|||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{#if @token}}
|
||||
{{#if this.token}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<div class="field">
|
||||
<label for="wrap-info" class="is-label">Wrapped token</label>
|
||||
<div class="control">
|
||||
<Hds::Copy::Snippet
|
||||
@textToCopy={{@token}}
|
||||
@color="secondary"
|
||||
data-test-tools-input="wrapping-token"
|
||||
@onError={{(fn
|
||||
(set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger")
|
||||
)}}
|
||||
/>
|
||||
</div>
|
||||
<Hds::Copy::Snippet
|
||||
@textToCopy={{this.token}}
|
||||
@color="secondary"
|
||||
data-test-tools-input="wrapping-token"
|
||||
@onError={{fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger")}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button @icon="arrow-left" @text="Back" @color="tertiary" {{on "click" @onBack}} data-test-button="Back" />
|
||||
<Hds::Button @text="Done" @color="secondary" {{on "click" @onClear}} data-test-button="Done" />
|
||||
<Hds::Button
|
||||
@icon="arrow-left"
|
||||
@text="Back"
|
||||
@color="tertiary"
|
||||
{{on "click" (fn this.reset false)}}
|
||||
data-test-button="Back"
|
||||
/>
|
||||
<Hds::Button @text="Done" @color="secondary" {{on "click" this.reset}} data-test-button="Done" />
|
||||
</Hds::ButtonSet>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="perform" @noun="wrap" />
|
||||
<MessageError @errors={{@errors}} />
|
||||
<div class="field">
|
||||
<form {{on "submit" this.handleSubmit}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="perform" @noun="wrap" />
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<JsonEditor
|
||||
@title="Data to wrap"
|
||||
@subTitle="json-formatted"
|
||||
@value={{this.wrapData}}
|
||||
@valueUpdated={{this.codemirrorUpdated}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TtlPicker
|
||||
@label="Wrap TTL"
|
||||
@initialValue="30m"
|
||||
@onChange={{this.updateTtl}}
|
||||
@helperTextDisabled="Vault will use the default (30m)"
|
||||
@helperTextEnabled="Wrap will expire after"
|
||||
@changeOnInit={{true}}
|
||||
/>
|
||||
</div>
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<JsonEditor
|
||||
@title="Data to wrap"
|
||||
@subTitle="json-formatted"
|
||||
@value={{@data}}
|
||||
@valueUpdated={{this.codemirrorUpdated}}
|
||||
/>
|
||||
<Hds::Button @text="Wrap data" type="submit" disabled={{this.buttonDisabled}} data-test-tools-submit />
|
||||
</div>
|
||||
</div>
|
||||
<TtlPicker
|
||||
@label="Wrap TTL"
|
||||
@initialValue="30m"
|
||||
@onChange={{this.updateTtl}}
|
||||
@helperTextDisabled="Vault will use the default (30m)"
|
||||
@helperTextEnabled="Wrap will expire after"
|
||||
@changeOnInit={{true}}
|
||||
/>
|
||||
</div>
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<Hds::Button @text="Wrap data" type="submit" disabled={{this.buttonDisabled}} data-test-tools-submit />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{/if}}
|
||||
|
|
@ -6,76 +6,140 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { click, fillIn, render } from '@ember/test-helpers';
|
||||
import { Response } from 'miragejs';
|
||||
import { click, fillIn, find, render, waitUntil } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import sinon from 'sinon';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import codemirror from 'vault/tests/helpers/codemirror';
|
||||
import { TTL_PICKER as TTL } from 'vault/tests/helpers/components/ttl-picker-selectors';
|
||||
import { TOOLS_SELECTORS as TS } from 'vault/tests/helpers/tools-selectors';
|
||||
import codemirror from 'vault/tests/helpers/codemirror';
|
||||
|
||||
module('Integration | Component | tools/tool-wrap', function (hooks) {
|
||||
module('Integration | Component | tools/wrap', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.onBack = sinon.spy();
|
||||
this.onClear = sinon.spy();
|
||||
this.onChange = sinon.spy();
|
||||
this.data = '{\n}';
|
||||
this.renderComponent = async () => {
|
||||
await render(hbs`
|
||||
<ToolWrap
|
||||
@token={{this.token}}
|
||||
@errors={{this.errors}}
|
||||
@onClear={{this.onClear}}
|
||||
@onBack={{this.onBack}}
|
||||
@onChange={{this.onChange}}
|
||||
@data={{this.data}}
|
||||
/>`);
|
||||
<ToolWrap />`);
|
||||
};
|
||||
this.wrapData = `{"foo": "bar"}`;
|
||||
this.token = 'blah.jhfel7SmsVeZwihaGiIKHGh2cy5XZWtEeEt5WmRwS1VYSTNDb1BBVUNsVFAQ3JIK';
|
||||
// default mirage response here is overridden in some tests
|
||||
this.server.post('sys/wrapping/wrap', () => {
|
||||
// removed superfluous response data for this test
|
||||
return { wrap_info: { token: this.token } };
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders defaults', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom('h1').hasText('Wrap Data', 'Title renders');
|
||||
assert.dom('label').hasText('Data to wrap (json-formatted)');
|
||||
assert.strictEqual(codemirror().getValue(' '), '{ }', 'json editor initializes with empty object');
|
||||
assert.dom(GENERAL.toggleInput('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked');
|
||||
assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked');
|
||||
assert.dom(TS.submit).isEnabled();
|
||||
assert.dom(TS.toolsInput('wrapping-token')).doesNotExist();
|
||||
assert.dom(TS.button('Back')).doesNotExist();
|
||||
assert.dom(TS.button('Done')).doesNotExist();
|
||||
|
||||
await click(TTL.toggleByLabel('Wrap TTL'));
|
||||
assert.dom(TTL.valueInputByLabel('Wrap TTL')).hasValue('30', 'ttl defaults to 30 when toggled');
|
||||
assert.dom(TTL.ttlUnit).hasValue('m', 'ttl defaults to minutes when toggled');
|
||||
});
|
||||
|
||||
test('it renders token view', async function (assert) {
|
||||
this.token = 'blah.jhfel7SmsVeZwihaGiIKHGh2cy5XZWtEeEt5WmRwS1VYSTNDb1BBVUNsVFAQ3JIK';
|
||||
test('it renders errors', async function (assert) {
|
||||
this.server.post('sys/wrapping/wrap', () => new Response(500, {}, { errors: ['Something is wrong'] }));
|
||||
await this.renderComponent();
|
||||
await click(TS.submit);
|
||||
await waitUntil(() => find(GENERAL.messageError));
|
||||
assert.dom(GENERAL.messageError).hasText('Error Something is wrong', 'Error renders');
|
||||
});
|
||||
|
||||
assert.dom('h1').hasText('Wrap Data');
|
||||
test('it submits with defaults', async function (assert) {
|
||||
assert.expect(5);
|
||||
this.server.post('sys/wrapping/wrap', (schema, { requestBody, requestHeaders }) => {
|
||||
const payload = JSON.parse(requestBody);
|
||||
const expectedHeaders = {
|
||||
'X-Vault-Wrap-TTL': '30m',
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
};
|
||||
assert.propEqual(payload, JSON.parse(this.wrapData), `payload contains data: ${requestBody}`);
|
||||
assert.propEqual(requestHeaders, expectedHeaders, 'header has default wrap ttl');
|
||||
return {
|
||||
wrap_info: {
|
||||
token: this.token,
|
||||
accessor: '5yjKx6Om9NmBx1mjiN1aIrnm',
|
||||
ttl: 1800,
|
||||
creation_time: '2024-06-07T12:02:22.096254-07:00',
|
||||
creation_path: 'sys/wrapping/wrap',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await codemirror().setValue(this.wrapData);
|
||||
await click(TS.submit);
|
||||
await waitUntil(() => find(TS.toolsInput('wrapping-token')));
|
||||
assert.dom(TS.toolsInput('wrapping-token')).hasText(this.token);
|
||||
assert.dom('label').hasText('Wrapped token');
|
||||
assert.dom('.CodeMirror').doesNotExist();
|
||||
assert.dom(TS.toolsInput('wrapping-token')).hasText(this.token);
|
||||
await click(TS.button('Back'));
|
||||
assert.true(this.onBack.calledOnce, 'onBack is called');
|
||||
await click(TS.button('Done'));
|
||||
assert.true(this.onClear.calledOnce, 'onClear is called');
|
||||
});
|
||||
|
||||
test('it calls onChange for json editor', async function (assert) {
|
||||
const data = `{"foo": "bar"}`;
|
||||
test('it submits with updated ttl', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.post('sys/wrapping/wrap', (schema, { requestBody, requestHeaders }) => {
|
||||
const payload = JSON.parse(requestBody);
|
||||
const expectedHeaders = {
|
||||
'X-Vault-Wrap-TTL': '1200s',
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
};
|
||||
assert.propEqual(payload, JSON.parse(this.wrapData), `payload contains data: ${requestBody}`);
|
||||
assert.propEqual(requestHeaders, expectedHeaders, 'header has updated wrap ttl');
|
||||
// only testing payload/header assertions, no need for return here
|
||||
return {};
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await codemirror().setValue(this.wrapData);
|
||||
await click(TTL.toggleByLabel('Wrap TTL'));
|
||||
await fillIn(TTL.valueInputByLabel('Wrap TTL'), '20');
|
||||
await click(TS.submit);
|
||||
});
|
||||
|
||||
test('it resets on done', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await codemirror().setValue(this.wrapData);
|
||||
await click(TTL.toggleByLabel('Wrap TTL'));
|
||||
await fillIn(TTL.valueInputByLabel('Wrap TTL'), '20');
|
||||
await click(TS.submit);
|
||||
|
||||
await waitUntil(() => find(TS.button('Done')));
|
||||
await click(TS.button('Done'));
|
||||
assert.strictEqual(codemirror().getValue(' '), '{ }', 'json editor resets to empty object');
|
||||
assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL resets to unchecked');
|
||||
await click(TTL.toggleByLabel('Wrap TTL'));
|
||||
assert.dom(TTL.valueInputByLabel('Wrap TTL')).hasValue('30', 'ttl resets to default when toggled');
|
||||
});
|
||||
|
||||
test('it preserves input data on back', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await codemirror().setValue(this.wrapData);
|
||||
await click(TS.submit);
|
||||
|
||||
await waitUntil(() => find(TS.button('Back')));
|
||||
await click(TS.button('Back'));
|
||||
assert.strictEqual(codemirror().getValue(' '), `{"foo": "bar"}`, 'json editor has original data');
|
||||
assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked');
|
||||
});
|
||||
|
||||
test('it disables/enables submit based on json linting', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await codemirror().setValue(`{bad json}`);
|
||||
assert.dom(TS.submit).isDisabled('submit disables if json editor has linting errors');
|
||||
|
||||
await codemirror().setValue(data);
|
||||
await codemirror().setValue(this.wrapData);
|
||||
assert.dom(TS.submit).isEnabled('submit reenables if json editor has no linting errors');
|
||||
assert.propEqual(this.onChange.lastCall.args, ['data', data], 'onChange is called with json data');
|
||||
});
|
||||
|
||||
test('it calls onChange for ttl picker', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await click(GENERAL.toggleInput('Wrap TTL'));
|
||||
await fillIn(GENERAL.ttl.input('Wrap TTL'), '20');
|
||||
assert.propEqual(this.onChange.lastCall.args, ['wrapTTL', '1200s'], 'onChange is called with wrapTTL');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue