mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-21 20:06:37 +00:00
feat(rustfs): gate audit/notify by global env switches (#2669)
This commit is contained in:
@@ -26,6 +26,15 @@ pub const RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT: bool = false;
|
|||||||
pub const ENABLE_KEY: &str = "enable";
|
pub const ENABLE_KEY: &str = "enable";
|
||||||
pub const COMMENT_KEY: &str = "comment";
|
pub const COMMENT_KEY: &str = "comment";
|
||||||
|
|
||||||
|
/// Global switch for enabling the audit module.
|
||||||
|
pub const ENV_AUDIT_ENABLE: &str = "RUSTFS_AUDIT_ENABLE";
|
||||||
|
/// Global switch for enabling the notify module.
|
||||||
|
pub const ENV_NOTIFY_ENABLE: &str = "RUSTFS_NOTIFY_ENABLE";
|
||||||
|
/// Default global audit switch (disabled by default).
|
||||||
|
pub const DEFAULT_AUDIT_ENABLE: bool = false;
|
||||||
|
/// Default global notify switch (disabled by default).
|
||||||
|
pub const DEFAULT_NOTIFY_ENABLE: bool = false;
|
||||||
|
|
||||||
/// Medium-drawn lines separator
|
/// Medium-drawn lines separator
|
||||||
/// This is used to separate words in environment variable names.
|
/// This is used to separate words in environment variable names.
|
||||||
pub const ENV_WORD_DELIMITER_DASH: &str = "-";
|
pub const ENV_WORD_DELIMITER_DASH: &str = "-";
|
||||||
@@ -290,4 +299,10 @@ mod tests {
|
|||||||
assert!(state.is_disabled());
|
assert!(state.is_disabled());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_global_audit_notify_switch_constants() {
|
||||||
|
assert_eq!(ENV_AUDIT_ENABLE, "RUSTFS_AUDIT_ENABLE");
|
||||||
|
assert_eq!(ENV_NOTIFY_ENABLE, "RUSTFS_NOTIFY_ENABLE");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -457,6 +457,7 @@ async fn run(config: rustfs::config::Config) -> Result<()> {
|
|||||||
|
|
||||||
// Initialize event notifier
|
// Initialize event notifier
|
||||||
init_event_notifier().await;
|
init_event_notifier().await;
|
||||||
|
|
||||||
// Start the audit system
|
// Start the audit system
|
||||||
match start_audit_system().await {
|
match start_audit_system().await {
|
||||||
Ok(_) => info!(target: "rustfs::main::run","Audit system started successfully."),
|
Ok(_) => info!(target: "rustfs::main::run","Audit system started successfully."),
|
||||||
|
|||||||
@@ -14,12 +14,25 @@
|
|||||||
|
|
||||||
use crate::app::context::resolve_server_config;
|
use crate::app::context::resolve_server_config;
|
||||||
use rustfs_audit::{AuditError, AuditResult, audit_system, init_audit_system, system::AuditSystemState};
|
use rustfs_audit::{AuditError, AuditResult, audit_system, init_audit_system, system::AuditSystemState};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
static AUDIT_MODULE_ENABLED: AtomicBool = AtomicBool::new(rustfs_config::DEFAULT_AUDIT_ENABLE);
|
||||||
|
|
||||||
fn server_config_from_context() -> Option<rustfs_ecstore::config::Config> {
|
fn server_config_from_context() -> Option<rustfs_ecstore::config::Config> {
|
||||||
resolve_server_config()
|
resolve_server_config()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn refresh_audit_module_enabled() -> bool {
|
||||||
|
let enabled = rustfs_utils::get_env_bool(rustfs_config::ENV_AUDIT_ENABLE, rustfs_config::DEFAULT_AUDIT_ENABLE);
|
||||||
|
AUDIT_MODULE_ENABLED.store(enabled, Ordering::Relaxed);
|
||||||
|
enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_audit_module_enabled() -> bool {
|
||||||
|
AUDIT_MODULE_ENABLED.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
fn has_any_audit_targets(config: &rustfs_ecstore::config::Config) -> bool {
|
fn has_any_audit_targets(config: &rustfs_ecstore::config::Config) -> bool {
|
||||||
for subsystem in [
|
for subsystem in [
|
||||||
rustfs_config::audit::AUDIT_MQTT_SUB_SYS,
|
rustfs_config::audit::AUDIT_MQTT_SUB_SYS,
|
||||||
@@ -43,6 +56,15 @@ fn has_any_audit_targets(config: &rustfs_ecstore::config::Config) -> bool {
|
|||||||
/// If not configured, it skips the initialization.
|
/// If not configured, it skips the initialization.
|
||||||
/// It also handles cases where the audit system is already running or if the global configuration is not loaded.
|
/// It also handles cases where the audit system is already running or if the global configuration is not loaded.
|
||||||
pub async fn start_audit_system() -> AuditResult<()> {
|
pub async fn start_audit_system() -> AuditResult<()> {
|
||||||
|
let enabled = refresh_audit_module_enabled();
|
||||||
|
if !enabled {
|
||||||
|
info!(
|
||||||
|
target: "rustfs::main::start_audit_system",
|
||||||
|
"Audit module is disabled by RUSTFS_AUDIT_ENABLE=false, audit system initialization is skipped."
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
target: "rustfs::main::start_audit_system",
|
target: "rustfs::main::start_audit_system",
|
||||||
"Initializing the audit system..."
|
"Initializing the audit system..."
|
||||||
|
|||||||
@@ -16,13 +16,26 @@ use crate::app::context::resolve_server_config;
|
|||||||
use rustfs_ecstore::event_notification::{EventArgs as EcstoreEventArgs, register_event_dispatch_hook};
|
use rustfs_ecstore::event_notification::{EventArgs as EcstoreEventArgs, register_event_dispatch_hook};
|
||||||
use rustfs_notify::EventArgs as NotifyEventArgs;
|
use rustfs_notify::EventArgs as NotifyEventArgs;
|
||||||
use rustfs_s3_common::EventName;
|
use rustfs_s3_common::EventName;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tracing::{error, info, instrument, warn};
|
use tracing::{error, info, instrument, warn};
|
||||||
|
|
||||||
|
static NOTIFY_MODULE_ENABLED: AtomicBool = AtomicBool::new(rustfs_config::DEFAULT_NOTIFY_ENABLE);
|
||||||
|
|
||||||
fn server_config_from_context() -> Option<rustfs_ecstore::config::Config> {
|
fn server_config_from_context() -> Option<rustfs_ecstore::config::Config> {
|
||||||
resolve_server_config()
|
resolve_server_config()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn refresh_notify_module_enabled() -> bool {
|
||||||
|
let enabled = rustfs_utils::get_env_bool(rustfs_config::ENV_NOTIFY_ENABLE, rustfs_config::DEFAULT_NOTIFY_ENABLE);
|
||||||
|
NOTIFY_MODULE_ENABLED.store(enabled, Ordering::Relaxed);
|
||||||
|
enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_notify_module_enabled() -> bool {
|
||||||
|
NOTIFY_MODULE_ENABLED.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
fn convert_ecstore_event_args(args: EcstoreEventArgs) -> NotifyEventArgs {
|
fn convert_ecstore_event_args(args: EcstoreEventArgs) -> NotifyEventArgs {
|
||||||
let version_id = args.object.version_id.map(|v| v.to_string()).unwrap_or_default();
|
let version_id = args.object.version_id.map(|v| v.to_string()).unwrap_or_default();
|
||||||
let (host, port) = match args.host.rsplit_once(':') {
|
let (host, port) = match args.host.rsplit_once(':') {
|
||||||
@@ -85,6 +98,15 @@ pub async fn shutdown_event_notifier() {
|
|||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
pub async fn init_event_notifier() {
|
pub async fn init_event_notifier() {
|
||||||
|
let enabled = refresh_notify_module_enabled();
|
||||||
|
if !enabled {
|
||||||
|
info!(
|
||||||
|
target: "rustfs::main::init_event_notifier",
|
||||||
|
"Notify module is disabled by RUSTFS_NOTIFY_ENABLE=false, event notifier initialization is skipped."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
target: "rustfs::main::init_event_notifier",
|
target: "rustfs::main::init_event_notifier",
|
||||||
"Initializing event notifier..."
|
"Initializing event notifier..."
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ mod service_state;
|
|||||||
pub mod tls_material;
|
pub mod tls_material;
|
||||||
|
|
||||||
// Items used by main.rs (binary crate) and/or embedded.rs — must be fully pub.
|
// Items used by main.rs (binary crate) and/or embedded.rs — must be fully pub.
|
||||||
pub use audit::{start_audit_system, stop_audit_system};
|
pub use audit::{is_audit_module_enabled, refresh_audit_module_enabled, start_audit_system, stop_audit_system};
|
||||||
pub use event::{init_event_notifier, shutdown_event_notifier};
|
pub use event::{init_event_notifier, is_notify_module_enabled, refresh_notify_module_enabled, shutdown_event_notifier};
|
||||||
pub use http::start_http_server;
|
pub use http::start_http_server;
|
||||||
pub use prefix::LOGO;
|
pub use prefix::LOGO;
|
||||||
pub use runtime::build_tokio_runtime;
|
pub use runtime::build_tokio_runtime;
|
||||||
|
|||||||
+249
-125
@@ -12,6 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::server::{is_audit_module_enabled, is_notify_module_enabled};
|
||||||
use crate::storage::access::{ReqInfo, request_context_from_req};
|
use crate::storage::access::{ReqInfo, request_context_from_req};
|
||||||
use crate::storage::request_context::{RequestContext, extract_request_id_from_headers};
|
use crate::storage::request_context::{RequestContext, extract_request_id_from_headers};
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
@@ -70,7 +71,14 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A unified helper structure for building and distributing audit logs and event notifications via RAII mode at the end of an S3 operation scope.
|
/// A unified helper structure for building and distributing audit logs and event notifications via RAII mode at the end of an S3 operation scope.
|
||||||
pub struct OperationHelper {
|
pub enum OperationHelper {
|
||||||
|
Disabled,
|
||||||
|
Enabled(Box<EnabledOperationHelper>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EnabledOperationHelper {
|
||||||
|
audit_enabled: bool,
|
||||||
|
notify_enabled: bool,
|
||||||
audit_builder: Option<AuditEntryBuilder>,
|
audit_builder: Option<AuditEntryBuilder>,
|
||||||
api_builder: ApiDetailsBuilder,
|
api_builder: ApiDetailsBuilder,
|
||||||
event_builder: Option<EventArgsBuilder>,
|
event_builder: Option<EventArgsBuilder>,
|
||||||
@@ -81,7 +89,17 @@ pub struct OperationHelper {
|
|||||||
impl OperationHelper {
|
impl OperationHelper {
|
||||||
/// Create a new OperationHelper for S3 requests.
|
/// Create a new OperationHelper for S3 requests.
|
||||||
pub fn new(req: &S3Request<impl Send + Sync>, event: EventName, op: S3Operation) -> Self {
|
pub fn new(req: &S3Request<impl Send + Sync>, event: EventName, op: S3Operation) -> Self {
|
||||||
counter!("rustfs.log.chain.audit.total").increment(1);
|
let audit_enabled = is_audit_module_enabled();
|
||||||
|
let notify_enabled = is_notify_module_enabled();
|
||||||
|
|
||||||
|
// Fast path: when both chains are disabled, avoid all request parsing/builder work.
|
||||||
|
if !audit_enabled && !notify_enabled {
|
||||||
|
return Self::Disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if audit_enabled {
|
||||||
|
counter!("rustfs.log.chain.audit.total").increment(1);
|
||||||
|
}
|
||||||
// Parse path -> bucket/object
|
// Parse path -> bucket/object
|
||||||
let path = req.uri.path().trim_start_matches('/');
|
let path = req.uri.path().trim_start_matches('/');
|
||||||
let mut segs = path.splitn(2, '/');
|
let mut segs = path.splitn(2, '/');
|
||||||
@@ -131,13 +149,19 @@ impl OperationHelper {
|
|||||||
.map(|ctx| ctx.request_id.clone())
|
.map(|ctx| ctx.request_id.clone())
|
||||||
.unwrap_or_else(|| extract_request_id_from_headers(&req.headers));
|
.unwrap_or_else(|| extract_request_id_from_headers(&req.headers));
|
||||||
|
|
||||||
let audit_builder = AuditEntryBuilder::new("1.0", event, trigger, ApiDetails::default())
|
let audit_builder = if audit_enabled {
|
||||||
.remote_host(remote_host)
|
Some(
|
||||||
.user_agent(get_request_user_agent(&req.headers))
|
AuditEntryBuilder::new("1.0", event, trigger, ApiDetails::default())
|
||||||
.req_host(get_request_host(&req.headers))
|
.remote_host(remote_host)
|
||||||
.req_path(req.uri.path().to_string())
|
.user_agent(get_request_user_agent(&req.headers))
|
||||||
.req_query(extract_req_params(req))
|
.req_host(get_request_host(&req.headers))
|
||||||
.request_id(&request_id);
|
.req_path(req.uri.path().to_string())
|
||||||
|
.req_query(extract_req_params(req))
|
||||||
|
.request_id(&request_id),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let event_object = ObjectInfo {
|
let event_object = ObjectInfo {
|
||||||
bucket: bucket.clone(),
|
bucket: bucket.clone(),
|
||||||
@@ -162,54 +186,67 @@ impl OperationHelper {
|
|||||||
|
|
||||||
// initialize event builder
|
// initialize event builder
|
||||||
// object is a placeholder that must be set later using the `object()` method.
|
// object is a placeholder that must be set later using the `object()` method.
|
||||||
let mut event_builder = EventArgsBuilder::new(event, bucket, event_object)
|
let event_builder = if notify_enabled {
|
||||||
.host(get_request_host(&req.headers))
|
let mut event_builder = EventArgsBuilder::new(event, bucket, event_object)
|
||||||
.port(get_request_port(&req.headers))
|
.host(get_request_host(&req.headers))
|
||||||
.user_agent(get_request_user_agent(&req.headers))
|
.port(get_request_port(&req.headers))
|
||||||
.req_params(req_params);
|
.user_agent(get_request_user_agent(&req.headers))
|
||||||
if let Some(version_id) = req_info
|
.req_params(req_params);
|
||||||
.and_then(|info| info.version_id.clone())
|
if let Some(version_id) = req_info
|
||||||
.filter(|value| !value.is_empty())
|
.and_then(|info| info.version_id.clone())
|
||||||
{
|
.filter(|value| !value.is_empty())
|
||||||
event_builder = event_builder.version_id(version_id);
|
{
|
||||||
}
|
event_builder = event_builder.version_id(version_id);
|
||||||
|
}
|
||||||
|
Some(event_builder)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self::Enabled(Box::new(EnabledOperationHelper {
|
||||||
audit_builder: Some(audit_builder),
|
audit_enabled,
|
||||||
|
notify_enabled,
|
||||||
|
audit_builder,
|
||||||
api_builder,
|
api_builder,
|
||||||
event_builder: Some(event_builder),
|
event_builder,
|
||||||
start_time: request_context
|
start_time: request_context
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|ctx| ctx.start_time)
|
.map(|ctx| ctx.start_time)
|
||||||
.unwrap_or_else(std::time::Instant::now),
|
.unwrap_or_else(std::time::Instant::now),
|
||||||
request_context,
|
request_context,
|
||||||
}
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the ObjectInfo for event notification.
|
/// Sets the ObjectInfo for event notification.
|
||||||
pub fn object(mut self, object_info: ObjectInfo) -> Self {
|
pub fn object(mut self, object_info: ObjectInfo) -> Self {
|
||||||
if let Some(builder) = self.event_builder.take() {
|
if let Self::Enabled(state) = &mut self
|
||||||
self.event_builder = Some(builder.object(object_info));
|
&& let Some(builder) = state.event_builder.take()
|
||||||
|
{
|
||||||
|
state.event_builder = Some(builder.object(object_info));
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the version ID for event notifications.
|
/// Set the version ID for event notifications.
|
||||||
pub fn version_id(mut self, version_id: impl Into<String>) -> Self {
|
pub fn version_id(mut self, version_id: impl Into<String>) -> Self {
|
||||||
if let Some(builder) = self.event_builder.take() {
|
if let Self::Enabled(state) = &mut self
|
||||||
self.event_builder = Some(builder.version_id(version_id));
|
&& let Some(builder) = state.event_builder.take()
|
||||||
|
{
|
||||||
|
state.event_builder = Some(builder.version_id(version_id));
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the event name for event notifications.
|
/// Set the event name for event notifications.
|
||||||
pub fn event_name(mut self, event_name: EventName) -> Self {
|
pub fn event_name(mut self, event_name: EventName) -> Self {
|
||||||
if let Some(builder) = self.event_builder.take() {
|
if let Self::Enabled(state) = &mut self {
|
||||||
self.event_builder = Some(builder.event_name(event_name));
|
if let Some(builder) = state.event_builder.take() {
|
||||||
}
|
state.event_builder = Some(builder.event_name(event_name));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(builder) = self.audit_builder.take() {
|
if let Some(builder) = state.audit_builder.take() {
|
||||||
self.audit_builder = Some(builder.event(event_name));
|
state.audit_builder = Some(builder.event(event_name));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -219,19 +256,26 @@ impl OperationHelper {
|
|||||||
/// This method should be called immediately before the function returns.
|
/// This method should be called immediately before the function returns.
|
||||||
/// It consumes and prepares auxiliary structures for use during `drop`.
|
/// It consumes and prepares auxiliary structures for use during `drop`.
|
||||||
pub fn complete<T>(mut self, result: &S3Result<S3Response<T>>) -> Self {
|
pub fn complete<T>(mut self, result: &S3Result<S3Response<T>>) -> Self {
|
||||||
// Complete audit log
|
let Self::Enabled(state) = &mut self else {
|
||||||
if let Some(builder) = self.audit_builder.take() {
|
return self;
|
||||||
let (status, status_code, error_msg) = match result {
|
};
|
||||||
Ok(res) => ("success".to_string(), res.status.unwrap_or(StatusCode::OK).as_u16() as i32, None),
|
|
||||||
Err(e) => (
|
|
||||||
"failure".to_string(),
|
|
||||||
e.status_code().unwrap_or(StatusCode::BAD_REQUEST).as_u16() as i32,
|
|
||||||
e.message().map(|s| s.to_string()),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let ttr = self.start_time.elapsed();
|
let (status, status_code, error_msg) = match result {
|
||||||
let api_details = self
|
Ok(res) => ("success".to_string(), res.status.unwrap_or(StatusCode::OK).as_u16() as i32, None),
|
||||||
|
Err(e) => (
|
||||||
|
"failure".to_string(),
|
||||||
|
e.status_code().unwrap_or(StatusCode::BAD_REQUEST).as_u16() as i32,
|
||||||
|
e.message().map(|s| s.to_string()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
state.api_builder = state.api_builder.clone().status(status.clone()).status_code(status_code);
|
||||||
|
|
||||||
|
// Complete audit log
|
||||||
|
if state.audit_enabled
|
||||||
|
&& let Some(builder) = state.audit_builder.take()
|
||||||
|
{
|
||||||
|
let ttr = state.start_time.elapsed();
|
||||||
|
let api_details = state
|
||||||
.api_builder
|
.api_builder
|
||||||
.clone()
|
.clone()
|
||||||
.status(status)
|
.status(status)
|
||||||
@@ -253,7 +297,7 @@ impl OperationHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inject OpenTelemetry trace context into audit tags for distributed tracing correlation
|
// Inject OpenTelemetry trace context into audit tags for distributed tracing correlation
|
||||||
if let Some(ref ctx) = self.request_context
|
if let Some(ref ctx) = state.request_context
|
||||||
&& (ctx.trace_id.is_some() || ctx.span_id.is_some())
|
&& (ctx.trace_id.is_some() || ctx.span_id.is_some())
|
||||||
{
|
{
|
||||||
let mut tags = HashMap::new();
|
let mut tags = HashMap::new();
|
||||||
@@ -266,13 +310,15 @@ impl OperationHelper {
|
|||||||
final_builder = final_builder.tags(tags);
|
final_builder = final_builder.tags(tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.audit_builder = Some(final_builder);
|
state.audit_builder = Some(final_builder);
|
||||||
self.api_builder = ApiDetailsBuilder(api_details); // Store final details for Drop use
|
state.api_builder = ApiDetailsBuilder(api_details); // Store final details for Drop use
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completion event notification (only on success)
|
// Completion event notification (only on success)
|
||||||
if let (Some(builder), Ok(res)) = (self.event_builder.take(), result) {
|
if state.notify_enabled
|
||||||
self.event_builder = Some(builder.resp_elements(extract_resp_elements(res)));
|
&& let (Some(builder), Ok(res)) = (state.event_builder.take(), result)
|
||||||
|
{
|
||||||
|
state.event_builder = Some(builder.resp_elements(extract_resp_elements(res)));
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -280,29 +326,38 @@ impl OperationHelper {
|
|||||||
|
|
||||||
/// Suppresses the automatic event notification on drop.
|
/// Suppresses the automatic event notification on drop.
|
||||||
pub fn suppress_event(mut self) -> Self {
|
pub fn suppress_event(mut self) -> Self {
|
||||||
self.event_builder = None;
|
if let Self::Enabled(state) = &mut self {
|
||||||
|
state.event_builder = None;
|
||||||
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for OperationHelper {
|
impl Drop for OperationHelper {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
let Self::Enabled(state) = self else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
// Distribute audit logs
|
// Distribute audit logs
|
||||||
if let Some(builder) = self.audit_builder.take() {
|
if state.audit_enabled
|
||||||
let ctx = self.request_context.clone();
|
&& let Some(builder) = state.audit_builder.take()
|
||||||
|
{
|
||||||
|
let ctx = state.request_context.clone();
|
||||||
spawn_background_with_context(ctx, async move {
|
spawn_background_with_context(ctx, async move {
|
||||||
AuditLogger::log(builder.build()).await;
|
AuditLogger::log(builder.build()).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Distribute event notification (only on success)
|
// Distribute event notification (only on success)
|
||||||
if self.api_builder.0.status.as_deref() == Some("success")
|
if state.notify_enabled
|
||||||
&& let Some(builder) = self.event_builder.take()
|
&& state.api_builder.0.status.as_deref() == Some("success")
|
||||||
|
&& let Some(builder) = state.event_builder.take()
|
||||||
{
|
{
|
||||||
let event_args = builder.build();
|
let event_args = builder.build();
|
||||||
// Avoid generating notifications for copy requests
|
// Avoid generating notifications for copy requests
|
||||||
if !event_args.is_replication_request() {
|
if !event_args.is_replication_request() {
|
||||||
let ctx = self.request_context.clone();
|
let ctx = state.request_context.clone();
|
||||||
spawn_background_with_context(ctx, async move {
|
spawn_background_with_context(ctx, async move {
|
||||||
notifier_global::notify(event_args).await;
|
notifier_global::notify(event_args).await;
|
||||||
});
|
});
|
||||||
@@ -314,9 +369,11 @@ impl Drop for OperationHelper {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::server::{refresh_audit_module_enabled, refresh_notify_module_enabled};
|
||||||
use http::{Extensions, HeaderMap, HeaderValue, Method, Uri};
|
use http::{Extensions, HeaderMap, HeaderValue, Method, Uri};
|
||||||
use rustfs_credentials::Credentials;
|
use rustfs_credentials::Credentials;
|
||||||
use s3s::dto::DeleteObjectTaggingInput;
|
use s3s::dto::DeleteObjectTaggingInput;
|
||||||
|
use temp_env::with_vars;
|
||||||
|
|
||||||
fn build_request<T>(input: T, method: Method, uri: Uri) -> S3Request<T> {
|
fn build_request<T>(input: T, method: Method, uri: Uri) -> S3Request<T> {
|
||||||
S3Request {
|
S3Request {
|
||||||
@@ -334,91 +391,158 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn operation_helper_uses_req_info_for_notification_context() {
|
fn operation_helper_uses_req_info_for_notification_context() {
|
||||||
let input = DeleteObjectTaggingInput::builder()
|
with_vars(
|
||||||
.bucket("input-bucket".to_string())
|
[
|
||||||
.key("input-object".to_string())
|
(rustfs_config::ENV_NOTIFY_ENABLE, Some("true")),
|
||||||
.build()
|
(rustfs_config::ENV_AUDIT_ENABLE, Some("true")),
|
||||||
.unwrap();
|
],
|
||||||
let mut req = build_request(input, Method::DELETE, Uri::from_static("/from-uri/ignored"));
|
|| {
|
||||||
req.headers.insert("host", HeaderValue::from_static("example.com"));
|
refresh_notify_module_enabled();
|
||||||
req.headers.insert("user-agent", HeaderValue::from_static("rustfs-test"));
|
refresh_audit_module_enabled();
|
||||||
req.extensions.insert(ReqInfo {
|
let input = DeleteObjectTaggingInput::builder()
|
||||||
cred: Some(Credentials {
|
.bucket("input-bucket".to_string())
|
||||||
access_key: "notifyTag".to_string(),
|
.key("input-object".to_string())
|
||||||
..Default::default()
|
.build()
|
||||||
}),
|
.unwrap();
|
||||||
bucket: Some("issue-2292-bucket".to_string()),
|
let mut req = build_request(input, Method::DELETE, Uri::from_static("/from-uri/ignored"));
|
||||||
object: Some("prefix/issue-2292.txt".to_string()),
|
req.headers.insert("host", HeaderValue::from_static("example.com"));
|
||||||
version_id: Some("version-123".to_string()),
|
req.headers.insert("user-agent", HeaderValue::from_static("rustfs-test"));
|
||||||
..Default::default()
|
req.extensions.insert(ReqInfo {
|
||||||
});
|
cred: Some(Credentials {
|
||||||
|
access_key: "notifyTag".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
bucket: Some("issue-2292-bucket".to_string()),
|
||||||
|
object: Some("prefix/issue-2292.txt".to_string()),
|
||||||
|
version_id: Some("version-123".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
let helper = OperationHelper::new(&req, EventName::ObjectTaggingPut, S3Operation::PutObjectTagging);
|
let helper = OperationHelper::new(&req, EventName::ObjectTaggingPut, S3Operation::PutObjectTagging);
|
||||||
let event_args = helper.event_builder.clone().expect("event builder should exist").build();
|
let event_args = match &helper {
|
||||||
|
OperationHelper::Enabled(state) => state.event_builder.clone().expect("event builder should exist").build(),
|
||||||
|
OperationHelper::Disabled => panic!("helper should be enabled when notify/audit switches are on"),
|
||||||
|
};
|
||||||
|
|
||||||
assert_eq!(event_args.bucket_name, "issue-2292-bucket");
|
assert_eq!(event_args.bucket_name, "issue-2292-bucket");
|
||||||
assert_eq!(event_args.object.bucket, "issue-2292-bucket");
|
assert_eq!(event_args.object.bucket, "issue-2292-bucket");
|
||||||
assert_eq!(event_args.object.name, "prefix/issue-2292.txt");
|
assert_eq!(event_args.object.name, "prefix/issue-2292.txt");
|
||||||
assert_eq!(event_args.version_id, "version-123");
|
assert_eq!(event_args.version_id, "version-123");
|
||||||
assert_eq!(event_args.req_params.get("principalId").map(String::as_str), Some("notifyTag"));
|
assert_eq!(event_args.req_params.get("principalId").map(String::as_str), Some("notifyTag"));
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn operation_helper_prioritizes_request_context_for_request_id() {
|
fn operation_helper_prioritizes_request_context_for_request_id() {
|
||||||
let input = DeleteObjectTaggingInput::builder()
|
with_vars(
|
||||||
.bucket("test-bucket".to_string())
|
[
|
||||||
.key("test-key".to_string())
|
(rustfs_config::ENV_NOTIFY_ENABLE, Some("true")),
|
||||||
.build()
|
(rustfs_config::ENV_AUDIT_ENABLE, Some("true")),
|
||||||
.unwrap();
|
],
|
||||||
let mut req = build_request(input, Method::DELETE, Uri::from_static("/test-bucket/test-key"));
|
|| {
|
||||||
req.headers.insert("host", HeaderValue::from_static("example.com"));
|
refresh_notify_module_enabled();
|
||||||
req.headers.insert("user-agent", HeaderValue::from_static("rustfs-test"));
|
refresh_audit_module_enabled();
|
||||||
|
|
||||||
// Insert RequestContext (set by ingress layer) with a specific request_id
|
let input = DeleteObjectTaggingInput::builder()
|
||||||
req.extensions.insert(RequestContext {
|
.bucket("test-bucket".to_string())
|
||||||
request_id: "ingress-canonical-uuid".to_string(),
|
.key("test-key".to_string())
|
||||||
x_amz_request_id: "ingress-canonical-uuid".to_string(),
|
.build()
|
||||||
trace_id: None,
|
.unwrap();
|
||||||
span_id: None,
|
let mut req = build_request(input, Method::DELETE, Uri::from_static("/test-bucket/test-key"));
|
||||||
start_time: std::time::Instant::now(),
|
req.headers.insert("host", HeaderValue::from_static("example.com"));
|
||||||
});
|
req.headers.insert("user-agent", HeaderValue::from_static("rustfs-test"));
|
||||||
|
|
||||||
req.extensions.insert(ReqInfo {
|
// Insert RequestContext (set by ingress layer) with a specific request_id
|
||||||
bucket: Some("test-bucket".to_string()),
|
req.extensions.insert(RequestContext {
|
||||||
object: Some("test-key".to_string()),
|
request_id: "ingress-canonical-uuid".to_string(),
|
||||||
..Default::default()
|
x_amz_request_id: "ingress-canonical-uuid".to_string(),
|
||||||
});
|
trace_id: None,
|
||||||
|
span_id: None,
|
||||||
|
start_time: std::time::Instant::now(),
|
||||||
|
});
|
||||||
|
|
||||||
let helper = OperationHelper::new(&req, EventName::ObjectAccessedGet, S3Operation::GetObject);
|
req.extensions.insert(ReqInfo {
|
||||||
|
bucket: Some("test-bucket".to_string()),
|
||||||
|
object: Some("test-key".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
// Verify the helper stored the RequestContext
|
let helper = OperationHelper::new(&req, EventName::ObjectAccessedGet, S3Operation::GetObject);
|
||||||
assert!(helper.request_context.is_some());
|
|
||||||
assert_eq!(helper.request_context.as_ref().unwrap().request_id, "ingress-canonical-uuid");
|
// Verify the helper stored the RequestContext
|
||||||
|
match &helper {
|
||||||
|
OperationHelper::Enabled(state) => {
|
||||||
|
assert!(state.request_context.is_some());
|
||||||
|
assert_eq!(state.request_context.as_ref().unwrap().request_id, "ingress-canonical-uuid");
|
||||||
|
}
|
||||||
|
OperationHelper::Disabled => panic!("helper should be enabled when notify/audit switches are on"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn operation_helper_no_request_context_when_absent() {
|
fn operation_helper_no_request_context_when_absent() {
|
||||||
let input = DeleteObjectTaggingInput::builder()
|
with_vars(
|
||||||
.bucket("test-bucket".to_string())
|
[
|
||||||
.key("test-key".to_string())
|
(rustfs_config::ENV_NOTIFY_ENABLE, Some("true")),
|
||||||
.build()
|
(rustfs_config::ENV_AUDIT_ENABLE, Some("true")),
|
||||||
.unwrap();
|
],
|
||||||
let mut req = build_request(input, Method::DELETE, Uri::from_static("/test-bucket/test-key"));
|
|| {
|
||||||
req.headers.insert("host", HeaderValue::from_static("example.com"));
|
refresh_notify_module_enabled();
|
||||||
req.headers.insert("user-agent", HeaderValue::from_static("rustfs-test"));
|
refresh_audit_module_enabled();
|
||||||
req.headers
|
|
||||||
.insert("x-amz-request-id", HeaderValue::from_static("amz-header-uuid"));
|
|
||||||
|
|
||||||
// No RequestContext inserted
|
let input = DeleteObjectTaggingInput::builder()
|
||||||
req.extensions.insert(ReqInfo {
|
.bucket("test-bucket".to_string())
|
||||||
bucket: Some("test-bucket".to_string()),
|
.key("test-key".to_string())
|
||||||
object: Some("test-key".to_string()),
|
.build()
|
||||||
..Default::default()
|
.unwrap();
|
||||||
});
|
let mut req = build_request(input, Method::DELETE, Uri::from_static("/test-bucket/test-key"));
|
||||||
|
req.headers.insert("host", HeaderValue::from_static("example.com"));
|
||||||
|
req.headers.insert("user-agent", HeaderValue::from_static("rustfs-test"));
|
||||||
|
req.headers
|
||||||
|
.insert("x-amz-request-id", HeaderValue::from_static("amz-header-uuid"));
|
||||||
|
|
||||||
let helper = OperationHelper::new(&req, EventName::ObjectAccessedGet, S3Operation::GetObject);
|
// No RequestContext inserted
|
||||||
|
req.extensions.insert(ReqInfo {
|
||||||
|
bucket: Some("test-bucket".to_string()),
|
||||||
|
object: Some("test-key".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
// Verify the helper has no RequestContext
|
let helper = OperationHelper::new(&req, EventName::ObjectAccessedGet, S3Operation::GetObject);
|
||||||
assert!(helper.request_context.is_none());
|
|
||||||
|
// Verify the helper has no RequestContext
|
||||||
|
match &helper {
|
||||||
|
OperationHelper::Enabled(state) => assert!(state.request_context.is_none()),
|
||||||
|
OperationHelper::Disabled => panic!("helper should be enabled when notify/audit switches are on"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn operation_helper_returns_disabled_when_both_switches_off() {
|
||||||
|
with_vars(
|
||||||
|
[
|
||||||
|
(rustfs_config::ENV_NOTIFY_ENABLE, Some("false")),
|
||||||
|
(rustfs_config::ENV_AUDIT_ENABLE, Some("false")),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
refresh_notify_module_enabled();
|
||||||
|
refresh_audit_module_enabled();
|
||||||
|
|
||||||
|
let input = DeleteObjectTaggingInput::builder()
|
||||||
|
.bucket("test-bucket".to_string())
|
||||||
|
.key("test-key".to_string())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let req = build_request(input, Method::DELETE, Uri::from_static("/test-bucket/test-key"));
|
||||||
|
let helper = OperationHelper::new(&req, EventName::ObjectAccessedGet, S3Operation::GetObject);
|
||||||
|
|
||||||
|
assert!(matches!(helper, OperationHelper::Disabled));
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user