Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: add warning before downloading secret data #23260

Merged
merged 9 commits into from
Sep 22, 2023
3 changes: 3 additions & 0 deletions changelog/23260.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: Adds warning before downloading KV v2 secret values
```
4 changes: 4 additions & 0 deletions ui/lib/core/addon/components/download-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { assert } from '@ember/debug';
* @param {function} [fetchData] - function that fetches data and returns download content
* @param {string} [extension='txt'] - file extension, the download service uses this to determine the mimetype
* @param {boolean} [stringify=false] - argument to stringify the data before passing to the File constructor
* @param {callback} [onSuccess] - callback from parent to invoke if download is successful
*/

export default class DownloadButton extends Component {
Expand Down Expand Up @@ -73,6 +74,9 @@ export default class DownloadButton extends Component {
try {
this.download.miscExtension(this.filename, this.content, this.extension);
this.flashMessages.info(`Downloading ${this.filename}`);
if (this.args.onSuccess) {
this.args.onSuccess();
}
hellobontempo marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
this.flashMessages.danger(errorMessage(error, 'There was a problem downloading. Please try again.'));
}
Expand Down
44 changes: 34 additions & 10 deletions ui/lib/core/addon/components/masked-input.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,9 @@
/>
{{/if}}
{{#if @allowDownload}}
<DownloadButton
class="button download-button"
@filename={{or @name "secret-value"}}
@data={{@value}}
@stringify={{true}}
aria-label="Download secret value"
>
<Icon @name="download" />
</DownloadButton>
<button type="button" class="button download-button" {{on "click" (fn (mut this.modalOpen) true)}}>
<Icon data-test-download-icon @name="download" />
</button>
{{/if}}
<button
onclick={{this.toggleMask}}
Expand All @@ -64,4 +58,34 @@
>
<Icon @name={{if this.showValue "eye" "eye-off"}} />
</button>
</div>
</div>

{{! CONFIRM DOWNLOAD MODAL }}
{{#if @allowDownload}}
<Modal
@title="Download secret value?"
@onClose={{action (mut this.modalOpen) false}}
@isActive={{this.modalOpen}}
@type="warning"
>
<section class="modal-card-body">
This download is
<strong>unencrypted</strong>. Are you sure you want to download this secret data as plaintext?
</section>
<footer class="modal-card-foot modal-card-foot-outlined">
<DownloadButton
class="button is-primary"
@filename={{or @name "secret-value"}}
@data={{@value}}
@stringify={{true}}
aria-label="Download secret value"
@onSuccess={{fn (mut this.modalOpen) false}}
>
Download
</DownloadButton>
<button type="button" class="button is-secondary" {{on "click" (fn (mut this.modalOpen) false)}}>
Cancel
</button>
</footer>
</Modal>
{{/if}}
2 changes: 2 additions & 0 deletions ui/lib/core/addon/components/masked-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ import autosize from 'autosize';
* @param name {String} - The key correlated to the value. Used for the download file name.
* @param [onChange=Callback] {Function|action} - Callback triggered on change, sends new value. Must set the value of @value
* @param [allowCopy=false] {bool} - Whether or not the input should render with a copy button.
* @param [allowDownload=false] {bool} - Renders a download button that prompts a confirmation modal to download the secret value
* @param [displayOnly=false] {bool} - Whether or not to display the value as a display only `pre` element or as an input.
*
*/
export default class MaskedInputComponent extends Component {
textareaId = 'textarea-' + guidFor(this);
@tracked showValue = false;
@tracked modalOpen = false;

constructor() {
super(...arguments);
Expand Down
25 changes: 11 additions & 14 deletions ui/tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,27 @@

<html>
<head>
<meta charset="utf-8">
<meta charset="utf-8" />
<title>Vault Tests</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

{{content-for "head"}}
{{content-for "test-head"}}
{{content-for "head"}} {{content-for "test-head"}}

<link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
<link rel="stylesheet" href="{{rootURL}}assets/vault.css">
<link rel="stylesheet" href="{{rootURL}}assets/test-support.css">
<link rel="stylesheet" href="{{rootURL}}assets/vendor.css" />
<link rel="stylesheet" href="{{rootURL}}assets/vault.css" />
<link rel="stylesheet" href="{{rootURL}}assets/test-support.css" />

{{content-for "head-footer"}}
{{content-for "test-head-footer"}}
{{content-for "head-footer"}} {{content-for "test-head-footer"}}
</head>
<body>
{{content-for "body"}}
{{content-for "test-body"}}
{{content-for "body"}} {{content-for "test-body"}}

<div id="qunit"></div>
<div id="qunit-fixture">
<div id="ember-testing-container">
<div id="ember-testing"></div>
<div id="modal-wormhole"></div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! 🎉

</div>
</div>

Expand All @@ -38,7 +36,6 @@
<script src="{{rootURL}}assets/vault.js"></script>
<script src="{{rootURL}}assets/tests.js"></script>

{{content-for "body-footer"}}
{{content-for "test-body-footer"}}
{{content-for "body-footer"}} {{content-for "test-body-footer"}}
</body>
</html>
14 changes: 10 additions & 4 deletions ui/tests/integration/components/masked-input-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, focus, triggerKeyEvent, typeIn, fillIn } from '@ember/test-helpers';
import { render, focus, triggerKeyEvent, typeIn, fillIn, click } from '@ember/test-helpers';
import { create } from 'ember-cli-page-object';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
Expand Down Expand Up @@ -51,8 +51,14 @@ module('Integration | Component | masked input', function (hooks) {
});

test('it renders a download button when allowDownload is true', async function (assert) {
await render(hbs`<MaskedInput @allowDownload={{true}} />`);
assert.ok(component.downloadButtonIsPresent);
await render(hbs`<MaskedInput @allowDownload={{true}} /> <div id="modal-wormhole"></div>
`);
assert.ok(component.downloadIconIsPresent);

await click('[data-test-download-icon]');
assert.ok(component.downloadButtonIsPresent, 'clicking download icon opens modal with download button');

assert;
});

test('it shortens all outputs when displayOnly and masked', async function (assert) {
Expand Down Expand Up @@ -116,7 +122,7 @@ module('Integration | Component | masked input', function (hooks) {
`);
assert.dom('[data-test-masked-input]').exists('shows masked input');
assert.ok(component.copyButtonIsPresent);
assert.ok(component.downloadButtonIsPresent);
assert.ok(component.downloadIconIsPresent);
assert.dom('[data-test-button="toggle-masked"]').exists('shows toggle mask button');

await component.toggleMasked();
Expand Down
1 change: 1 addition & 0 deletions ui/tests/pages/components/masked-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { clickable, isPresent } from 'ember-cli-page-object';
export default {
textareaIsPresent: isPresent('[data-test-textarea]'),
copyButtonIsPresent: isPresent('[data-test-copy-button]'),
downloadIconIsPresent: isPresent('[data-test-download-icon]'),
downloadButtonIsPresent: isPresent('[data-test-download-button]'),
toggleMasked: clickable('[data-test-button="toggle-masked"]'),
copyValue: clickable('[data-test-copy-button]'),
Expand Down