mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 08:13:34 +00:00
fix(cli): 修复连接展示的匹配与脱敏
- 优先按唯一连接 ID 命中 conn show,避免被同名连接歧义阻断 - headless context 不再输出绝对 dbx.db 路径 - 脱敏 SQLite 和 DuckDB redactedUrl 中的本地文件路径
This commit is contained in:
@@ -156,9 +156,21 @@ fn redacted_config(config: &dbx_core::models::connection::ConnectionConfig) -> s
|
||||
}
|
||||
|
||||
fn redacted_url(config: &dbx_core::models::connection::ConnectionConfig) -> String {
|
||||
if matches!(
|
||||
config.db_type,
|
||||
dbx_core::models::connection::DatabaseType::Sqlite | dbx_core::models::connection::DatabaseType::DuckDb
|
||||
) {
|
||||
return redact_embedded_path_url(&config.redacted_connection_url());
|
||||
}
|
||||
|
||||
redact_uri_credentials(&config.redacted_connection_url())
|
||||
}
|
||||
|
||||
fn redact_embedded_path_url(value: &str) -> String {
|
||||
let suffix_start = value.find(['?', '#']).unwrap_or(value.len());
|
||||
format!("<redacted-path>{}", &value[suffix_start..])
|
||||
}
|
||||
|
||||
fn redact_uri_credentials(value: &str) -> String {
|
||||
let Some(scheme_index) = value.find("://") else {
|
||||
return value.to_string();
|
||||
@@ -188,7 +200,21 @@ async fn find_connection(
|
||||
name: &str,
|
||||
) -> Result<dbx_core::models::connection::ConnectionConfig, CliEnvelope<serde_json::Value>> {
|
||||
let configs = load_headless_connections().await?;
|
||||
let matches: Vec<_> = configs.into_iter().filter(|config| config.name == name || config.id == name).collect();
|
||||
let id_matches: Vec<_> = configs.iter().filter(|config| config.id == name).collect();
|
||||
match id_matches.len() {
|
||||
1 => return Ok(id_matches[0].clone()),
|
||||
0 => {}
|
||||
_ => {
|
||||
return Err(fail(
|
||||
CliSource::Headless,
|
||||
CliErrorCode::AmbiguousConnection,
|
||||
"Connection id is ambiguous",
|
||||
true,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let matches: Vec<_> = configs.into_iter().filter(|config| config.name == name).collect();
|
||||
|
||||
match matches.len() {
|
||||
1 => Ok(matches.into_iter().next().unwrap()),
|
||||
@@ -214,7 +240,7 @@ async fn context(args: &[String]) -> CliEnvelope<serde_json::Value> {
|
||||
serde_json::json!({
|
||||
"runtime": "headless",
|
||||
"activeConnection": configs.first().map(redacted_config),
|
||||
"configSource": crate::runtime_client::app_data_dir().join("dbx.db").display().to_string(),
|
||||
"configSource": "headless",
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -448,6 +474,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn embedded_fixture(id: &str, name: &str, db_type: DatabaseType, path: &std::path::Path) -> ConnectionConfig {
|
||||
let mut config = mysql_fixture(id, name, None, "");
|
||||
config.db_type = db_type;
|
||||
config.host = path.display().to_string();
|
||||
config.port = 0;
|
||||
config.username = String::new();
|
||||
config.password = String::new();
|
||||
config.database = None;
|
||||
config.connection_string = None;
|
||||
config
|
||||
}
|
||||
|
||||
async fn seed_connections(dir: &std::path::Path, configs: &[ConnectionConfig]) {
|
||||
std::fs::create_dir_all(dir).unwrap();
|
||||
let storage = dbx_core::storage::Storage::open(&dir.join("dbx.db")).await.unwrap();
|
||||
@@ -602,8 +640,10 @@ mod tests {
|
||||
assert_eq!(data["activeConnection"]["id"], "prod-id");
|
||||
assert_eq!(data["activeConnection"]["name"], "prod-main");
|
||||
assert_eq!(data["activeConnection"]["risk"]["isProduction"], true);
|
||||
assert_eq!(data["configSource"], "headless");
|
||||
let json = serde_json::to_string(&data).unwrap();
|
||||
assert!(json.contains("configSource"));
|
||||
assert!(!json.contains(&dir.path().join("dbx.db").display().to_string()));
|
||||
assert!(!json.contains("super-secret"));
|
||||
assert!(!json.contains("ssh-secret"));
|
||||
assert!(!json.contains("key-secret"));
|
||||
@@ -642,6 +682,35 @@ mod tests {
|
||||
std::env::remove_var("DBX_APP_DATA_DIR");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn conn_list_redacts_sqlite_and_duckdb_file_paths() {
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::env::set_var("DBX_APP_DATA_DIR", dir.path());
|
||||
let sqlite_path = dir.path().join("private").join("app.sqlite");
|
||||
let duckdb_path = dir.path().join("private").join("warehouse.duckdb");
|
||||
seed_connections(
|
||||
dir.path(),
|
||||
&[
|
||||
embedded_fixture("sqlite-id", "local-sqlite", DatabaseType::Sqlite, &sqlite_path),
|
||||
embedded_fixture("duckdb-id", "local-duckdb", DatabaseType::DuckDb, &duckdb_path),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let data = success_data(dispatch(vec!["conn".into(), "list".into(), "--format".into(), "json".into()]).await);
|
||||
let connections = data["connections"].as_array().expect("connections should be an array");
|
||||
|
||||
assert_eq!(connections[0]["redactedUrl"], "<redacted-path>?mode=rwc");
|
||||
assert_eq!(connections[1]["redactedUrl"], "<redacted-path>?mode=rwc");
|
||||
let json = serde_json::to_string(&data).unwrap();
|
||||
assert!(!json.contains("app.sqlite"));
|
||||
assert!(!json.contains("warehouse.duckdb"));
|
||||
assert!(!json.contains(dir.path().to_str().unwrap()));
|
||||
|
||||
std::env::remove_var("DBX_APP_DATA_DIR");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn conn_show_supports_id_lookup_and_returns_not_found_or_ambiguous_errors() {
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
@@ -684,6 +753,35 @@ mod tests {
|
||||
std::env::remove_var("DBX_APP_DATA_DIR");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn conn_show_unique_id_match_takes_precedence_over_ambiguous_name() {
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::env::set_var("DBX_APP_DATA_DIR", dir.path());
|
||||
seed_connections(
|
||||
dir.path(),
|
||||
&[
|
||||
mysql_fixture("shared", "id-target", Some("#22c55e"), "target-secret"),
|
||||
mysql_fixture("dup-1", "shared", None, "first-secret"),
|
||||
mysql_fixture("dup-2", "shared", None, "second-secret"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let shown = success_data(
|
||||
dispatch(vec!["conn".into(), "show".into(), "shared".into(), "--format".into(), "json".into()]).await,
|
||||
);
|
||||
|
||||
assert_eq!(shown["id"], "shared");
|
||||
assert_eq!(shown["name"], "id-target");
|
||||
let json = serde_json::to_string(&shown).unwrap();
|
||||
assert!(!json.contains("target-secret"));
|
||||
assert!(!json.contains("first-secret"));
|
||||
assert!(!json.contains("second-secret"));
|
||||
|
||||
std::env::remove_var("DBX_APP_DATA_DIR");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_command_returns_internal_error_envelope() {
|
||||
assert_failure_code(
|
||||
|
||||
Reference in New Issue
Block a user