fix: populate tagging notification principalId and object metadata (#2342)

This commit is contained in:
houseme
2026-03-30 14:32:39 +08:00
committed by GitHub
parent 387c385dfa
commit 16db18216d
11 changed files with 481 additions and 365 deletions
+117 -3
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use chrono::{DateTime, Utc};
use chrono::{DateTime, SecondsFormat, Utc};
use hashbrown::HashMap;
use rustfs_s3_common::EventName;
use serde::{Deserialize, Serialize};
@@ -90,6 +90,21 @@ pub struct Source {
pub user_agent: String,
}
/// Additional data included for restore-completed events.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GlacierEventData {
pub restore_event_data: RestoreEventData,
}
/// Restore-specific event attributes for `s3:ObjectRestore:Completed`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RestoreEventData {
pub lifecycle_restoration_expiry_time: String,
pub lifecycle_restore_storage_class: String,
}
/// Represents a storage event
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -112,11 +127,33 @@ pub struct Event {
pub response_elements: HashMap<String, String>,
/// Metadata about the event
pub s3: Metadata,
/// Additional restore event data when present.
#[serde(skip_serializing_if = "Option::is_none")]
pub glacier_event_data: Option<GlacierEventData>,
/// Information about the source of the event
pub source: Source,
}
impl Event {
fn event_version_for(event_name: EventName) -> &'static str {
match event_name {
EventName::ObjectReplicationFailed
| EventName::ObjectReplicationComplete
| EventName::ObjectReplicationMissedThreshold
| EventName::ObjectReplicationReplicatedAfterThreshold
| EventName::ObjectReplicationNotTracked => "2.2",
EventName::ObjectRestoreCompleted
| EventName::ObjectAclPut
| EventName::ObjectTaggingPut
| EventName::ObjectTaggingDelete
| EventName::LifecycleExpirationDelete
| EventName::LifecycleExpirationDeleteMarkerCreated
| EventName::LifecycleTransition
| EventName::IntelligentTiering => "2.3",
_ => "2.1",
}
}
/// Creates a test event for a given bucket and object
pub fn new_test_event(bucket: &str, key: &str, event_name: EventName) -> Self {
let mut user_metadata = HashMap::new();
@@ -139,7 +176,7 @@ impl Event {
user_metadata.insert("x-request-time".to_string(), Utc::now().to_rfc3339());
Event {
event_version: "2.1".to_string(),
event_version: Self::event_version_for(event_name).to_string(),
event_source: "rustfs:s3".to_string(),
aws_region: "us-east-1".to_string(),
event_time: Utc::now(),
@@ -169,6 +206,7 @@ impl Event {
sequencer: "0055AED6DCD90281E5".to_string(),
},
},
glacier_event_data: None,
source: Source {
host: "127.0.0.1".to_string(),
port: "9000".to_string(),
@@ -237,8 +275,25 @@ impl Event {
s3_metadata.object.user_metadata = Some(user_metadata);
}
let glacier_event_data = if args.event_name == EventName::ObjectRestoreCompleted {
args.object.restore_expires.and_then(|expiry| {
let expiry_time = DateTime::<Utc>::from_timestamp(expiry.unix_timestamp(), expiry.nanosecond())?;
let storage_class = args.object.storage_class.clone().or_else(|| {
(!args.object.transitioned_object.tier.is_empty()).then_some(args.object.transitioned_object.tier.clone())
})?;
Some(GlacierEventData {
restore_event_data: RestoreEventData {
lifecycle_restoration_expiry_time: expiry_time.to_rfc3339_opts(SecondsFormat::Millis, true),
lifecycle_restore_storage_class: storage_class,
},
})
})
} else {
None
};
Self {
event_version: "2.1".to_string(),
event_version: Self::event_version_for(args.event_name).to_string(),
event_source: "rustfs:s3".to_string(),
aws_region: args.req_params.get("region").cloned().unwrap_or_default(),
event_time: event_time.and_utc(),
@@ -247,6 +302,7 @@ impl Event {
request_parameters: args.req_params,
response_elements: resp_elements,
s3: s3_metadata,
glacier_event_data,
source: Source {
host: args.host,
port: if args.port == 0 {
@@ -410,3 +466,61 @@ impl EventArgsBuilder {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_test_event_uses_aws_compatible_event_versions() {
let acl_event = Event::new_test_event("bucket", "key", EventName::ObjectAclPut);
assert_eq!(acl_event.event_version, "2.3");
let tagging_event = Event::new_test_event("bucket", "key", EventName::ObjectTaggingPut);
assert_eq!(tagging_event.event_version, "2.3");
let lifecycle_event = Event::new_test_event("bucket", "key", EventName::LifecycleExpirationDelete);
assert_eq!(lifecycle_event.event_version, "2.3");
let put_event = Event::new_test_event("bucket", "key", EventName::ObjectCreatedPut);
assert_eq!(put_event.event_version, "2.1");
}
#[test]
fn event_new_uses_aws_compatible_event_versions() {
let args = EventArgsBuilder::new(
EventName::LifecycleTransition,
"bucket",
rustfs_ecstore::store_api::ObjectInfo {
bucket: "bucket".to_string(),
name: "key".to_string(),
..Default::default()
},
)
.build();
let event = Event::new(args);
assert_eq!(event.event_version, "2.3");
}
#[test]
fn object_restore_completed_includes_glacier_event_data() {
let args = EventArgsBuilder::new(
EventName::ObjectRestoreCompleted,
"bucket",
rustfs_ecstore::store_api::ObjectInfo {
bucket: "bucket".to_string(),
name: "key".to_string(),
restore_expires: Some(time::OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap()),
storage_class: Some("GLACIER".to_string()),
..Default::default()
},
)
.build();
let event = Event::new(args);
assert_eq!(event.event_version, "2.3");
let glacier = event.glacier_event_data.expect("glacier event data should be present");
assert_eq!(glacier.restore_event_data.lifecycle_restoration_expiry_time, "2023-11-14T22:13:20.000Z");
assert_eq!(glacier.restore_event_data.lifecycle_restore_storage_class, "GLACIER");
}
}
+4 -5
View File
@@ -417,16 +417,12 @@ mod integration_tests {
let rules_map = config.get_rules_map();
// ObjectCreated:* should be expanded to all ObjectCreated events
// AWS ObjectCreated:* should only include object creation operations.
let event_types = [
EventName::ObjectCreatedPut,
EventName::ObjectCreatedPost,
EventName::ObjectCreatedCopy,
EventName::ObjectCreatedCompleteMultipartUpload,
EventName::ObjectCreatedPutRetention,
EventName::ObjectCreatedPutLegalHold,
EventName::ObjectCreatedPutTagging,
EventName::ObjectCreatedDeleteTagging,
];
for event_type in event_types {
@@ -436,5 +432,8 @@ mod integration_tests {
let targets = rules_map.match_rules(event_type, "data/file.csv");
assert!(!targets.is_empty(), "Event {:?} should match", event_type);
}
assert!(!rules_map.has_subscriber(&EventName::ObjectTaggingPut));
assert!(!rules_map.has_subscriber(&EventName::ObjectTaggingDelete));
}
}