UI/obscure secret on input (#11284)

* new font and add as font-family to be used in masked-input

* clean up logic

* refactor for displayOnly

* start cert masking

* work on certificates

* upload cert work

* fix global styling

* fix styling for class no longer used

* make mask by default and remove option

* glimmerize start and certificate on LDAP a file field

* glimmerize actions

* first part of glimmerizing text-file still need to do some clean up

* not doing awesome over here

* getting ready to un-glimmer

* unglimmerize

* remove placeholder based on conversations with design

* clean up text-file

* cleanup

* fix class bindings

* handle class binding

* set up for test

* fix elementId

* track down index

* update masked-input test

* add more to the masked-input test

* test-file test

* fix broken test

* clear old style

* clean up

* remove pgp key masked font, this really needs to be refactored to text-file component

* changelog

* cover other certificate view

* add allowCopy

* address some pr styling comments

* improve test coverage

* fix some issues

* add attr.options.masked
This commit is contained in:
Angel Garbarino 2021-04-22 08:58:37 -06:00 committed by GitHub
parent 29d91d09ff
commit 2e35e9578c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 339 additions and 244 deletions

3
changelog/11284.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Obscure secret values on input and displayOnly fields like certificates.
```

View File

@ -1,84 +1,84 @@
import Component from '@ember/component';
import Component from '@glimmer/component';
import { set } from '@ember/object';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { guidFor } from '@ember/object/internals';
export default Component.extend({
'data-test-component': 'text-file',
classNames: ['box', 'is-fullwidth', 'is-marginless', 'is-shadowless'],
classNameBindings: ['inputOnly:is-paddingless'],
/**
* @module TextFile
* `TextFile` components are file upload components where you can either toggle to upload a file or enter text.
*
* @example
* <TextFile
* @inputOnly={{true}}
* @helpText="help text"
* @file={{object}}
* @onChange={{action "someOnChangeFunction"}}
* @label={{"string"}}
* />
*
* @param [inputOnly] {bool} - When true, only the file input will be rendered
* @param [helpText] {string} - Text underneath label.
* @param file {object} - * Object in the shape of:
* {
* value: 'file contents here',
* fileName: 'nameOfFile.txt',
* enterAsText: boolean ability to enter as text
* }
* @param [onChange=Function.prototype] {Function|action} - A function to call when the value of the input changes.
* @param [label=null] {string} - Text to use as the label for the file input. If null, a default will be rendered.
*/
/*
* @public
* @param Object
* Object in the shape of:
* {
* value: 'file contents here',
* fileName: 'nameOfFile.txt',
* enterAsText: bool
* }
*/
file: null,
export default class TextFile extends Component {
fileHelpText = 'Select a file from your computer';
textareaHelpText = 'Enter the value as text';
elementId = guidFor(this);
index = '';
index: null,
onChange: () => {},
@tracked file = null;
@tracked showValue = false;
/*
* @public
* @param Boolean
* When true, only the file input will be rendered
*/
inputOnly: false,
/*
* @public
* @param String
* Text to use as the label for the file input
* If null, a default will be rendered
*/
label: null,
/*
* @public
* @param String
* Text to use as help under the file input
* If null, a default will be rendered
*/
fileHelpText: 'Select a file from your computer',
/*
* @public
* @param String
* Text to use as help under the textarea in text-input mode
* If null, a default will be rendered
*/
textareaHelpText: 'Enter the value as text',
get inputOnly() {
return this.args.inputOnly || false;
}
get label() {
return this.args.label || null;
}
readFile(file) {
const reader = new FileReader();
reader.onload = () => this.setFile(reader.result, file.name);
reader.readAsText(file);
},
}
setFile(contents, filename) {
this.onChange(this.index, { value: contents, fileName: filename });
},
this.args.onChange(this.index, { value: contents, fileName: filename });
}
actions: {
pickedFile(e) {
const { files } = e.target;
if (!files.length) {
return;
}
for (let i = 0, len = files.length; i < len; i++) {
this.readFile(files[i]);
}
},
updateData(e) {
const file = this.file;
set(file, 'value', e.target.value);
this.onChange(this.index, this.file);
},
clearFile() {
this.onChange(this.index, { value: '' });
},
},
});
@action
pickedFile(e) {
e.preventDefault();
const { files } = e.target;
if (!files.length) {
return;
}
for (let i = 0, len = files.length; i < len; i++) {
this.readFile(files[i]);
}
}
@action
updateData(e) {
e.preventDefault();
let file = this.args.file;
set(file, 'value', e.target.value);
this.args.onChange(this.index, file);
}
@action
clearFile() {
this.args.onChange(this.index, { value: '' });
}
@action
toggleMask() {
this.showValue = !this.showValue;
}
}

View File

@ -9,7 +9,7 @@ export default AuthConfig.extend({
useOpenAPI: true,
certificate: attr({
label: 'Certificate',
editType: 'textarea',
editType: 'file',
}),
fieldGroups: computed('newFields', function() {
let groups = [

View File

@ -143,6 +143,7 @@ export default Certificate.extend({
csr: attr('string', {
editType: 'textarea',
label: 'CSR',
masked: true,
}),
expiration: attr(),

View File

@ -63,14 +63,20 @@ export default Model.extend({
defaultValue: false,
}),
certificate: attr('string'),
certificate: attr('string', {
masked: true,
}),
issuingCa: attr('string', {
label: 'Issuing CA',
masked: true,
}),
caChain: attr('string', {
label: 'CA chain',
masked: true,
}),
privateKey: attr('string', {
masked: true,
}),
privateKey: attr('string'),
privateKeyType: attr('string'),
serialNumber: attr('string'),

View File

@ -1,3 +1,12 @@
@import 'ember-basic-dropdown';
@import 'ember-power-select';
@import './core';
@mixin font-face($name) {
@font-face {
font-family: $name;
src: url('/ui/fonts/#{$name}.woff2') format('woff2'), url('/ui/fonts/#{$name}.woff') format('woff');
}
}
@include font-face('obscure');

View File

@ -1,13 +1,12 @@
.masked-font {
color: $ui-gray-300;
}
.masked-input {
display: flex;
align-items: center;
}
.masked-input.masked.display-only,
.masked-input:not(.masked) {
align-items: start;
}
.has-label .masked-input {
padding-top: $spacing-s;
}
@ -91,10 +90,6 @@
color: $grey-light;
}
.masked-input:not(.masked) .masked-input-toggle {
color: $blue;
}
.masked-input .input:focus + .masked-input-toggle {
background: rgba($white, 0.95);
}

View File

@ -0,0 +1,14 @@
.text-file {
.has-icon-right {
display: flex;
width: 97%;
.textarea {
line-height: inherit;
}
}
.button.masked-input-toggle,
.button.copy-button {
display: flex;
}
}

View File

@ -99,6 +99,7 @@
@import './components/splash-page';
@import './components/status-menu';
@import './components/tabs';
@import './components/text-file';
@import './components/token-expire-warning';
@import './components/toolbar';
@import './components/tool-tip';

View File

@ -11,6 +11,13 @@ label {
}
}
.masked-font {
font-family: obscure;
font-size: $size-medium;
letter-spacing: 2px;
color: $ui-gray-300;
}
.label {
color: $grey-darker;
text-transform: uppercase;

View File

@ -9,7 +9,27 @@
</h2>
{{#if (or model.certificate model.csr)}}
{{#each model.attrs as |attr|}}
{{info-table-row data-test-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
{{#if attr.options.masked}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
<MaskedInput
@value={{get model attr.name}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
{{else if (eq attr.name "expiration")}}
{{info-table-row
data-test-table-row
label=(capitalize (or attr.options.label (humanize (dasherize attr.name))))
value=(date-format (get model attr.name) 'MMM dd, yyyy hh:mm:ss a')
}}
{{else}}
{{info-table-row
data-test-table-row
label=(capitalize (or attr.options.label (humanize (dasherize attr.name))))
value=(get model attr.name)
}}
{{/if}}
{{/each}}
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
@ -70,7 +90,17 @@
data-test-warning
/>
{{#each model.attrs as |attr|}}
{{info-table-row data-test-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
{{#if attr.options.masked}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
<MaskedInput
@value={{get model attr.name}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
{{else}}
{{info-table-row data-test-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
{{/if}}
{{/each}}
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">

View File

@ -49,12 +49,14 @@
(eq attr.name "secretKey")
(eq attr.name "securityToken")
(eq attr.name "privateKey")
attr.options.masked
)}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
<MaskedInput
@value={{get model attr.name}}
@name={{attr.name}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
{{else}}

View File

@ -15,7 +15,17 @@
{{#if (eq attr.type "object")}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{stringify (get model attr.name)}} />
{{else}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}} />
{{#if attr.options.masked}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}}>
<MaskedInput
@value={{get model attr.name}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
{{else}}
<InfoTableRow @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}} @value={{get model attr.name}} />
{{/if}}
{{/if}}
{{/each}}
</div>

View File

@ -1,13 +1,13 @@
{{#unless inputOnly}}
{{#unless this.inputOnly}}
<div class="level is-mobile">
<div class="level-left">
<label class="is-label" data-test-text-label=true>
{{#if label}}
{{label}}
{{#if helpText}}
{{#if this.label}}
{{this.label}}
{{#if @helpText}}
<InfoTooltip>
<span data-test-help-text>
{{helpText}}
{{@helpText}}
</span>
</InfoTooltip>
{{/if}}
@ -20,50 +20,57 @@
<div class="control is-flex">
<input
data-test-text-toggle=true
id={{concat "useText-" elementId}}
id={{concat "useText-" this.elementId}}
type="checkbox"
name={{concat "useText-" elementId}}
name={{concat "useText-" this.elementId}}
class="switch is-rounded is-success is-small"
checked={{file.enterAsText}}
onchange={{action (toggle "enterAsText" file)}}
checked={{@file.enterAsText}}
{{on 'change' (toggle "enterAsText" @file)}}
/>
<label for={{concat "useText-" elementId}}>
<label for={{concat "useText-" this.elementId}}>
Enter as text
</label>
</div>
</div>
</div>
{{/unless}}
<div class="field">
{{#if file.enterAsText}}
<div class="control">
<div class="field text-file box is-fullwidth is-marginless is-shadowless {{if this.inputOnly "is-paddingless"}}" data-test-component="text-file">
{{#if @file.enterAsText}}
<div class="control has-icon-right">
<textarea
class="textarea"
oninput={{action "updateData"}}
class="textarea {{unless showValue "masked-font"}}"
{{on 'input' this.updateData}}
data-test-text-file-textarea=true
>{{file.value}}</textarea>
>{{@file.value}}</textarea>
<button
{{on 'click' this.toggleMask}}
type="button"
class="{{if (eq value "") "has-text-grey"}} masked-input-toggle button {{if displayOnly "is-compact"}}"
data-test-button>
<Icon @glyph={{if showValue "visibility-show" "visibility-hide"}} aria-hidden="true" />
</button>
</div>
<p class="help has-text-grey">
{{textareaHelpText}}
{{this.textareaHelpText}}
</p>
{{else}}
<div class="control is-expanded">
<div class="file has-name is-fullwidth">
<label class="file-label">
<input class="file-input" type="file" onchange={{action "pickedFile"}} data-test-text-file-input=true>
<input class="file-input" type="file" {{on 'change' this.pickedFile}} data-test-text-file-input=true>
<span class="file-cta button">
<Icon @glyph="upload" class="has-light-grey-text" />
Choose a file…
</span>
<span class="file-name has-text-grey-dark" data-test-text-file-input-label=true>
{{#if file.fileName}}
{{file.fileName}}
{{#if @file.fileName}}
{{@file.fileName}}
{{else}}
No file chosen
{{/if}}
</span>
{{#if file.fileName}}
<button type="button" class="file-delete-button" {{action 'clearFile'}} data-test-text-clear=true>
{{#if @file.fileName}}
<button type="button" class="file-delete-button" {{on 'click' this.clearFile}} data-test-text-clear=true>
<Icon @glyph="cancel-circle-outline" />
</button>
{{/if}}
@ -71,7 +78,7 @@
</div>
</div>
<p class="help has-text-grey">
{{fileHelpText}}
{{this.fileHelpText}}
</p>
{{/if}}
</div>

View File

@ -5,13 +5,20 @@
Public key
</label>
<div class="control">
<Textarea @name="publicKey" @id="publicKey" class="textarea" @value={{model.publicKey}} @readonly={{true}} data-test-ssh-input="public-key" />
<MaskedInput
@name="publickey"
@id="publicKey"
@value={{model.publicKey}}
@displayOnly={{true}}
@allowCopy={{true}}
data-test-ssh-input="public-key"
/>
</div>
</div>
</div>
<div class="field is-grouped-split box is-fullwidth is-bottomless">
<div class="control">
<CopyButton @clipboardTarget="#publicKey" @class="button is-primary" @buttonType="button" @success={{action (set-flash-message "Public Key copied!")}}>
<CopyButton @clipboardText={{model.publicKey}} @class="button is-primary" @buttonType="button" @success={{action (set-flash-message "Public Key copied!")}}>
Copy
</CopyButton>
</div>

View File

@ -46,7 +46,7 @@
</div>
</div>
{{#if showFileUpload}}
<TextFile @inputOnly={{true}} @index="" @file={{file}} @onChange={{action "setPolicyFromFile"}} />
<TextFile @inputOnly={{true}} @file={{file}} @onChange={{action "setPolicyFromFile"}} />
{{else}}
<IvyCodemirror @value={{model.policy}} @id="policy" @valueUpdated={{action (mut model.policy)}} @options={{hash
lineNumbers=true

View File

@ -67,6 +67,7 @@ module.exports = function(environment) {
ENV.contentSecurityPolicy = {
'connect-src': ["'self'"],
'img-src': ["'self'", 'data:'],
'font-src': ["'self'"],
'form-action': ["'none'"],
'script-src': ["'self'"],
'style-src': ["'unsafe-inline'", "'self'"],

View File

@ -1,5 +1,4 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import autosize from 'autosize';
import layout from '../templates/components/masked-input';
@ -10,24 +9,21 @@ import layout from '../templates/components/masked-input';
* @example
* <MaskedInput
* @value={{attr.options.defaultValue}}
* @placeholder="secret"
* @allowCopy={{true}}
* @onChange={{action "someAction"}}
* />
*
* @param [value] {String} - The value to display in the input.
* @param [placeholder=value] {String} - The placeholder to display before the user has entered any input.
* @param [allowCopy=null] {bool} - Whether or not the input should render with a copy button.
* @param [displayOnly=false] {bool} - Whether or not to display the value as a display only `pre` element or as an input.
* @param [onChange=Function.prototype] {Function|action} - A function to call when the value of the input changes.
*
* @param [isCertificate=false] {bool} - If certificate display the label and icons differently.
*
*/
export default Component.extend({
layout,
value: null,
placeholder: 'value',
showValue: false,
didInsertElement() {
this._super(...arguments);
autosize(this.element.querySelector('textarea'));
@ -40,30 +36,12 @@ export default Component.extend({
this._super(...arguments);
autosize.destroy(this.element.querySelector('textarea'));
},
shouldObscure: computed('isMasked', 'isFocused', 'value', function() {
if (this.value === '') {
return false;
}
if (this.isFocused === true) {
return false;
}
return this.isMasked;
}),
displayValue: computed('shouldObscure', 'value', function() {
if (this.shouldObscure) {
return '■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■';
} else {
return this.value;
}
}),
isMasked: true,
isFocused: false,
displayOnly: false,
onKeyDown() {},
onChange() {},
actions: {
toggleMask() {
this.toggleProperty('isMasked');
this.toggleProperty('showValue');
},
updateValue(e) {
let value = e.target.value;

View File

@ -4,6 +4,12 @@ import { format, parseISO } from 'date-fns';
export function dateFormat([date, style]) {
// see format breaking in upgrade to date-fns 2.x https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md#changed-5
let number = typeof date === 'string' ? parseISO(date) : date;
if (!number) {
return;
}
if (number.toString().length === 10) {
number = new Date(number * 1000);
}
return format(number, style);
}

View File

@ -110,12 +110,9 @@
{{else if (eq attr.options.editType "file")}}
{{!-- File Input --}}
{{text-file
index=""
fileHelpText=attr.options.helpText
textareaHelpText=attr.options.helpText
helpText=attr.options.helpText
file=file
onChange=(action "setFile")
warning=attr.options.warning
label=labelString
}}
{{else if (eq attr.options.editType "ttl")}}
@ -175,8 +172,11 @@
}}
{{else if (eq attr.options.sensitive true)}}
{{!-- Masked Input --}}
<MaskedInput @value={{or (get model valuePath) attr.options.defaultValue}} @placeholder="" @allowCopy="true"
@onChange={{action (action "setAndBroadcast" valuePath)}} @maskWhileTyping={{if (eq attr.name "bindpass") true}}/>
<MaskedInput
@value={{or (get model valuePath) attr.options.defaultValue}}
@allowCopy="true"
@onChange={{action (action "setAndBroadcast" valuePath)}}
/>
{{else if (or (eq attr.type "number") (eq attr.type "string"))}}
<div class="control">
{{#if (eq attr.options.editType "textarea")}}

View File

@ -1,35 +1,41 @@
<div class="masked-input {{if shouldObscure "masked"}} {{if displayOnly "display-only"}} {{if allowCopy "allow-copy"}}"
<div class="masked-input {{if displayOnly "display-only"}} {{if allowCopy "allow-copy"}}"
data-test-masked-input data-test-field>
{{#if displayOnly}}
<pre class="masked-value display-only is-word-break">{{displayValue}}</pre>
{{else if maskWhileTyping}}
<pre class="masked-value display-only is-word-break {{unless showValue "masked-font"}}">{{unless showValue (truncate value 20) value}}</pre>
{{else if inputField}}
<input
autocomplete="off"
spellcheck="false"
type={{if isMasked "password" "text"}}
value={{value}}
class="input {{unless showValue "masked-font"}}"
onchange={{action "updateValue"}}
class="input"
data-test-input
/>
{{else}}
<textarea class="input masked-value" rows=1 wrap="off" placeholder={{placeholder}}
onfocus={{action (mut isFocused) true}} onblur={{action (mut isFocused) false}} onkeydown={{action onKeyDown}}
onchange={{action "updateValue"}} value={{readonly displayValue}} data-test-textarea />
<textarea
class="input masked-value {{unless showValue "masked-font"}}"
rows=1 wrap="off"
onkeydown={{action onKeyDown}}
onchange={{action "updateValue"}}
value={{value}}
data-test-textarea
/>
{{/if}}
{{#if allowCopy}}
<CopyButton
@clipboardText={{value}}
@success={{action (set-flash-message 'Data copied!')}}
class="copy-button button {{if displayOnly "is-compact"}}"
data-test-copy-button>
@clipboardText={{value}}
@success={{action (set-flash-message 'Data copied!')}}
class="copy-button button {{if displayOnly "is-compact"}}"
data-test-copy-button>
<Icon @glyph="copy-action" aria-hidden="Copy value" />
</CopyButton>
{{/if}}
<button {{action "toggleMask"}}
<button
onclick={{action "toggleMask"}}
type="button"
class="{{if (eq value "") "has-text-grey"}} masked-input-toggle button {{if displayOnly "is-compact"}}"
class="{{if (eq value "") "has-text-grey"}} masked-input-toggle button"
data-test-button>
<Icon @glyph={{if shouldObscure "visibility-hide" "visibility-show"}} aria-hidden="true" />
<Icon @glyph={{if showValue "visibility-show" "visibility-hide"}} aria-hidden="true" />
</button>
</div>
</div>

View File

@ -19,10 +19,13 @@
</Toolbar>
{{#if model}}
<FieldGroupShow @model={{model}} />
<InfoTableRow
@label="CA PEM"
@value={{model.ca.caPem}}
/>
<InfoTableRow @label="CA PEM" @value={{model.ca.caPem}}>
<MaskedInput
@value={{model.ca.caPem}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
{{else}}
<EmptyState
@title="No configuration for this secrets engine"

View File

@ -51,10 +51,13 @@
</ToolbarActions>
</Toolbar>
<div class="box is-shadowless is-fullwidth is-sideless">
<InfoTableRow
@label="Serial number"
@value={{model.id}}
/>
<InfoTableRow @label="Serial number" @value={{model.id}}>
<MaskedInput
@value={{model.id}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
<InfoTableRow
@label="Private key"
@value={{model.privateKey}}
@ -74,17 +77,24 @@
/>
</div>
</InfoTableRow>
<InfoTableRow
@label="Certificate"
@value={{model.certificate}}
/>
<InfoTableRow @label="Certificate" @value={{model.certificate}}>
<MaskedInput
@value={{model.certificate}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
</InfoTableRow>
<InfoTableRow
@label="CA Chain"
@value={{model.caChain}}
>
<div class="is-block">
{{#each model.caChain as |chain|}}
<code class="is-block is-word-break has-text-black">{{chain}}</code>
<MaskedInput
@value={{chain}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
{{/each}}
</div>
</InfoTableRow>

BIN
ui/public/fonts/obscure.woff (Stored with Git LFS) Normal file

Binary file not shown.

BIN
ui/public/fonts/obscure.woff2 (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,10 +1,9 @@
import { currentRouteName, settled } from '@ember/test-helpers';
import { currentRouteName, settled, click } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import editPage from 'vault/tests/pages/secrets/backend/pki/edit-role';
import listPage from 'vault/tests/pages/secrets/backend/list';
import generatePage from 'vault/tests/pages/secrets/backend/pki/generate-cert';
import showPage from 'vault/tests/pages/secrets/backend/pki/show';
import configPage from 'vault/tests/pages/settings/configure-secret-backends/pki/section-cert';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
import authPage from 'vault/tests/pages/auth';
@ -56,7 +55,11 @@ elRplAzrMF4=
await settled();
await generatePage.issueCert('foo');
await settled();
assert.ok(generatePage.hasCert, 'displays the cert');
let countMaskedFonts = document.querySelectorAll('.masked-font').length;
assert.equal(countMaskedFonts, 3); // certificate, issuing ca, and private key
let firstUnMaskButton = document.querySelectorAll('.masked-input-toggle')[0];
await click(firstUnMaskButton);
assert.dom('.masked-value').hasTextContaining('-----BEGIN CERTIFICATE-----');
await settled();
await generatePage.back();
await settled();
@ -68,7 +71,9 @@ elRplAzrMF4=
await settled();
await generatePage.sign('common', CSR);
await settled();
assert.ok(generatePage.hasCert, 'displays the cert');
let firstUnMaskButton = document.querySelectorAll('.masked-input-toggle')[0];
await click(firstUnMaskButton);
assert.dom('.masked-value').hasTextContaining('-----BEGIN CERTIFICATE-----');
});
test('it views a cert', async function(assert) {
@ -82,6 +87,8 @@ elRplAzrMF4=
await listPage.secrets.objectAt(0).click();
await settled();
assert.equal(currentRouteName(), 'vault.cluster.secrets.backend.show', 'navigates to the show page');
assert.ok(showPage.hasCert, 'shows the cert');
let firstUnMaskButton = document.querySelectorAll('.masked-input-toggle')[0];
await click(firstUnMaskButton);
assert.dom('.masked-value').hasTextContaining('-----BEGIN CERTIFICATE-----');
});
});

View File

@ -1,4 +1,4 @@
import { currentRouteName, settled } from '@ember/test-helpers';
import { currentRouteName, settled, click } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import page from 'vault/tests/pages/settings/configure-secret-backends/pki/section-cert';
@ -112,7 +112,8 @@ BXUV2Uwtxf+QCphnlht9muX2fsLIzDJea0JipWj1uf2H8OZsjE8=
await page.form.generateCA('Intermediate CA', 'intermediate');
await settled();
// cache csr
csrVal = page.form.csr;
await click('.masked-input-toggle');
csrVal = document.querySelector('.masked-value').innerText;
await page.form.back();
await settled();
await page.visit({ backend: rootPath });
@ -121,7 +122,8 @@ BXUV2Uwtxf+QCphnlht9muX2fsLIzDJea0JipWj1uf2H8OZsjE8=
await settled();
await page.form.csrField(csrVal).submit();
await settled();
intermediateCert = page.form.certificate;
await click('.masked-input-toggle');
intermediateCert = document.querySelector('[data-test-masked-input]').innerText;
await page.form.back();
await settled();
await page.visit({ backend: intermediatePath });

View File

@ -28,7 +28,7 @@ const setupWrapping = async () => {
module('Acceptance | wrapped_token query param functionality', function(hooks) {
setupApplicationTest(hooks);
test('it authenticates you if the query param is present meep', async function(assert) {
test('it authenticates you if the query param is present', async function(assert) {
let token = await setupWrapping();
await auth.visit({ wrapped_token: token });
await settled();

View File

@ -1,7 +1,7 @@
import EmberObject from '@ember/object';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { render, click, fillIn } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import { create } from 'ember-cli-page-object';
import sinon from 'sinon';
@ -102,6 +102,11 @@ module('Integration | Component | form field', function(hooks) {
test('it renders: editType file', async function(assert) {
await setup.call(this, createAttr('foo', 'string', { editType: 'file' }));
assert.ok(component.hasTextFile, 'renders the text-file component');
await click('[data-test-text-toggle="true"]');
await fillIn('[data-test-text-file-textarea="true"]', 'hello world');
assert.dom('[data-test-text-file-textarea="true"]').hasClass('masked-font');
await click('[data-test-button]');
assert.dom('[data-test-text-file-textarea="true"]').doesNotHaveClass('masked-font');
});
test('it renders: editType ttl', async function(assert) {

View File

@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { render, focus } from '@ember/test-helpers';
import { create } from 'ember-cli-page-object';
import hbs from 'htmlbars-inline-precompile';
import maskedInput from 'vault/tests/pages/components/masked-input';
@ -10,14 +10,9 @@ const component = create(maskedInput);
module('Integration | Component | masked input', function(hooks) {
setupRenderingTest(hooks);
const hasClass = (classString = '', classToFind) => {
return classString.split(' ').includes(classToFind);
};
test('it renders', async function(assert) {
await render(hbs`{{masked-input}}`);
assert.ok(hasClass(component.wrapperClass, 'masked'));
assert.dom('[data-test-masked-input]').exists('shows expiration beacon');
});
test('it renders a textarea', async function(assert) {
@ -26,14 +21,17 @@ module('Integration | Component | masked input', function(hooks) {
assert.ok(component.textareaIsPresent);
});
test('it renders an input with type password when maskWhileTyping is true', async function(assert) {
await render(hbs`{{masked-input maskWhileTyping=true}}`);
assert.ok(component.inputIsPresent);
assert.equal(
this.element.querySelector('input').getAttribute('type'),
'password',
'default type equals password'
);
test('it renders an input with obscure font', async function(assert) {
await render(hbs`{{masked-input}}`);
assert.dom('[data-test-textarea]').hasClass('masked-font', 'loading class with correct font');
});
test('it renders obscure font when displayOnly', async function(assert) {
this.set('value', 'value');
await render(hbs`{{masked-input displayOnly=true value=value}}`);
assert.dom('.masked-value').hasClass('masked-font', 'loading class with correct font');
});
test('it does not render a textarea when displayOnly is true', async function(assert) {
@ -54,36 +52,12 @@ module('Integration | Component | masked input', function(hooks) {
assert.notOk(component.copyButtonIsPresent);
});
test('it unmasks text on focus', async function(assert) {
this.set('value', 'value');
await render(hbs`{{masked-input value=value}}`);
assert.ok(hasClass(component.wrapperClass, 'masked'));
await component.focusField();
assert.notOk(hasClass(component.wrapperClass, 'masked'));
});
test('it remasks text on blur', async function(assert) {
this.set('value', 'value');
await render(hbs`{{masked-input value=value}}`);
assert.ok(hasClass(component.wrapperClass, 'masked'));
await component.focusField();
await component.blurField();
assert.ok(hasClass(component.wrapperClass, 'masked'));
});
test('it unmasks text when button is clicked', async function(assert) {
this.set('value', 'value');
await render(hbs`{{masked-input value=value}}`);
assert.ok(hasClass(component.wrapperClass, 'masked'));
await component.toggleMasked();
assert.notOk(hasClass(component.wrapperClass, 'masked'));
assert.dom('.masked-value').doesNotHaveClass('masked-font');
});
test('it remasks text when button is clicked', async function(assert) {
@ -93,18 +67,25 @@ module('Integration | Component | masked input', function(hooks) {
await component.toggleMasked();
await component.toggleMasked();
assert.ok(hasClass(component.wrapperClass, 'masked'));
assert.dom('.masked-value').hasClass('masked-font');
});
test('it changes type to text when unmasked button is clicked', async function(assert) {
this.set('value', 'value');
await render(hbs`{{masked-input value=value maskWhileTyping=true}}`);
await component.toggleMasked();
test('it shortens long outputs when displayOnly and masked', async function(assert) {
this.set('value', '123456789-123456789-123456789');
await render(hbs`{{masked-input value=value displayOnly=true}}`);
let maskedValue = document.querySelector('.masked-value').innerText;
assert.equal(maskedValue.length, 20);
assert.equal(
this.element.querySelector('input').getAttribute('type'),
'text',
'when unmasked type changes to text'
);
await component.toggleMasked();
let unMaskedValue = document.querySelector('.masked-value').innerText;
assert.equal(unMaskedValue.length, this.value.length);
});
test('it does not unmask text on focus', async function(assert) {
this.set('value', '123456789-123456789-123456789');
await render(hbs`{{masked-input value=value}}`);
assert.dom('.masked-value').hasClass('masked-font');
await focus('.masked-value');
assert.dom('.masked-value').hasClass('masked-font');
});
});

View File

@ -34,4 +34,12 @@ module('Integration | Helper | date-format', function(hooks) {
.dom('[data-test-date-format]')
.includesText(todayString, 'it renders the a date if passed in as a string');
});
test('it supports ten digit dates', async function(assert) {
let tenDigitDate = 1621785298;
this.set('tenDigitDate', tenDigitDate);
await render(hbs`<p data-test-date-format>Date: {{date-format tenDigitDate "MM/dd/yyyy"}}</p>`);
assert.dom('[data-test-date-format]').includesText('05/23/2021');
});
});

View File

@ -1,17 +1,7 @@
import { attribute, clickable, fillable, isPresent } from 'ember-cli-page-object';
import { focus, blur } from '@ember/test-helpers';
import { clickable, isPresent } from 'ember-cli-page-object';
export default {
wrapperClass: attribute('class', '[data-test-masked-input]'),
enterText: fillable('[data-test-textarea]'),
textareaIsPresent: isPresent('[data-test-textarea]'),
inputIsPresent: isPresent('[data-test-input]'),
copyButtonIsPresent: isPresent('[data-test-copy-button]'),
toggleMasked: clickable('[data-test-button]'),
async focusField() {
return focus('[data-test-textarea]');
},
async blurField() {
return blur('[data-test-textarea]');
},
};

View File

@ -32,7 +32,7 @@ type UIConfig struct {
// NewUIConfig creates a new UI config
func NewUIConfig(enabled bool, physicalStorage physical.Backend, barrierStorage logical.Storage) *UIConfig {
defaultHeaders := http.Header{}
defaultHeaders.Set("Content-Security-Policy", "default-src 'none'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'unsafe-inline' 'self'; form-action 'none'; frame-ancestors 'none'")
defaultHeaders.Set("Content-Security-Policy", "default-src 'none'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'unsafe-inline' 'self'; form-action 'none'; frame-ancestors 'none'; font-src 'self'")
defaultHeaders.Set("Service-Worker-Allowed", "/")
return &UIConfig{