From 91f29655c536817ec2242242049b47386a643aa2 Mon Sep 17 00:00:00 2001 From: Daniel Marshall Date: Tue, 15 Sep 2026 12:07:51 -0400 Subject: [PATCH] Add SSH agent authentication option Signed-off-by: Daniel Marshall --- .../modules/db-editor/ConnectionEditor.tsx | 33 +++++++- .../ConnectionEditorTestConnection.spec.tsx | 80 ++++++++++++++----- 2 files changed, 89 insertions(+), 24 deletions(-) diff --git a/gui/frontend/src/modules/db-editor/ConnectionEditor.tsx b/gui/frontend/src/modules/db-editor/ConnectionEditor.tsx index 2375f234d..bb7c77065 100644 --- a/gui/frontend/src/modules/db-editor/ConnectionEditor.tsx +++ b/gui/frontend/src/modules/db-editor/ConnectionEditor.tsx @@ -65,6 +65,8 @@ const globalDefaultEditor = "Default"; const mysqlSocketProtocol = "mysql (socket/pipe)"; const mysqlSshTunnelMethod = "mysql + SSH tunnel"; const mysqlBastionMethod = "mysql + OCI Bastion"; +const sshAgentAuthentication = "SSH Agent"; +const sshKeyFileAuthentication = "Private Key File"; const connectionMethodDiagram = (method: string): string => { switch (method) { @@ -467,7 +469,8 @@ export class ConnectionEditor extends ComponentBase { + const values = dialog.getDialogValues(); + const sshKeyFile = values.sections.get("ssh")?.values.sshKeyFile; + if (!sshKeyFile) { + return; + } + + sshKeyFile.options = method === sshAgentAuthentication + ? [CommonDialogValueOption.Hidden] : undefined; + if (method === sshAgentAuthentication) { + sshKeyFile.value = ""; + } + dialog.setDialogValues(values); + }, + horizontalSpan: 4, + }, sshKeyFile: { type: "resource", caption: "SSH Private Key File", value: optionsMySQL["ssh-identity-file"] ?? "", + options: optionsMySQL["ssh-identity-file"] + ? undefined : [CommonDialogValueOption.Hidden], horizontalSpan: 4, }, sshConfigFilePath: { diff --git a/gui/frontend/src/tests/unit-tests/modules/documents/ConnectionEditorTestConnection.spec.tsx b/gui/frontend/src/tests/unit-tests/modules/documents/ConnectionEditorTestConnection.spec.tsx index d962d6690..65c22731e 100644 --- a/gui/frontend/src/tests/unit-tests/modules/documents/ConnectionEditorTestConnection.spec.tsx +++ b/gui/frontend/src/tests/unit-tests/modules/documents/ConnectionEditorTestConnection.spec.tsx @@ -26,6 +26,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { IDialogValues } from "../../../../components/Dialogs/ValueEditDialog.js"; +import type { IMySQLConnectionOptions } from "../../../../communication/MySQL.js"; import { ConnectionEditor } from "../../../../modules/db-editor/ConnectionEditor.js"; import { registerUiLayer } from "../../../../app-logic/UILayer.js"; import { ShellInterface } from "../../../../supplement/ShellInterface/ShellInterface.js"; @@ -36,6 +37,35 @@ interface ITestableConnectionEditor { testConnection(values: IDialogValues): Promise; } +const createMySQLDialogValues = (sshAuthentication = "SSH Agent"): IDialogValues => { + return { + sections: new Map([ + ["general", { values: { databaseType: { value: DBType.MySQL } } }], + ["information", { values: { caption: { value: "Test" }, description: { value: "" } } }], + ["sqliteDetails", { values: {} }], + ["sqliteAdvanced", { values: {} }], + ["mysqlDetails", { values: { + scheme: { value: "mysql" }, userName: { value: "root" }, hostName: { value: "localhost" }, + port: { value: 3306 }, defaultSchema: { value: "" }, + } }], + ["mdsBastion", { values: {} }], + ["ssh", { values: { + ssh: { value: "daniel@ssh.example.com:22" }, sshAuthentication: { value: sshAuthentication }, + sshKeyFile: { value: "C:\\Users\\daniel\\.ssh\\id_ed25519" }, + sshConfigFilePath: { value: "" }, + } }], + ["mysqlAdvanced", { values: { + compressionAlgorithms: { value: [] }, compression: { value: "" }, compressionLevel: { value: 0 }, + timeout: { value: 10 }, disableHeatwaveCheck: { value: false }, extraOptions: { value: {} }, + } }], + ["ssl", { values: { + sslMode: { value: "Disable" }, sslCaFile: { value: "" }, sslCertFile: { value: "" }, + sslKeyFile: { value: "" }, sslCipher: { value: "" }, + } }], + ]), + } as unknown as IDialogValues; +}; + describe("ConnectionEditor", (): void => { beforeEach(() => { registerUiLayer(uiLayerMock); @@ -50,28 +80,7 @@ describe("ConnectionEditor", (): void => { const requestPasswordSpy = vi.spyOn(uiLayerMock, "requestPassword"); const editor = new ConnectionEditor({ onAddConnection: vi.fn(), onUpdateConnection: vi.fn() }); const testableEditor = editor as unknown as ITestableConnectionEditor; - const values = { - sections: new Map([ - ["general", { values: { databaseType: { value: DBType.MySQL } } }], - ["information", { values: { caption: { value: "Test" }, description: { value: "" } } }], - ["sqliteDetails", { values: {} }], - ["sqliteAdvanced", { values: {} }], - ["mysqlDetails", { values: { - scheme: { value: "mysql" }, userName: { value: "root" }, hostName: { value: "localhost" }, - port: { value: 3306 }, defaultSchema: { value: "" }, - } }], - ["mdsBastion", { values: {} }], - ["ssh", { values: {} }], - ["mysqlAdvanced", { values: { - compressionAlgorithms: { value: [] }, compression: { value: "" }, compressionLevel: { value: 0 }, - timeout: { value: 10 }, disableHeatwaveCheck: { value: false }, extraOptions: { value: {} }, - } }], - ["ssl", { values: { - sslMode: { value: "Disable" }, sslCaFile: { value: "" }, sslCertFile: { value: "" }, - sslKeyFile: { value: "" }, sslCipher: { value: "" }, - } }], - ]), - } as unknown as IDialogValues; + const values = createMySQLDialogValues(); await testableEditor.testConnection(values); @@ -81,4 +90,31 @@ describe("ConnectionEditor", (): void => { expect(connection.dbType).toBe(DBType.MySQL); expect(connection.options).toMatchObject({ host: "localhost", user: "root" }); }); + + it("omits the identity file when SSH Agent authentication is selected", async () => { + const testConnectionSpy = vi.spyOn(ShellInterface.dbConnections, "testConnection").mockResolvedValue(); + const editor = new ConnectionEditor({ onAddConnection: vi.fn(), onUpdateConnection: vi.fn() }); + const values = createMySQLDialogValues(); + values.sections.get("mysqlDetails")!.values.scheme.value = "mysql + SSH tunnel"; + + await (editor as unknown as ITestableConnectionEditor).testConnection(values); + + const [connection] = testConnectionSpy.mock.calls[0]; + const options = connection.options as IMySQLConnectionOptions; + expect(options.ssh).toBe("daniel@ssh.example.com:22"); + expect(options["ssh-identity-file"]).toBeUndefined(); + }); + + it("uses the selected identity file for private-key authentication", async () => { + const testConnectionSpy = vi.spyOn(ShellInterface.dbConnections, "testConnection").mockResolvedValue(); + const editor = new ConnectionEditor({ onAddConnection: vi.fn(), onUpdateConnection: vi.fn() }); + const values = createMySQLDialogValues("Private Key File"); + values.sections.get("mysqlDetails")!.values.scheme.value = "mysql + SSH tunnel"; + + await (editor as unknown as ITestableConnectionEditor).testConnection(values); + + const [connection] = testConnectionSpy.mock.calls[0]; + const options = connection.options as IMySQLConnectionOptions; + expect(options["ssh-identity-file"]).toBe("C:\\Users\\daniel\\.ssh\\id_ed25519"); + }); });