fix(notify): accept case-insensitive filter rule names (#2990)

This commit is contained in:
Henry Guo
2026-05-18 11:41:48 +08:00
committed by GitHub
parent cdfe83877b
commit 17ae9f34c7
5 changed files with 129 additions and 31 deletions
+33
View File
@@ -161,6 +161,39 @@ mod integration_tests {
assert!(targets.is_empty(), "Files not in images/ should not match"); assert!(targets.is_empty(), "Files not in images/ should not match");
} }
#[test]
fn test_capitalized_filter_names_xml() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<NotificationConfiguration>
<QueueConfiguration>
<Id>test-queue</Id>
<Queue>arn:rustfs:sqs:ap-northeast-1:primary:webhook</Queue>
<Event>s3:ObjectCreated:*</Event>
<Filter>
<S3Key>
<FilterRule>
<Name>Prefix</Name>
<Value>uploads/</Value>
</FilterRule>
<FilterRule>
<Name>Suffix</Name>
<Value>.csv</Value>
</FilterRule>
</S3Key>
</Filter>
</QueueConfiguration>
</NotificationConfiguration>"#;
let current_region = "ap-northeast-1";
let arn_list = vec!["arn:rustfs:sqs:ap-northeast-1:primary:webhook".to_string()];
let config = BucketNotificationConfig::from_xml(Cursor::new(xml.as_bytes()), current_region, &arn_list).unwrap();
let rules_map = config.get_rules_map();
let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "uploads/report.csv");
assert!(!targets.is_empty(), "capitalized filter names should still build prefix/suffix rules");
}
/// Test suffix only filter /// Test suffix only filter
#[test] #[test]
fn test_suffix_only_filter_xml() { fn test_suffix_only_filter_xml() {
+5 -5
View File
@@ -66,7 +66,7 @@ pub struct FilterRule {
impl FilterRule { impl FilterRule {
fn validate(&self) -> Result<(), ParseConfigError> { fn validate(&self) -> Result<(), ParseConfigError> {
if self.name != "prefix" && self.name != "suffix" { if !self.name.eq_ignore_ascii_case("prefix") && !self.name.eq_ignore_ascii_case("suffix") {
return Err(ParseConfigError::InvalidFilterName(self.name.clone())); return Err(ParseConfigError::InvalidFilterName(self.name.clone()));
} }
// ValidateFilterRuleValue from Go: // ValidateFilterRuleValue from Go:
@@ -98,12 +98,12 @@ impl S3KeyFilter {
let mut has_suffix = false; let mut has_suffix = false;
for rule in &self.filter_rule_list { for rule in &self.filter_rule_list {
rule.validate()?; rule.validate()?;
if rule.name == "prefix" { if rule.name.eq_ignore_ascii_case("prefix") {
if has_prefix { if has_prefix {
return Err(ParseConfigError::DuplicatePrefixFilter); return Err(ParseConfigError::DuplicatePrefixFilter);
} }
has_prefix = true; has_prefix = true;
} else if rule.name == "suffix" { } else if rule.name.eq_ignore_ascii_case("suffix") {
if has_suffix { if has_suffix {
return Err(ParseConfigError::DuplicateSuffixFilter); return Err(ParseConfigError::DuplicateSuffixFilter);
} }
@@ -126,9 +126,9 @@ impl S3KeyFilter {
let mut suffix_val: Option<&str> = None; let mut suffix_val: Option<&str> = None;
for rule in &self.filter_rule_list { for rule in &self.filter_rule_list {
if rule.name == "prefix" { if rule.name.eq_ignore_ascii_case("prefix") {
prefix_val = Some(&rule.value); prefix_val = Some(&rule.value);
} else if rule.name == "suffix" { } else if rule.name.eq_ignore_ascii_case("suffix") {
suffix_val = Some(&rule.value); suffix_val = Some(&rule.value);
} }
} }
+40 -16
View File
@@ -145,22 +145,18 @@ fn validate_notification_filter_rules(
return Err(s3_error!(InvalidArgument, "{}", invalid_filter_value_message(&cfg_scope, value))); return Err(s3_error!(InvalidArgument, "{}", invalid_filter_value_message(&cfg_scope, value)));
} }
match name.as_str() { if name.as_str().eq_ignore_ascii_case("prefix") {
"prefix" => { if has_prefix {
if has_prefix { return Err(s3_error!(InvalidArgument, "duplicate notification filter name 'prefix' ({cfg_scope})"));
return Err(s3_error!(InvalidArgument, "duplicate notification filter name 'prefix' ({cfg_scope})"));
}
has_prefix = true;
} }
"suffix" => { has_prefix = true;
if has_suffix { } else if name.as_str().eq_ignore_ascii_case("suffix") {
return Err(s3_error!(InvalidArgument, "duplicate notification filter name 'suffix' ({cfg_scope})")); if has_suffix {
} return Err(s3_error!(InvalidArgument, "duplicate notification filter name 'suffix' ({cfg_scope})"));
has_suffix = true;
}
other => {
return Err(s3_error!(InvalidArgument, "{}", invalid_filter_name_message(&cfg_scope, other)));
} }
has_suffix = true;
} else {
return Err(s3_error!(InvalidArgument, "{}", invalid_filter_name_message(&cfg_scope, name.as_str())));
} }
} }
@@ -3047,7 +3043,7 @@ mod tests {
#[test] #[test]
fn validate_notification_configuration_filters_rejects_invalid_filter_name() { fn validate_notification_configuration_filters_rejects_invalid_filter_name() {
let raw_name = "Prefix".repeat(100); let raw_name = "unsupported".repeat(100);
let cfg = NotificationConfiguration { let cfg = NotificationConfiguration {
queue_configurations: Some(vec![QueueConfiguration { queue_configurations: Some(vec![QueueConfiguration {
id: Some("q1".to_string()), id: Some("q1".to_string()),
@@ -3072,6 +3068,34 @@ mod tests {
assert!(!msg.contains(&raw_name), "error message should not echo full raw filter name"); assert!(!msg.contains(&raw_name), "error message should not echo full raw filter name");
} }
#[test]
fn validate_notification_configuration_filters_accepts_case_insensitive_filter_names() {
let cfg = NotificationConfiguration {
queue_configurations: Some(vec![QueueConfiguration {
id: Some("q1".to_string()),
queue_arn: "arn:rustfs:sqs:us-east-1:1:webhook".to_string(),
events: vec!["s3:ObjectCreated:*".to_string().into()],
filter: Some(NotificationConfigurationFilter {
key: Some(S3KeyFilter {
filter_rules: Some(vec![
FilterRule {
name: Some(FilterRuleName::from("Prefix".to_string())),
value: Some("uploads/".to_string()),
},
FilterRule {
name: Some(FilterRuleName::from("Suffix".to_string())),
value: Some(".csv".to_string()),
},
]),
}),
}),
}]),
..Default::default()
};
validate_notification_configuration_filters(&cfg).expect("capitalized filter names should be accepted");
}
#[test] #[test]
fn validate_notification_configuration_filters_rejects_duplicate_prefix_rules() { fn validate_notification_configuration_filters_rejects_duplicate_prefix_rules() {
let cfg = NotificationConfiguration { let cfg = NotificationConfiguration {
@@ -3229,7 +3253,7 @@ mod tests {
filter: Some(NotificationConfigurationFilter { filter: Some(NotificationConfigurationFilter {
key: Some(S3KeyFilter { key: Some(S3KeyFilter {
filter_rules: Some(vec![FilterRule { filter_rules: Some(vec![FilterRule {
name: Some(FilterRuleName::from("Prefix".to_string())), name: Some(FilterRuleName::from("unsupported".to_string())),
value: Some("uploads/".to_string()), value: Some("uploads/".to_string()),
}]), }]),
}), }),
+4 -4
View File
@@ -624,10 +624,10 @@ pub(crate) fn extract_prefix_suffix(filter: Option<&NotificationConfigurationFil
if let Some(rules) = &filter_rules.filter_rules { if let Some(rules) = &filter_rules.filter_rules {
for rule in rules { for rule in rules {
if let (Some(name), Some(value)) = (rule.name.as_ref(), rule.value.as_ref()) { if let (Some(name), Some(value)) = (rule.name.as_ref(), rule.value.as_ref()) {
match name.as_str() { if name.as_str().eq_ignore_ascii_case("prefix") {
"prefix" => prefix = value.clone(), prefix = value.clone();
"suffix" => suffix = value.clone(), } else if name.as_str().eq_ignore_ascii_case("suffix") {
_ => {} suffix = value.clone();
} }
} }
} }
+47 -6
View File
@@ -36,12 +36,12 @@ mod tests {
}; };
use rustfs_zip::CompressionFormat; use rustfs_zip::CompressionFormat;
use s3s::dto::{ use s3s::dto::{
CORSConfiguration, CORSRule, DefaultRetention, DeleteObjectTaggingInput, Delimiter, GetBucketAclInput, GetObjectAclInput, CORSConfiguration, CORSRule, DefaultRetention, DeleteObjectTaggingInput, Delimiter, FilterRule, FilterRuleName,
GetObjectLegalHoldInput, GetObjectRetentionInput, GetObjectTaggingInput, LambdaFunctionConfiguration, GetBucketAclInput, GetObjectAclInput, GetObjectLegalHoldInput, GetObjectRetentionInput, GetObjectTaggingInput,
ObjectLockConfiguration, ObjectLockEnabled, ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention, LambdaFunctionConfiguration, NotificationConfigurationFilter, ObjectLockConfiguration, ObjectLockEnabled,
ObjectLockRetentionMode, ObjectLockRule, PutBucketAclInput, PutObjectAclInput, PutObjectLegalHoldInput, ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention, ObjectLockRetentionMode, ObjectLockRule,
PutObjectLockConfigurationInput, PutObjectRetentionInput, PutObjectTaggingInput, QueueConfiguration, Tag, Tagging, PutBucketAclInput, PutObjectAclInput, PutObjectLegalHoldInput, PutObjectLockConfigurationInput, PutObjectRetentionInput,
TopicConfiguration, PutObjectTaggingInput, QueueConfiguration, S3KeyFilter, Tag, Tagging, TopicConfiguration,
}; };
use s3s::{S3, S3Error, S3ErrorCode, S3Request, s3_error}; use s3s::{S3, S3Error, S3ErrorCode, S3Request, s3_error};
use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use time::{OffsetDateTime, format_description::well_known::Rfc3339};
@@ -1734,6 +1734,47 @@ mod tests {
assert_eq!(event_rules.len(), 1, "Should add one rule"); assert_eq!(event_rules.len(), 1, "Should add one rule");
} }
#[test]
fn test_process_queue_configurations_accepts_capitalized_filter_names() {
use rustfs_targets::arn::{ARN, TargetIDError};
let mut event_rules = Vec::new();
let valid_arn = "arn:rustfs:sqs:us-east-1:1:webhook";
let result = process_queue_configurations(
&mut event_rules,
Some(vec![QueueConfiguration {
events: vec!["s3:ObjectCreated:*".to_string().into()],
queue_arn: valid_arn.to_string(),
filter: Some(NotificationConfigurationFilter {
key: Some(S3KeyFilter {
filter_rules: Some(vec![
FilterRule {
name: Some(FilterRuleName::from("Prefix".to_string())),
value: Some("uploads/".to_string()),
},
FilterRule {
name: Some(FilterRuleName::from("Suffix".to_string())),
value: Some(".csv".to_string()),
},
]),
}),
}),
id: None,
}]),
|arn_str| {
ARN::parse(arn_str)
.map(|arn| arn.target_id)
.map_err(|e| TargetIDError::InvalidFormat(e.to_string()))
},
);
assert!(result.is_ok(), "capitalized filter names should be compatible");
assert_eq!(event_rules.len(), 1, "Should add one rule");
assert_eq!(event_rules[0].1, "uploads/");
assert_eq!(event_rules[0].2, ".csv");
}
// --- Object tag conditions for bucket policy (s3:ExistingObjectTag) --- // --- Object tag conditions for bucket policy (s3:ExistingObjectTag) ---
/// Verifies that object tags are formatted as ExistingObjectTag/<key> condition keys /// Verifies that object tags are formatted as ExistingObjectTag/<key> condition keys