Commit ddc541bb authored by Miguel Rincon's avatar Miguel Rincon

Mask runner registration

This change prevents users from accidentally revealing the runner
registration token by adding a "****" style mask.

Users can click on the corresponding icon to reveal the token if needed.

Changelog: changed
parent b05c6c7f
<script>
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlButton,
},
props: {
value: {
type: String,
required: false,
default: '',
},
},
data() {
return {
isMasked: true,
};
},
computed: {
label() {
if (this.isMasked) {
return __('Click to reveal');
}
return __('Click to hide');
},
icon() {
if (this.isMasked) {
return 'eye';
}
return 'eye-slash';
},
displayedValue() {
if (this.isMasked && this.value?.length) {
return '*'.repeat(this.value.length);
}
return this.value;
},
},
methods: {
toggleMasked() {
this.isMasked = !this.isMasked;
},
},
};
</script>
<template>
<span
>{{ displayedValue }}
<gl-button
:aria-label="label"
:icon="icon"
class="gl-text-body!"
data-testid="toggle-masked"
variant="link"
@click="toggleMasked"
/>
</span>
</template>
<script> <script>
import { GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; import { GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import MaskedValue from '~/runner/components/helpers/masked_value.vue';
import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue'; import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue'; import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
...@@ -11,6 +12,7 @@ export default { ...@@ -11,6 +12,7 @@ export default {
GlLink, GlLink,
GlSprintf, GlSprintf,
ClipboardButton, ClipboardButton,
MaskedValue,
RunnerInstructions, RunnerInstructions,
RunnerRegistrationTokenReset, RunnerRegistrationTokenReset,
}, },
...@@ -92,7 +94,9 @@ export default { ...@@ -92,7 +94,9 @@ export default {
{{ __('And this registration token:') }} {{ __('And this registration token:') }}
<br /> <br />
<code data-testid="registration-token">{{ currentRegistrationToken }}</code> <code data-testid="registration-token"
><masked-value :value="currentRegistrationToken"
/></code>
<clipboard-button :title="__('Copy token')" :text="currentRegistrationToken" /> <clipboard-button :title="__('Copy token')" :text="currentRegistrationToken" />
</li> </li>
</ol> </ol>
......
...@@ -6834,6 +6834,12 @@ msgstr "" ...@@ -6834,6 +6834,12 @@ msgstr ""
msgid "Click to expand text" msgid "Click to expand text"
msgstr "" msgstr ""
msgid "Click to hide"
msgstr ""
msgid "Click to reveal"
msgstr ""
msgid "Client authentication certificate" msgid "Client authentication certificate"
msgstr "" msgstr ""
......
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MaskedValue from '~/runner/components/helpers/masked_value.vue';
const mockSecret = '01234567890';
const mockMasked = '***********';
describe('MaskedValue', () => {
let wrapper;
const findButton = () => wrapper.findComponent(GlButton);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(MaskedValue, {
propsData: {
value: mockSecret,
...props,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('Displays masked value by default', () => {
expect(wrapper.text()).toBe(mockMasked);
});
describe('When the icon is clicked', () => {
beforeEach(() => {
findButton().vm.$emit('click');
});
it('Displays the actual value', () => {
expect(wrapper.text()).toBe(mockSecret);
expect(wrapper.text()).not.toBe(mockMasked);
});
it('When user clicks again, displays masked value', async () => {
await findButton().vm.$emit('click');
expect(wrapper.text()).toBe(mockMasked);
expect(wrapper.text()).not.toBe(mockSecret);
});
});
});
...@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import MaskedValue from '~/runner/components/helpers/masked_value.vue';
import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue'; import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue'; import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
...@@ -37,6 +38,7 @@ describe('RunnerManualSetupHelp', () => { ...@@ -37,6 +38,7 @@ describe('RunnerManualSetupHelp', () => {
...props, ...props,
}, },
stubs: { stubs: {
MaskedValue,
GlSprintf, GlSprintf,
}, },
}), }),
...@@ -93,7 +95,11 @@ describe('RunnerManualSetupHelp', () => { ...@@ -93,7 +95,11 @@ describe('RunnerManualSetupHelp', () => {
expect(findRunnerInstructions().exists()).toBe(true); expect(findRunnerInstructions().exists()).toBe(true);
}); });
it('Displays the registration token', () => { it('Displays the registration token', async () => {
findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click');
await nextTick();
expect(findRegistrationToken().text()).toBe(mockRegistrationToken); expect(findRegistrationToken().text()).toBe(mockRegistrationToken);
expect(findClipboardButtons().at(1).props('text')).toBe(mockRegistrationToken); expect(findClipboardButtons().at(1).props('text')).toBe(mockRegistrationToken);
}); });
...@@ -105,6 +111,7 @@ describe('RunnerManualSetupHelp', () => { ...@@ -105,6 +111,7 @@ describe('RunnerManualSetupHelp', () => {
it('Replaces the runner reset button', async () => { it('Replaces the runner reset button', async () => {
const mockNewRegistrationToken = 'NEW_MOCK_REGISTRATION_TOKEN'; const mockNewRegistrationToken = 'NEW_MOCK_REGISTRATION_TOKEN';
findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click');
findRunnerRegistrationTokenReset().vm.$emit('tokenReset', mockNewRegistrationToken); findRunnerRegistrationTokenReset().vm.$emit('tokenReset', mockNewRegistrationToken);
await nextTick(); await nextTick();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment