Skip to content

Commit 635b2d3

Browse files
fix(connections): skip password prompt for AWS IAM connections (#1566)
* fix(connections): skip password prompt for AWS IAM connections * refactor(connections): gate password prompt on a single hidesPassword source of truth --------- Co-authored-by: Ngo Quoc Dat <datlechin@gmail.com>
1 parent 5673fae commit 635b2d3

7 files changed

Lines changed: 155 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3535
- The Delete shortcut in the data grid now follows a custom binding.
3636
- Find Next (Cmd+G) and Find Previous (Cmd+Shift+G) now work in the editor.
3737
- Pagination buttons no longer fire their page shortcut twice.
38+
- AWS IAM connections no longer ask for a password on connect or reconnect. IAM supplies the credentials, so the prompt was never needed. The same now holds for any auth mode that replaces the password, such as a Postgres password file.
3839

3940
## [0.48.0] - 2026-06-02
4041

TablePro/Core/Database/DatabaseManager+Health.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,10 @@ extension DatabaseManager {
240240

241241
// Resolve password for prompt-for-password connections
242242
var passwordOverride = activeSessions[sessionId]?.cachedPassword
243-
if session.connection.promptForPassword && passwordOverride == nil {
243+
if session.connection.promptForPassword,
244+
!pluginManager.hidesPassword(for: session.connection),
245+
passwordOverride == nil
246+
{
244247
let isApiOnly = pluginManager.connectionMode(for: session.connection.type) == .apiOnly
245248
guard let prompted = await PasswordPromptHelper.prompt(
246249
connectionName: session.connection.name,

TablePro/Core/Database/DatabaseManager+Sessions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ extension DatabaseManager {
5858
}
5959

6060
var passwordOverride: String?
61-
if connection.promptForPassword {
61+
if connection.promptForPassword, !pluginManager.hidesPassword(for: connection) {
6262
if let cached = activeSessions[connection.id]?.cachedPassword {
6363
passwordOverride = cached
6464
} else {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// ConnectionField+PasswordHiding.swift
3+
// TablePro
4+
//
5+
6+
import TableProPluginKit
7+
8+
extension Sequence where Element == ConnectionField {
9+
func hidesPassword(forValues values: [String: String]) -> Bool {
10+
contains { field in
11+
guard field.section == .authentication, field.hidesPassword else { return false }
12+
switch field.fieldType {
13+
case .toggle:
14+
return values[field.id] == "true"
15+
case .dropdown:
16+
let value = values[field.id] ?? field.defaultValue
17+
return value != field.defaultValue
18+
default:
19+
return true
20+
}
21+
}
22+
}
23+
}
24+
25+
extension PluginManager {
26+
func hidesPassword(for connection: DatabaseConnection) -> Bool {
27+
additionalConnectionFields(for: connection.type)
28+
.hidesPassword(forValues: connection.additionalFields)
29+
}
30+
}

TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ final class ConnectionFormCoordinator {
249249
finalAdditionalFields.removeValue(forKey: "preConnectScript")
250250
}
251251

252-
finalAdditionalFields["promptForPassword"] = auth.promptForPassword ? "true" : nil
252+
finalAdditionalFields["promptForPassword"] = auth.effectivePromptForPassword ? "true" : nil
253253

254254
let secureFields = services.pluginManager.additionalConnectionFields(for: network.type)
255255
.filter(\.isSecure)
@@ -292,7 +292,7 @@ final class ConnectionFormCoordinator {
292292
additionalFields: finalAdditionalFields.isEmpty ? nil : finalAdditionalFields
293293
)
294294

295-
if auth.promptForPassword {
295+
if auth.effectivePromptForPassword {
296296
storage.deletePassword(for: connectionToSave.id)
297297
} else if !auth.password.isEmpty {
298298
storage.savePassword(auth.password, for: connectionToSave.id)
@@ -472,7 +472,7 @@ final class ConnectionFormCoordinator {
472472
temporaryTestIds.insert(testConn.id)
473473

474474
let password = auth.password
475-
let promptForPassword = auth.promptForPassword
475+
let promptForPassword = auth.effectivePromptForPassword
476476
let connectionType = network.type
477477
let displayName = network.name.isEmpty ? network.host : network.name
478478
let sshState = ssh.state

TablePro/Views/ConnectionForm/ViewModels/AuthPaneViewModel.swift

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,11 @@ final class AuthPaneViewModel {
4141
}
4242

4343
var hidesPassword: Bool {
44-
authFields.contains { field in
45-
guard field.hidesPassword else { return false }
46-
switch field.fieldType {
47-
case .toggle:
48-
return additionalFieldValues[field.id] == "true"
49-
case .dropdown:
50-
let value = additionalFieldValues[field.id] ?? field.defaultValue
51-
return value != field.defaultValue
52-
default:
53-
return true
54-
}
55-
}
44+
authFields.hidesPassword(forValues: additionalFieldValues)
45+
}
46+
47+
var effectivePromptForPassword: Bool {
48+
promptForPassword && !hidesPassword
5649
}
5750

5851
var usePgpass: Bool {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//
2+
// PasswordHidingTests.swift
3+
// TableProTests
4+
//
5+
// The single source of truth for "does this connection's auth mode replace the
6+
// password" feeds both the connection form (whether to show the prompt toggle)
7+
// and the runtime connect/reconnect paths (whether to prompt at all).
8+
//
9+
10+
import Foundation
11+
import TableProPluginKit
12+
import Testing
13+
14+
@testable import TablePro
15+
16+
@Suite("Password hiding from connection fields")
17+
struct PasswordHidingTests {
18+
private func dropdown(default defaultValue: String, _ values: [String]) -> ConnectionField {
19+
ConnectionField(
20+
id: "awsAuth",
21+
label: "Authentication",
22+
defaultValue: defaultValue,
23+
fieldType: .dropdown(options: values.map { .init(value: $0, label: $0) }),
24+
section: .authentication,
25+
hidesPassword: true
26+
)
27+
}
28+
29+
private let pgpassToggle = ConnectionField(
30+
id: "usePgpass",
31+
label: "Use Password File",
32+
defaultValue: "false",
33+
fieldType: .toggle,
34+
section: .authentication,
35+
hidesPassword: true
36+
)
37+
38+
private let secretField = ConnectionField(
39+
id: "serviceAccountJson",
40+
label: "Service Account",
41+
fieldType: .secure,
42+
section: .authentication,
43+
hidesPassword: true
44+
)
45+
46+
@Test("A dropdown hides the password only when set off its default")
47+
func dropdownAwayFromDefault() {
48+
let fields = [dropdown(default: "off", ["off", "accessKey", "profile"])]
49+
#expect(fields.hidesPassword(forValues: [:]) == false)
50+
#expect(fields.hidesPassword(forValues: ["awsAuth": "off"]) == false)
51+
#expect(fields.hidesPassword(forValues: ["awsAuth": "accessKey"]) == true)
52+
#expect(fields.hidesPassword(forValues: ["awsAuth": "profile"]) == true)
53+
}
54+
55+
@Test("A toggle hides the password only when on")
56+
func toggleOn() {
57+
let fields = [pgpassToggle]
58+
#expect(fields.hidesPassword(forValues: [:]) == false)
59+
#expect(fields.hidesPassword(forValues: ["usePgpass": "false"]) == false)
60+
#expect(fields.hidesPassword(forValues: ["usePgpass": "true"]) == true)
61+
}
62+
63+
@Test("A secure field that always replaces the password hides it unconditionally")
64+
func secureFieldAlwaysHides() {
65+
#expect([secretField].hidesPassword(forValues: [:]) == true)
66+
}
67+
68+
@Test("Fields without the hidesPassword flag never hide the password")
69+
func plainFieldsDoNotHide() {
70+
let plain = ConnectionField(id: "region", label: "Region", section: .authentication)
71+
#expect([plain].hidesPassword(forValues: ["region": "us-east-1"]) == false)
72+
#expect([ConnectionField]().hidesPassword(forValues: [:]) == false)
73+
}
74+
75+
@Test("Only authentication-section fields are considered")
76+
func ignoresNonAuthenticationFields() {
77+
let advanced = ConnectionField(
78+
id: "advancedToggle",
79+
label: "Advanced",
80+
defaultValue: "false",
81+
fieldType: .toggle,
82+
section: .advanced,
83+
hidesPassword: true
84+
)
85+
#expect([advanced].hidesPassword(forValues: ["advancedToggle": "true"]) == false)
86+
}
87+
}
88+
89+
@Suite("Password hiding resolved from plugin metadata")
90+
@MainActor
91+
struct PluginManagerPasswordHidingTests {
92+
private func connection(type: DatabaseType, fields: [String: String]) -> DatabaseConnection {
93+
var connection = DatabaseConnection(name: "test", type: type)
94+
connection.additionalFields = fields
95+
return connection
96+
}
97+
98+
@Test("AWS IAM modes hide the password for relational types")
99+
func iamHidesPassword() {
100+
let manager = PluginManager.shared
101+
#expect(manager.hidesPassword(for: connection(type: .mysql, fields: ["awsAuth": "accessKey"])))
102+
#expect(manager.hidesPassword(for: connection(type: .postgresql, fields: ["awsAuth": "profile"])))
103+
}
104+
105+
@Test("Password auth does not hide the password")
106+
func passwordModeDoesNotHide() {
107+
let manager = PluginManager.shared
108+
#expect(!manager.hidesPassword(for: connection(type: .mysql, fields: ["awsAuth": "off"])))
109+
#expect(!manager.hidesPassword(for: connection(type: .mysql, fields: [:])))
110+
}
111+
}

0 commit comments

Comments
 (0)