feat(auth): keep unsaved passwords in session memory

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-08-14 18:37:53 +08:00
1 parent bcec60ebef
commit 1ff24aaeb4
23 files changed
+1265 -65

No files matched your search

+105 -10
View File
@@ -7,10 +7,10 @@ pub use dbx_core::agent_connection::{
oracle_error_with_driver_hint, should_retry_mongo_with_legacy_driver,
};
pub use dbx_core::connection::{
agent_connect_timeout, connect_bare_metadata_pool, connect_mysql_metadata_pool, connection_url_for_endpoint,
gaussdb_m_jdbc_config_for_endpoint, gaussdb_uses_m_jdbc_driver, metadata_connection_config,
prestosql_jdbc_config_for_endpoint, probe_connection_endpoint, redacted_connection_url_for_endpoint, AppState,
MysqlMode, PoolKind,
agent_connect_timeout, connect_bare_metadata_pool, connect_mysql_metadata_pool, connection_configs_pool_equivalent,
connection_url_for_endpoint, gaussdb_m_jdbc_config_for_endpoint, gaussdb_uses_m_jdbc_driver,
metadata_connection_config, prestosql_jdbc_config_for_endpoint, probe_connection_endpoint,
redacted_connection_url_for_endpoint, AppState, MysqlMode, PoolKind,
};
use dbx_core::database_capabilities;
use dbx_core::db;
@@ -170,7 +170,7 @@ mod tests {
use super::{
connect_sqlite_from_config, gaussdb_m_jdbc_command_config, mark_mongo_legacy_driver,
mongo_legacy_connect_params, persist_mongo_legacy_driver_profile, save_connection_configs,
MONGO_LEGACY_DRIVER_LABEL, MONGO_LEGACY_DRIVER_PROFILE,
sync_connection_configs, MONGO_LEGACY_DRIVER_LABEL, MONGO_LEGACY_DRIVER_PROFILE,
};
use dbx_core::connection::{AppState, PoolKind};
use dbx_core::models::connection::{AttachedDatabaseConfig, ConnectionConfig, DatabaseType};
@@ -593,6 +593,39 @@ mod tests {
let _ = std::fs::remove_dir_all(dir);
}
#[tokio::test]
async fn sync_connection_configs_ignores_password_only_changes() {
let dir = std::env::temp_dir().join(format!("dbx-tauri-conn-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).unwrap();
let storage = Storage::open(&dir.join("storage.db")).await.unwrap();
let state = AppState::new_with_plugin_dir(storage, dir.join("plugins"));
let mut initial = mongodb_config();
initial.id = "conn-a".to_string();
initial.save_password = false;
initial.password = "session-secret".to_string();
let _ = state.session_credentials.set("", "conn-a", "session-secret");
state.configs.write().await.insert(initial.id.clone(), initial.clone());
// 持久化同步的空密码 config 覆盖运行态:save_password=false 连接仅密码
// 差异不应销毁池(会话密码由内存仓库提供,与运行态 config 无关)。
let mut updated = initial.clone();
updated.password.clear();
let sync = sync_connection_configs(&state, std::slice::from_ref(&updated)).await;
assert!(sync.connection_pool_ids_to_drop.is_empty());
assert_eq!(state.configs.read().await.get("conn-a").map(|c| c.password.as_str()), Some(""));
assert!(state.session_credentials.has("", "conn-a"));
// 真实连接参数(host)变化应销毁池,并清除旧会话凭据以便重新输入。
let mut host_changed = updated.clone();
host_changed.host = "other-host".to_string();
let sync2 = sync_connection_configs(&state, std::slice::from_ref(&host_changed)).await;
assert_eq!(sync2.connection_pool_ids_to_drop.as_slice(), &["conn-a".to_string()]);
assert!(!state.session_credentials.has("", "conn-a"));
let _ = std::fs::remove_dir_all(dir);
}
}
#[tauri::command]
@@ -636,6 +669,8 @@ async fn sync_connection_configs(state: &AppState, configs: &[ConnectionConfig])
true
} else {
connection_pool_ids_to_drop.insert(id.clone());
// 连接已被删除:同步清理本次运行期会话凭据。
state.session_credentials.clear_connection(id);
if existing.db_type == DatabaseType::Nacos {
nacos_adapter_ids_to_drop.insert(id.clone());
}
@@ -659,7 +694,12 @@ async fn sync_connection_configs(state: &AppState, configs: &[ConnectionConfig])
if previous.db_type == DatabaseType::MessageQueue {
mq_adapter_ids_to_drop.insert(config.id.clone());
}
if &previous != config {
// 仅在真实连接参数变化时销毁池;save_password=false 连接因持久化
// 空密码与运行态密码产生的差异被忽略(见 connection_configs_pool_equivalent)。
if !connection_configs_pool_equivalent(&previous, config) {
// 连接端点/认证参数已变:旧会话凭据不再适配,清除以便下次重新输入,
// 避免复用旧密码去连新端点而直接认证失败。
state.session_credentials.clear_connection(&config.id);
connection_pool_ids_to_drop.insert(config.id.clone());
}
}
@@ -1198,6 +1238,14 @@ async fn test_connection_with_info_inner(
result.map(|message| ConnectionTestResult::success(message).with_database_info(database_info))
}
/// 连接成功且 `save_password=false` 时,把本次输入的密码记入内存会话凭据仓库,
/// 供本次运行内 AI / 元数据 / 池重建复用(进程退出即丢,绝不落盘)。
fn record_session_credential(state: &AppState, config: &ConnectionConfig, connection_id: &str) {
if !config.save_password && !config.password.is_empty() {
let _ = state.session_credentials.set("", connection_id, &config.password);
}
}
#[tauri::command]
pub async fn connect_db(
state: State<'_, Arc<AppState>>,
@@ -1213,7 +1261,10 @@ pub async fn connect_db(
)?;
}
let id = config.id.clone();
let db_config = metadata_connection_config(&config);
let mut db_config = metadata_connection_config(&config);
// save_password=false 连接:前端在会话凭据存在时跳过弹窗并以空密码请求,
// 此处从运行期会话凭据仓库补主密码,使重连/AI 新建池不再 ORA-01005。
state.apply_session_credential(&config, &mut db_config, &id);
let attempt = state.begin_connection_attempt_with_client_attempt(&id, client_attempt).await;
let mut connected_config = config.clone();
let mut connected_db_config = db_config.clone();
@@ -1313,7 +1364,12 @@ pub async fn connect_db(
state.reset_connection_transport_for_config(&id, &db_config).await;
return Err(err);
}
state.configs.write().await.insert(id.clone(), config);
record_session_credential(state.inner(), &config, &id);
let mut stored = config;
if !stored.save_password {
stored.password.clear();
}
state.configs.write().await.insert(id.clone(), stored);
return Ok(id);
}
Err(e) => e,
@@ -1552,7 +1608,13 @@ pub async fn connect_db(
state.reset_connection_transport_for_config(&id, &connected_db_config).await;
return Err(err);
}
state.configs.write().await.insert(id.clone(), connected_config);
record_session_credential(state.inner(), &connected_config, &id);
// 存入全局运行态 configs 的配置脱敏(no-save 密码恒为空),明文只存在于会话凭据仓库。
let mut stored = connected_config;
if !stored.save_password {
stored.password.clear();
}
state.configs.write().await.insert(id.clone(), stored);
Ok(id)
}
@@ -1576,9 +1638,16 @@ pub async fn connection_final_proxy_port(
let connection_id = runtime_config.id.clone();
let db_config = metadata_connection_config(&runtime_config);
state.configs.write().await.insert(connection_id.clone(), runtime_config);
// This pre-connect path caches the configuration for tunnel resolution. Keep
// no-save passwords out of that shared runtime cache just like connect_db.
let mut stored_config = runtime_config.clone();
if !stored_config.save_password {
stored_config.password.clear();
}
state.configs.write().await.insert(connection_id.clone(), stored_config);
let (_, port) = state.connection_host_port(&connection_id, &db_config).await?;
record_session_credential(state.inner(), &runtime_config, &connection_id);
Ok(port)
}
@@ -1619,6 +1688,32 @@ pub async fn close_database_connection(
state.close_database_pool(&connection_id, database).await
}
/// 查询连接在本次运行期是否已输入并暂存密码(`save_password=false`)。
/// 供前端决定是否需要弹密码框;仅返回布尔状态,不泄露密码本身。
#[tauri::command]
pub async fn session_credential_status(state: State<'_, Arc<AppState>>, connection_id: String) -> Result<bool, String> {
Ok(state.session_credentials.has("", &connection_id))
}
/// "断开并忘记本次密码":清除连接本次运行期的临时密码,下次连接需重新输入。
/// 只清内存会话凭据,不影响持久化配置与已保存密码。
#[tauri::command]
pub async fn forget_session_credential(state: State<'_, Arc<AppState>>, connection_id: String) -> Result<(), String> {
if !state.session_credentials.has("", &connection_id) {
return Err(format!("Connection has no transient session credential to forget: {connection_id}"));
}
state.session_credentials.remove("", &connection_id);
Ok(())
}
/// 清空全部运行期会话凭据(桌面端退出前调用;Web 端登出时走 `auth.rs logout`)。
/// 密码只存在于本次进程内存,进程退出本就会丢失;显式清除用于退出前兜底。
#[tauri::command]
pub async fn clear_all_session_credentials(state: State<'_, Arc<AppState>>) -> Result<(), String> {
state.session_credentials.clear();
Ok(())
}
#[tauri::command]
pub async fn refresh_connections(state: State<'_, Arc<AppState>>) -> Result<(), String> {
state.refresh_connections().await;