use crate::{AuditEntry, AuditError, AuditResult, factory::builtin_target_plugins};
use rustfs_config::audit::AUDIT_ROUTE_PREFIX;
use rustfs_config::server_config::{Config, KVS};
use rustfs_targets::arn::TargetID;
use rustfs_targets::{SharedTarget, Target, TargetError, TargetPluginRegistry, TargetRuntimeManager};
use tracing::info;
const LOG_COMPONENT_AUDIT: &str = "audit";
const LOG_SUBSYSTEM_REGISTRY: &str = "registry";
const EVENT_AUDIT_TARGET_REGISTRY_KEY_CREATED: &str = "audit_target_registry_key_created";
const EVENT_AUDIT_TARGET_REGISTRY_STATE: &str = "audit_target_registry_state";
pub struct AuditRegistry {
targets: TargetRuntimeManager<AuditEntry>,
plugins: TargetPluginRegistry<AuditEntry>,
}
impl Default for AuditRegistry {
fn default() -> Self {
Self::new()
}
}
impl AuditRegistry {
pub fn new() -> Self {
let mut plugins = TargetPluginRegistry::new();
plugins.register_all(builtin_target_plugins());
AuditRegistry {
targets: TargetRuntimeManager::new(),
plugins,
}
}
pub fn supports_target_type(&self, target_type: &str) -> bool {
self.plugins.supports_target_type(target_type)
}
pub async fn create_target(
&self,
target_type: &str,
id: String,
config: &KVS,
) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
self.plugins.create_target(target_type, id, config)
}
pub async fn create_audit_targets_from_config(
&self,
config: &Config,
) -> AuditResult<Vec<Box<dyn Target<AuditEntry> + Send + Sync>>> {
self.plugins
.create_targets_from_config(config, AUDIT_ROUTE_PREFIX)
.await
.map_err(AuditError::from)
}
pub fn add_target(&mut self, _id: String, target: Box<dyn Target<AuditEntry> + Send + Sync>) {
debug_assert_eq!(_id, target.id().to_string());
self.targets.add_boxed(target);
}
pub fn add_shared_target(&mut self, _id: String, target: SharedTarget<AuditEntry>) {
debug_assert_eq!(_id, target.id().to_string());
self.targets.add_arc(target);
}
pub async fn remove_target(&mut self, id: &str) -> Option<SharedTarget<AuditEntry>> {
self.targets.remove_and_close(id).await
}
pub fn get_target(&self, id: &str) -> Option<SharedTarget<AuditEntry>> {
self.targets.get(id)
}
pub fn list_target_values(&self) -> Vec<SharedTarget<AuditEntry>> {
self.targets.values()
}
pub fn runtime_manager(&self) -> &TargetRuntimeManager<AuditEntry> {
&self.targets
}
pub fn runtime_manager_mut(&mut self) -> &mut TargetRuntimeManager<AuditEntry> {
&mut self.targets
}
pub fn list_targets(&self) -> Vec<String> {
self.targets.keys()
}
pub async fn close_all(&mut self) -> AuditResult<()> {
let mut first_error = None;
for target_id in self.targets.keys() {
if let Some(target) = self.targets.remove(&target_id)
&& let Err(err) = target.close().await
{
tracing::error!(
event = EVENT_AUDIT_TARGET_REGISTRY_STATE,
component = LOG_COMPONENT_AUDIT,
subsystem = LOG_SUBSYSTEM_REGISTRY,
target_id = %target_id,
state = "close_failed",
error = %err,
"Failed to close target during shutdown"
);
if first_error.is_none() {
first_error = Some(err);
}
}
}
match first_error {
Some(err) => Err(AuditError::Target(err)),
None => Ok(()),
}
}
pub fn create_key(&self, target_type: &str, target_id: &str) -> String {
let key = TargetID::new(target_id.to_string(), target_type.to_string());
info!(
event = EVENT_AUDIT_TARGET_REGISTRY_KEY_CREATED,
component = LOG_COMPONENT_AUDIT,
subsystem = LOG_SUBSYSTEM_REGISTRY,
target_type = %target_type,
target_id = %target_id,
registry_key = %key,
"audit target registry state"
);
key.to_string()
}
pub fn enable_target(&self, target_type: &str, target_id: &str) -> AuditResult<()> {
let key = self.create_key(target_type, target_id);
if self.get_target(&key).is_some() {
info!(
event = EVENT_AUDIT_TARGET_REGISTRY_STATE,
component = LOG_COMPONENT_AUDIT,
subsystem = LOG_SUBSYSTEM_REGISTRY,
target_type = %target_type,
target_id = %target_id,
state = "enabled",
"audit target registry state"
);
Ok(())
} else {
Err(AuditError::Configuration(
format!("Target not found: {}-{}", target_type, target_id),
None,
))
}
}
pub fn disable_target(&self, target_type: &str, target_id: &str) -> AuditResult<()> {
let key = self.create_key(target_type, target_id);
if self.get_target(&key).is_some() {
info!(
event = EVENT_AUDIT_TARGET_REGISTRY_STATE,
component = LOG_COMPONENT_AUDIT,
subsystem = LOG_SUBSYSTEM_REGISTRY,
target_type = %target_type,
target_id = %target_id,
state = "disabled",
"audit target registry state"
);
Ok(())
} else {
Err(AuditError::Configuration(
format!("Target not found: {}-{}", target_type, target_id),
None,
))
}
}
pub fn upsert_target(
&mut self,
target_type: &str,
target_id: &str,
target: Box<dyn Target<AuditEntry> + Send + Sync>,
) -> AuditResult<()> {
let key = self.create_key(target_type, target_id);
debug_assert_eq!(key, target.id().to_string());
self.targets.add_boxed(target);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::AuditRegistry;
use crate::AuditError;
use rustfs_targets::TargetError;
use rustfs_targets::target::ChannelTargetType;
use rustfs_targets::testkit::MockTarget;
#[test]
fn registry_registers_amqp_factory() {
let registry = AuditRegistry::new();
assert!(registry.supports_target_type(ChannelTargetType::Amqp.as_str()));
}
#[tokio::test]
async fn close_all_returns_first_error_and_clears_targets() {
let mut registry = AuditRegistry::new();
let ok = MockTarget::new("ok", "webhook");
let ok_observer = ok.clone();
let fail = MockTarget::new("fail", "webhook")
.with_close_failures(usize::MAX)
.with_close_failure_error(|| TargetError::Unknown("close failed".to_string()));
let fail_observer = fail.clone();
registry.add_target(ok.target_id().to_string(), Box::new(ok));
registry.add_target(fail.target_id().to_string(), Box::new(fail));
let result = registry.close_all().await;
assert!(matches!(result, Err(AuditError::Target(TargetError::Unknown(_)))));
assert_eq!(ok_observer.close_call_count(), 1);
assert_eq!(fail_observer.close_call_count(), 1);
assert!(registry.list_targets().is_empty());
}
}