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:
parent
29d91d09ff
commit
2e35e9578c
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: Obscure secret values on input and displayOnly fields like certificates.
|
||||
```
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export default AuthConfig.extend({
|
|||
useOpenAPI: true,
|
||||
certificate: attr({
|
||||
label: 'Certificate',
|
||||
editType: 'textarea',
|
||||
editType: 'file',
|
||||
}),
|
||||
fieldGroups: computed('newFields', function() {
|
||||
let groups = [
|
||||
|
|
|
@ -143,6 +143,7 @@ export default Certificate.extend({
|
|||
csr: attr('string', {
|
||||
editType: 'textarea',
|
||||
label: 'CSR',
|
||||
masked: true,
|
||||
}),
|
||||
expiration: attr(),
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'"],
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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")}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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-----');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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]');
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Reference in New Issue