From 0c4d470795ad7966c0a495d63d4698d5c94e9185 Mon Sep 17 00:00:00 2001 From: overtrue Date: Sun, 2 Aug 2026 16:15:38 +0800 Subject: [PATCH] fix(targets): explain webhook outbound allowlist failures --- crates/targets/src/config/common.rs | 40 ++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/crates/targets/src/config/common.rs b/crates/targets/src/config/common.rs index 12e622352..bf0b2e888 100644 --- a/crates/targets/src/config/common.rs +++ b/crates/targets/src/config/common.rs @@ -23,7 +23,7 @@ use rustfs_config::{ NATS_TLS_CLIENT_KEY, NATS_TOKEN, NATS_USERNAME, PULSAR_AUTH_TOKEN, PULSAR_PASSWORD, PULSAR_QUEUE_DIR, PULSAR_TLS_CA, PULSAR_TOPIC, PULSAR_USERNAME, }; -use rustfs_utils::egress::OutboundPolicy; +use rustfs_utils::egress::{ENV_OUTBOUND_ALLOW_ORIGINS, OutboundPolicy, OutboundPolicyError}; use std::collections::HashSet; use std::path::Path; use std::str::FromStr; @@ -223,12 +223,26 @@ pub(super) fn validate_outbound_http_url(value: &Url, field_label: &str) -> Resu OutboundPolicy::from_env_cached().map_err(|err| TargetError::Configuration(format!("invalid outbound policy: {err}")))?; policy .validate_url(value) - .map_err(|e| TargetError::Configuration(format!("{field_label} is not allowed: {e}"))) + .map_err(|err| TargetError::Configuration(format_outbound_http_url_error(field_label, value, err))) +} + +fn format_outbound_http_url_error(field_label: &str, value: &Url, err: OutboundPolicyError) -> String { + let base = format!("{field_label} is not allowed: {err}"); + if matches!(err, OutboundPolicyError::ForbiddenHost { .. }) { + let origin = value.origin().ascii_serialization(); + return format!( + "{base}; set {ENV_OUTBOUND_ALLOW_ORIGINS}={origin} to allow this operator-owned endpoint (origin only, no path)" + ); + } + base } #[cfg(test)] mod tests { - use super::{parse_jetstream_enable, parse_url, validate_nats_server_config, validate_pulsar_broker_config}; + use super::{ + format_outbound_http_url_error, parse_jetstream_enable, parse_url, validate_nats_server_config, + validate_pulsar_broker_config, + }; use async_nats::ServerAddr; use rustfs_config::server_config::KVS; use rustfs_config::{ @@ -236,7 +250,9 @@ mod tests { NATS_SUBJECT, NATS_TOKEN, NATS_USERNAME, PULSAR_TLS_ALLOW_INSECURE, PULSAR_TLS_CA, PULSAR_TLS_HOSTNAME_VERIFICATION, PULSAR_TOPIC, }; + use rustfs_utils::egress::{ENV_OUTBOUND_ALLOW_ORIGINS, OutboundPolicyError}; use std::str::FromStr; + use url::Url; fn nats_server() -> ServerAddr { ServerAddr::from_str("nats://127.0.0.1:4222").expect("valid nats address") @@ -248,6 +264,24 @@ mod tests { assert!(!err.to_string().contains("secret-token")); } + #[test] + fn outbound_http_error_names_allowlist_origin_without_path() { + let url = Url::parse("http://192.168.1.2:1880/webhook/rustfs").expect("valid endpoint"); + let err = format_outbound_http_url_error( + "endpoint URL", + &url, + OutboundPolicyError::ForbiddenHost { + host: "192.168.1.2".to_string(), + reason: "private address", + }, + ); + + assert!(err.contains(ENV_OUTBOUND_ALLOW_ORIGINS)); + assert!(err.contains("http://192.168.1.2:1880")); + assert!(!err.contains("webhook/rustfs")); + assert!(err.contains("origin only, no path")); + } + // Absolute on Linux, macOS, and Windows. temp_dir needs no filesystem to exist for a // validation-only test, and Path::is_absolute stays true across platforms. fn nats_queue_dir() -> String {