fix(mongodb): expose authentication database

This commit is contained in:
t8y2 2026-05-19 11:51:39 +08:00
parent bae487867b
commit f89b3a2bee
7 changed files with 143 additions and 9 deletions

View File

@ -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) {
<Input v-model="form.password" type="password" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.database") }}</Label>
<Label class="text-right">{{ t("connection.defaultDatabase") }}</Label>
<Input
v-model="form.database"
class="col-span-3"
:placeholder="t('connection.databasePlaceholder')"
/>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.authDatabase") }}</Label>
<Input
v-model="mongoAuthDatabase"
class="col-span-3"
:placeholder="t('connection.authDatabasePlaceholder')"
/>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.authMechanism") }}</Label>
<Select v-model="mongoAuthMechanism">
<SelectTrigger class="col-span-3">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">{{ t("connection.authMechanismDefault") }}</SelectItem>
<SelectItem value="SCRAM-SHA-1">SCRAM-SHA-1</SelectItem>
<SelectItem value="SCRAM-SHA-256">SCRAM-SHA-256</SelectItem>
</SelectContent>
</Select>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.urlParams") }}</Label>
<Input

View File

@ -103,6 +103,11 @@ export default {
database: "Database",
databasePlaceholder: "Optional",
databasePlaceholderWithDefault: "Optional, defaults to {database}",
defaultDatabase: "Default DB",
authDatabase: "Auth DB",
authDatabasePlaceholder: "Optional, often admin",
authMechanism: "Auth Mechanism",
authMechanismDefault: "Default",
serviceName: "Service/SID",
driverName: "Driver Name",
driverNamePlaceholder: "Vendor or environment name",
@ -180,7 +185,7 @@ export default {
dmCompatHint: "Requires DM8 ODBC driver installed on your system.",
dmDownload: "Download from Dameng",
mongoLegacyHint:
"MongoDB below 4.2 requires the MongoDB (Legacy) driver from Driver Manager; connections will switch automatically.",
"MongoDB below 4.2 requires the MongoDB (Legacy) driver. If authentication fails and the user was created in admin, set Auth DB to admin.",
compatible: "Compatible",
mainstream: "Popular",
color: "Color",

View File

@ -102,6 +102,11 @@ export default {
database: "数据库",
databasePlaceholder: "可选",
databasePlaceholderWithDefault: "可选,默认 {database}",
defaultDatabase: "默认库",
authDatabase: "认证库",
authDatabasePlaceholder: "可选,通常为 admin",
authMechanism: "认证机制",
authMechanismDefault: "默认",
serviceName: "服务名/SID",
driverName: "驱动名称",
driverNamePlaceholder: "厂商或环境名称",
@ -177,7 +182,8 @@ export default {
jdbcPluginHint: "先安装 DBX JDBC 插件,再导入数据库厂商提供的 JDBC 驱动 JAR。",
dmCompatHint: "需要在系统上安装达梦 DM8 ODBC 驱动程序。",
dmDownload: "前往达梦官网下载",
mongoLegacyHint: "MongoDB 4.2 以下版本需在驱动管理中安装 MongoDB (Legacy) 驱动,连接时将自动切换。",
mongoLegacyHint:
"MongoDB 4.2 以下版本需安装 MongoDB (Legacy) 驱动。若账号创建在 admin 且认证失败,请将认证库设为 admin。",
compatible: "兼容",
mainstream: "主流",
color: "颜色",

View File

@ -0,0 +1,28 @@
export function mongoUrlParam(urlParams: string | undefined, key: string): string {
const params = parseMongoUrlParams(urlParams);
return params.get(key) || "";
}
export function setMongoUrlParam(urlParams: string | undefined, key: string, value: string): string {
const params = parseMongoUrlParams(urlParams);
const normalized = value.trim();
if (normalized) {
params.set(key, normalized);
} else {
params.delete(key);
}
return params.toString();
}
export function mongodbAuthFailureHint(message: string): string {
if (message.includes("Current authentication database:")) return message;
const source = message.match(/source='([^']+)'/)?.[1];
if (!source || !message.includes("Exception authenticating MongoCredential")) return message;
return `${message}\n\nCurrent authentication database: ${source}. If this user was created in admin, set Authentication database to admin or add authSource=admin to URL params.`;
}
function parseMongoUrlParams(urlParams: string | undefined): URLSearchParams {
return new URLSearchParams((urlParams || "").trim().replace(/^\?/, ""));
}

View File

@ -192,7 +192,10 @@ impl AppState {
log::info!("Native MongoDB driver failed ({native_err}), falling back to agent driver");
let connect_params = serde_json::json!({ "connection": agent_connect_params(&db_config, &host, port, db_config.effective_database().unwrap_or("")) });
let mut client = self.agent_manager.spawn(&DatabaseType::MongoDb, None).await?;
client.call::<serde_json::Value>("connect", connect_params).await?;
client
.call::<serde_json::Value>("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"));

View File

@ -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.",
);
});

View File

@ -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<AppState>>, config: Connection
let mut client = am.spawn(&config.db_type, config.driver_profile.as_deref()).await?;
client
.call::<serde_json::Value>("connect", mongo_legacy_connect_params(&config, &host, port))
.await?;
.await
.map_err(|err| mongo_legacy_error_with_auth_hint(&err))?;
client.call::<serde_json::Value>("disconnect", serde_json::json!({})).await.ok();
Ok("Connection successful (via legacy driver)".to_string())
} else {