mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
security: redact IAM and target debug secrets (#3306)
This commit is contained in:
+51
-1
@@ -32,6 +32,7 @@ use rustfs_policy::policy::{ClaimLookup, get_claim_case_insensitive};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::net::IpAddr;
|
||||
use std::pin::Pin;
|
||||
@@ -275,8 +276,14 @@ impl<'c> AsyncHttpClient<'c> for ReqwestHttpClient {
|
||||
|
||||
// ---- Public types (unchanged API) ----
|
||||
|
||||
const REDACTED_SECRET: &str = "***redacted***";
|
||||
|
||||
fn redacted_optional_secret(value: Option<&str>) -> &'static str {
|
||||
value.filter(|secret| !secret.is_empty()).map_or("", |_| REDACTED_SECRET)
|
||||
}
|
||||
|
||||
/// Parsed configuration for a single OIDC provider.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct OidcProviderConfig {
|
||||
pub id: String,
|
||||
pub enabled: bool,
|
||||
@@ -298,6 +305,31 @@ pub struct OidcProviderConfig {
|
||||
pub hide_from_ui: bool,
|
||||
}
|
||||
|
||||
impl fmt::Debug for OidcProviderConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("OidcProviderConfig")
|
||||
.field("id", &self.id)
|
||||
.field("enabled", &self.enabled)
|
||||
.field("config_url", &self.config_url)
|
||||
.field("client_id", &self.client_id)
|
||||
.field("client_secret", &redacted_optional_secret(self.client_secret.as_deref()))
|
||||
.field("scopes", &self.scopes)
|
||||
.field("other_audiences", &self.other_audiences)
|
||||
.field("redirect_uri", &self.redirect_uri)
|
||||
.field("redirect_uri_dynamic", &self.redirect_uri_dynamic)
|
||||
.field("claim_name", &self.claim_name)
|
||||
.field("claim_prefix", &self.claim_prefix)
|
||||
.field("role_policy", &self.role_policy)
|
||||
.field("display_name", &self.display_name)
|
||||
.field("groups_claim", &self.groups_claim)
|
||||
.field("roles_claim", &self.roles_claim)
|
||||
.field("email_claim", &self.email_claim)
|
||||
.field("username_claim", &self.username_claim)
|
||||
.field("hide_from_ui", &self.hide_from_ui)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OidcProviderConfigSource {
|
||||
@@ -1978,6 +2010,24 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oidc_provider_config_debug_redacts_client_secret() {
|
||||
let config = OidcProviderConfig {
|
||||
client_secret: Some("oidc-client-secret".to_string()),
|
||||
..test_config("default")
|
||||
};
|
||||
let sourced = SourcedOidcProviderConfig {
|
||||
config,
|
||||
source: OidcProviderConfigSource::Persisted,
|
||||
};
|
||||
|
||||
let rendered = format!("{sourced:?}");
|
||||
|
||||
assert!(!rendered.contains("oidc-client-secret"));
|
||||
assert!(rendered.contains(REDACTED_SECRET));
|
||||
assert!(rendered.contains("client-id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_enable_state_on() {
|
||||
assert!(OidcSys::parse_enable_state("on", false, false));
|
||||
|
||||
@@ -44,6 +44,16 @@ pub(crate) use crate::runtime::tls::fingerprint::TargetTlsGeneration;
|
||||
pub(crate) use crate::runtime::tls::fingerprint::TargetTlsState;
|
||||
pub(crate) use crate::runtime::tls::fingerprint::build_target_tls_fingerprint;
|
||||
|
||||
pub(crate) const REDACTED_SECRET: &str = "***redacted***";
|
||||
|
||||
pub(crate) fn redacted_secret(value: &str) -> &'static str {
|
||||
if value.is_empty() { "" } else { REDACTED_SECRET }
|
||||
}
|
||||
|
||||
pub(crate) fn redacted_optional_secret(value: Option<&str>) -> &'static str {
|
||||
value.filter(|secret| !secret.is_empty()).map_or("", |_| REDACTED_SECRET)
|
||||
}
|
||||
|
||||
/// A read-only snapshot of delivery counters for a target.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct TargetDeliverySnapshot {
|
||||
|
||||
@@ -24,7 +24,7 @@ use crate::{
|
||||
target::{
|
||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||
TargetType, build_queued_payload_with_records, mark_target_disconnected_on_connectivity_error, open_target_queue_store,
|
||||
persist_queued_payload_to_store,
|
||||
persist_queued_payload_to_store, redacted_secret,
|
||||
},
|
||||
};
|
||||
use arc_swap::ArcSwap;
|
||||
@@ -41,6 +41,7 @@ use rustfs_tls_runtime::{load_certs, load_private_key};
|
||||
use rustls::ClientConfig;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
@@ -78,7 +79,7 @@ impl MQTTTlsPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
pub struct MQTTTlsConfig {
|
||||
pub policy: Option<MQTTTlsPolicy>,
|
||||
pub ca_path: String,
|
||||
@@ -88,6 +89,19 @@ pub struct MQTTTlsConfig {
|
||||
pub ws_path_allowlist: Vec<String>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for MQTTTlsConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("MQTTTlsConfig")
|
||||
.field("policy", &self.policy)
|
||||
.field("ca_path", &self.ca_path)
|
||||
.field("client_cert_path", &self.client_cert_path)
|
||||
.field("client_key_path", &redacted_secret(&self.client_key_path))
|
||||
.field("trust_leaf_as_ca", &self.trust_leaf_as_ca)
|
||||
.field("ws_path_allowlist", &self.ws_path_allowlist)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl MQTTTlsConfig {
|
||||
pub fn from_values(
|
||||
policy: Option<&str>,
|
||||
@@ -422,7 +436,7 @@ pub(crate) fn build_mqtt_options(
|
||||
}
|
||||
|
||||
/// Arguments for configuring an MQTT target
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct MQTTArgs {
|
||||
/// Whether the target is enabled
|
||||
pub enable: bool,
|
||||
@@ -450,6 +464,25 @@ pub struct MQTTArgs {
|
||||
pub target_type: TargetType,
|
||||
}
|
||||
|
||||
impl fmt::Debug for MQTTArgs {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("MQTTArgs")
|
||||
.field("enable", &self.enable)
|
||||
.field("broker", &self.broker)
|
||||
.field("topic", &self.topic)
|
||||
.field("qos", &self.qos)
|
||||
.field("username", &self.username)
|
||||
.field("password", &redacted_secret(&self.password))
|
||||
.field("tls", &self.tls)
|
||||
.field("max_reconnect_interval", &self.max_reconnect_interval)
|
||||
.field("keep_alive", &self.keep_alive)
|
||||
.field("queue_dir", &self.queue_dir)
|
||||
.field("queue_limit", &self.queue_limit)
|
||||
.field("target_type", &self.target_type)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl MQTTArgs {
|
||||
pub fn validate(&self) -> Result<(), TargetError> {
|
||||
if !self.enable {
|
||||
@@ -1087,7 +1120,9 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MQTTTlsConfig, validate_mqtt_broker_url};
|
||||
use super::{MQTTArgs, MQTTTlsConfig, QoS, validate_mqtt_broker_url};
|
||||
use crate::target::{REDACTED_SECRET, TargetType};
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
#[test]
|
||||
@@ -1125,6 +1160,34 @@ mod tests {
|
||||
assert!(err.to_string().contains("must not embed username or password"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_redacts_mqtt_secret_fields() {
|
||||
let args = MQTTArgs {
|
||||
enable: true,
|
||||
broker: Url::parse("mqtt://broker.example.com:1883").expect("valid broker"),
|
||||
topic: "rustfs/events".to_string(),
|
||||
qos: QoS::AtLeastOnce,
|
||||
username: "mqtt-user".to_string(),
|
||||
password: "mqtt-password".to_string(),
|
||||
tls: MQTTTlsConfig {
|
||||
client_key_path: "/etc/rustfs/mqtt.key".to_string(),
|
||||
..MQTTTlsConfig::default()
|
||||
},
|
||||
max_reconnect_interval: Duration::from_secs(1),
|
||||
keep_alive: Duration::from_secs(30),
|
||||
queue_dir: String::new(),
|
||||
queue_limit: 0,
|
||||
target_type: TargetType::NotifyEvent,
|
||||
};
|
||||
|
||||
let rendered = format!("{args:?}");
|
||||
|
||||
assert!(!rendered.contains("mqtt-password"));
|
||||
assert!(!rendered.contains("/etc/rustfs/mqtt.key"));
|
||||
assert!(rendered.contains(REDACTED_SECRET));
|
||||
assert!(rendered.contains("mqtt-user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_mqtt_broker_url_requires_explicit_tls_policy_for_secure_scheme() {
|
||||
let url = Url::parse("mqtts://broker.example.com:8883").expect("valid url");
|
||||
|
||||
@@ -24,7 +24,7 @@ use crate::{
|
||||
target::{
|
||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||
TargetType, build_queued_payload, delete_stored_payload, is_connectivity_error, open_target_queue_store,
|
||||
persist_queued_payload_to_store,
|
||||
persist_queued_payload_to_store, redacted_secret,
|
||||
},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
@@ -33,6 +33,7 @@ use rustfs_config::{MYSQL_TLS_CA, MYSQL_TLS_CLIENT_CERT, MYSQL_TLS_CLIENT_KEY};
|
||||
use rustfs_tls_runtime::{load_certs, load_private_key};
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
@@ -43,7 +44,7 @@ use tracing::{debug, error, info, warn};
|
||||
///
|
||||
/// Contains all configuration values needed to connect to a MySQL/TiDB
|
||||
/// database and write event notification records.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct MySqlArgs {
|
||||
/// Whether the target is enabled
|
||||
pub enable: bool,
|
||||
@@ -69,6 +70,24 @@ pub struct MySqlArgs {
|
||||
pub target_type: TargetType,
|
||||
}
|
||||
|
||||
impl fmt::Debug for MySqlArgs {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("MySqlArgs")
|
||||
.field("enable", &self.enable)
|
||||
.field("dsn_string", &redact_mysql_dsn(&self.dsn_string))
|
||||
.field("table", &self.table)
|
||||
.field("format", &self.format)
|
||||
.field("tls_ca", &self.tls_ca)
|
||||
.field("tls_client_cert", &self.tls_client_cert)
|
||||
.field("tls_client_key", &redacted_secret(&self.tls_client_key))
|
||||
.field("queue_dir", &self.queue_dir)
|
||||
.field("queue_limit", &self.queue_limit)
|
||||
.field("max_open_connections", &self.max_open_connections)
|
||||
.field("target_type", &self.target_type)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl MySqlArgs {
|
||||
/// Validates the MySQL target configuration.
|
||||
pub fn validate(&self) -> Result<(), TargetError> {
|
||||
@@ -122,7 +141,7 @@ impl MySqlArgs {
|
||||
///
|
||||
/// Produced by [`MySqlDsn::parse`] and consumed by the MySQL
|
||||
/// target runtime to build connection options.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct MySqlDsn {
|
||||
/// MySQL user name
|
||||
pub user: String,
|
||||
@@ -138,6 +157,19 @@ pub struct MySqlDsn {
|
||||
pub tls: bool,
|
||||
}
|
||||
|
||||
impl fmt::Debug for MySqlDsn {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("MySqlDsn")
|
||||
.field("user", &self.user)
|
||||
.field("password", &redacted_secret(&self.password))
|
||||
.field("host", &self.host)
|
||||
.field("port", &self.port)
|
||||
.field("database", &self.database)
|
||||
.field("tls", &self.tls)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl MySqlDsn {
|
||||
/// Parses a MySQL DSN string into its components.
|
||||
///
|
||||
@@ -931,6 +963,7 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::target::REDACTED_SECRET;
|
||||
|
||||
fn absolute_test_path(path: &str) -> String {
|
||||
std::env::temp_dir().join(path).to_string_lossy().into_owned()
|
||||
@@ -1036,6 +1069,33 @@ mod tests {
|
||||
assert_eq!(redacted, "root:***@tcp(127.0.0.1:4000)/testdb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_redacts_mysql_secret_fields() {
|
||||
let args = MySqlArgs {
|
||||
enable: true,
|
||||
dsn_string: "rustfs:mysql-password@tcp(127.0.0.1:3306)/db".to_string(),
|
||||
table: "events".to_string(),
|
||||
format: "access".to_string(),
|
||||
tls_ca: String::new(),
|
||||
tls_client_cert: String::new(),
|
||||
tls_client_key: "/etc/rustfs/mysql.key".to_string(),
|
||||
queue_dir: String::new(),
|
||||
queue_limit: 0,
|
||||
max_open_connections: 0,
|
||||
target_type: TargetType::NotifyEvent,
|
||||
};
|
||||
let dsn = MySqlDsn::parse(&args.dsn_string).expect("valid DSN");
|
||||
|
||||
let rendered_args = format!("{args:?}");
|
||||
let rendered_dsn = format!("{dsn:?}");
|
||||
|
||||
assert!(!rendered_args.contains("mysql-password"));
|
||||
assert!(!rendered_args.contains("/etc/rustfs/mysql.key"));
|
||||
assert!(!rendered_dsn.contains("mysql-password"));
|
||||
assert!(rendered_args.contains("rustfs:***@"));
|
||||
assert!(rendered_dsn.contains(REDACTED_SECRET));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_table_name_accepts_valid_identifier() {
|
||||
validate_table_name("rustfs_events").expect("valid table name");
|
||||
|
||||
@@ -24,13 +24,14 @@ use crate::{
|
||||
target::{
|
||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||
TargetTlsState, TargetType, build_queued_payload_with_records, build_target_tls_fingerprint, open_target_queue_store,
|
||||
persist_queued_payload_to_store,
|
||||
persist_queued_payload_to_store, redacted_secret,
|
||||
},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use rustfs_config::{NATS_CREDENTIALS_FILE, NATS_TLS_CA, NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY};
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -38,7 +39,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct NATSArgs {
|
||||
pub enable: bool,
|
||||
pub address: String,
|
||||
@@ -56,6 +57,27 @@ pub struct NATSArgs {
|
||||
pub target_type: TargetType,
|
||||
}
|
||||
|
||||
impl fmt::Debug for NATSArgs {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("NATSArgs")
|
||||
.field("enable", &self.enable)
|
||||
.field("address", &self.address)
|
||||
.field("subject", &self.subject)
|
||||
.field("username", &self.username)
|
||||
.field("password", &redacted_secret(&self.password))
|
||||
.field("token", &redacted_secret(&self.token))
|
||||
.field("credentials_file", &redacted_secret(&self.credentials_file))
|
||||
.field("tls_ca", &self.tls_ca)
|
||||
.field("tls_client_cert", &self.tls_client_cert)
|
||||
.field("tls_client_key", &redacted_secret(&self.tls_client_key))
|
||||
.field("tls_required", &self.tls_required)
|
||||
.field("queue_dir", &self.queue_dir)
|
||||
.field("queue_limit", &self.queue_limit)
|
||||
.field("target_type", &self.target_type)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl NATSArgs {
|
||||
pub fn validate(&self) -> Result<(), TargetError> {
|
||||
if !self.enable {
|
||||
@@ -429,6 +451,7 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::target::REDACTED_SECRET;
|
||||
|
||||
fn base_args() -> NATSArgs {
|
||||
NATSArgs {
|
||||
@@ -449,6 +472,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_redacts_nats_secret_fields() {
|
||||
let args = NATSArgs {
|
||||
password: "nats-password".to_string(),
|
||||
token: "nats-token".to_string(),
|
||||
credentials_file: "/etc/rustfs/nats.creds".to_string(),
|
||||
tls_client_key: "/etc/rustfs/nats.key".to_string(),
|
||||
..base_args()
|
||||
};
|
||||
|
||||
let rendered = format!("{args:?}");
|
||||
|
||||
assert!(!rendered.contains("nats-password"));
|
||||
assert!(!rendered.contains("nats-token"));
|
||||
assert!(!rendered.contains("/etc/rustfs/nats.creds"));
|
||||
assert!(!rendered.contains("/etc/rustfs/nats.key"));
|
||||
assert!(rendered.contains(REDACTED_SECRET));
|
||||
assert!(rendered.contains("rustfs.events"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_nats_rejects_multiple_auth_methods() {
|
||||
let args = NATSArgs {
|
||||
|
||||
@@ -36,7 +36,8 @@ use crate::{
|
||||
store::{Key, Store},
|
||||
target::{
|
||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||
TargetType, build_queued_payload, open_target_queue_store, persist_queued_payload_to_store,
|
||||
TargetType, build_queued_payload, open_target_queue_store, persist_queued_payload_to_store, redacted_optional_secret,
|
||||
redacted_secret,
|
||||
},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
@@ -94,7 +95,7 @@ pub fn parse_postgres_format(value: Option<&str>) -> Result<PostgresFormat, Targ
|
||||
}
|
||||
|
||||
/// Parsed representation of a PostgreSQL DSN string.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct PostgresDsn {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
@@ -104,6 +105,19 @@ pub struct PostgresDsn {
|
||||
pub schema: String,
|
||||
}
|
||||
|
||||
impl fmt::Debug for PostgresDsn {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("PostgresDsn")
|
||||
.field("host", &self.host)
|
||||
.field("port", &self.port)
|
||||
.field("user", &self.user)
|
||||
.field("password", &redacted_optional_secret(self.password.as_deref()))
|
||||
.field("database", &self.database)
|
||||
.field("schema", &self.schema)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PostgresDsn {
|
||||
/// Parses and validates PostgreSQL DSN string.
|
||||
///
|
||||
@@ -300,14 +314,7 @@ impl fmt::Debug for PostgresArgs {
|
||||
.field("tls_required", &self.tls_required)
|
||||
.field("tls_ca", &self.tls_ca)
|
||||
.field("tls_client_cert", &self.tls_client_cert)
|
||||
.field(
|
||||
"tls_client_key",
|
||||
if self.tls_client_key.is_empty() {
|
||||
&""
|
||||
} else {
|
||||
&"***REDACTED***"
|
||||
},
|
||||
)
|
||||
.field("tls_client_key", &redacted_secret(&self.tls_client_key))
|
||||
.field("queue_dir", &self.queue_dir)
|
||||
.field("queue_limit", &self.queue_limit)
|
||||
.field("target_type", &self.target_type)
|
||||
@@ -831,6 +838,7 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::target::REDACTED_SECRET;
|
||||
|
||||
fn base_args() -> PostgresArgs {
|
||||
PostgresArgs {
|
||||
@@ -1042,6 +1050,18 @@ mod tests {
|
||||
assert!(!rendered.contains(":***@"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_redacts_postgres_dsn_password() {
|
||||
let dsn = PostgresDsn::parse("postgres://postgres:pg-secret@localhost:5432/rustfs_events?search_path=public")
|
||||
.expect("valid DSN");
|
||||
|
||||
let rendered = format!("{dsn:?}");
|
||||
|
||||
assert!(!rendered.contains("pg-secret"));
|
||||
assert!(rendered.contains(REDACTED_SECRET));
|
||||
assert!(rendered.contains("rustfs_events"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redact_postgres_dsn_masks_password_query_parameter() {
|
||||
let redacted = redact_postgres_dsn("postgres://postgres@localhost:5432/db?search_path=public&password=secret");
|
||||
|
||||
@@ -24,7 +24,7 @@ use crate::{
|
||||
target::{
|
||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||
TargetTlsState, TargetType, build_queued_payload_with_records, build_target_tls_fingerprint, open_target_queue_store,
|
||||
persist_queued_payload_to_store,
|
||||
persist_queued_payload_to_store, redacted_secret,
|
||||
},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
@@ -32,6 +32,7 @@ use pulsar::{Authentication, Producer, Pulsar, TokioExecutor};
|
||||
use rustfs_tls_runtime::load_cert_bundle_der_bytes;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -39,7 +40,7 @@ use tokio::sync::Mutex as AsyncMutex;
|
||||
use tracing::{info, instrument};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct PulsarArgs {
|
||||
pub enable: bool,
|
||||
pub broker: String,
|
||||
@@ -55,6 +56,25 @@ pub struct PulsarArgs {
|
||||
pub target_type: TargetType,
|
||||
}
|
||||
|
||||
impl fmt::Debug for PulsarArgs {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("PulsarArgs")
|
||||
.field("enable", &self.enable)
|
||||
.field("broker", &self.broker)
|
||||
.field("topic", &self.topic)
|
||||
.field("auth_token", &redacted_secret(&self.auth_token))
|
||||
.field("username", &self.username)
|
||||
.field("password", &redacted_secret(&self.password))
|
||||
.field("tls_ca", &self.tls_ca)
|
||||
.field("tls_allow_insecure", &self.tls_allow_insecure)
|
||||
.field("tls_hostname_verification", &self.tls_hostname_verification)
|
||||
.field("queue_dir", &self.queue_dir)
|
||||
.field("queue_limit", &self.queue_limit)
|
||||
.field("target_type", &self.target_type)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PulsarArgs {
|
||||
pub fn validate(&self) -> Result<(), TargetError> {
|
||||
if !self.enable {
|
||||
@@ -463,6 +483,7 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::target::REDACTED_SECRET;
|
||||
|
||||
fn base_args() -> PulsarArgs {
|
||||
PulsarArgs {
|
||||
@@ -481,6 +502,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_redacts_pulsar_secret_fields() {
|
||||
let args = PulsarArgs {
|
||||
auth_token: "pulsar-token".to_string(),
|
||||
password: "pulsar-password".to_string(),
|
||||
..base_args()
|
||||
};
|
||||
|
||||
let rendered = format!("{args:?}");
|
||||
|
||||
assert!(!rendered.contains("pulsar-token"));
|
||||
assert!(!rendered.contains("pulsar-password"));
|
||||
assert!(rendered.contains(REDACTED_SECRET));
|
||||
assert!(rendered.contains("persistent://public/default/rustfs-events"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_pulsar_rejects_mixed_auth_methods() {
|
||||
let args = PulsarArgs {
|
||||
|
||||
@@ -24,7 +24,7 @@ use crate::{
|
||||
target::{
|
||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||
TargetTlsState, TargetType, build_queued_payload, build_target_tls_fingerprint, open_target_queue_store,
|
||||
persist_queued_payload_to_store,
|
||||
persist_queued_payload_to_store, redacted_secret,
|
||||
},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
@@ -34,6 +34,7 @@ use rustfs_tls_runtime::load_cert_bundle_der_bytes;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::{
|
||||
fmt,
|
||||
marker::PhantomData,
|
||||
sync::{
|
||||
Arc,
|
||||
@@ -45,7 +46,7 @@ use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
/// Arguments for configuring a Webhook target
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct WebhookArgs {
|
||||
/// Whether the target is enabled
|
||||
pub enable: bool,
|
||||
@@ -69,6 +70,23 @@ pub struct WebhookArgs {
|
||||
pub target_type: TargetType,
|
||||
}
|
||||
|
||||
impl fmt::Debug for WebhookArgs {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("WebhookArgs")
|
||||
.field("enable", &self.enable)
|
||||
.field("endpoint", &self.endpoint)
|
||||
.field("auth_token", &redacted_secret(&self.auth_token))
|
||||
.field("queue_dir", &self.queue_dir)
|
||||
.field("queue_limit", &self.queue_limit)
|
||||
.field("client_cert", &self.client_cert)
|
||||
.field("client_key", &redacted_secret(&self.client_key))
|
||||
.field("client_ca", &self.client_ca)
|
||||
.field("skip_tls_verify", &self.skip_tls_verify)
|
||||
.field("target_type", &self.target_type)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl WebhookArgs {
|
||||
/// WebhookArgs verification method
|
||||
pub fn validate(&self) -> Result<(), TargetError> {
|
||||
@@ -562,7 +580,7 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{WebhookArgs, WebhookTarget};
|
||||
use crate::target::{Target, TargetType, decode_object_name};
|
||||
use crate::target::{REDACTED_SECRET, Target, TargetType, decode_object_name};
|
||||
use tokio::net::TcpListener;
|
||||
use url::Url;
|
||||
use url::form_urlencoded;
|
||||
@@ -582,6 +600,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_redacts_webhook_secret_fields() {
|
||||
let args = WebhookArgs {
|
||||
auth_token: "webhook-token".to_string(),
|
||||
client_key: "/etc/rustfs/webhook.key".to_string(),
|
||||
..base_args()
|
||||
};
|
||||
|
||||
let rendered = format!("{args:?}");
|
||||
|
||||
assert!(!rendered.contains("webhook-token"));
|
||||
assert!(!rendered.contains("/etc/rustfs/webhook.key"));
|
||||
assert!(rendered.contains(REDACTED_SECRET));
|
||||
assert!(rendered.contains("WebhookArgs"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_skip_tls_verify_and_client_ca_mutually_exclusive() {
|
||||
let args = WebhookArgs {
|
||||
|
||||
Reference in New Issue
Block a user