From ab0712a544ca1a860956089849564f05114fb746 Mon Sep 17 00:00:00 2001 From: Anton CAZALET Date: Wed, 3 Jun 2026 10:12:47 +0200 Subject: [PATCH 1/2] fix(connections): skip password prompt for AWS IAM connections --- CHANGELOG.md | 1 + TablePro/Core/Database/DatabaseManager+Health.swift | 2 +- TablePro/Core/Database/DatabaseManager+Sessions.swift | 2 +- TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f18f0ffa..01830ce95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The Delete shortcut in the data grid now follows a custom binding. - Find Next (Cmd+G) and Find Previous (Cmd+Shift+G) now work in the editor. - Pagination buttons no longer fire their page shortcut twice. +- AWS IAM connections no longer ask for a password on connect. IAM provides the credentials, so the prompt was never needed. ## [0.48.0] - 2026-06-02 diff --git a/TablePro/Core/Database/DatabaseManager+Health.swift b/TablePro/Core/Database/DatabaseManager+Health.swift index 52b16a5ba..05f1bdc76 100644 --- a/TablePro/Core/Database/DatabaseManager+Health.swift +++ b/TablePro/Core/Database/DatabaseManager+Health.swift @@ -240,7 +240,7 @@ extension DatabaseManager { // Resolve password for prompt-for-password connections var passwordOverride = activeSessions[sessionId]?.cachedPassword - if session.connection.promptForPassword && passwordOverride == nil { + if session.connection.promptForPassword, !session.connection.usesAWSIAM, passwordOverride == nil { let isApiOnly = pluginManager.connectionMode(for: session.connection.type) == .apiOnly guard let prompted = await PasswordPromptHelper.prompt( connectionName: session.connection.name, diff --git a/TablePro/Core/Database/DatabaseManager+Sessions.swift b/TablePro/Core/Database/DatabaseManager+Sessions.swift index 68a64f778..b7ecde200 100644 --- a/TablePro/Core/Database/DatabaseManager+Sessions.swift +++ b/TablePro/Core/Database/DatabaseManager+Sessions.swift @@ -58,7 +58,7 @@ extension DatabaseManager { } var passwordOverride: String? - if connection.promptForPassword { + if connection.promptForPassword, !connection.usesAWSIAM { if let cached = activeSessions[connection.id]?.cachedPassword { passwordOverride = cached } else { diff --git a/TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift b/TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift index fda73af64..a2132d580 100644 --- a/TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift +++ b/TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift @@ -472,7 +472,7 @@ final class ConnectionFormCoordinator { temporaryTestIds.insert(testConn.id) let password = auth.password - let promptForPassword = auth.promptForPassword + let promptForPassword = auth.promptForPassword && !auth.hidesPassword let connectionType = network.type let displayName = network.name.isEmpty ? network.host : network.name let sshState = ssh.state From 0527c225a002b60648f889232cb3598557a7d9c3 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 3 Jun 2026 17:40:17 +0700 Subject: [PATCH 2/2] refactor(connections): gate password prompt on a single hidesPassword source of truth --- CHANGELOG.md | 2 +- .../Database/DatabaseManager+Health.swift | 5 +- .../Database/DatabaseManager+Sessions.swift | 2 +- .../ConnectionField+PasswordHiding.swift | 30 +++++ .../ConnectionFormCoordinator.swift | 6 +- .../ViewModels/AuthPaneViewModel.swift | 17 +-- .../Core/Plugins/PasswordHidingTests.swift | 111 ++++++++++++++++++ 7 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 TablePro/Core/Plugins/ConnectionField+PasswordHiding.swift create mode 100644 TableProTests/Core/Plugins/PasswordHidingTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 01830ce95..444194bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The Delete shortcut in the data grid now follows a custom binding. - Find Next (Cmd+G) and Find Previous (Cmd+Shift+G) now work in the editor. - Pagination buttons no longer fire their page shortcut twice. -- AWS IAM connections no longer ask for a password on connect. IAM provides the credentials, so the prompt was never needed. +- 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. ## [0.48.0] - 2026-06-02 diff --git a/TablePro/Core/Database/DatabaseManager+Health.swift b/TablePro/Core/Database/DatabaseManager+Health.swift index 05f1bdc76..4f82cc72b 100644 --- a/TablePro/Core/Database/DatabaseManager+Health.swift +++ b/TablePro/Core/Database/DatabaseManager+Health.swift @@ -240,7 +240,10 @@ extension DatabaseManager { // Resolve password for prompt-for-password connections var passwordOverride = activeSessions[sessionId]?.cachedPassword - if session.connection.promptForPassword, !session.connection.usesAWSIAM, passwordOverride == nil { + if session.connection.promptForPassword, + !pluginManager.hidesPassword(for: session.connection), + passwordOverride == nil + { let isApiOnly = pluginManager.connectionMode(for: session.connection.type) == .apiOnly guard let prompted = await PasswordPromptHelper.prompt( connectionName: session.connection.name, diff --git a/TablePro/Core/Database/DatabaseManager+Sessions.swift b/TablePro/Core/Database/DatabaseManager+Sessions.swift index b7ecde200..b41073c0b 100644 --- a/TablePro/Core/Database/DatabaseManager+Sessions.swift +++ b/TablePro/Core/Database/DatabaseManager+Sessions.swift @@ -58,7 +58,7 @@ extension DatabaseManager { } var passwordOverride: String? - if connection.promptForPassword, !connection.usesAWSIAM { + if connection.promptForPassword, !pluginManager.hidesPassword(for: connection) { if let cached = activeSessions[connection.id]?.cachedPassword { passwordOverride = cached } else { diff --git a/TablePro/Core/Plugins/ConnectionField+PasswordHiding.swift b/TablePro/Core/Plugins/ConnectionField+PasswordHiding.swift new file mode 100644 index 000000000..4da2ff8d7 --- /dev/null +++ b/TablePro/Core/Plugins/ConnectionField+PasswordHiding.swift @@ -0,0 +1,30 @@ +// +// ConnectionField+PasswordHiding.swift +// TablePro +// + +import TableProPluginKit + +extension Sequence where Element == ConnectionField { + func hidesPassword(forValues values: [String: String]) -> Bool { + contains { field in + guard field.section == .authentication, field.hidesPassword else { return false } + switch field.fieldType { + case .toggle: + return values[field.id] == "true" + case .dropdown: + let value = values[field.id] ?? field.defaultValue + return value != field.defaultValue + default: + return true + } + } + } +} + +extension PluginManager { + func hidesPassword(for connection: DatabaseConnection) -> Bool { + additionalConnectionFields(for: connection.type) + .hidesPassword(forValues: connection.additionalFields) + } +} diff --git a/TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift b/TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift index a2132d580..c4313dd7e 100644 --- a/TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift +++ b/TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift @@ -249,7 +249,7 @@ final class ConnectionFormCoordinator { finalAdditionalFields.removeValue(forKey: "preConnectScript") } - finalAdditionalFields["promptForPassword"] = auth.promptForPassword ? "true" : nil + finalAdditionalFields["promptForPassword"] = auth.effectivePromptForPassword ? "true" : nil let secureFields = services.pluginManager.additionalConnectionFields(for: network.type) .filter(\.isSecure) @@ -292,7 +292,7 @@ final class ConnectionFormCoordinator { additionalFields: finalAdditionalFields.isEmpty ? nil : finalAdditionalFields ) - if auth.promptForPassword { + if auth.effectivePromptForPassword { storage.deletePassword(for: connectionToSave.id) } else if !auth.password.isEmpty { storage.savePassword(auth.password, for: connectionToSave.id) @@ -472,7 +472,7 @@ final class ConnectionFormCoordinator { temporaryTestIds.insert(testConn.id) let password = auth.password - let promptForPassword = auth.promptForPassword && !auth.hidesPassword + let promptForPassword = auth.effectivePromptForPassword let connectionType = network.type let displayName = network.name.isEmpty ? network.host : network.name let sshState = ssh.state diff --git a/TablePro/Views/ConnectionForm/ViewModels/AuthPaneViewModel.swift b/TablePro/Views/ConnectionForm/ViewModels/AuthPaneViewModel.swift index 1d6eb92b3..c90d41790 100644 --- a/TablePro/Views/ConnectionForm/ViewModels/AuthPaneViewModel.swift +++ b/TablePro/Views/ConnectionForm/ViewModels/AuthPaneViewModel.swift @@ -41,18 +41,11 @@ final class AuthPaneViewModel { } var hidesPassword: Bool { - authFields.contains { field in - guard field.hidesPassword else { return false } - switch field.fieldType { - case .toggle: - return additionalFieldValues[field.id] == "true" - case .dropdown: - let value = additionalFieldValues[field.id] ?? field.defaultValue - return value != field.defaultValue - default: - return true - } - } + authFields.hidesPassword(forValues: additionalFieldValues) + } + + var effectivePromptForPassword: Bool { + promptForPassword && !hidesPassword } var usePgpass: Bool { diff --git a/TableProTests/Core/Plugins/PasswordHidingTests.swift b/TableProTests/Core/Plugins/PasswordHidingTests.swift new file mode 100644 index 000000000..26b46eb1d --- /dev/null +++ b/TableProTests/Core/Plugins/PasswordHidingTests.swift @@ -0,0 +1,111 @@ +// +// PasswordHidingTests.swift +// TableProTests +// +// The single source of truth for "does this connection's auth mode replace the +// password" feeds both the connection form (whether to show the prompt toggle) +// and the runtime connect/reconnect paths (whether to prompt at all). +// + +import Foundation +import TableProPluginKit +import Testing + +@testable import TablePro + +@Suite("Password hiding from connection fields") +struct PasswordHidingTests { + private func dropdown(default defaultValue: String, _ values: [String]) -> ConnectionField { + ConnectionField( + id: "awsAuth", + label: "Authentication", + defaultValue: defaultValue, + fieldType: .dropdown(options: values.map { .init(value: $0, label: $0) }), + section: .authentication, + hidesPassword: true + ) + } + + private let pgpassToggle = ConnectionField( + id: "usePgpass", + label: "Use Password File", + defaultValue: "false", + fieldType: .toggle, + section: .authentication, + hidesPassword: true + ) + + private let secretField = ConnectionField( + id: "serviceAccountJson", + label: "Service Account", + fieldType: .secure, + section: .authentication, + hidesPassword: true + ) + + @Test("A dropdown hides the password only when set off its default") + func dropdownAwayFromDefault() { + let fields = [dropdown(default: "off", ["off", "accessKey", "profile"])] + #expect(fields.hidesPassword(forValues: [:]) == false) + #expect(fields.hidesPassword(forValues: ["awsAuth": "off"]) == false) + #expect(fields.hidesPassword(forValues: ["awsAuth": "accessKey"]) == true) + #expect(fields.hidesPassword(forValues: ["awsAuth": "profile"]) == true) + } + + @Test("A toggle hides the password only when on") + func toggleOn() { + let fields = [pgpassToggle] + #expect(fields.hidesPassword(forValues: [:]) == false) + #expect(fields.hidesPassword(forValues: ["usePgpass": "false"]) == false) + #expect(fields.hidesPassword(forValues: ["usePgpass": "true"]) == true) + } + + @Test("A secure field that always replaces the password hides it unconditionally") + func secureFieldAlwaysHides() { + #expect([secretField].hidesPassword(forValues: [:]) == true) + } + + @Test("Fields without the hidesPassword flag never hide the password") + func plainFieldsDoNotHide() { + let plain = ConnectionField(id: "region", label: "Region", section: .authentication) + #expect([plain].hidesPassword(forValues: ["region": "us-east-1"]) == false) + #expect([ConnectionField]().hidesPassword(forValues: [:]) == false) + } + + @Test("Only authentication-section fields are considered") + func ignoresNonAuthenticationFields() { + let advanced = ConnectionField( + id: "advancedToggle", + label: "Advanced", + defaultValue: "false", + fieldType: .toggle, + section: .advanced, + hidesPassword: true + ) + #expect([advanced].hidesPassword(forValues: ["advancedToggle": "true"]) == false) + } +} + +@Suite("Password hiding resolved from plugin metadata") +@MainActor +struct PluginManagerPasswordHidingTests { + private func connection(type: DatabaseType, fields: [String: String]) -> DatabaseConnection { + var connection = DatabaseConnection(name: "test", type: type) + connection.additionalFields = fields + return connection + } + + @Test("AWS IAM modes hide the password for relational types") + func iamHidesPassword() { + let manager = PluginManager.shared + #expect(manager.hidesPassword(for: connection(type: .mysql, fields: ["awsAuth": "accessKey"]))) + #expect(manager.hidesPassword(for: connection(type: .postgresql, fields: ["awsAuth": "profile"]))) + } + + @Test("Password auth does not hide the password") + func passwordModeDoesNotHide() { + let manager = PluginManager.shared + #expect(!manager.hidesPassword(for: connection(type: .mysql, fields: ["awsAuth": "off"]))) + #expect(!manager.hidesPassword(for: connection(type: .mysql, fields: [:]))) + } +}