1 parent
3acf3a66b9
commit
16e3bb7d7a
5 files changed
+104
-53
No files matched your search
@@ -23,6 +23,7 @@ import type { NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosMetri
|
||||
import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
|
||||
import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSummary } from "@/lib/connection/tunnelProfiles";
|
||||
import { applySshAuthMethod, inferSshAuthMethod } from "@/lib/connection/sshAuthMethod";
|
||||
import { applySshConfigHostAliasPrefill as prefillSshConfigHostAlias } from "@/lib/connection/sshConfigHosts";
|
||||
import { canPersistConnectionTestResult, connectionEditDraftSyncAction } from "./connectionEditDraftSync";
|
||||
import { createConnectionNoteVisibilityDraft, persistConnectionNoteVisibilityDraft as persistConnectionNoteVisibilityDraftState, resetConnectionNoteVisibilityDraft, setConnectionNoteVisibilityDraft, syncConnectionNoteVisibilityDraft } from "./connectionNoteVisibilityDraft";
|
||||
@@ -372,20 +373,6 @@ function defaultSshTunnel(): SshTunnelConfig {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Infers a login method for connections saved before `auth_method` existed
|
||||
* (or imported from a source that never set it), so the dropdown shows a
|
||||
* sensible current state instead of defaulting blindly to "password".
|
||||
* Mirrors the priority `connect_and_authenticate` actually uses at connect
|
||||
* time (key > password > agent > none) — see `db/ssh_tunnel.rs`.
|
||||
*/
|
||||
function inferSshAuthMethod(hop: Partial<SshTunnelConfig>): "password" | "key" | "agent" | "none" {
|
||||
if (hop.key_path?.trim()) return "key";
|
||||
if (hop.password) return "password";
|
||||
if (hop.use_ssh_agent) return "agent";
|
||||
return "none";
|
||||
}
|
||||
|
||||
function normalizeSshTunnel(hop: Partial<SshTunnelConfig>): SshTunnelConfig {
|
||||
return {
|
||||
id: hop.id || uuid(),
|
||||
@@ -4964,31 +4951,10 @@ function updateSelectedProxyType(value: unknown) {
|
||||
resetTestState();
|
||||
}
|
||||
|
||||
/**
|
||||
* "agent" is legacy-only: it's never chosen from this dropdown, only ever
|
||||
* inherited from a connection saved before this selector existed. Once the
|
||||
* user picks something else, the option (and its underlying checkbox) is
|
||||
* gone from the form for good.
|
||||
*/
|
||||
function isLegacySshAgentMethod(hop: Partial<SshTunnelConfig> | null | undefined) {
|
||||
return hop?.auth_method === "agent";
|
||||
}
|
||||
|
||||
function updateSelectedSshAuthMethod(value: unknown) {
|
||||
const layer = selectedSshLayer.value;
|
||||
if (!layer) return;
|
||||
layer.auth_method = value === "key" ? "key" : value === "key+password" ? "key+password" : value === "none" ? "none" : "password";
|
||||
// Scrub credential fields that do not apply to the selected method so
|
||||
// they are not accidentally submitted or used by the backend fallback.
|
||||
// "key+password" keeps both key and password fields.
|
||||
if (layer.auth_method !== "password" && layer.auth_method !== "key+password") layer.password = "";
|
||||
if (layer.auth_method !== "key" && layer.auth_method !== "key+password") {
|
||||
layer.key_path = "";
|
||||
layer.key_passphrase = "";
|
||||
}
|
||||
if (layer.auth_method !== "key" && layer.auth_method !== "key+password") {
|
||||
layer.use_ssh_agent = false;
|
||||
}
|
||||
applySshAuthMethod(layer, value);
|
||||
resetTestState();
|
||||
}
|
||||
|
||||
@@ -8085,8 +8051,8 @@ function openExternalUrl(url: string) {
|
||||
<SelectItem value="password">{{ t("connection.sshAuthMethodPassword") }}</SelectItem>
|
||||
<SelectItem value="key">{{ t("connection.sshAuthMethodKey") }}</SelectItem>
|
||||
<SelectItem value="key+password">{{ t("connection.sshAuthMethodKeyPassword") }}</SelectItem>
|
||||
<SelectItem value="agent">{{ t("connection.sshUseAgent") }}</SelectItem>
|
||||
<SelectItem value="none">{{ t("connection.sshAuthMethodNone") }}</SelectItem>
|
||||
<SelectItem v-if="isLegacySshAgentMethod(selectedSshLayer)" value="agent" disabled>{{ t("connection.sshAuthMethodAgentLegacy") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -8116,19 +8082,10 @@ function openExternalUrl(url: string) {
|
||||
<span />
|
||||
<p class="col-span-3 text-xs text-muted-foreground">{{ t("connection.sshAuthMethodNoneHint") }}</p>
|
||||
</div>
|
||||
<template v-if="isLegacySshAgentMethod(selectedSshLayer)">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<span />
|
||||
<label class="col-span-3 flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="selectedSshLayer.use_ssh_agent" class="mr-0" :disabled="selectedSshLayer.enabled === false" />
|
||||
<span class="text-xs text-muted-foreground">{{ t("connection.sshUseAgent") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="selectedSshLayer.use_ssh_agent" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.sshAgentSockPath") }}</Label>
|
||||
<Input v-model="selectedSshLayer.ssh_agent_sock_path" class="col-span-3" :placeholder="t('connection.sshAgentSockPathPlaceholder')" :disabled="selectedSshLayer.enabled === false" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="selectedSshLayer.auth_method === 'agent'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.sshAgentSockPath") }}</Label>
|
||||
<Input v-model="selectedSshLayer.ssh_agent_sock_path" class="col-span-3" :placeholder="t('connection.sshAgentSockPathPlaceholder')" :disabled="selectedSshLayer.enabled === false" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<span />
|
||||
<label class="col-span-3 flex items-center gap-2 cursor-pointer">
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("SSH agent connection form", () => {
|
||||
it("offers SSH agent for a new tunnel and exposes its custom socket path", () => {
|
||||
expect(dialogSource).toContain('<SelectItem value="agent">{{ t("connection.sshUseAgent") }}</SelectItem>');
|
||||
expect(dialogSource).toContain('<div v-if="selectedSshLayer.auth_method === \'agent\'" class="grid grid-cols-4 items-center gap-4">');
|
||||
expect(dialogSource).toContain('v-model="selectedSshLayer.ssh_agent_sock_path"');
|
||||
expect(dialogSource).not.toContain('v-if="isLegacySshAgentMethod(selectedSshLayer)" value="agent" disabled');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { SshTunnelConfig } from "@/types/database";
|
||||
import { applySshAuthMethod, inferSshAuthMethod } from "../sshAuthMethod";
|
||||
|
||||
function sshLayer(overrides: Partial<SshTunnelConfig> = {}): SshTunnelConfig {
|
||||
return {
|
||||
id: "ssh-1",
|
||||
host: "jump.example.com",
|
||||
port: 22,
|
||||
user: "root",
|
||||
password: "password-secret",
|
||||
key_path: "~/.ssh/id_ed25519",
|
||||
key_passphrase: "key-secret",
|
||||
use_ssh_agent: false,
|
||||
ssh_agent_sock_path: "/custom/agent.sock",
|
||||
auth_method: "password",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("SSH authentication method state", () => {
|
||||
it("enables agent authentication while preserving its custom socket", () => {
|
||||
const layer = sshLayer();
|
||||
|
||||
applySshAuthMethod(layer, "agent");
|
||||
|
||||
expect(layer).toMatchObject({
|
||||
auth_method: "agent",
|
||||
use_ssh_agent: true,
|
||||
ssh_agent_sock_path: "/custom/agent.sock",
|
||||
password: "",
|
||||
key_path: "",
|
||||
key_passphrase: "",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["password", "password-secret", "", ""],
|
||||
["key", "", "~/.ssh/id_ed25519", "key-secret"],
|
||||
["key+password", "password-secret", "~/.ssh/id_ed25519", "key-secret"],
|
||||
["none", "", "", ""],
|
||||
] as const)("disables agent when switching to %s", (method, password, keyPath, keyPassphrase) => {
|
||||
const layer = sshLayer({ auth_method: "agent", use_ssh_agent: true });
|
||||
|
||||
applySshAuthMethod(layer, method);
|
||||
|
||||
expect(layer.auth_method).toBe(method);
|
||||
expect(layer.use_ssh_agent).toBe(false);
|
||||
expect(layer.password).toBe(password);
|
||||
expect(layer.key_path).toBe(keyPath);
|
||||
expect(layer.key_passphrase).toBe(keyPassphrase);
|
||||
expect(layer.ssh_agent_sock_path).toBe("/custom/agent.sock");
|
||||
});
|
||||
|
||||
it("infers agent for a legacy connection without an auth method", () => {
|
||||
const layer = sshLayer({ auth_method: undefined, password: "", key_path: "", use_ssh_agent: true });
|
||||
|
||||
expect(inferSshAuthMethod(layer)).toBe("agent");
|
||||
expect(layer.ssh_agent_sock_path).toBe("/custom/agent.sock");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { SshTunnelConfig } from "@/types/database";
|
||||
|
||||
export type SshAuthMethod = NonNullable<SshTunnelConfig["auth_method"]>;
|
||||
|
||||
export function inferSshAuthMethod(hop: Partial<SshTunnelConfig>): SshAuthMethod {
|
||||
if (hop.key_path?.trim()) return "key";
|
||||
if (hop.password) return "password";
|
||||
if (hop.use_ssh_agent) return "agent";
|
||||
return "none";
|
||||
}
|
||||
|
||||
export function applySshAuthMethod(layer: SshTunnelConfig, value: unknown) {
|
||||
const method: SshAuthMethod = value === "key" ? "key" : value === "key+password" ? "key+password" : value === "agent" ? "agent" : value === "none" ? "none" : "password";
|
||||
layer.auth_method = method;
|
||||
layer.use_ssh_agent = method === "agent";
|
||||
|
||||
if (method !== "password" && method !== "key+password") layer.password = "";
|
||||
if (method !== "key" && method !== "key+password") {
|
||||
layer.key_path = "";
|
||||
layer.key_passphrase = "";
|
||||
}
|
||||
}
|
||||
@@ -259,9 +259,7 @@ export interface SshTunnelConfig {
|
||||
* `"key+password"` tries private key auth first and falls back to
|
||||
* password auth if the key is rejected.
|
||||
*
|
||||
* `"agent"` is a legacy value: it's no longer offered as a dropdown
|
||||
* choice for new connections, but is preserved and displayed read-only
|
||||
* for connections that already have `use_ssh_agent` configured.
|
||||
* `"agent"` uses identities from the configured SSH agent socket.
|
||||
*/
|
||||
auth_method?: "password" | "key" | "key+password" | "agent" | "none";
|
||||
/** Allow `nc` through an SSH exec channel when direct-tcpip is prohibited. */
|
||||
|
||||
Reference in new issue
Block a user