mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 16:37:07 +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 serde::{Deserialize, Serialize};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use std::fmt;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
@@ -275,8 +276,14 @@ impl<'c> AsyncHttpClient<'c> for ReqwestHttpClient {
|
|||||||
|
|
||||||
// ---- Public types (unchanged API) ----
|
// ---- 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.
|
/// Parsed configuration for a single OIDC provider.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
pub struct OidcProviderConfig {
|
pub struct OidcProviderConfig {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
@@ -298,6 +305,31 @@ pub struct OidcProviderConfig {
|
|||||||
pub hide_from_ui: bool,
|
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)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum OidcProviderConfigSource {
|
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]
|
#[test]
|
||||||
fn test_parse_enable_state_on() {
|
fn test_parse_enable_state_on() {
|
||||||
assert!(OidcSys::parse_enable_state("on", false, false));
|
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::TargetTlsState;
|
||||||
pub(crate) use crate::runtime::tls::fingerprint::build_target_tls_fingerprint;
|
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.
|
/// A read-only snapshot of delivery counters for a target.
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
pub struct TargetDeliverySnapshot {
|
pub struct TargetDeliverySnapshot {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use crate::{
|
|||||||
target::{
|
target::{
|
||||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||||
TargetType, build_queued_payload_with_records, mark_target_disconnected_on_connectivity_error, open_target_queue_store,
|
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;
|
use arc_swap::ArcSwap;
|
||||||
@@ -41,6 +41,7 @@ use rustfs_tls_runtime::{load_certs, load_private_key};
|
|||||||
use rustls::ClientConfig;
|
use rustls::ClientConfig;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::fmt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
@@ -78,7 +79,7 @@ impl MQTTTlsPolicy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Clone, Default, PartialEq, Eq)]
|
||||||
pub struct MQTTTlsConfig {
|
pub struct MQTTTlsConfig {
|
||||||
pub policy: Option<MQTTTlsPolicy>,
|
pub policy: Option<MQTTTlsPolicy>,
|
||||||
pub ca_path: String,
|
pub ca_path: String,
|
||||||
@@ -88,6 +89,19 @@ pub struct MQTTTlsConfig {
|
|||||||
pub ws_path_allowlist: Vec<String>,
|
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 {
|
impl MQTTTlsConfig {
|
||||||
pub fn from_values(
|
pub fn from_values(
|
||||||
policy: Option<&str>,
|
policy: Option<&str>,
|
||||||
@@ -422,7 +436,7 @@ pub(crate) fn build_mqtt_options(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Arguments for configuring an MQTT target
|
/// Arguments for configuring an MQTT target
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MQTTArgs {
|
pub struct MQTTArgs {
|
||||||
/// Whether the target is enabled
|
/// Whether the target is enabled
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -450,6 +464,25 @@ pub struct MQTTArgs {
|
|||||||
pub target_type: TargetType,
|
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 {
|
impl MQTTArgs {
|
||||||
pub fn validate(&self) -> Result<(), TargetError> {
|
pub fn validate(&self) -> Result<(), TargetError> {
|
||||||
if !self.enable {
|
if !self.enable {
|
||||||
@@ -1087,7 +1120,9 @@ where
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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;
|
use url::Url;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1125,6 +1160,34 @@ mod tests {
|
|||||||
assert!(err.to_string().contains("must not embed username or password"));
|
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]
|
#[test]
|
||||||
fn validate_mqtt_broker_url_requires_explicit_tls_policy_for_secure_scheme() {
|
fn validate_mqtt_broker_url_requires_explicit_tls_policy_for_secure_scheme() {
|
||||||
let url = Url::parse("mqtts://broker.example.com:8883").expect("valid url");
|
let url = Url::parse("mqtts://broker.example.com:8883").expect("valid url");
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use crate::{
|
|||||||
target::{
|
target::{
|
||||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||||
TargetType, build_queued_payload, delete_stored_payload, is_connectivity_error, open_target_queue_store,
|
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;
|
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 rustfs_tls_runtime::{load_certs, load_private_key};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::fmt;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
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
|
/// Contains all configuration values needed to connect to a MySQL/TiDB
|
||||||
/// database and write event notification records.
|
/// database and write event notification records.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MySqlArgs {
|
pub struct MySqlArgs {
|
||||||
/// Whether the target is enabled
|
/// Whether the target is enabled
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -69,6 +70,24 @@ pub struct MySqlArgs {
|
|||||||
pub target_type: TargetType,
|
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 {
|
impl MySqlArgs {
|
||||||
/// Validates the MySQL target configuration.
|
/// Validates the MySQL target configuration.
|
||||||
pub fn validate(&self) -> Result<(), TargetError> {
|
pub fn validate(&self) -> Result<(), TargetError> {
|
||||||
@@ -122,7 +141,7 @@ impl MySqlArgs {
|
|||||||
///
|
///
|
||||||
/// Produced by [`MySqlDsn::parse`] and consumed by the MySQL
|
/// Produced by [`MySqlDsn::parse`] and consumed by the MySQL
|
||||||
/// target runtime to build connection options.
|
/// target runtime to build connection options.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
pub struct MySqlDsn {
|
pub struct MySqlDsn {
|
||||||
/// MySQL user name
|
/// MySQL user name
|
||||||
pub user: String,
|
pub user: String,
|
||||||
@@ -138,6 +157,19 @@ pub struct MySqlDsn {
|
|||||||
pub tls: bool,
|
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 {
|
impl MySqlDsn {
|
||||||
/// Parses a MySQL DSN string into its components.
|
/// Parses a MySQL DSN string into its components.
|
||||||
///
|
///
|
||||||
@@ -931,6 +963,7 @@ where
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::target::REDACTED_SECRET;
|
||||||
|
|
||||||
fn absolute_test_path(path: &str) -> String {
|
fn absolute_test_path(path: &str) -> String {
|
||||||
std::env::temp_dir().join(path).to_string_lossy().into_owned()
|
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");
|
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]
|
#[test]
|
||||||
fn validate_table_name_accepts_valid_identifier() {
|
fn validate_table_name_accepts_valid_identifier() {
|
||||||
validate_table_name("rustfs_events").expect("valid table name");
|
validate_table_name("rustfs_events").expect("valid table name");
|
||||||
|
|||||||
@@ -24,13 +24,14 @@ use crate::{
|
|||||||
target::{
|
target::{
|
||||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||||
TargetTlsState, TargetType, build_queued_payload_with_records, build_target_tls_fingerprint, open_target_queue_store,
|
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 async_trait::async_trait;
|
||||||
use rustfs_config::{NATS_CREDENTIALS_FILE, NATS_TLS_CA, NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY};
|
use rustfs_config::{NATS_CREDENTIALS_FILE, NATS_TLS_CA, NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::fmt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -38,7 +39,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{info, instrument, warn};
|
use tracing::{info, instrument, warn};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct NATSArgs {
|
pub struct NATSArgs {
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
pub address: String,
|
pub address: String,
|
||||||
@@ -56,6 +57,27 @@ pub struct NATSArgs {
|
|||||||
pub target_type: TargetType,
|
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 {
|
impl NATSArgs {
|
||||||
pub fn validate(&self) -> Result<(), TargetError> {
|
pub fn validate(&self) -> Result<(), TargetError> {
|
||||||
if !self.enable {
|
if !self.enable {
|
||||||
@@ -429,6 +451,7 @@ where
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::target::REDACTED_SECRET;
|
||||||
|
|
||||||
fn base_args() -> NATSArgs {
|
fn base_args() -> NATSArgs {
|
||||||
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]
|
#[test]
|
||||||
fn validate_nats_rejects_multiple_auth_methods() {
|
fn validate_nats_rejects_multiple_auth_methods() {
|
||||||
let args = NATSArgs {
|
let args = NATSArgs {
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ use crate::{
|
|||||||
store::{Key, Store},
|
store::{Key, Store},
|
||||||
target::{
|
target::{
|
||||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
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;
|
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.
|
/// Parsed representation of a PostgreSQL DSN string.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
pub struct PostgresDsn {
|
pub struct PostgresDsn {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
@@ -104,6 +105,19 @@ pub struct PostgresDsn {
|
|||||||
pub schema: String,
|
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 {
|
impl PostgresDsn {
|
||||||
/// Parses and validates PostgreSQL DSN string.
|
/// Parses and validates PostgreSQL DSN string.
|
||||||
///
|
///
|
||||||
@@ -300,14 +314,7 @@ impl fmt::Debug for PostgresArgs {
|
|||||||
.field("tls_required", &self.tls_required)
|
.field("tls_required", &self.tls_required)
|
||||||
.field("tls_ca", &self.tls_ca)
|
.field("tls_ca", &self.tls_ca)
|
||||||
.field("tls_client_cert", &self.tls_client_cert)
|
.field("tls_client_cert", &self.tls_client_cert)
|
||||||
.field(
|
.field("tls_client_key", &redacted_secret(&self.tls_client_key))
|
||||||
"tls_client_key",
|
|
||||||
if self.tls_client_key.is_empty() {
|
|
||||||
&""
|
|
||||||
} else {
|
|
||||||
&"***REDACTED***"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.field("queue_dir", &self.queue_dir)
|
.field("queue_dir", &self.queue_dir)
|
||||||
.field("queue_limit", &self.queue_limit)
|
.field("queue_limit", &self.queue_limit)
|
||||||
.field("target_type", &self.target_type)
|
.field("target_type", &self.target_type)
|
||||||
@@ -831,6 +838,7 @@ where
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::target::REDACTED_SECRET;
|
||||||
|
|
||||||
fn base_args() -> PostgresArgs {
|
fn base_args() -> PostgresArgs {
|
||||||
PostgresArgs {
|
PostgresArgs {
|
||||||
@@ -1042,6 +1050,18 @@ mod tests {
|
|||||||
assert!(!rendered.contains(":***@"));
|
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]
|
#[test]
|
||||||
fn redact_postgres_dsn_masks_password_query_parameter() {
|
fn redact_postgres_dsn_masks_password_query_parameter() {
|
||||||
let redacted = redact_postgres_dsn("postgres://postgres@localhost:5432/db?search_path=public&password=secret");
|
let redacted = redact_postgres_dsn("postgres://postgres@localhost:5432/db?search_path=public&password=secret");
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use crate::{
|
|||||||
target::{
|
target::{
|
||||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||||
TargetTlsState, TargetType, build_queued_payload_with_records, build_target_tls_fingerprint, open_target_queue_store,
|
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 async_trait::async_trait;
|
||||||
@@ -32,6 +32,7 @@ use pulsar::{Authentication, Producer, Pulsar, TokioExecutor};
|
|||||||
use rustfs_tls_runtime::load_cert_bundle_der_bytes;
|
use rustfs_tls_runtime::load_cert_bundle_der_bytes;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::fmt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
@@ -39,7 +40,7 @@ use tokio::sync::Mutex as AsyncMutex;
|
|||||||
use tracing::{info, instrument};
|
use tracing::{info, instrument};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PulsarArgs {
|
pub struct PulsarArgs {
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
pub broker: String,
|
pub broker: String,
|
||||||
@@ -55,6 +56,25 @@ pub struct PulsarArgs {
|
|||||||
pub target_type: TargetType,
|
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 {
|
impl PulsarArgs {
|
||||||
pub fn validate(&self) -> Result<(), TargetError> {
|
pub fn validate(&self) -> Result<(), TargetError> {
|
||||||
if !self.enable {
|
if !self.enable {
|
||||||
@@ -463,6 +483,7 @@ where
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::target::REDACTED_SECRET;
|
||||||
|
|
||||||
fn base_args() -> PulsarArgs {
|
fn base_args() -> PulsarArgs {
|
||||||
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]
|
#[test]
|
||||||
fn validate_pulsar_rejects_mixed_auth_methods() {
|
fn validate_pulsar_rejects_mixed_auth_methods() {
|
||||||
let args = PulsarArgs {
|
let args = PulsarArgs {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use crate::{
|
|||||||
target::{
|
target::{
|
||||||
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
ChannelTargetType, EntityTarget, QueuedPayload, QueuedPayloadMeta, TargetDeliveryCounters, TargetDeliverySnapshot,
|
||||||
TargetTlsState, TargetType, build_queued_payload, build_target_tls_fingerprint, open_target_queue_store,
|
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;
|
use async_trait::async_trait;
|
||||||
@@ -34,6 +34,7 @@ use rustfs_tls_runtime::load_cert_bundle_der_bytes;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use std::{
|
use std::{
|
||||||
|
fmt,
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc,
|
||||||
@@ -45,7 +46,7 @@ use tokio::sync::mpsc;
|
|||||||
use tracing::{debug, error, info, instrument, warn};
|
use tracing::{debug, error, info, instrument, warn};
|
||||||
|
|
||||||
/// Arguments for configuring a Webhook target
|
/// Arguments for configuring a Webhook target
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WebhookArgs {
|
pub struct WebhookArgs {
|
||||||
/// Whether the target is enabled
|
/// Whether the target is enabled
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -69,6 +70,23 @@ pub struct WebhookArgs {
|
|||||||
pub target_type: TargetType,
|
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 {
|
impl WebhookArgs {
|
||||||
/// WebhookArgs verification method
|
/// WebhookArgs verification method
|
||||||
pub fn validate(&self) -> Result<(), TargetError> {
|
pub fn validate(&self) -> Result<(), TargetError> {
|
||||||
@@ -562,7 +580,7 @@ where
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{WebhookArgs, WebhookTarget};
|
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 tokio::net::TcpListener;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use url::form_urlencoded;
|
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]
|
#[test]
|
||||||
fn test_validate_skip_tls_verify_and_client_ca_mutually_exclusive() {
|
fn test_validate_skip_tls_verify_and_client_ca_mutually_exclusive() {
|
||||||
let args = WebhookArgs {
|
let args = WebhookArgs {
|
||||||
|
|||||||
Reference in New Issue
Block a user