diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index a827d9e9d..4f755a578 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -17,6 +17,7 @@ import * as api from "@/lib/api"; import { isTauriRuntime } from "@/lib/tauriRuntime"; import { applyParsedConnectionUrl, parseConnectionUrl } from "@/lib/connectionUrl"; import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connectionPresentation"; +import { mongodbAuthFailureHint, mongoUrlParam, setMongoUrlParam } from "@/lib/mongoConnectionOptions"; import { showAgentDriverInstallHint, type AgentDriverInstallState } from "@/lib/agentDriverInstallHint"; import { ArrowLeft, ChevronRight, Copy, ExternalLink, FolderOpen, Grid3X3, Link2, List, Search } from "lucide-vue-next"; @@ -533,6 +534,18 @@ const testResultMessage = computed(() => { if (!testResult.value) return ""; return testResult.value.ok ? t("connection.testSuccess") : testResult.value.message; }); +const mongoAuthDatabase = computed({ + get: () => mongoUrlParam(form.value.url_params, "authSource"), + set: (value: string) => { + form.value.url_params = setMongoUrlParam(form.value.url_params, "authSource", value); + }, +}); +const mongoAuthMechanism = computed({ + get: () => mongoUrlParam(form.value.url_params, "authMechanism") || "default", + set: (value: string) => { + form.value.url_params = setMongoUrlParam(form.value.url_params, "authMechanism", value === "default" ? "" : value); + }, +}); function goToConnectionStep(value = selectedType.value) { if (value !== selectedType.value) { @@ -565,7 +578,7 @@ async function testConnection() { testResult.value = { ok: true, message: msg }; } catch (e: any) { if (runId !== testRunId) return; - testResult.value = { ok: false, message: String(e) }; + testResult.value = { ok: false, message: mongodbAuthFailureHint(String(e)) }; } finally { if (runId === testRunId) { isTesting.value = false; @@ -703,13 +716,13 @@ async function save() { emit("connectSucceeded", config.name); }) .catch((e: any) => { - emit("connectFailed", String(e?.message || e)); + emit("connectFailed", mongodbAuthFailureHint(String(e?.message || e))); }); return; } open.value = false; } catch (e: any) { - testResult.value = { ok: false, message: String(e?.message || e) }; + testResult.value = { ok: false, message: mongodbAuthFailureHint(String(e?.message || e)) }; } finally { isSaving.value = false; } @@ -1196,13 +1209,34 @@ function openExternalUrl(url: string) {
- +
+
+ + +
+
+ + +
("connect", connect_params).await?; + client + .call::("connect", connect_params) + .await + .map_err(|err| mongo_legacy_error_with_auth_hint(&err))?; PoolKind::Agent(Arc::new(tokio::sync::Mutex::new(client))) } else { return Err(native_err); @@ -466,6 +469,23 @@ pub fn agent_connect_params(config: &ConnectionConfig, host: &str, port: u16, da }) } +pub fn mongo_legacy_error_with_auth_hint(err: &str) -> String { + let Some(source_start) = err.find("source='") else { + return err.to_string(); + }; + if !err.contains("Exception authenticating MongoCredential") || err.contains("Current authentication database:") { + return err.to_string(); + } + let source = &err[source_start + "source='".len()..]; + let Some(source_end) = source.find('\'') else { + return err.to_string(); + }; + let source = &source[..source_end]; + format!( + "{err}\n\nCurrent authentication database: {source}. If this user was created in admin, set Authentication database to admin or add authSource=admin to URL params." + ) +} + fn oracle_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> String { let database = database.trim(); if database.is_empty() { @@ -602,6 +622,16 @@ mod tests { assert_eq!(params["connection_string"], "mongodb://mongouser:secret@172.22.4.42:27017/RestCloud%5FV45PUB%5FGateway?authSource=admin&authMechanism=SCRAM-SHA-1"); } + #[test] + fn mongo_legacy_auth_error_adds_auth_source_hint() { + let err = "Agent RPC error: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='rwuser', source='gray_lite_twin_fat'}"; + + assert_eq!( + super::mongo_legacy_error_with_auth_hint(err), + "Agent RPC error: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='rwuser', source='gray_lite_twin_fat'}\n\nCurrent authentication database: gray_lite_twin_fat. If this user was created in admin, set Authentication database to admin or add authSource=admin to URL params." + ); + } + #[test] fn agent_connect_params_build_oracle_service_connection_string() { let mut config = mysql_config(Some("ORCLPDB1")); diff --git a/packages/app-tests/mongoConnectionOptions.test.ts b/packages/app-tests/mongoConnectionOptions.test.ts new file mode 100644 index 000000000..1fe9c1d4c --- /dev/null +++ b/packages/app-tests/mongoConnectionOptions.test.ts @@ -0,0 +1,29 @@ +import { strict as assert } from "node:assert"; +import test from "node:test"; +import { + mongoUrlParam, + setMongoUrlParam, + mongodbAuthFailureHint, +} from "../../apps/desktop/src/lib/mongoConnectionOptions.ts"; + +test("reads MongoDB authSource from URL params", () => { + assert.equal(mongoUrlParam("?replicaSet=rs0&authSource=admin", "authSource"), "admin"); +}); + +test("sets MongoDB authSource while preserving other URL params", () => { + assert.equal(setMongoUrlParam("replicaSet=rs0", "authSource", "admin"), "replicaSet=rs0&authSource=admin"); +}); + +test("removes empty MongoDB authSource from URL params", () => { + assert.equal(setMongoUrlParam("replicaSet=rs0&authSource=admin", "authSource", ""), "replicaSet=rs0"); +}); + +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'}"; + + assert.equal( + mongodbAuthFailureHint(message), + "Agent RPC error: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='rwuser', source='gray_lite_twin_fat'}\n\nCurrent authentication database: gray_lite_twin_fat. If this user was created in admin, set Authentication database to admin or add authSource=admin to URL params.", + ); +}); diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index 28303e7f7..802943242 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -3,7 +3,8 @@ use tauri::State; pub use dbx_core::connection::{ agent_connect_params, connection_url_for_endpoint, expand_tilde, metadata_connection_config, - probe_connection_endpoint, redacted_connection_url_for_endpoint, AppState, MysqlMode, PoolKind, + mongo_legacy_error_with_auth_hint, probe_connection_endpoint, redacted_connection_url_for_endpoint, AppState, + MysqlMode, PoolKind, }; use dbx_core::database_capabilities; use dbx_core::db; @@ -167,7 +168,8 @@ pub async fn test_connection(state: State<'_, Arc>, config: Connection let mut client = am.spawn(&config.db_type, config.driver_profile.as_deref()).await?; client .call::("connect", mongo_legacy_connect_params(&config, &host, port)) - .await?; + .await + .map_err(|err| mongo_legacy_error_with_auth_hint(&err))?; client.call::("disconnect", serde_json::json!({})).await.ok(); Ok("Connection successful (via legacy driver)".to_string()) } else {