diff --git a/crates/iam/src/oidc.rs b/crates/iam/src/oidc.rs index 6b51881ee..51ec120d2 100644 --- a/crates/iam/src/oidc.rs +++ b/crates/iam/src/oidc.rs @@ -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)); diff --git a/crates/targets/src/target/mod.rs b/crates/targets/src/target/mod.rs index a29e5673e..00d0edaa9 100644 --- a/crates/targets/src/target/mod.rs +++ b/crates/targets/src/target/mod.rs @@ -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 { diff --git a/crates/targets/src/target/mqtt.rs b/crates/targets/src/target/mqtt.rs index 9c366c351..e99ad4b18 100644 --- a/crates/targets/src/target/mqtt.rs +++ b/crates/targets/src/target/mqtt.rs @@ -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, pub ca_path: String, @@ -88,6 +89,19 @@ pub struct MQTTTlsConfig { pub ws_path_allowlist: Vec, } +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"); diff --git a/crates/targets/src/target/mysql.rs b/crates/targets/src/target/mysql.rs index 9afcd22d1..8607b528e 100644 --- a/crates/targets/src/target/mysql.rs +++ b/crates/targets/src/target/mysql.rs @@ -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"); diff --git a/crates/targets/src/target/nats.rs b/crates/targets/src/target/nats.rs index 17bfbf4e7..b32ba4c6a 100644 --- a/crates/targets/src/target/nats.rs +++ b/crates/targets/src/target/nats.rs @@ -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 { diff --git a/crates/targets/src/target/postgres.rs b/crates/targets/src/target/postgres.rs index 42d3a4bc3..c0dc49aa7 100644 --- a/crates/targets/src/target/postgres.rs +++ b/crates/targets/src/target/postgres.rs @@ -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) -> 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"); diff --git a/crates/targets/src/target/pulsar.rs b/crates/targets/src/target/pulsar.rs index 6866086ea..40c77ac58 100644 --- a/crates/targets/src/target/pulsar.rs +++ b/crates/targets/src/target/pulsar.rs @@ -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 { diff --git a/crates/targets/src/target/webhook.rs b/crates/targets/src/target/webhook.rs index c1ead7555..07edd269c 100644 --- a/crates/targets/src/target/webhook.rs +++ b/crates/targets/src/target/webhook.rs @@ -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 {