diff --git a/shell/assets/translations/en-us.yaml b/shell/assets/translations/en-us.yaml index b58aad23bd2..fffd65e2429 100644 --- a/shell/assets/translations/en-us.yaml +++ b/shell/assets/translations/en-us.yaml @@ -2441,6 +2441,7 @@ fleet: resources: Resources unready: Non-Ready auth: + title: Authentication label: Authentication git: Git Authentication helm: Helm Authentication @@ -5144,10 +5145,22 @@ secret: username: Username ssh: keys: Keys + keysAndHosts: Keys and Known Hosts public: Public Key publicPlaceholder: "Paste in your public key" private: Private Key privatePlaceholder: "Paste in your private key" + knownHosts: Known Hosts + knownHostsPlaceholder: "Known hosts metadata, one per line" + editKnownHosts: + title: SSH Known Hosts Configuration + entries: |- + {entries, plural, + =0 {Empty} + =1 {1 Entry} + other {{entries} Entries} + } + serviceAcct: ca: CA Certificate token: Token diff --git a/shell/components/form/SSHKnownHosts/KnownHostsEditDialog.vue b/shell/components/form/SSHKnownHosts/KnownHostsEditDialog.vue new file mode 100644 index 00000000000..ca65739214e --- /dev/null +++ b/shell/components/form/SSHKnownHosts/KnownHostsEditDialog.vue @@ -0,0 +1,192 @@ + + + + + diff --git a/shell/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts b/shell/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts new file mode 100644 index 00000000000..cc0069dd870 --- /dev/null +++ b/shell/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts @@ -0,0 +1,104 @@ +import { mount, VueWrapper } from '@vue/test-utils'; +import { _EDIT } from '@shell/config/query-params'; +import KnownHostsEditDialog from '@shell/components/form/SSHKnownHosts/KnownHostsEditDialog.vue'; +import CodeMirror from '@shell/components/CodeMirror.vue'; +import FileSelector from '@shell/components/form/FileSelector.vue'; + +let wrapper: VueWrapper>; + +const mockedStore = () => { + return { getters: { 'prefs/get': () => jest.fn() } }; +}; + +const requiredSetup = () => { + return { global: { mocks: { $store: mockedStore() } } }; +}; + +describe('component: KnownHostsEditDialog', () => { + beforeEach(() => { + document.body.innerHTML = '
'; + wrapper = mount(KnownHostsEditDialog, { + attachTo: document.body, + props: { + mode: _EDIT, + value: 'line1\nline2\n', + }, + ...requiredSetup(), + }); + }); + + afterEach(() => { + wrapper.unmount(); + document.body.innerHTML = ''; + }); + + it('should update text from CodeMirror', async() => { + await wrapper.setData({ showModal: true }); + + expect(wrapper.vm.text).toBe('line1\nline2\n'); + + const codeMirror = wrapper.getComponent(CodeMirror); + + expect(codeMirror.element).toBeDefined(); + + await codeMirror.setData({ loaded: true }); + + // Emit CodeMirror value + codeMirror.vm.$emit('onInput', 'bar'); + await codeMirror.vm.$nextTick(); + + expect(wrapper.vm.text).toBe('bar'); + }); + + it('should update text from FileSelector', async() => { + await wrapper.setData({ showModal: true }); + + expect(wrapper.vm.text).toBe('line1\nline2\n'); + + const fileSelector = wrapper.getComponent(FileSelector); + + expect(fileSelector.element).toBeDefined(); + + // Emit Fileselector value + fileSelector.vm.$emit('selected', 'foo'); + await fileSelector.vm.$nextTick(); + + expect(wrapper.vm.text).toBe('foo'); + }); + + it('should save changes and close dialog', async() => { + await wrapper.setData({ + showModal: true, + text: 'foo', + }); + + expect(wrapper.vm.value).toBe('line1\nline2\n'); + expect(wrapper.vm.text).toBe('foo'); + + await wrapper.vm.closeDialog(true); + + expect((wrapper.emitted('closed') as any)[0][0].value).toBe('foo'); + + const dialog = wrapper.vm.$refs['sshKnownHostsDialog']; + + expect(dialog).toBeNull(); + }); + + it('should discard changes and close dialog', async() => { + await wrapper.setData({ + showModal: true, + text: 'foo', + }); + + expect(wrapper.vm.value).toBe('line1\nline2\n'); + expect(wrapper.vm.text).toBe('foo'); + + await wrapper.vm.closeDialog(false); + + expect((wrapper.emitted('closed') as any)[0][0].value).toBe('line1\nline2\n'); + + const dialog = wrapper.vm.$refs['sshKnownHostsDialog']; + + expect(dialog).toBeNull(); + }); +}); diff --git a/shell/components/form/SSHKnownHosts/index.vue b/shell/components/form/SSHKnownHosts/index.vue new file mode 100644 index 00000000000..4c7bcaa1592 --- /dev/null +++ b/shell/components/form/SSHKnownHosts/index.vue @@ -0,0 +1,101 @@ + + + diff --git a/shell/components/form/SelectOrCreateAuthSecret.vue b/shell/components/form/SelectOrCreateAuthSecret.vue index 973486e1695..0a6e02283bf 100644 --- a/shell/components/form/SelectOrCreateAuthSecret.vue +++ b/shell/components/form/SelectOrCreateAuthSecret.vue @@ -3,6 +3,7 @@ import { _EDIT } from '@shell/config/query-params'; import { Banner } from '@components/Banner'; import { LabeledInput } from '@components/Form/LabeledInput'; import LabeledSelect from '@shell/components/form/LabeledSelect'; +import SSHKnownHosts from '@shell/components/form/SSHKnownHosts'; import { AUTH_TYPE, NORMAN, SECRET } from '@shell/config/types'; import { SECRET_TYPES } from '@shell/config/secret'; import { base64Encode } from '@shell/utils/crypto'; @@ -23,6 +24,7 @@ export default { Banner, LabeledInput, LabeledSelect, + SSHKnownHosts, }, props: { @@ -142,6 +144,11 @@ export default { type: Boolean, default: false, }, + + showSshKnownHosts: { + type: Boolean, + default: true, + }, }, async fetch() { @@ -172,6 +179,7 @@ export default { if ( !this.value ) { this.publicKey = this.preSelect?.publicKey || ''; this.privateKey = this.preSelect?.privateKey || ''; + this.sshKnownHosts = this.preSelect?.sshKnownHosts || ''; } this.updateSelectedFromValue(); @@ -189,9 +197,10 @@ export default { filterByNamespace: this.namespace && this.limitToNamespace, - publicKey: '', - privateKey: '', - uniqueId: new Date().getTime(), // Allows form state to be individually tracked if the form is in a list + publicKey: '', + privateKey: '', + sshKnownHosts: '', + uniqueId: new Date().getTime(), // Allows form state to be individually tracked if the form is in a list SSH: AUTH_TYPE._SSH, BASIC: AUTH_TYPE._BASIC, @@ -372,15 +381,16 @@ export default { return 'mt-20'; } - return 'col span-4'; + return (this.selected === AUTH_TYPE._SSH) && this.showSshKnownHosts ? 'col span-3' : 'col span-4'; } }, watch: { - selected: 'update', - publicKey: 'updateKeyVal', - privateKey: 'updateKeyVal', - value: 'updateSelectedFromValue', + selected: 'update', + publicKey: 'updateKeyVal', + privateKey: 'updateKeyVal', + sshKnownHosts: 'updateKeyVal', + value: 'updateSelectedFromValue', async namespace(ns) { if (ns && !this.selected.startsWith(`${ ns }/`)) { @@ -463,13 +473,20 @@ export default { if ( ![AUTH_TYPE._SSH, AUTH_TYPE._BASIC, AUTH_TYPE._S3, AUTH_TYPE._RKE].includes(this.selected) ) { this.privateKey = ''; this.publicKey = ''; + this.sshKnownHosts = ''; } - this.$emit('inputauthval', { + const value = { selected: this.selected, privateKey: this.privateKey, - publicKey: this.publicKey - }); + publicKey: this.publicKey, + }; + + if (this.sshKnownHosts) { + value.sshKnownHosts = this.sshKnownHosts; + } + + this.$emit('inputauthval', value); }, update() { @@ -550,6 +567,12 @@ export default { [publicField]: base64Encode(this.publicKey), [privateField]: base64Encode(this.privateKey), }; + + // Add ssh known hosts data key - we will add a key with an empty value if the inout field was left blank + // This ensures on edit of the secret, we allow the user to edit the known_hosts field + if ((this.selected === AUTH_TYPE._SSH) && this.showSshKnownHosts) { + secret.data.known_hosts = base64Encode(this.sshKnownHosts || ''); + } } } @@ -603,6 +626,15 @@ export default { label-key="selectOrCreateAuthSecret.ssh.privateKey" /> +
+ +
diff --git a/shell/edit/fleet.cattle.io.gitrepo.vue b/shell/edit/fleet.cattle.io.gitrepo.vue index 356625ac1bf..71a749446d0 100644 --- a/shell/edit/fleet.cattle.io.gitrepo.vue +++ b/shell/edit/fleet.cattle.io.gitrepo.vue @@ -408,7 +408,12 @@ export default { }, async doCreate(name, credentials) { - const { selected, publicKey, privateKey } = credentials; + const { + selected, + publicKey, + privateKey, + sshKnownHosts + } = credentials; if ( ![AUTH_TYPE._SSH, AUTH_TYPE._BASIC, AUTH_TYPE._S3].includes(selected) ) { return; @@ -456,6 +461,11 @@ export default { [publicField]: base64Encode(publicKey), [privateField]: base64Encode(privateKey), }; + + // Add ssh known hosts + if (selected === AUTH_TYPE._SSH && sshKnownHosts) { + secret.data.known_hosts = base64Encode(sshKnownHosts); + } } await secret.save(); @@ -576,6 +586,10 @@ export default { /> + +
+

+