Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export default defineComponent({
:value="value || ''"
:placeholder="_placeholder"
autocapitalize="off"
:class="{ conceal: type === 'multiline-password' }"
:class="{ 'multiline-password': type === 'multiline-password' }"
:aria-describedby="ariaDescribedBy"
:aria-required="requiredField"
@update:value="onInput"
Expand Down Expand Up @@ -464,6 +464,10 @@ export default defineComponent({
</div>
</template>
<style scoped lang="scss">
.multiline-password:not(:focus) {
-webkit-text-security: disc;
}

.labeled-input.view {
input {
text-overflow: ellipsis;
Expand Down
1 change: 0 additions & 1 deletion shell/assets/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
@import "./base/spacing";

@import "./fonts/fontstack";
@import "./fonts/dots";
@import "./fonts/zerowidthspace";
@import "./fonts/icons";

Expand Down
18 changes: 0 additions & 18 deletions shell/assets/styles/fonts/_dots.scss

This file was deleted.

15 changes: 12 additions & 3 deletions shell/components/DetailText.vue
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,9 @@ export default {
>{{ body }}</span>

<CodeMirror
v-else-if="jsonStr"
v-else-if="jsonStr && !concealed"
:options="{mode:{name:'javascript', json:true}, lineNumbers:false, foldGutter:false, readOnly:true}"
:value="jsonStr"
:class="{'conceal': concealed}"
aria-live="polite"
/>

Expand All @@ -199,9 +198,16 @@ export default {
:class="{'conceal-wrapper': concealed}"
>
<span
v-if="concealed"
data-testid="detail-top_html"
class="conceal"
aria-live="polite"
/>
<span
v-else
v-clean-html="bodyHtml"
data-testid="detail-top_html"
:class="{'conceal': concealed, 'monospace': monospace && !isBinary}"
:class="{'monospace': monospace && !isBinary}"
aria-live="polite"
/>
</div>
Expand Down Expand Up @@ -270,6 +276,9 @@ export default {
.conceal {
white-space: nowrap;
display: block;
&::before {
content: '••••••••••••••••••••••••';
}
}

.action-group {
Expand Down
113 changes: 113 additions & 0 deletions shell/components/__tests__/DetailText.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { mount } from '@vue/test-utils';

import DetailText from '@shell/components/DetailText.vue';

jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));

describe('component: DetailText', () => {
const defaultMocks = {
$store: {
getters: {
'i18n/t': jest.fn((key: string) => `%${ key }%`),
'prefs/get': jest.fn(() => true),
}
}
};

describe('concealment', () => {
it('should not render the actual secret value in the content area when concealed', () => {
const secretValue = 'super-secret-password-xyz';
const wrapper = mount(DetailText, {
props: {
value: secretValue,
conceal: true,
label: 'Password',
},

global: {
mocks: defaultMocks,
directives: {
'clean-html': () => {},
'clean-tooltip': () => {},
t: () => {},
},
stubs: {
CopyToClipboard: true,
CodeMirror: true,
},
},
});

const concealedSpan = wrapper.find('[data-testid="detail-top_html"]');

expect(concealedSpan.exists()).toBe(true);
expect(concealedSpan.classes()).toContain('conceal');
expect(concealedSpan.text()).not.toContain(secretValue);
});

it('should render the actual value when not concealed', () => {
const visibleValue = 'visible-value-123';
const wrapper = mount(DetailText, {
props: {
value: visibleValue,
conceal: false,
label: 'Data',
},

global: {
mocks: defaultMocks,
directives: {
'clean-html': (el: HTMLElement, binding: { value: string }) => {
el.innerHTML = binding.value;
},
'clean-tooltip': () => {},
t: () => {},
},
stubs: {
CopyToClipboard: true,
CodeMirror: true,
},
},
});

const contentSpan = wrapper.find('[data-testid="detail-top_html"]');

expect(contentSpan.exists()).toBe(true);
expect(contentSpan.classes()).not.toContain('conceal');
});

it('should not render JSON secret values in CodeMirror when concealed', () => {
const jsonSecret = '{"api_key": "secret-key-123"}';
const wrapper = mount(DetailText, {
props: {
value: jsonSecret,
conceal: true,
label: 'Config',
},

global: {
mocks: defaultMocks,
directives: {
'clean-html': () => {},
'clean-tooltip': () => {},
t: () => {},
},
stubs: {
CopyToClipboard: true,
CodeMirror: true,
},
},
});

const codeMirror = wrapper.findComponent({ name: 'CodeMirror' });

expect(codeMirror.exists()).toBe(false);

const concealedSpan = wrapper.find('[data-testid="detail-top_html"]');

expect(concealedSpan.exists()).toBe(true);
expect(concealedSpan.classes()).toContain('conceal');
expect(concealedSpan.text()).not.toContain('secret-key-123');
});
});
});
16 changes: 15 additions & 1 deletion shell/components/form/KeyValue.vue
Original file line number Diff line number Diff line change
Expand Up @@ -778,11 +778,16 @@ export default {
@onInput="onInputMarkdownMultiline(i, $event)"
@onFocus="onFocusMarkdownMultiline(i, $event)"
/>
<div
v-else-if="valueConcealed"
class="concealed-value conceal"
data-testid="concealed-value"
:aria-label="t('generic.ariaLabel.value', {index: i+1})"
/>
<TextAreaAutoGrow
v-else-if="valueMultiline && row[valueName] !== undefined"
v-model:value="row[valueName]"
data-testid="value-multiline"
:class="{'conceal': valueConcealed}"
:disabled="disabled"
:mode="mode"
:placeholder="_valuePlaceholder"
Expand Down Expand Up @@ -941,6 +946,15 @@ export default {
padding: 10px 10px 10px 10px;
}

.concealed-value {
padding: 10px;
min-height: 40px;
user-select: none;
&::before {
content: '••••••••••••••••••••';
}
}
Comment thread
adifsgaid marked this conversation as resolved.

.text-monospace:not(.conceal) {
font-family: monospace, monospace;
}
Expand Down
66 changes: 66 additions & 0 deletions shell/components/form/__tests__/KeyValue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,72 @@ describe('component: KeyValue', () => {
expect(firstValueInput.element.value).toBe('testvalue1');
});

describe('valueConcealed', () => {
it('should not render actual secret values in the DOM when valueConcealed is true', () => {
const secretValue = 'super-secret-api-key-12345';
const wrapper = mount(KeyValue, {
props: {
value: { mySecret: secretValue },
mode: 'view',
valueConcealed: true,
},

global: {
mocks: { $store: { getters: { 'i18n/t': jest.fn() } } },
stubs: { CodeMirror: true },
},
});

const concealedEl = wrapper.find('[data-testid="concealed-value"]');

expect(concealedEl.exists()).toBe(true);
expect(wrapper.html()).not.toContain(secretValue);
});

it('should render a TextAreaAutoGrow with the real value when valueConcealed is false', () => {
const secretValue = 'visible-value';
const wrapper = mount(KeyValue, {
props: {
value: { myKey: secretValue },
mode: 'view',
valueConcealed: false,
},

global: {
mocks: { $store: { getters: { 'i18n/t': jest.fn() } } },
stubs: { CodeMirror: true },
},
});

const concealedEl = wrapper.find('[data-testid="concealed-value"]');

expect(concealedEl.exists()).toBe(false);

const multilineEl = wrapper.find('[data-testid="value-multiline"]');

expect(multilineEl.exists()).toBe(true);
});

it('should have user-select none on the concealed placeholder to prevent text selection', () => {
const wrapper = mount(KeyValue, {
props: {
value: { mySecret: 'secret' },
mode: 'view',
valueConcealed: true,
},

global: {
mocks: { $store: { getters: { 'i18n/t': jest.fn() } } },
stubs: { CodeMirror: true },
},
});

const concealedEl = wrapper.find('[data-testid="concealed-value"]');

expect(concealedEl.classes()).toContain('concealed-value');
});
});

it('a11y: adding ARIA props should correctly fill out the appropriate fields on the component', async() => {
const value = [{ key: 'testkey1', value: 'testvalue1' }];

Expand Down
Loading