mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-22 12:26:37 +00:00
feat(admin): add audit target APIs and harden target source handling (#2350)
Signed-off-by: houseme <housemecn@gmail.com> Co-authored-by: 安正超 <anzhengchao@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,8 @@ use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Formatter;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tracing::warn;
|
||||
|
||||
pub mod mqtt;
|
||||
pub mod webhook;
|
||||
@@ -45,14 +47,43 @@ where
|
||||
/// Saves an event (either sends it immediately or stores it for later)
|
||||
async fn save(&self, event: Arc<EntityTarget<E>>) -> Result<(), TargetError>;
|
||||
|
||||
/// Sends an event from the store
|
||||
async fn send_from_store(&self, key: Key) -> Result<(), TargetError>;
|
||||
/// Sends an event from the store using the queued raw body and metadata.
|
||||
async fn send_raw_from_store(&self, key: Key, body: Vec<u8>, meta: QueuedPayloadMeta) -> Result<(), TargetError>;
|
||||
|
||||
/// Sends an event from the store.
|
||||
async fn send_from_store(&self, key: Key) -> Result<(), TargetError> {
|
||||
let store = self
|
||||
.store()
|
||||
.ok_or_else(|| TargetError::Configuration("No store configured".to_string()))?;
|
||||
|
||||
let raw = match store.get_raw(&key) {
|
||||
Ok(raw) => raw,
|
||||
Err(StoreError::NotFound) => return Ok(()),
|
||||
Err(err) => return Err(TargetError::Storage(format!("Failed to read queued payload from store: {err}"))),
|
||||
};
|
||||
|
||||
let queued = match QueuedPayload::decode(&raw) {
|
||||
Ok(queued) => queued,
|
||||
Err(err) => {
|
||||
delete_stored_payload(store, &key).map_err(|delete_err| {
|
||||
TargetError::Storage(format!(
|
||||
"Failed to delete invalid queued payload {key} after decode error '{err}': {delete_err}"
|
||||
))
|
||||
})?;
|
||||
warn!("Dropped invalid queued payload {key}: {err}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
self.send_raw_from_store(key.clone(), queued.body, queued.meta).await?;
|
||||
delete_stored_payload(store, &key)
|
||||
}
|
||||
|
||||
/// Closes the target and releases resources
|
||||
async fn close(&self) -> Result<(), TargetError>;
|
||||
|
||||
/// Returns the store associated with the target (if any)
|
||||
fn store(&self) -> Option<&(dyn Store<EntityTarget<E>, Error = StoreError, Key = Key> + Send + Sync)>;
|
||||
fn store(&self) -> Option<&(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync)>;
|
||||
|
||||
/// Returns the type of the target
|
||||
fn clone_dyn(&self) -> Box<dyn Target<E> + Send + Sync>;
|
||||
@@ -78,6 +109,106 @@ where
|
||||
pub data: E,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QueuedPayloadMeta {
|
||||
pub event_name: EventName,
|
||||
pub bucket_name: String,
|
||||
pub object_name: String,
|
||||
pub content_type: String,
|
||||
pub queued_at_unix_ms: u64,
|
||||
pub payload_len: usize,
|
||||
}
|
||||
|
||||
impl QueuedPayloadMeta {
|
||||
pub fn new(
|
||||
event_name: EventName,
|
||||
bucket_name: String,
|
||||
object_name: String,
|
||||
content_type: impl Into<String>,
|
||||
payload_len: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
event_name,
|
||||
bucket_name,
|
||||
object_name,
|
||||
content_type: content_type.into(),
|
||||
queued_at_unix_ms: SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64,
|
||||
payload_len,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn best_effort_preview(&self, body: &[u8], limit: usize) -> String {
|
||||
if limit == 0 || body.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let slice = &body[..body.len().min(limit)];
|
||||
match std::str::from_utf8(slice) {
|
||||
Ok(text) => {
|
||||
if body.len() > limit {
|
||||
format!("{text}...")
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
Err(_) => format!("<{} bytes binary>", body.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QueuedPayload {
|
||||
pub meta: QueuedPayloadMeta,
|
||||
pub body: Vec<u8>,
|
||||
}
|
||||
|
||||
impl QueuedPayload {
|
||||
const MAGIC: [u8; 4] = *b"RQP1";
|
||||
|
||||
pub fn new(meta: QueuedPayloadMeta, body: Vec<u8>) -> Self {
|
||||
Self { meta, body }
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Result<Vec<u8>, TargetError> {
|
||||
let meta = serde_json::to_vec(&self.meta)
|
||||
.map_err(|err| TargetError::Serialization(format!("Failed to serialize queued payload metadata: {err}")))?;
|
||||
let meta_len = u32::try_from(meta.len())
|
||||
.map_err(|_| TargetError::Serialization("Queued payload metadata is too large".to_string()))?;
|
||||
|
||||
let mut out = Vec::with_capacity(Self::MAGIC.len() + 4 + meta.len() + self.body.len());
|
||||
out.extend_from_slice(&Self::MAGIC);
|
||||
out.extend_from_slice(&meta_len.to_le_bytes());
|
||||
out.extend_from_slice(&meta);
|
||||
out.extend_from_slice(&self.body);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn decode(raw: &[u8]) -> Result<Self, TargetError> {
|
||||
if raw.len() < Self::MAGIC.len() + 4 {
|
||||
return Err(TargetError::Serialization("Queued payload is too short".to_string()));
|
||||
}
|
||||
if raw[..Self::MAGIC.len()] != Self::MAGIC {
|
||||
return Err(TargetError::Serialization("Queued payload magic mismatch".to_string()));
|
||||
}
|
||||
|
||||
let mut meta_len_bytes = [0u8; 4];
|
||||
meta_len_bytes.copy_from_slice(&raw[Self::MAGIC.len()..Self::MAGIC.len() + 4]);
|
||||
let meta_len = u32::from_le_bytes(meta_len_bytes) as usize;
|
||||
let meta_start = Self::MAGIC.len() + 4;
|
||||
let meta_end = meta_start + meta_len;
|
||||
|
||||
if meta_end > raw.len() {
|
||||
return Err(TargetError::Serialization("Queued payload metadata length exceeds input".to_string()));
|
||||
}
|
||||
|
||||
let meta = serde_json::from_slice(&raw[meta_start..meta_end])
|
||||
.map_err(|err| TargetError::Serialization(format!("Failed to deserialize queued payload metadata: {err}")))?;
|
||||
let body = raw[meta_end..].to_vec();
|
||||
|
||||
Ok(Self { meta, body })
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ChannelTargetType` enum represents the different types of channel Target
|
||||
/// used in the notification system.
|
||||
///
|
||||
@@ -187,3 +318,45 @@ pub fn decode_object_name(encoded: &str) -> Result<String, TargetError> {
|
||||
.map(|s| s.into_owned())
|
||||
.map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {e}")))
|
||||
}
|
||||
|
||||
pub(crate) fn delete_stored_payload(
|
||||
store: &(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send + Sync),
|
||||
key: &Key,
|
||||
) -> Result<(), TargetError> {
|
||||
match store.del(key) {
|
||||
Ok(()) | Err(StoreError::NotFound) => Ok(()),
|
||||
Err(err) => Err(TargetError::Storage(format!("Failed to delete event from store: {err}"))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn queued_payload_round_trips_meta_and_body() {
|
||||
let meta = QueuedPayloadMeta::new(
|
||||
EventName::ObjectCreatedPut,
|
||||
"bucket-a".to_string(),
|
||||
"folder/object.txt".to_string(),
|
||||
"application/json",
|
||||
12,
|
||||
);
|
||||
let payload = QueuedPayload::new(meta.clone(), br#"{"ok":true}"#.to_vec());
|
||||
|
||||
let encoded = payload.encode().unwrap();
|
||||
let decoded = QueuedPayload::decode(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded.meta.event_name, meta.event_name);
|
||||
assert_eq!(decoded.meta.bucket_name, meta.bucket_name);
|
||||
assert_eq!(decoded.meta.object_name, meta.object_name);
|
||||
assert_eq!(decoded.meta.content_type, meta.content_type);
|
||||
assert_eq!(decoded.body, br#"{"ok":true}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_payload_decode_rejects_invalid_magic() {
|
||||
let err = QueuedPayload::decode(b"bad-payload").unwrap_err();
|
||||
assert!(err.to_string().contains("magic") || err.to_string().contains("short"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user