Skip to content

Commit 1d338f0

Browse files
committed
fix(ssh): allow empty username in form when alias supplies User from ssh config
1 parent 9d45000 commit 1d338f0

5 files changed

Lines changed: 23 additions & 26 deletions

File tree

TablePro/Core/SSH/LibSSH2TunnelFactory.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ internal enum LibSSH2TunnelFactory {
125125
let resolvedJumps: [ResolvedSSHTarget] = (formJumps.isEmpty ? resolvedPrimary.proxyJump : formJumps)
126126
.map { SSHConfigResolver.resolve($0, document: document) }
127127

128+
if resolvedPrimary.username.isEmpty {
129+
throw SSHTunnelError.tunnelCreationFailed(
130+
"SSH username not set. Add it to the form or set `User` for `\(config.host)` in ~/.ssh/config."
131+
)
132+
}
133+
128134
let firstHop = resolvedJumps.first ?? resolvedPrimary
129135
let socketFD = try connectTCP(host: firstHop.host, port: firstHop.port)
130136

TablePro/Models/Connection/SSHTypes.swift

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,9 @@ struct SSHJumpHost: Codable, Hashable, Identifiable {
9494
var privateKeyPath: String = ""
9595

9696
var isValid: Bool {
97-
!host.isEmpty && !username.isEmpty &&
98-
(authMethod == .sshAgent || !privateKeyPath.isEmpty)
97+
// Username and port may be empty: the runtime resolver fills them
98+
// from ~/.ssh/config (User, Port directives) when the alias matches.
99+
!host.isEmpty && (authMethod == .sshAgent || !privateKeyPath.isEmpty)
99100
}
100101

101102
var proxyJumpString: String {
@@ -118,24 +119,12 @@ struct SSHConfiguration: Codable, Hashable {
118119
var totpDigits: Int = 6
119120
var totpPeriod: Int = 30
120121

121-
/// Check if SSH configuration is complete enough for connection
122+
/// Username may be empty: the runtime resolver supplies `User` from
123+
/// `~/.ssh/config` when the host is an alias.
122124
var isValid: Bool {
123-
guard enabled else { return true } // Not enabled = valid (skip SSH)
124-
guard !host.isEmpty, !username.isEmpty else { return false }
125-
126-
let authValid: Bool
127-
switch authMethod {
128-
case .password:
129-
authValid = true
130-
case .privateKey:
131-
authValid = true
132-
case .sshAgent:
133-
authValid = true
134-
case .keyboardInteractive:
135-
authValid = true
136-
}
137-
138-
return authValid && jumpHosts.allSatisfy(\.isValid)
125+
guard enabled else { return true }
126+
guard !host.isEmpty else { return false }
127+
return jumpHosts.allSatisfy(\.isValid)
139128
}
140129
}
141130

TablePro/Views/Connection/ConnectionFormView+Helpers.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ extension ConnectionFormView {
5757
if sshState.enabled && sshState.profileId == nil {
5858
let sshPortValid = sshState.port.isEmpty
5959
|| (Int(sshState.port).map { (1...65_535).contains($0) } ?? false)
60-
let sshValid = !sshState.host.isEmpty && !sshState.username.isEmpty && sshPortValid
60+
// Username may be empty: the runtime resolver fills it from
61+
// `~/.ssh/config` if the host is an alias with a User directive.
62+
let sshValid = !sshState.host.isEmpty && sshPortValid
6163
let authValid =
6264
sshState.authMethod == .password || sshState.authMethod == .sshAgent
6365
|| sshState.authMethod == .keyboardInteractive || sshState.authMethod == .privateKey

TableProTests/Core/SSH/SSHConfigurationTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ struct SSHConfigurationTests {
6868
#expect(config.isValid == false)
6969
}
7070

71-
@Test("Missing username makes config invalid")
72-
func testMissingUsernameInvalid() {
71+
@Test("Empty username is allowed (runtime resolver fills it from ssh config)")
72+
func testEmptyUsernameAllowed() {
7373
let config = SSHConfiguration(
7474
enabled: true, host: "example.com", username: "",
7575
authMethod: .sshAgent
7676
)
77-
#expect(config.isValid == false)
77+
#expect(config.isValid == true)
7878
}
7979

8080
@Test("Agent socket path defaults to empty string")

TableProTests/Core/SSH/SSHJumpHostTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ struct SSHJumpHostTests {
5353
#expect(jumpHost.isValid == false)
5454
}
5555

56-
@Test("isValid fails with empty username")
57-
func testIsInvalidWithEmptyUsername() {
56+
@Test("isValid allows empty username (filled by runtime resolver)")
57+
func testValidWithEmptyUsername() {
5858
let jumpHost = SSHJumpHost(host: "bastion.example.com", username: "")
59-
#expect(jumpHost.isValid == false)
59+
#expect(jumpHost.isValid == true)
6060
}
6161

6262
@Test("Codable round-trip preserves all fields")

0 commit comments

Comments
 (0)