Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

Expand Down
5 changes: 4 additions & 1 deletion TablePro/Core/Database/DatabaseManager+Health.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,10 @@ extension DatabaseManager {

// Resolve password for prompt-for-password connections
var passwordOverride = activeSessions[sessionId]?.cachedPassword
if session.connection.promptForPassword && 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,
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/Database/DatabaseManager+Sessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ extension DatabaseManager {
}

var passwordOverride: String?
if connection.promptForPassword {
if connection.promptForPassword, !pluginManager.hidesPassword(for: connection) {
if let cached = activeSessions[connection.id]?.cachedPassword {
passwordOverride = cached
} else {
Expand Down
30 changes: 30 additions & 0 deletions TablePro/Core/Plugins/ConnectionField+PasswordHiding.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
6 changes: 3 additions & 3 deletions TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -472,7 +472,7 @@ final class ConnectionFormCoordinator {
temporaryTestIds.insert(testConn.id)

let password = auth.password
let promptForPassword = auth.promptForPassword
let promptForPassword = auth.effectivePromptForPassword
let connectionType = network.type
let displayName = network.name.isEmpty ? network.host : network.name
let sshState = ssh.state
Expand Down
17 changes: 5 additions & 12 deletions TablePro/Views/ConnectionForm/ViewModels/AuthPaneViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
111 changes: 111 additions & 0 deletions TableProTests/Core/Plugins/PasswordHidingTests.swift
Original file line number Diff line number Diff line change
@@ -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: [:])))
}
}
Loading