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
12 changes: 10 additions & 2 deletions lib/core/database/postgres_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,13 @@ class PostgresConnection {
);
}

@visibleForTesting
ConnectionSettings buildSettingsForTest() => _buildSettings();

ConnectionSettings _buildSettings() {
SecurityContext? securityContext;
if ((sslRootCert != null && sslRootCert!.trim().isNotEmpty) ||
if (useSSL ||
(sslRootCert != null && sslRootCert!.trim().isNotEmpty) ||
(sslCert != null && sslCert!.trim().isNotEmpty) ||
(sslKey != null && sslKey!.trim().isNotEmpty)) {
securityContext = SecurityContext(withTrustedRoots: true);
Expand Down Expand Up @@ -314,7 +318,11 @@ class PostgresConnection {
encoding: parsed.encoding,
replicationMode: parsed.replicationMode,
queryTimeout: parsed.queryTimeout ?? const Duration(seconds: 30),
securityContext: parsed.securityContext ?? _buildSettings().securityContext,
securityContext: parsed.securityContext ??
_buildSettings().securityContext ??
(sslMode != SslMode.disable
? SecurityContext(withTrustedRoots: true)
: null),
sslMode: sslMode,
),
);
Expand Down
93 changes: 79 additions & 14 deletions lib/features/postgresql/postgresql_connection_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class _PostgresConnectionFormContentState
final SshTunnelSecrets _sshSecrets = SshTunnelSecrets();

bool _useSSL = false;
String _sslMode = 'require';
bool _removeSavedPassword = false;
bool _showPassword = false;
bool _isTesting = false;
Expand Down Expand Up @@ -102,8 +103,18 @@ class _PostgresConnectionFormContentState
_usernameController.text = initial.username ?? '';
_databaseController.text = initial.databaseName ?? '';
_useSSL = initial.useSSL;
final connStr = initial.connectionString;
_connectionStringController.text =
redactUriPassword(initial.connectionString) ?? '';
redactUriPassword(connStr) ?? '';
if (connStr != null && connStr.isNotEmpty) {
final parsed = Uri.tryParse(connStr);
final mode = parsed?.queryParameters['sslmode'];
if (mode != null && mode.isNotEmpty) {
_sslMode = mode;
} else if (_useSSL) {
_sslMode = 'require';
}
}
if (initial.sshTunnelConfig != null) {
_sshConfig = initial.sshTunnelConfig!;
}
Expand Down Expand Up @@ -136,6 +147,11 @@ class _PostgresConnectionFormContentState
_sslRootCertController.text = parsed.queryParameters['sslrootcert'] ?? '';
_sslCertController.text = parsed.queryParameters['sslcert'] ?? '';
_sslKeyController.text = parsed.queryParameters['sslkey'] ?? '';
final mode = parsed.queryParameters['sslmode'];
if (mode != null && mode.isNotEmpty) {
_sslMode = mode;
_useSSL = mode != 'disable';
}
}

void _setOrRemoveSslParam(
Expand All @@ -160,9 +176,13 @@ class _PostgresConnectionFormContentState
_setOrRemoveSslParam(params, 'sslrootcert', _sslRootCertController);
_setOrRemoveSslParam(params, 'sslcert', _sslCertController);
_setOrRemoveSslParam(params, 'sslkey', _sslKeyController);
if (!params.containsKey('sslmode') &&
_sslRootCertController.text.trim().isNotEmpty) {
params['sslmode'] = 'verify-full';
if (_useSSL) {
params['sslmode'] = _sslRootCertController.text.trim().isNotEmpty &&
_sslMode == 'require'
? 'verify-full'
: _sslMode;
} else {
params.remove('sslmode');
}
final newUri = Uri(
scheme: parsed.scheme,
Expand Down Expand Up @@ -199,18 +219,27 @@ class _PostgresConnectionFormContentState
String? sslRootCert,
String? sslCert,
String? sslKey,
String? sslMode,
}) {
final userInfoParts = <String>[
if (username != null && username.isNotEmpty)
Uri.encodeComponent(username),
if (password != null && password.isNotEmpty)
Uri.encodeComponent(password),
];
// A root CA with the default `require` mode means the user wants the
// server verified, same as _syncUriSslParams does for a typed URI.
final hasRootCert = sslRootCert != null && sslRootCert.isNotEmpty;
final requestedMode = sslMode ?? (_useSSL ? _sslMode : null);
final effectiveMode =
hasRootCert && (requestedMode == null || requestedMode == 'require')
? 'verify-full'
: requestedMode;
final queryParams = <String, String>{
if (sslRootCert != null && sslRootCert.isNotEmpty) ...{
if (sslRootCert != null && sslRootCert.isNotEmpty)
'sslrootcert': sslRootCert,
'sslmode': 'verify-full',
},
if (effectiveMode != null && effectiveMode.isNotEmpty)
'sslmode': effectiveMode,
if (sslCert != null && sslCert.isNotEmpty) 'sslcert': sslCert,
if (sslKey != null && sslKey.isNotEmpty) 'sslkey': sslKey,
};
Expand All @@ -230,16 +259,19 @@ class _PostgresConnectionFormContentState
final sslRootCert = _sslRootCertController.text.trim();
final sslCert = _sslCertController.text.trim();
final sslKey = _sslKeyController.text.trim();
if (sslRootCert.isEmpty && sslCert.isEmpty && sslKey.isEmpty) return '';
if (!_useSSL && sslRootCert.isEmpty && sslCert.isEmpty && sslKey.isEmpty) {
return '';
}
return _buildConnectionUri(
host: _hostController.text.trim(),
port: int.tryParse(_portController.text.trim()) ?? 5432,
username: _usernameController.text.trim(),
password: _passwordController.text,
database: _databaseController.text.trim(),
sslRootCert: sslRootCert,
sslCert: sslCert,
sslKey: sslKey,
sslRootCert: sslRootCert.isEmpty ? null : sslRootCert,
sslCert: sslCert.isEmpty ? null : sslCert,
sslKey: sslKey.isEmpty ? null : sslKey,
sslMode: _useSSL ? _sslMode : null,
);
}

Expand Down Expand Up @@ -666,11 +698,44 @@ class _PostgresConnectionFormContentState
],
),
if (_useSSL) ...[
const Gap(8),
material.Row(
children: [
const Text('SSL Mode').small(),
const Gap(12),
material.SizedBox(
width: 220,
child: QueryaDropdown<String>(
expandToParent: true,
value: _sslMode,
onSelected: (mode) {
if (mode != null) {
setState(() => _sslMode = mode);
_syncUriSslParams();
}
},
items: const [
QueryaDropdownItem(
value: 'require',
label: 'require (encrypted)',
),
QueryaDropdownItem(
value: 'verify-ca',
label: 'verify-ca (check CA)',
),
QueryaDropdownItem(
value: 'verify-full',
label: 'verify-full (strict)',
),
],
),
),
],
),
const Gap(8),
const Text(
'Without a Root CA this is sslmode=require: traffic is '
'encrypted but the server certificate is not checked '
'(MITM is possible). A Root CA enables verify-full. '
'require encrypts traffic without checking server certificates. '
'verify-ca and verify-full enforce CA validation against trusted roots. '
'A URI sslmode= value always wins.',
).muted().small(),
const Gap(16),
Expand Down
19 changes: 19 additions & 0 deletions test/core/database/postgres_connection_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,25 @@ void main() {
),
);
});

test('buildSettingsForTest initializes SecurityContext with trusted roots when useSSL is true (#1367)', () {
final connWithoutSsl = PostgresConnection(
id: 1,
name: 'test',
host: 'localhost',
useSSL: false,
);
expect(connWithoutSsl.buildSettingsForTest().securityContext, isNull);

final connWithSsl = PostgresConnection(
id: 2,
name: 'test-ssl',
host: 'localhost',
useSSL: true,
);
final settings = connWithSsl.buildSettingsForTest();
expect(settings.securityContext, isNotNull);
});
});

group('PostgresConnection socket state reflection (#1373)', () {
Expand Down
Loading