feat(auth): keep unsaved passwords in session memory
This commit is contained in:
1 parent
bcec60ebef
commit
1ff24aaeb4
23 files changed
+1265
-65
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user