Skip to content

Commit ebd85e4

Browse files
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.
1 parent 18296ec commit ebd85e4

3 files changed

Lines changed: 126 additions & 2 deletions

File tree

lib/core/database/postgres_connection.dart

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:io' show SecurityContext;
12
import 'package:postgres/postgres.dart';
23
import 'package:querya_desktop/core/storage/local_db.dart';
34

@@ -43,13 +44,28 @@ class PostgresConnection {
4344
this.database,
4445
this.useSSL = false,
4546
this.connectionString,
47+
this.sslRootCert,
48+
this.sslCert,
49+
this.sslKey,
4650
});
4751

4852
/// Builds a connection from a saved [ConnectionRow] (host/port or URI).
4953
factory PostgresConnection.fromConnectionRow(
5054
ConnectionRow row, {
5155
String? database,
5256
}) {
57+
String? rootCert;
58+
String? clientCert;
59+
String? clientKey;
60+
if (row.connectionString != null &&
61+
row.connectionString!.trim().isNotEmpty) {
62+
final uri = Uri.tryParse(row.connectionString!.trim());
63+
if (uri != null) {
64+
rootCert = uri.queryParameters['sslrootcert'];
65+
clientCert = uri.queryParameters['sslcert'];
66+
clientKey = uri.queryParameters['sslkey'];
67+
}
68+
}
5369
return PostgresConnection(
5470
id: row.id ?? 0,
5571
name: row.name,
@@ -60,6 +76,9 @@ class PostgresConnection {
6076
database: database ?? row.databaseName ?? 'postgres',
6177
useSSL: row.useSSL,
6278
connectionString: row.connectionString,
79+
sslRootCert: rootCert,
80+
sslCert: clientCert,
81+
sslKey: clientKey,
6382
);
6483
}
6584

@@ -72,6 +91,9 @@ class PostgresConnection {
7291
final String? database;
7392
final bool useSSL;
7493
final String? connectionString;
94+
final String? sslRootCert;
95+
final String? sslCert;
96+
final String? sslKey;
7597

7698
Connection? _conn;
7799
bool _isConnected = false;
@@ -92,10 +114,28 @@ class PostgresConnection {
92114
}
93115

94116
ConnectionSettings _buildSettings() {
117+
SecurityContext? securityContext;
118+
if ((sslRootCert != null && sslRootCert!.trim().isNotEmpty) ||
119+
(sslCert != null && sslCert!.trim().isNotEmpty) ||
120+
(sslKey != null && sslKey!.trim().isNotEmpty)) {
121+
securityContext = SecurityContext();
122+
if (sslCert != null && sslCert!.trim().isNotEmpty) {
123+
securityContext.useCertificateChain(sslCert!.trim());
124+
}
125+
if (sslKey != null && sslKey!.trim().isNotEmpty) {
126+
securityContext.usePrivateKey(sslKey!.trim());
127+
}
128+
if (sslRootCert != null && sslRootCert!.trim().isNotEmpty) {
129+
securityContext.setTrustedCertificates(sslRootCert!.trim());
130+
}
131+
}
95132
return ConnectionSettings(
96-
sslMode: useSSL ? SslMode.require : SslMode.disable,
133+
sslMode: (useSSL || securityContext != null)
134+
? SslMode.require
135+
: SslMode.disable,
97136
connectTimeout: const Duration(seconds: 10),
98137
queryTimeout: const Duration(seconds: 30),
138+
securityContext: securityContext,
99139
);
100140
}
101141

@@ -126,7 +166,7 @@ class PostgresConnection {
126166
encoding: parsed.encoding,
127167
replicationMode: parsed.replicationMode,
128168
queryTimeout: parsed.queryTimeout ?? const Duration(seconds: 30),
129-
securityContext: parsed.securityContext,
169+
securityContext: parsed.securityContext ?? _buildSettings().securityContext,
130170
sslMode: sslMode,
131171
),
132172
);
@@ -407,6 +447,9 @@ class PostgresConnection {
407447
database: dbName,
408448
useSSL: useSSL,
409449
connectionString: newCs,
450+
sslRootCert: sslRootCert,
451+
sslCert: sslCert,
452+
sslKey: sslKey,
410453
);
411454
}
412455

