From 33184a06d4a56a2d44d93c1ef560531933087f5c Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Fri, 10 Jul 2026 15:01:18 +0300 Subject: [PATCH] fix(postgresql): pass SSL client certificates when connecting via host/port (#273) - Add sslRootCert, sslCert, sslKey fields to PostgresConnection and extract them from connectionString query parameters in fromConnectionRow. - Build a SecurityContext in _buildSettings() when any certificate path is present and pass it to ConnectionSettings. - For URI-based connections, fall back to the extracted SecurityContext when the parsed URI does not provide one. - Forward SSL certificate parameters in connectToDatabase() so database-switch connections keep the same certs. - Pass the certificate paths from the form to the temporary PostgresConnection used for testConnection(). - Add tests covering extraction, preservation, and actual use during connect() for host/port mode. --- lib/core/database/postgres_connection.dart | 47 +++++++++++- .../postgresql_connection_form.dart | 9 +++ .../database/postgres_connection_test.dart | 72 +++++++++++++++++++ 3 files changed, 126 insertions(+), 2 deletions(-) diff --git a/lib/core/database/postgres_connection.dart b/lib/core/database/postgres_connection.dart index 8aae9434..ac28c97f 100644 --- a/lib/core/database/postgres_connection.dart +++ b/lib/core/database/postgres_connection.dart @@ -1,3 +1,4 @@ +import 'dart:io' show SecurityContext; import 'package:postgres/postgres.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; @@ -43,6 +44,9 @@ class PostgresConnection { this.database, this.useSSL = false, this.connectionString, + this.sslRootCert, + this.sslCert, + this.sslKey, }); /// Builds a connection from a saved [ConnectionRow] (host/port or URI). @@ -50,6 +54,18 @@ class PostgresConnection { ConnectionRow row, { String? database, }) { + String? rootCert; + String? clientCert; + String? clientKey; + if (row.connectionString != null && + row.connectionString!.trim().isNotEmpty) { + final uri = Uri.tryParse(row.connectionString!.trim()); + if (uri != null) { + rootCert = uri.queryParameters['sslrootcert']; + clientCert = uri.queryParameters['sslcert']; + clientKey = uri.queryParameters['sslkey']; + } + } return PostgresConnection( id: row.id ?? 0, name: row.name, @@ -60,6 +76,9 @@ class PostgresConnection { database: database ?? row.databaseName ?? 'postgres', useSSL: row.useSSL, connectionString: row.connectionString, + sslRootCert: rootCert, + sslCert: clientCert, + sslKey: clientKey, ); } @@ -72,6 +91,9 @@ class PostgresConnection { final String? database; final bool useSSL; final String? connectionString; + final String? sslRootCert; + final String? sslCert; + final String? sslKey; Connection? _conn; bool _isConnected = false; @@ -92,10 +114,28 @@ class PostgresConnection { } ConnectionSettings _buildSettings() { + SecurityContext? securityContext; + if ((sslRootCert != null && sslRootCert!.trim().isNotEmpty) || + (sslCert != null && sslCert!.trim().isNotEmpty) || + (sslKey != null && sslKey!.trim().isNotEmpty)) { + securityContext = SecurityContext(); + if (sslCert != null && sslCert!.trim().isNotEmpty) { + securityContext.useCertificateChain(sslCert!.trim()); + } + if (sslKey != null && sslKey!.trim().isNotEmpty) { + securityContext.usePrivateKey(sslKey!.trim()); + } + if (sslRootCert != null && sslRootCert!.trim().isNotEmpty) { + securityContext.setTrustedCertificates(sslRootCert!.trim()); + } + } return ConnectionSettings( - sslMode: useSSL ? SslMode.require : SslMode.disable, + sslMode: (useSSL || securityContext != null) + ? SslMode.require + : SslMode.disable, connectTimeout: const Duration(seconds: 10), queryTimeout: const Duration(seconds: 30), + securityContext: securityContext, ); } @@ -126,7 +166,7 @@ class PostgresConnection { encoding: parsed.encoding, replicationMode: parsed.replicationMode, queryTimeout: parsed.queryTimeout ?? const Duration(seconds: 30), - securityContext: parsed.securityContext, + securityContext: parsed.securityContext ?? _buildSettings().securityContext, sslMode: sslMode, ), ); @@ -407,6 +447,9 @@ class PostgresConnection { database: dbName, useSSL: useSSL, connectionString: newCs, + sslRootCert: sslRootCert, + sslCert: sslCert, + sslKey: sslKey, ); } diff --git a/lib/features/postgresql/postgresql_connection_form.dart b/lib/features/postgresql/postgresql_connection_form.dart index a7d5bb84..cfb17082 100644 --- a/lib/features/postgresql/postgresql_connection_form.dart +++ b/lib/features/postgresql/postgresql_connection_form.dart @@ -248,6 +248,15 @@ class _PostgresConnectionFormContentState _passwordController.text.isEmpty ? null : _passwordController.text, useSSL: _useSSL || _hasSslCertificateFields(), connectionString: hasUri ? uri : null, + sslRootCert: _sslRootCertController.text.trim().isEmpty + ? null + : _sslRootCertController.text.trim(), + sslCert: _sslCertController.text.trim().isEmpty + ? null + : _sslCertController.text.trim(), + sslKey: _sslKeyController.text.trim().isEmpty + ? null + : _sslKeyController.text.trim(), ); final result = await conn.testConnection(); if (mounted) { diff --git a/test/core/database/postgres_connection_test.dart b/test/core/database/postgres_connection_test.dart index 673a87bb..29dc064e 100644 --- a/test/core/database/postgres_connection_test.dart +++ b/test/core/database/postgres_connection_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:querya_desktop/core/database/postgres_connection.dart'; +import 'package:querya_desktop/core/storage/local_db.dart'; void main() { group('PostgresConnection initial state', () { @@ -503,4 +504,75 @@ void main() { ); }); }); + + group('PostgresConnection SSL certificates', () { + test('stores optional sslRootCert, sslCert, sslKey parameters', () { + final conn = PostgresConnection( + id: 1, + name: 'test', + host: 'localhost', + sslRootCert: '/path/to/ca.pem', + sslCert: '/path/to/client.crt', + sslKey: '/path/to/client.key', + ); + expect(conn.sslRootCert, '/path/to/ca.pem'); + expect(conn.sslCert, '/path/to/client.crt'); + expect(conn.sslKey, '/path/to/client.key'); + }); + + test('extracts sslrootcert, sslcert, sslkey from connectionString in fromConnectionRow', () { + const row = ConnectionRow( + id: 10, + type: 'postgresql', + name: 'ssl_row', + host: 'db.example.com', + port: 5432, + connectionString: + 'postgresql://user:password@db.example.com:5432/mydb?sslrootcert=%2Fca.crt&sslcert=%2Fclient.crt&sslkey=%2Fclient.key', + createdAt: '2026-07-10T12:00:00Z', + ); + final conn = PostgresConnection.fromConnectionRow(row); + expect(conn.sslRootCert, '/ca.crt'); + expect(conn.sslCert, '/client.crt'); + expect(conn.sslKey, '/client.key'); + }); + + test('connectToDatabase preserves SSL certificate parameters', () async { + final conn = PostgresConnection( + id: 1, + name: 'test', + host: 'localhost', + sslRootCert: '/path/to/ca.pem', + sslCert: '/path/to/client.crt', + sslKey: '/path/to/client.key', + ); + final dbConn = await conn.connectToDatabase('newdb'); + expect(dbConn.database, 'newdb'); + expect(dbConn.sslRootCert, '/path/to/ca.pem'); + expect(dbConn.sslCert, '/path/to/client.crt'); + expect(dbConn.sslKey, '/path/to/client.key'); + }); + + test('uses SSL certificates when connecting via host/port fields', () async { + final conn = PostgresConnection( + id: 1, + name: 'test', + host: 'localhost', + port: 5433, + sslRootCert: '/nonexistent/ca.pem', + sslCert: '/nonexistent/client.crt', + sslKey: '/nonexistent/client.key', + ); + expect( + conn.connect, + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('/nonexistent'), + ), + ), + ); + }); + }); }