diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 6696542484..60d7edeb69 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -30,7 +30,7 @@ import { h2ConnectionModeForConfig, h2FileJdbcUrlWithPath, h2FilePathFromJdbcUrl import { firstZooKeeperEndpoint, normalizeZooKeeperConnectString } from "@/lib/zookeeperConnection"; import { isLocalFileTypeDb } from "@/lib/connectionFile"; import { MQ_PINNED_VERSION_OPTIONS, pinnedVersionToSelection, selectionToPinnedVersion } from "@/lib/mqPinnedVersionOptions"; -import { mongodbAuthFailureHint, mongoUrlParam, setMongoUrlParam } from "@/lib/mongoConnectionOptions"; +import { mongodbAuthFailureHint, mongoUrlParam, mongoUrlParamIsTrue, normalizeMongoTlsFormState, setMongoUrlParam, setMongoUrlParamBoolean } from "@/lib/mongoConnectionOptions"; import { mysqlCleartextPasswordAuthEnabled, setMysqlCleartextPasswordAuthEnabled } from "@/lib/mysqlConnectionOptions"; import { copyToClipboard } from "@/lib/clipboard"; import { agentDriverInstallKey, appendAgentDriverUpdateHint, hasAgentDriverUpdate, showAgentDriverInstallHint, type AgentDriverInstallState } from "@/lib/agentDriverInstallHint"; @@ -1786,6 +1786,20 @@ const mongoAuthMechanism = computed({ form.value.url_params = setMongoUrlParam(form.value.url_params, "authMechanism", value === "default" ? "" : value); }, }); +const mongoTlsAllowInvalidCertificates = computed({ + get: () => mongoUrlParamIsTrue(form.value.url_params, "tlsAllowInvalidCertificates"), + set: (value: boolean) => { + let next = setMongoUrlParamBoolean(form.value.url_params, "tlsAllowInvalidCertificates", value); + next = setMongoUrlParam(next, "tlsAllowInvalidHostnames", ""); + form.value.url_params = next; + }, +}); +const mongoRetryWrites = computed({ + get: () => mongoUrlParamIsTrue(form.value.url_params, "retryWrites", true), + set: (value: boolean) => { + form.value.url_params = setMongoUrlParamBoolean(form.value.url_params, "retryWrites", value, true); + }, +}); const mongoDriverMode = computed({ get: () => (form.value.driver_profile === "mongodb-legacy" ? "legacy" : "auto"), set: (value: string) => { @@ -2058,6 +2072,11 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { config.driver_profile = "mongodb"; config.driver_label = "MongoDB"; } + if (config.db_type === "mongodb") { + const mongoTls = normalizeMongoTlsFormState(!!config.ssl, config.url_params, config.ca_cert_path); + config.url_params = mongoTls.urlParams; + config.ca_cert_path = mongoTls.caCertPath; + } if (config.db_type !== "oracle") { config.sysdba = undefined; config.oracle_connection_type = undefined; @@ -2140,7 +2159,7 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { config.client_cert_path = undefined; config.client_key_path = undefined; } - if (config.db_type !== "mysql" && config.db_type !== "clickhouse" && config.db_type !== "etcd" && config.db_type !== "starrocks") { + if (config.db_type !== "mysql" && config.db_type !== "clickhouse" && config.db_type !== "etcd" && config.db_type !== "starrocks" && config.db_type !== "mongodb") { config.ca_cert_path = undefined; } else { config.ca_cert_path = config.ca_cert_path?.trim() || ""; @@ -3984,6 +4003,40 @@ function openExternalUrl(url: string) { {{ t("connection.sslEnable") }} +
@@ -4015,7 +4068,7 @@ function openExternalUrl(url: string) {
- +
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index d2a59cf24d..0480cd6299 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -385,6 +385,10 @@ export default { dmCompatHint: "Requires DM8 ODBC driver installed on your system.", dmDownload: "Download from Dameng", mongoLegacyHint: "Use Legacy for older MongoDB servers, especially versions below 4.2. If authentication fails and the user was created in admin, set Auth DB to admin.", + mongoTlsAllowInvalidCertificates: "Disable TLS certificate verification", + mongoTlsAllowInvalidCertificatesHint: "Maps to tlsAllowInvalidCertificates=true and disables all certificate validation, not just hostname checks. This increases MITM risk. Commonly required for AWS DocumentDB over an SSH tunnel.", + mongoRetryWrites: "Retry writes", + mongoRetryWritesHint: "Maps to retryWrites. Disable for AWS DocumentDB, which does not support retryable writes.", compatible: "Compatible", mainstream: "Popular", color: "Color", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 9116611bb0..4e732ac18c 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -388,6 +388,10 @@ export default withEnglishFallback({ dmCompatHint: "Requiere el driver ODBC DM8 instalado en el sistema.", dmDownload: "Descargar desde Dameng", mongoLegacyHint: "Usa Legacy para servidores MongoDB antiguos, especialmente versiones anteriores a 4.2. Si falla la autenticación y el usuario se creó en admin, define Auth DB como admin.", + mongoTlsAllowInvalidCertificates: "Desactivar verificación de certificado TLS", + mongoTlsAllowInvalidCertificatesHint: "Se asigna a tlsAllowInvalidCertificates=true y desactiva toda la validación de certificados, no solo la del nombre de host. Aumenta el riesgo de MITM. Suele ser necesario para AWS DocumentDB a través de un túnel SSH.", + mongoRetryWrites: "Reintentar escrituras", + mongoRetryWritesHint: "Se asigna a retryWrites. Desactívalo para AWS DocumentDB, que no admite escrituras reintentables.", compatible: "Compatible", mainstream: "Popular", color: "Color", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index bfd1b6eaf0..a45fb64ba4 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -386,6 +386,10 @@ export default withEnglishFallback({ dmCompatHint: "Richiede il driver ODBC DM8 installato sul sistema.", dmDownload: "Scarica da Dameng", mongoLegacyHint: "Usa Legacy per server MongoDB meno recenti, specialmente versioni precedenti alla 4.2. Se l'autenticazione fallisce e l'utente è stato creato in admin, imposta DB di Autenticazione su admin.", + mongoTlsAllowInvalidCertificates: "Disabilita verifica certificato TLS", + mongoTlsAllowInvalidCertificatesHint: "Corrisponde a tlsAllowInvalidCertificates=true e disabilita tutta la validazione del certificato, non solo il controllo dell'hostname. Aumenta il rischio MITM. Di solito è necessario per AWS DocumentDB tramite tunnel SSH.", + mongoRetryWrites: "Riprova scritture", + mongoRetryWritesHint: "Corrisponde a retryWrites. Disabilitalo per AWS DocumentDB, che non supporta scritture ripetibili.", compatible: "Compatibile", mainstream: "Popolare", color: "Colore", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 1a91e24466..77d21b587d 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -380,6 +380,10 @@ export default withEnglishFallback({ dmCompatHint: "システムにDM8 ODBCドライバーがインストールされている必要があります。", dmDownload: "達夢からダウンロード", mongoLegacyHint: "古いMongoDBサーバー(特に4.2未満)ではレガシーを使用してください。認証に失敗しユーザーがadminで作成された場合は、認証DBをadminに設定してください。", + mongoTlsAllowInvalidCertificates: "TLS 証明書検証を無効化", + mongoTlsAllowInvalidCertificatesHint: "tlsAllowInvalidCertificates=true に対応し、ホスト名だけでなくすべての証明書検証を無効化します。MITM 攻撃のリスクが高まります。SSH トンネル経由の AWS DocumentDB では通常必要です。", + mongoRetryWrites: "書き込み再試行", + mongoRetryWritesHint: "retryWrites に対応します。AWS DocumentDB は再試行可能な書き込みをサポートしないため、無効にしてください。", compatible: "互換", mainstream: "人気", color: "色", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 97c2443a17..975d57bc54 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -387,6 +387,10 @@ export default withEnglishFallback({ dmCompatHint: "Requer o driver ODBC DM8 instalado no seu sistema.", dmDownload: "Baixar da Dameng", mongoLegacyHint: "Use Legacy para servidores MongoDB antigos, especialmente versões anteriores à 4.2. Se a autenticação falhar e o usuário foi criado em admin, defina o BD de autenticação como admin.", + mongoTlsAllowInvalidCertificates: "Desativar verificação de certificado TLS", + mongoTlsAllowInvalidCertificatesHint: "Corresponde a tlsAllowInvalidCertificates=true e desativa toda a validação de certificados, não apenas a do hostname. Aumenta o risco de MITM. Geralmente é necessário para AWS DocumentDB via túnel SSH.", + mongoRetryWrites: "Repetir gravações", + mongoRetryWritesHint: "Corresponde a retryWrites. Desative para AWS DocumentDB, que não suporta gravações repetíveis.", compatible: "Compatível", mainstream: "Popular", color: "Cor", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 2187216a5e..e9d0e5de96 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -387,6 +387,10 @@ export default withEnglishFallback({ dmCompatHint: "需要在系统上安装达梦 DM8 ODBC 驱动程序。", dmDownload: "前往达梦官网下载", mongoLegacyHint: "旧版 MongoDB(尤其是 4.2 以下版本)请选择旧版兼容。若账号创建在 admin 且认证失败,请将认证库设为 admin。", + mongoTlsAllowInvalidCertificates: "禁用 TLS 证书校验", + mongoTlsAllowInvalidCertificatesHint: "对应 tlsAllowInvalidCertificates=true,会禁用全部证书校验(不仅是主机名),存在中间人攻击风险。经 SSH 隧道访问 AWS DocumentDB 时通常需要开启。", + mongoRetryWrites: "重试写入", + mongoRetryWritesHint: "对应 retryWrites。AWS DocumentDB 不支持可重试写入,请关闭。", compatible: "兼容", mainstream: "主流", color: "颜色", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 052890a648..2e5fd5d91c 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -387,6 +387,10 @@ export default withEnglishFallback({ dmCompatHint: "需要在系統上安裝達夢 DM8 ODBC 驅動程式。", dmDownload: "前往達夢官網下載", mongoLegacyHint: "舊版 MongoDB(尤其是 4.2 以下版本)請選擇舊版相容。若帳號建立在 admin 且認證失敗,請將認證庫設為 admin。", + mongoTlsAllowInvalidCertificates: "停用 TLS 憑證校驗", + mongoTlsAllowInvalidCertificatesHint: "對應 tlsAllowInvalidCertificates=true,會停用全部憑證校驗(不只是主機名),存在中間人攻擊風險。經 SSH 隧道存取 AWS DocumentDB 時通常需要開啟。", + mongoRetryWrites: "重試寫入", + mongoRetryWritesHint: "對應 retryWrites。AWS DocumentDB 不支援可重試寫入,請關閉。", compatible: "相容", mainstream: "熱門", color: "顏色", diff --git a/apps/desktop/src/lib/mongoConnectionOptions.ts b/apps/desktop/src/lib/mongoConnectionOptions.ts index 49bccc8998..7fb36fc585 100644 --- a/apps/desktop/src/lib/mongoConnectionOptions.ts +++ b/apps/desktop/src/lib/mongoConnectionOptions.ts @@ -14,7 +14,42 @@ export function setMongoUrlParam(urlParams: string | undefined, key: string, val return params.toString(); } +export function mongoUrlParamIsTrue(urlParams: string | undefined, key: string, defaultValue = false): boolean { + const value = mongoUrlParam(urlParams, key); + if (!value) return defaultValue; + return ["true", "1", "yes"].includes(value.toLowerCase()); +} + +export function setMongoUrlParamBoolean(urlParams: string | undefined, key: string, value: boolean, defaultWhenEnabled = false): string { + if (value === defaultWhenEnabled) { + return setMongoUrlParam(urlParams, key, ""); + } + return setMongoUrlParam(urlParams, key, value ? "true" : "false"); +} + +export function normalizeMongoTlsFormState(ssl: boolean, urlParams: string | undefined, caCertPath: string | undefined): { urlParams: string; caCertPath: string } { + if (ssl) { + const nextCaCertPath = caCertPath?.trim() || ""; + let nextUrlParams = urlParams || ""; + if (nextCaCertPath) { + nextUrlParams = setMongoUrlParam(nextUrlParams, "tlsCAFile", ""); + } + return { urlParams: nextUrlParams, caCertPath: nextCaCertPath }; + } + const next = setMongoUrlParam(setMongoUrlParam(setMongoUrlParam(urlParams, "tlsAllowInvalidCertificates", ""), "tlsAllowInvalidHostnames", ""), "tlsCAFile", ""); + return { urlParams: next, caCertPath: "" }; +} + export function mongodbAuthFailureHint(message: string): string { + const normalizedMessage = message.toLowerCase(); + if (normalizedMessage.includes("tlsallowinvalidhostnames is an invalid option")) { + return `${message}\n\nDBX uses the Rust MongoDB driver (rustls), which does not accept tlsAllowInvalidHostnames in the connection string. Use tlsAllowInvalidCertificates=true instead. This is commonly required for AWS DocumentDB over an SSH tunnel because the local endpoint is 127.0.0.1 while the server certificate is issued for the DocumentDB hostname.`; + } + + if (message.includes('certificate not valid for name "127.0.0.1"') || (message.includes("invalid peer certificate") && message.includes("127.0.0.1") && message.includes("docdb"))) { + return `${message}\n\nTLS hostname verification failed because the SSH tunnel connects to 127.0.0.1, but the DocumentDB certificate is issued for the cluster hostname. Add tlsAllowInvalidCertificates=true to URL params. Keep tlsCAFile configured so the certificate chain is still trusted when possible.`; + } + if (message.includes("cannot specify multiple seeds with directConnection=true")) { return `${message}\n\nMongoDB directConnection=true can only be used with a single host. Remove directConnection=true when using a multi-host replica set URL, or keep only one reachable host if you need a direct connection.`; } diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index a070099c4d..2d3527cd3c 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -1110,7 +1110,9 @@ impl ConnectionConfig { } DatabaseType::Databend => normalize_bare_mysql_url_params(value), DatabaseType::Postgres | DatabaseType::Redshift => normalize_postgres_url_params(value, self.ssl), - DatabaseType::MongoDb => normalize_mongo_url_params(value, self.ssl, !self.username.trim().is_empty()), + DatabaseType::MongoDb => { + normalize_mongo_url_params(value, self.ssl, !self.username.trim().is_empty(), self.ca_cert_path.trim()) + } _ => value.trim_start_matches('?').to_string(), } } @@ -1270,7 +1272,7 @@ fn normalize_mysql_url_params(value: &str, force_tls: bool, accept_invalid_certs parts.join("&") } -fn normalize_mongo_url_params(value: &str, force_tls: bool, default_auth_source: bool) -> String { +fn normalize_mongo_url_params(value: &str, force_tls: bool, default_auth_source: bool, ca_cert_path: &str) -> String { let value = value.trim_start_matches('?'); let mut parts: Vec = value.split('&').filter(|part| !part.is_empty()).map(str::to_string).collect(); @@ -1279,6 +1281,29 @@ fn normalize_mongo_url_params(value: &str, force_tls: bool, default_auth_source: parts.insert(0, "tls=true".to_string()); } + normalize_mongo_tls_compat_params(&mut parts); + + if !force_tls { + parts.retain(|part| { + !url_param_key_is(part, "tlsAllowInvalidCertificates") && !url_param_key_is(part, "tlsCAFile") + }); + } + + let existing_tls_ca_file = if ca_cert_path.is_empty() { + parts.iter().find(|part| url_param_key_is(part, "tlsCAFile")).cloned() + } else { + None + }; + parts.retain(|part| !url_param_key_is(part, "tlsCAFile")); + + if force_tls { + if !ca_cert_path.is_empty() { + parts.push(format!("tlsCAFile={}", encode_mongo_tls_file_path(ca_cert_path))); + } else if let Some(part) = existing_tls_ca_file { + parts.push(part); + } + } + if default_auth_source && !parts.iter().any(|part| url_param_key_is(part, "authSource")) { parts.push("authSource=admin".to_string()); } @@ -1286,6 +1311,29 @@ fn normalize_mongo_url_params(value: &str, force_tls: bool, default_auth_source: parts.join("&") } +/// The Rust MongoDB driver uses rustls by default, which does not accept +/// `tlsAllowInvalidHostnames` in the connection string. Map it to the +/// supported `tlsAllowInvalidCertificates` option instead. +fn normalize_mongo_tls_compat_params(parts: &mut Vec) { + let allow_invalid_hostnames = + parts.iter().any(|part| url_param_key_is(part, "tlsAllowInvalidHostnames") && mongo_url_param_is_truthy(part)); + parts.retain(|part| !url_param_key_is(part, "tlsAllowInvalidHostnames")); + if allow_invalid_hostnames && !parts.iter().any(|part| url_param_key_is(part, "tlsAllowInvalidCertificates")) { + parts.push("tlsAllowInvalidCertificates=true".to_string()); + } +} + +fn mongo_url_param_is_truthy(part: &str) -> bool { + let Some((_, value)) = part.split_once('=') else { + return true; + }; + matches!(percent_decode_str(value).decode_utf8_lossy().trim().to_ascii_lowercase().as_str(), "true" | "1" | "yes") +} + +fn encode_mongo_tls_file_path(path: &str) -> String { + utf8_percent_encode(path, NON_ALPHANUMERIC).to_string() +} + fn mongo_uri_db_part_for_suffix<'a>(db_part: &'a str, suffix: &str) -> &'a str { if db_part.is_empty() && !suffix.is_empty() { "/" @@ -2495,6 +2543,56 @@ mod tests { assert_eq!(config.connection_url(), "mongodb://root:secret@10.1.2.3:17000/admin?tls=true&authSource=admin"); } + #[test] + fn mongodb_form_tls_ca_cert_adds_tls_ca_file_param() { + let mut config = mongodb_config("root", "secret", Some("admin")); + config.ssl = true; + config.ca_cert_path = "/tmp/mongo-ca.pem".to_string(); + + assert_eq!( + config.connection_url(), + "mongodb://root:secret@10.1.2.3:17000/admin?tls=true&tlsCAFile=%2Ftmp%2Fmongo%2Dca%2Epem&authSource=admin" + ); + } + + #[test] + fn mongodb_form_tls_cert_params_replace_existing_url_values() { + let mut config = mongodb_config("root", "secret", Some("admin")); + config.ssl = true; + config.ca_cert_path = "/tmp/new-ca.pem".to_string(); + config.url_params = Some("tlsCAFile=%2Fold-ca.pem&authSource=admin".to_string()); + + assert_eq!( + config.connection_url(), + "mongodb://root:secret@10.1.2.3:17000/admin?tls=true&authSource=admin&tlsCAFile=%2Ftmp%2Fnew%2Dca%2Epem" + ); + } + + #[test] + fn mongodb_form_tls_preserves_legacy_tls_ca_file_in_url_params() { + let mut config = mongodb_config("root", "secret", Some("admin")); + config.ssl = true; + config.url_params = Some("tlsCAFile=%2Ftmp%2Flegacy-ca.pem&authSource=admin".to_string()); + + assert_eq!( + config.connection_url(), + "mongodb://root:secret@10.1.2.3:17000/admin?tls=true&authSource=admin&tlsCAFile=%2Ftmp%2Flegacy-ca.pem" + ); + } + + #[test] + fn mongodb_form_tls_allow_invalid_hostnames_maps_to_allow_invalid_certificates() { + let mut config = mongodb_config("root", "secret", Some("admin")); + config.ssl = true; + config.url_params = Some("replicaSet=rs0&tlsAllowInvalidHostnames=true".to_string()); + + assert_eq!( + config.connection_url(), + "mongodb://root:secret@10.1.2.3:17000/admin?tls=true&replicaSet=rs0&tlsAllowInvalidCertificates=true&authSource=admin" + ); + assert!(!config.connection_url().contains("tlsAllowInvalidHostnames")); + } + #[test] fn parse_mongo_first_host_replica_set() { let uri = "mongodb://user:pass@host1:27017,host2:27017,host3:27017/admin?replicaSet=rs0"; diff --git a/packages/app-tests/mongoConnectionOptions.test.ts b/packages/app-tests/mongoConnectionOptions.test.ts index 697a47e634..0868b3fde6 100644 --- a/packages/app-tests/mongoConnectionOptions.test.ts +++ b/packages/app-tests/mongoConnectionOptions.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import { test } from "vitest"; -import { mongoUrlParam, setMongoUrlParam, mongodbAuthFailureHint } from "../../apps/desktop/src/lib/mongoConnectionOptions.ts"; +import { mongoUrlParam, mongoUrlParamIsTrue, normalizeMongoTlsFormState, setMongoUrlParam, setMongoUrlParamBoolean, mongodbAuthFailureHint } from "../../apps/desktop/src/lib/mongoConnectionOptions.ts"; test("reads MongoDB authSource from URL params", () => { assert.equal(mongoUrlParam("?replicaSet=rs0&authSource=admin", "authSource"), "admin"); @@ -14,6 +14,43 @@ test("removes empty MongoDB authSource from URL params", () => { assert.equal(setMongoUrlParam("replicaSet=rs0&authSource=admin", "authSource", ""), "replicaSet=rs0"); }); +test("reads MongoDB boolean URL params", () => { + assert.equal(mongoUrlParamIsTrue("tlsAllowInvalidCertificates=true", "tlsAllowInvalidCertificates"), true); + assert.equal(mongoUrlParamIsTrue("retryWrites=false", "retryWrites", true), false); + assert.equal(mongoUrlParamIsTrue("", "retryWrites", true), true); +}); + +test("sets MongoDB boolean URL params", () => { + assert.equal(setMongoUrlParamBoolean("replicaSet=rs0", "retryWrites", false, true), "replicaSet=rs0&retryWrites=false"); + assert.equal(setMongoUrlParamBoolean("replicaSet=rs0&retryWrites=false", "retryWrites", true, true), "replicaSet=rs0"); + assert.equal(setMongoUrlParamBoolean("", "tlsAllowInvalidCertificates", true), "tlsAllowInvalidCertificates=true"); +}); + +test("clears MongoDB TLS form state when SSL is disabled", () => { + assert.deepEqual( + normalizeMongoTlsFormState(false, "replicaSet=rs0&tlsAllowInvalidCertificates=true&tlsAllowInvalidHostnames=true&tlsCAFile=%2Ftmp%2Fca.pem", "/tmp/ca.pem"), + { urlParams: "replicaSet=rs0", caCertPath: "" }, + ); + assert.deepEqual(normalizeMongoTlsFormState(true, "replicaSet=rs0", " /tmp/ca.pem "), { + urlParams: "replicaSet=rs0", + caCertPath: "/tmp/ca.pem", + }); +}); + +test("preserves MongoDB tlsCAFile in URL params when caCertPath is empty", () => { + assert.deepEqual(normalizeMongoTlsFormState(true, "replicaSet=rs0&tlsCAFile=%2Ftmp%2Flegacy-ca.pem", ""), { + urlParams: "replicaSet=rs0&tlsCAFile=%2Ftmp%2Flegacy-ca.pem", + caCertPath: "", + }); +}); + +test("form caCertPath overrides tlsCAFile in URL params when set", () => { + assert.deepEqual(normalizeMongoTlsFormState(true, "replicaSet=rs0&tlsCAFile=%2Ftmp%2Flegacy-ca.pem", "/tmp/explicit-ca.pem"), { + urlParams: "replicaSet=rs0", + caCertPath: "/tmp/explicit-ca.pem", + }); +}); + test("adds a MongoDB authSource hint for legacy authentication failures", () => { const message = "Agent RPC error: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='rwuser', source='gray_lite_twin_fat'}"; @@ -52,3 +89,16 @@ test("adds a MongoDB listDatabases permission hint", () => { assert.match(mongodbAuthFailureHint(message), /does not have permission to run listDatabases/); }); + +test("adds a MongoDB rustls hint for tlsAllowInvalidHostnames", () => { + const message = "MongoDB connection failed: Kind: An invalid argument was provided: tlsAllowInvalidHostnames is an invalid option"; + + assert.match(mongodbAuthFailureHint(message), /tlsAllowInvalidCertificates=true/); +}); + +test("adds a MongoDB DocumentDB SSH tunnel hostname hint", () => { + const message = + 'MongoDB connection failed: Kind: Server selection timeout: invalid peer certificate: certificate not valid for name "127.0.0.1"; certificate is only valid for DnsName("docdb-2023-06-13-09-23-15.ctofbpmjm2lc.ap-northeast-1.docdb.amazonaws.com")'; + + assert.match(mongodbAuthFailureHint(message), /tlsAllowInvalidCertificates=true/); +});