lib/features/postgresql/postgresql_connection_form.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,15 @@ class _PostgresConnectionFormContentState
248248
_passwordController.text.isEmpty ? null : _passwordController.text,
249249
useSSL: _useSSL || _hasSslCertificateFields(),
250250
connectionString: hasUri ? uri : null,
251+
sslRootCert: _sslRootCertController.text.trim().isEmpty
252+
? null
253+
: _sslRootCertController.text.trim(),
254+
sslCert: _sslCertController.text.trim().isEmpty
255+
? null
256+
: _sslCertController.text.trim(),
257+
sslKey: _sslKeyController.text.trim().isEmpty
258+
? null
259+
: _sslKeyController.text.trim(),
251260
);
252261
final result = await conn.testConnection();
253262
if (mounted) {

test/core/database/postgres_connection_test.dart

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter_test/flutter_test.dart';
22
import 'package:querya_desktop/core/database/postgres_connection.dart';
3+
import 'package:querya_desktop/core/storage/local_db.dart';
34

45
void main() {
56
group('PostgresConnection initial state', () {
@@ -503,4 +504,75 @@ void main() {
503504
);
504505
});
505506
});
507+
508+
group('PostgresConnection SSL certificates', () {
509+
test('stores optional sslRootCert, sslCert, sslKey parameters', () {
510+
final conn = PostgresConnection(
511+
id: 1,
512+
name: 'test',
513+
host: 'localhost',
514+
sslRootCert: '/path/to/ca.pem',
515+
sslCert: '/path/to/client.crt',
516+
sslKey: '/path/to/client.key',
517+
);
518+
expect(conn.sslRootCert, '/path/to/ca.pem');
519+
expect(conn.sslCert, '/path/to/client.crt');
520+
expect(conn.sslKey, '/path/to/client.key');
521+
});
522+
523+
test('extracts sslrootcert, sslcert, sslkey from connectionString in fromConnectionRow', () {
524+
final row = ConnectionRow(
525+
id: 10,
526+
type: 'postgresql',
527+
name: 'ssl_row',
528+
host: 'db.example.com',
529+
port: 5432,
530+
connectionString:
531+
'postgresql://user:password@db.example.com:5432/mydb?sslrootcert=%2Fca.crt&sslcert=%2Fclient.crt&sslkey=%2Fclient.key',
532+
createdAt: '2026-07-10T12:00:00Z',
533+
);
534+
final conn = PostgresConnection.fromConnectionRow(row);
535+
expect(conn.sslRootCert, '/ca.crt');
536+
expect(conn.sslCert, '/client.crt');
537+
expect(conn.sslKey, '/client.key');
538+
});
539+
540+
test('connectToDatabase preserves SSL certificate parameters', () async {
541+
final conn = PostgresConnection(
542+
id: 1,
543+
name: 'test',
544+
host: 'localhost',
545+
sslRootCert: '/path/to/ca.pem',
546+
sslCert: '/path/to/client.crt',
547+
sslKey: '/path/to/client.key',
548+
);
549+
final dbConn = await conn.connectToDatabase('newdb');
550+
expect(dbConn.database, 'newdb');
551+
expect(dbConn.sslRootCert, '/path/to/ca.pem');
552+
expect(dbConn.sslCert, '/path/to/client.crt');
553+
expect(dbConn.sslKey, '/path/to/client.key');
554+
});
555+
556+
test('uses SSL certificates when connecting via host/port fields', () async {
557+
final conn = PostgresConnection(
558+
id: 1,
559+
name: 'test',
560+
host: 'localhost',
561+
port: 5433,
562+
sslRootCert: '/nonexistent/ca.pem',
563+
sslCert: '/nonexistent/client.crt',
564+
sslKey: '/nonexistent/client.key',
565+
);
566+
expect(
567+
conn.connect,
568+
throwsA(
569+
isA<PostgresConnectionException>().having(
570+
(e) => e.message,
571+
'message',
572+
contains('/nonexistent'),
573+
),
574+
),
575+
);
576+
});
577+
});
506578
}

0 commit comments

Comments
 (0)