feat(cassandra): add TLS store support
This commit is contained in:
26 files changed
+1203
-36
No files matched your search
@@ -47,6 +47,19 @@ fn h2_uses_custom_driver(config: &ConnectionConfig) -> bool {
|
||||
&& config.driver_profile.as_deref().is_some_and(|profile| profile.eq_ignore_ascii_case("h2-custom"))
|
||||
}
|
||||
|
||||
fn cassandra_tls_field<'a>(config: &'a ConnectionConfig, field: &str) -> &'a str {
|
||||
if config.db_type != DatabaseType::Cassandra {
|
||||
return "";
|
||||
}
|
||||
config
|
||||
.external_config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("tls"))
|
||||
.and_then(|value| value.get(field))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
pub fn agent_connect_params(
|
||||
config: &ConnectionConfig,
|
||||
host: &str,
|
||||
@@ -120,6 +133,10 @@ pub fn agent_connect_params_with_role(
|
||||
"ca_cert_path": config.ca_cert_path,
|
||||
"client_cert_path": config.client_cert_path,
|
||||
"client_key_path": config.client_key_path,
|
||||
"truststore_path": cassandra_tls_field(config, "truststore_path"),
|
||||
"truststore_password": cassandra_tls_field(config, "truststore_password"),
|
||||
"keystore_path": cassandra_tls_field(config, "keystore_path"),
|
||||
"keystore_password": cassandra_tls_field(config, "keystore_password"),
|
||||
"connect_timeout_secs": config.effective_connect_timeout_secs(),
|
||||
"etcd_endpoints": etcd_endpoints,
|
||||
"zookeeper_connect_string": zookeeper_connect_string,
|
||||
@@ -777,6 +794,27 @@ mod tests {
|
||||
assert_eq!(params["sessionRole"], "metadata");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cassandra_agent_connect_params_include_tls_stores() {
|
||||
let mut cfg = config(DatabaseType::Cassandra, Some("app"));
|
||||
cfg.ssl = true;
|
||||
cfg.external_config = Some(serde_json::json!({
|
||||
"tls": {
|
||||
"truststore_path": "/certs/client.truststore",
|
||||
"truststore_password": "trust-secret",
|
||||
"keystore_path": "/certs/client.keystore",
|
||||
"keystore_password": "key-secret"
|
||||
}
|
||||
}));
|
||||
|
||||
let params = agent_connect_params(&cfg, "cassandra.example.com", 9042, "app").unwrap();
|
||||
|
||||
assert_eq!(params["truststore_path"], "/certs/client.truststore");
|
||||
assert_eq!(params["truststore_password"], "trust-secret");
|
||||
assert_eq!(params["keystore_path"], "/certs/client.keystore");
|
||||
assert_eq!(params["keystore_password"], "key-secret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_form_connections_use_orcl_when_database_is_omitted() {
|
||||
for (mode, expected_url) in [
|
||||
|
||||
@@ -13,8 +13,9 @@ use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use crate::ai::AiConfigItem;
|
||||
use crate::connection_secrets::{
|
||||
MQ_AUTH_API_KEY_VALUE_KEY, MQ_AUTH_CLIENT_SECRET_KEY, MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY,
|
||||
MQ_TOKEN_SIGNING_KEY, NACOS_AUTH_PASSWORD_KEY, NACOS_RNACOS_CONSOLE_PASSWORD_KEY,
|
||||
CASSANDRA_KEYSTORE_PASSWORD_KEY, CASSANDRA_TRUSTSTORE_PASSWORD_KEY, MQ_AUTH_API_KEY_VALUE_KEY,
|
||||
MQ_AUTH_CLIENT_SECRET_KEY, MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY,
|
||||
NACOS_AUTH_PASSWORD_KEY, NACOS_RNACOS_CONSOLE_PASSWORD_KEY,
|
||||
};
|
||||
use crate::models::connection::{ConnectionConfig, DatabaseType, TransportLayerConfig};
|
||||
use crate::saved_sql::SavedSqlLibrary;
|
||||
@@ -42,6 +43,8 @@ const SECRET_KEYS: &[&str] = &[
|
||||
MQ_TOKEN_SIGNING_KEY,
|
||||
NACOS_AUTH_PASSWORD_KEY,
|
||||
NACOS_RNACOS_CONSOLE_PASSWORD_KEY,
|
||||
CASSANDRA_TRUSTSTORE_PASSWORD_KEY,
|
||||
CASSANDRA_KEYSTORE_PASSWORD_KEY,
|
||||
];
|
||||
const SSH_TUNNEL_SECRET_PREFIX: &str = "ssh_tunnels.";
|
||||
const TRANSPORT_LAYER_SECRET_PREFIX: &str = "transport_layers.";
|
||||
@@ -969,6 +972,7 @@ fn scrub_connection_secrets(config: &mut ConnectionConfig) {
|
||||
scrub_mqtt_auth_secrets(config);
|
||||
scrub_mq_external_config_secrets(config);
|
||||
scrub_nacos_auth_secrets(config);
|
||||
scrub_cassandra_tls_secrets(config);
|
||||
}
|
||||
|
||||
fn scrub_mqtt_auth_secrets(config: &mut ConnectionConfig) {
|
||||
@@ -1044,6 +1048,7 @@ async fn build_sensitive_payload(
|
||||
push_secret(&mut connection_secrets, &config.id, "connection_string", connection_string);
|
||||
}
|
||||
push_mq_external_config_secrets(&mut connection_secrets, config);
|
||||
push_cassandra_tls_secrets(&mut connection_secrets, config);
|
||||
if config.save_password {
|
||||
push_nacos_external_config_secrets(&mut connection_secrets, config);
|
||||
}
|
||||
@@ -1098,6 +1103,50 @@ fn scrub_mq_external_config_secrets(config: &mut ConnectionConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_cassandra_tls_secrets(secrets: &mut Vec<ConnectionSecretSnapshot>, config: &ConnectionConfig) {
|
||||
if config.db_type != DatabaseType::Cassandra {
|
||||
return;
|
||||
}
|
||||
let Some(tls) = config
|
||||
.external_config
|
||||
.as_ref()
|
||||
.and_then(|external_config| external_config.get("tls"))
|
||||
.and_then(serde_json::Value::as_object)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
push_json_secret(
|
||||
secrets,
|
||||
&config.id,
|
||||
CASSANDRA_TRUSTSTORE_PASSWORD_KEY,
|
||||
tls,
|
||||
"truststore_password",
|
||||
);
|
||||
push_json_secret(
|
||||
secrets,
|
||||
&config.id,
|
||||
CASSANDRA_KEYSTORE_PASSWORD_KEY,
|
||||
tls,
|
||||
"keystore_password",
|
||||
);
|
||||
}
|
||||
|
||||
fn scrub_cassandra_tls_secrets(config: &mut ConnectionConfig) {
|
||||
if config.db_type != DatabaseType::Cassandra {
|
||||
return;
|
||||
}
|
||||
let Some(tls) = config
|
||||
.external_config
|
||||
.as_mut()
|
||||
.and_then(|external_config| external_config.get_mut("tls"))
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
scrub_json_secret(tls, "truststore_password");
|
||||
scrub_json_secret(tls, "keystore_password");
|
||||
}
|
||||
|
||||
fn push_nacos_external_config_secrets(secrets: &mut Vec<ConnectionSecretSnapshot>, config: &ConnectionConfig) {
|
||||
if config.db_type != DatabaseType::Nacos {
|
||||
return;
|
||||
@@ -1615,7 +1664,10 @@ mod tests {
|
||||
SnippetProvider, SnippetSyncClient, SnippetSyncConfig, WebDavClient, WebDavConfig, DEFAULT_SNIPPET_FILE_NAME,
|
||||
};
|
||||
use crate::ai::{AiApiStyle, AiAuthMethod, AiConfig, AiConfigItem};
|
||||
use crate::connection_secrets::{NACOS_AUTH_PASSWORD_KEY, NACOS_RNACOS_CONSOLE_PASSWORD_KEY};
|
||||
use crate::connection_secrets::{
|
||||
CASSANDRA_KEYSTORE_PASSWORD_KEY, CASSANDRA_TRUSTSTORE_PASSWORD_KEY, NACOS_AUTH_PASSWORD_KEY,
|
||||
NACOS_RNACOS_CONSOLE_PASSWORD_KEY,
|
||||
};
|
||||
use crate::models::connection::{
|
||||
default_redis_key_separator, ConnectionConfig, DatabaseType, SshTunnelConfig, TransportLayerConfig,
|
||||
};
|
||||
@@ -1818,6 +1870,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn cassandra_connection(id: &str) -> ConnectionConfig {
|
||||
let mut config = postgres_connection(id, "");
|
||||
config.name = "Cassandra".to_string();
|
||||
config.db_type = DatabaseType::Cassandra;
|
||||
config.port = 9042;
|
||||
config.external_config = Some(serde_json::json!({
|
||||
"tls": {
|
||||
"truststore_path": "/certs/client.truststore",
|
||||
"truststore_password": "trust-secret",
|
||||
"keystore_path": "/certs/client.keystore",
|
||||
"keystore_password": "key-secret"
|
||||
}
|
||||
}));
|
||||
config
|
||||
}
|
||||
|
||||
fn nacos_connection(id: &str, password: &str) -> ConnectionConfig {
|
||||
ConnectionConfig {
|
||||
docs_notes_path: None,
|
||||
@@ -2161,6 +2229,33 @@ mod tests {
|
||||
assert!(super::SECRET_KEYS.contains(&"init_script"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cassandra_tls_store_passwords_move_to_sensitive_sync_payload() {
|
||||
let config = cassandra_connection("cassandra");
|
||||
let mut public_config = config.clone();
|
||||
scrub_connection_secrets(&mut public_config);
|
||||
let tls = public_config
|
||||
.external_config
|
||||
.as_ref()
|
||||
.and_then(|external_config| external_config.get("tls"))
|
||||
.unwrap();
|
||||
assert_eq!(tls["truststore_password"], "");
|
||||
assert_eq!(tls["keystore_password"], "");
|
||||
|
||||
let storage = Storage::open(&temp_db_path("cassandra-sensitive-payload")).await.unwrap();
|
||||
let payload = build_sensitive_payload(&storage, &[config], &[]).await.unwrap();
|
||||
assert!(payload.connection_secrets.iter().any(|secret| {
|
||||
secret.connection_id == "cassandra"
|
||||
&& secret.key == CASSANDRA_TRUSTSTORE_PASSWORD_KEY
|
||||
&& secret.secret == "trust-secret"
|
||||
}));
|
||||
assert!(payload.connection_secrets.iter().any(|secret| {
|
||||
secret.connection_id == "cassandra"
|
||||
&& secret.key == CASSANDRA_KEYSTORE_PASSWORD_KEY
|
||||
&& secret.secret == "key-secret"
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_sensitive_payload_round_trips() {
|
||||
let payload = SensitiveSyncPayload {
|
||||
|
||||
@@ -24,6 +24,9 @@ pub const NACOS_AUTH_PASSWORD_KEY: &str = "nacos.auth.password";
|
||||
pub const NACOS_RNACOS_CONSOLE_PASSWORD_KEY: &str = "nacos.auth.rnacos_console_password";
|
||||
pub const MQTT_AUTH_SECRET_PREFIX: &str = "mqtt.auth.";
|
||||
pub const MQTT_AUTH_PASSWORD_KEY: &str = "mqtt.auth.password";
|
||||
pub const CASSANDRA_TLS_SECRET_PREFIX: &str = "cassandra.tls.";
|
||||
pub const CASSANDRA_TRUSTSTORE_PASSWORD_KEY: &str = "cassandra.tls.truststore_password";
|
||||
pub const CASSANDRA_KEYSTORE_PASSWORD_KEY: &str = "cassandra.tls.keystore_password";
|
||||
|
||||
pub trait ConnectionSecretStore {
|
||||
fn set_secret(&self, connection_id: &str, key: &str, secret: &str) -> Result<(), String>;
|
||||
@@ -112,6 +115,7 @@ pub fn save_connections_to_file(
|
||||
persist_mq_auth_secrets(store, config)?;
|
||||
persist_mq_token_signing_secret(store, config)?;
|
||||
persist_mqtt_auth_secrets(store, config)?;
|
||||
persist_cassandra_tls_secrets(store, config)?;
|
||||
|
||||
// New configs persist transport-layer secrets only. Remove legacy transport secret slots after the
|
||||
// migrated layer values have been written so old configs do not keep two sources of truth.
|
||||
@@ -181,6 +185,7 @@ pub fn load_connections_from_file(
|
||||
hydrate_mq_auth_secrets(store, config, &mut needs_rewrite)?;
|
||||
hydrate_mq_token_signing_secret(store, config, &mut needs_rewrite)?;
|
||||
hydrate_mqtt_auth_secrets(store, config, &mut needs_rewrite)?;
|
||||
hydrate_cassandra_tls_secrets(store, config, &mut needs_rewrite)?;
|
||||
}
|
||||
|
||||
if needs_rewrite {
|
||||
@@ -360,6 +365,7 @@ fn delete_removed_connection_secrets(
|
||||
delete_secret_prefix(store, &config.id, MQ_AUTH_SECRET_PREFIX)?;
|
||||
delete_secret_prefix(store, &config.id, MQ_TOKEN_SIGNING_SECRET_PREFIX)?;
|
||||
delete_secret_prefix(store, &config.id, MQTT_AUTH_SECRET_PREFIX)?;
|
||||
delete_secret_prefix(store, &config.id, CASSANDRA_TLS_SECRET_PREFIX)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -554,6 +560,87 @@ fn scrub_mq_token_signing_secret(config: &mut ConnectionConfig) {
|
||||
scrub_json_secret(signing, "key");
|
||||
}
|
||||
|
||||
fn persist_cassandra_tls_secrets(
|
||||
store: &dyn ConnectionSecretStore,
|
||||
config: &ConnectionConfig,
|
||||
) -> Result<(), String> {
|
||||
if config.db_type != DatabaseType::Cassandra {
|
||||
return delete_secret_prefix(store, &config.id, CASSANDRA_TLS_SECRET_PREFIX);
|
||||
}
|
||||
let Some(tls) = cassandra_tls_object(config.external_config.as_ref()) else {
|
||||
return delete_secret_prefix(store, &config.id, CASSANDRA_TLS_SECRET_PREFIX);
|
||||
};
|
||||
persist_secret(
|
||||
store,
|
||||
&config.id,
|
||||
CASSANDRA_TRUSTSTORE_PASSWORD_KEY,
|
||||
tls.get("truststore_password")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or(""),
|
||||
)?;
|
||||
persist_secret(
|
||||
store,
|
||||
&config.id,
|
||||
CASSANDRA_KEYSTORE_PASSWORD_KEY,
|
||||
tls.get("keystore_password")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or(""),
|
||||
)
|
||||
}
|
||||
|
||||
fn hydrate_cassandra_tls_secrets(
|
||||
store: &dyn ConnectionSecretStore,
|
||||
config: &mut ConnectionConfig,
|
||||
needs_rewrite: &mut bool,
|
||||
) -> Result<(), String> {
|
||||
if config.db_type != DatabaseType::Cassandra {
|
||||
return Ok(());
|
||||
}
|
||||
let connection_id = config.id.clone();
|
||||
let Some(tls) = cassandra_tls_object_mut(config.external_config.as_mut()) else {
|
||||
return Ok(());
|
||||
};
|
||||
hydrate_json_secret(
|
||||
store,
|
||||
&connection_id,
|
||||
CASSANDRA_TRUSTSTORE_PASSWORD_KEY,
|
||||
tls,
|
||||
"truststore_password",
|
||||
needs_rewrite,
|
||||
)?;
|
||||
hydrate_json_secret(
|
||||
store,
|
||||
&connection_id,
|
||||
CASSANDRA_KEYSTORE_PASSWORD_KEY,
|
||||
tls,
|
||||
"keystore_password",
|
||||
needs_rewrite,
|
||||
)
|
||||
}
|
||||
|
||||
fn scrub_cassandra_tls_secrets(config: &mut ConnectionConfig) {
|
||||
if config.db_type != DatabaseType::Cassandra {
|
||||
return;
|
||||
}
|
||||
let Some(tls) = cassandra_tls_object_mut(config.external_config.as_mut()) else {
|
||||
return;
|
||||
};
|
||||
scrub_json_secret(tls, "truststore_password");
|
||||
scrub_json_secret(tls, "keystore_password");
|
||||
}
|
||||
|
||||
fn cassandra_tls_object(
|
||||
value: Option<&serde_json::Value>,
|
||||
) -> Option<&serde_json::Map<String, serde_json::Value>> {
|
||||
value?.get("tls")?.as_object()
|
||||
}
|
||||
|
||||
fn cassandra_tls_object_mut(
|
||||
value: Option<&mut serde_json::Value>,
|
||||
) -> Option<&mut serde_json::Map<String, serde_json::Value>> {
|
||||
value?.get_mut("tls")?.as_object_mut()
|
||||
}
|
||||
|
||||
// ── MQTT 密钥持久化 ──────────────────────────────────────────────
|
||||
|
||||
fn persist_mqtt_auth_secrets(store: &dyn ConnectionSecretStore, config: &ConnectionConfig) -> Result<(), String> {
|
||||
@@ -745,6 +832,7 @@ fn sanitize_connections(configs: &[ConnectionConfig]) -> Vec<ConnectionConfig> {
|
||||
scrub_mq_auth_secrets(&mut config);
|
||||
scrub_mq_token_signing_secret(&mut config);
|
||||
scrub_mqtt_auth_secrets(&mut config);
|
||||
scrub_cassandra_tls_secrets(&mut config);
|
||||
config
|
||||
})
|
||||
.collect()
|
||||
@@ -757,9 +845,11 @@ pub fn secret_account(connection_id: &str, key: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
load_connections_from_file, save_connections_to_file, ConnectionSecretStore, CONNECTION_STRING_KEY,
|
||||
INIT_SCRIPT_KEY, MAIN_PASSWORD_KEY, MQTT_AUTH_PASSWORD_KEY, MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY,
|
||||
MQ_TOKEN_SIGNING_KEY, REDIS_SENTINEL_PASSWORD_KEY, SSH_PASSWORD_KEY,
|
||||
load_connections_from_file, save_connections_to_file, ConnectionSecretStore,
|
||||
CASSANDRA_KEYSTORE_PASSWORD_KEY, CASSANDRA_TRUSTSTORE_PASSWORD_KEY,
|
||||
CONNECTION_STRING_KEY, INIT_SCRIPT_KEY, MAIN_PASSWORD_KEY, MQTT_AUTH_PASSWORD_KEY,
|
||||
MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY, REDIS_SENTINEL_PASSWORD_KEY,
|
||||
SSH_PASSWORD_KEY,
|
||||
};
|
||||
use crate::models::connection::{
|
||||
ConnectionConfig, DatabaseType, HttpTunnelConfig, SshTunnelConfig, TransportLayerConfig,
|
||||
@@ -1127,6 +1217,57 @@ mod tests {
|
||||
assert_eq!(auth.get("token").and_then(serde_json::Value::as_str), Some("mq-token-secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_connections_moves_cassandra_store_passwords_to_secret_store_and_restores_them() {
|
||||
let path = temp_connections_file("cassandra-tls");
|
||||
let store = MemorySecretStore::default();
|
||||
let mut config = connection("cassandra", "", "");
|
||||
config.db_type = DatabaseType::Cassandra;
|
||||
config.external_config = Some(serde_json::json!({
|
||||
"tls": {
|
||||
"truststore_path": "/certs/client.truststore",
|
||||
"truststore_password": "trust-secret",
|
||||
"keystore_path": "/certs/client.keystore",
|
||||
"keystore_password": "key-secret"
|
||||
}
|
||||
}));
|
||||
|
||||
save_connections_to_file(&path, &[config], &store).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
store
|
||||
.get_existing("cassandra", CASSANDRA_TRUSTSTORE_PASSWORD_KEY)
|
||||
.as_deref(),
|
||||
Some("trust-secret")
|
||||
);
|
||||
assert_eq!(
|
||||
store
|
||||
.get_existing("cassandra", CASSANDRA_KEYSTORE_PASSWORD_KEY)
|
||||
.as_deref(),
|
||||
Some("key-secret")
|
||||
);
|
||||
let persisted_json = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(!persisted_json.contains("trust-secret"));
|
||||
assert!(!persisted_json.contains("key-secret"));
|
||||
|
||||
let loaded = load_connections_from_file(&path, &store).unwrap();
|
||||
let tls = loaded[0]
|
||||
.external_config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("tls"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
tls.get("truststore_password")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("trust-secret")
|
||||
);
|
||||
assert_eq!(
|
||||
tls.get("keystore_password")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("key-secret")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_connections_moves_mq_basic_and_oauth_secrets_to_secret_store_and_restores_them() {
|
||||
let path = temp_connections_file("mq-auth-multiple");
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::ai::{
|
||||
AiRunStatus,
|
||||
};
|
||||
use crate::connection_secrets::{
|
||||
CASSANDRA_KEYSTORE_PASSWORD_KEY, CASSANDRA_TLS_SECRET_PREFIX, CASSANDRA_TRUSTSTORE_PASSWORD_KEY,
|
||||
MQ_AUTH_API_KEY_VALUE_KEY, MQ_AUTH_CLIENT_SECRET_KEY, MQ_AUTH_PASSWORD_KEY, MQ_AUTH_SECRET_PREFIX,
|
||||
MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY, MQ_TOKEN_SIGNING_SECRET_PREFIX, NACOS_AUTH_PASSWORD_KEY,
|
||||
NACOS_AUTH_SECRET_PREFIX, NACOS_RNACOS_CONSOLE_PASSWORD_KEY,
|
||||
@@ -1362,6 +1363,17 @@ fn scrub_nacos_auth_secrets(config: &mut ConnectionConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
fn scrub_cassandra_tls_secrets(config: &mut ConnectionConfig) {
|
||||
if config.db_type != DatabaseType::Cassandra {
|
||||
return;
|
||||
}
|
||||
let Some(tls) = cassandra_tls_object_mut(config.external_config.as_mut()) else {
|
||||
return;
|
||||
};
|
||||
scrub_json_secret(tls, "truststore_password");
|
||||
scrub_json_secret(tls, "keystore_password");
|
||||
}
|
||||
|
||||
fn delete_secret_prefix_in_tx(
|
||||
tx: &rusqlite::Transaction<'_>,
|
||||
connection_id: &str,
|
||||
@@ -3190,6 +3202,7 @@ fn sanitized_connection_config(config: &ConnectionConfig) -> ConnectionConfig {
|
||||
scrub_mq_auth_secrets(&mut sanitized);
|
||||
scrub_mq_token_signing_secret(&mut sanitized);
|
||||
scrub_nacos_auth_secrets(&mut sanitized);
|
||||
scrub_cassandra_tls_secrets(&mut sanitized);
|
||||
sanitized
|
||||
}
|
||||
|
||||
@@ -3265,7 +3278,8 @@ fn persist_connection_in_tx(tx: &rusqlite::Transaction<'_>, config: &ConnectionC
|
||||
}
|
||||
persist_mq_auth_secrets_in_tx(tx, &config)?;
|
||||
persist_mq_token_signing_secret_in_tx(tx, &config)?;
|
||||
persist_nacos_auth_secrets_in_tx(tx, &config)
|
||||
persist_nacos_auth_secrets_in_tx(tx, &config)?;
|
||||
persist_cassandra_tls_secrets_in_tx(tx, &config)
|
||||
}
|
||||
|
||||
fn insert_connection_copy_next_to_source(entries: &mut Vec<serde_json::Value>, source_id: &str, copy_id: &str) -> bool {
|
||||
@@ -3400,6 +3414,7 @@ impl Storage {
|
||||
scrub_mq_auth_secrets(&mut sanitized);
|
||||
scrub_mq_token_signing_secret(&mut sanitized);
|
||||
scrub_nacos_auth_secrets(&mut sanitized);
|
||||
scrub_cassandra_tls_secrets(&mut sanitized);
|
||||
let json = serde_json::to_string(&sanitized).map_err(|e| e.to_string())?;
|
||||
|
||||
tx.execute("INSERT INTO connections (id, config_json) VALUES (?1, ?2)", params![config_id, json])
|
||||
@@ -3721,13 +3736,17 @@ impl Storage {
|
||||
let needs_mq_auth_rewrite = self.hydrate_mq_auth_secrets(&id, &mut config).await?;
|
||||
let needs_mq_token_signing_rewrite = self.hydrate_mq_token_signing_secret(&id, &mut config).await?;
|
||||
let needs_nacos_auth_rewrite = self.hydrate_nacos_auth_secret(&id, &mut config).await?;
|
||||
let needs_external_secret_rewrite =
|
||||
needs_mq_auth_rewrite || needs_mq_token_signing_rewrite || needs_nacos_auth_rewrite;
|
||||
let needs_cassandra_tls_rewrite = self.hydrate_cassandra_tls_secrets(&id, &mut config).await?;
|
||||
let needs_external_secret_rewrite = needs_mq_auth_rewrite
|
||||
|| needs_mq_token_signing_rewrite
|
||||
|| needs_nacos_auth_rewrite
|
||||
|| needs_cassandra_tls_rewrite;
|
||||
if needs_external_secret_rewrite {
|
||||
let mut sanitized = config.clone().canonicalized();
|
||||
scrub_mq_auth_secrets(&mut sanitized);
|
||||
scrub_mq_token_signing_secret(&mut sanitized);
|
||||
scrub_nacos_auth_secrets(&mut sanitized);
|
||||
scrub_cassandra_tls_secrets(&mut sanitized);
|
||||
let sanitized_json = serde_json::to_string(&sanitized).map_err(|e| e.to_string())?;
|
||||
let update_id = id.clone();
|
||||
self.with_conn(move |conn| {
|
||||
@@ -3838,6 +3857,36 @@ impl Storage {
|
||||
}
|
||||
Ok(rewritten)
|
||||
}
|
||||
|
||||
async fn hydrate_cassandra_tls_secrets(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
config: &mut ConnectionConfig,
|
||||
) -> Result<bool, String> {
|
||||
if config.db_type != DatabaseType::Cassandra {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(tls) = cassandra_tls_object_mut(config.external_config.as_mut()) else {
|
||||
return Ok(false);
|
||||
};
|
||||
let truststore_rewrite = hydrate_mq_json_secret(
|
||||
self,
|
||||
connection_id,
|
||||
CASSANDRA_TRUSTSTORE_PASSWORD_KEY,
|
||||
tls,
|
||||
"truststore_password",
|
||||
)
|
||||
.await?;
|
||||
let keystore_rewrite = hydrate_mq_json_secret(
|
||||
self,
|
||||
connection_id,
|
||||
CASSANDRA_KEYSTORE_PASSWORD_KEY,
|
||||
tls,
|
||||
"keystore_password",
|
||||
)
|
||||
.await?;
|
||||
Ok(truststore_rewrite || keystore_rewrite)
|
||||
}
|
||||
}
|
||||
|
||||
// Saved SQL
|
||||
@@ -5104,6 +5153,36 @@ fn persist_nacos_auth_secrets_in_tx(tx: &rusqlite::Transaction<'_>, config: &Con
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn persist_cassandra_tls_secrets_in_tx(
|
||||
tx: &rusqlite::Transaction<'_>,
|
||||
config: &ConnectionConfig,
|
||||
) -> Result<(), String> {
|
||||
if config.db_type != DatabaseType::Cassandra {
|
||||
delete_secret_prefix_in_tx(tx, &config.id, CASSANDRA_TLS_SECRET_PREFIX)?;
|
||||
return Ok(());
|
||||
}
|
||||
let Some(tls) = cassandra_tls_object(config.external_config.as_ref()) else {
|
||||
delete_secret_prefix_in_tx(tx, &config.id, CASSANDRA_TLS_SECRET_PREFIX)?;
|
||||
return Ok(());
|
||||
};
|
||||
persist_secret_in_tx(
|
||||
tx,
|
||||
&config.id,
|
||||
CASSANDRA_TRUSTSTORE_PASSWORD_KEY,
|
||||
tls.get("truststore_password")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or(""),
|
||||
)?;
|
||||
persist_secret_in_tx(
|
||||
tx,
|
||||
&config.id,
|
||||
CASSANDRA_KEYSTORE_PASSWORD_KEY,
|
||||
tls.get("keystore_password")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or(""),
|
||||
)
|
||||
}
|
||||
|
||||
fn persist_json_secret_if_present_in_tx(
|
||||
tx: &rusqlite::Transaction<'_>,
|
||||
connection_id: &str,
|
||||
@@ -5165,6 +5244,18 @@ fn mq_token_signing_object_mut(
|
||||
value?.get_mut("tokenSigning")?.as_object_mut()
|
||||
}
|
||||
|
||||
fn cassandra_tls_object(
|
||||
value: Option<&serde_json::Value>,
|
||||
) -> Option<&serde_json::Map<String, serde_json::Value>> {
|
||||
value?.get("tls")?.as_object()
|
||||
}
|
||||
|
||||
fn cassandra_tls_object_mut(
|
||||
value: Option<&mut serde_json::Value>,
|
||||
) -> Option<&mut serde_json::Map<String, serde_json::Value>> {
|
||||
value?.get_mut("tls")?.as_object_mut()
|
||||
}
|
||||
|
||||
fn nacos_auth_object(value: Option<&serde_json::Value>) -> Option<&serde_json::Map<String, serde_json::Value>> {
|
||||
value?.get("auth")?.as_object()
|
||||
}
|
||||
@@ -5224,7 +5315,8 @@ mod tests {
|
||||
};
|
||||
use crate::connection_secrets::NACOS_RNACOS_CONSOLE_PASSWORD_KEY;
|
||||
use crate::connection_secrets::{
|
||||
MQ_AUTH_PASSWORD_KEY, MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY, NACOS_AUTH_PASSWORD_KEY,
|
||||
CASSANDRA_KEYSTORE_PASSWORD_KEY, CASSANDRA_TRUSTSTORE_PASSWORD_KEY, MQ_AUTH_PASSWORD_KEY,
|
||||
MQ_AUTH_TOKEN_KEY, MQ_TOKEN_SIGNING_KEY, NACOS_AUTH_PASSWORD_KEY,
|
||||
};
|
||||
use crate::history::{HistoryConnectionFilter, HistoryDatabaseFilter, HistoryEntry, HistorySearchRequest};
|
||||
use crate::models::connection::{
|
||||
@@ -5842,6 +5934,71 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn cassandra_connection(id: &str) -> ConnectionConfig {
|
||||
let mut config = plain_connection(id, "");
|
||||
config.name = "Cassandra".to_string();
|
||||
config.db_type = DatabaseType::Cassandra;
|
||||
config.port = 9042;
|
||||
config.ssl = true;
|
||||
config.external_config = Some(serde_json::json!({
|
||||
"tls": {
|
||||
"truststore_path": "/certs/client.truststore",
|
||||
"truststore_password": "trust-secret",
|
||||
"keystore_path": "/certs/client.keystore",
|
||||
"keystore_password": "key-secret"
|
||||
}
|
||||
}));
|
||||
config
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn save_connections_moves_cassandra_tls_passwords_to_secret_table_and_restores_them() {
|
||||
let path = temp_db_path("cassandra-tls-secrets");
|
||||
let storage = Storage::open(&path).await.unwrap();
|
||||
|
||||
storage.save_connections(&[cassandra_connection("cassandra")]).await.unwrap();
|
||||
|
||||
let raw_json = raw_connection_json(&storage, "cassandra").await;
|
||||
assert!(!raw_json.contains("trust-secret"));
|
||||
assert!(!raw_json.contains("key-secret"));
|
||||
assert_eq!(
|
||||
storage
|
||||
.get_secret("cassandra", CASSANDRA_TRUSTSTORE_PASSWORD_KEY)
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("trust-secret")
|
||||
);
|
||||
assert_eq!(
|
||||
storage
|
||||
.get_secret("cassandra", CASSANDRA_KEYSTORE_PASSWORD_KEY)
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("key-secret")
|
||||
);
|
||||
|
||||
let loaded = storage.load_connections().await.unwrap();
|
||||
let tls = loaded[0].external_config.as_ref().unwrap().get("tls").unwrap();
|
||||
assert_eq!(tls["truststore_password"], "trust-secret");
|
||||
assert_eq!(tls["keystore_password"], "key-secret");
|
||||
|
||||
let mut disabled = cassandra_connection("cassandra");
|
||||
disabled.ssl = false;
|
||||
disabled.external_config = None;
|
||||
storage.save_connections(&[disabled]).await.unwrap();
|
||||
assert_eq!(
|
||||
storage.get_secret("cassandra", CASSANDRA_TRUSTSTORE_PASSWORD_KEY).await.unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
storage.get_secret("cassandra", CASSANDRA_KEYSTORE_PASSWORD_KEY).await.unwrap(),
|
||||
None
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn save_connections_does_not_persist_password_when_save_password_false() {
|
||||
let path = temp_db_path("save-password-false");
|
||||
|
||||
Reference in new issue
Block a user