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");
}
#[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]
fn test_suffix_only_filter_xml() {
+5 -5
View File
@@ -66,7 +66,7 @@ pub struct FilterRule {
impl FilterRule {
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()));
}
// ValidateFilterRuleValue from Go:
@@ -98,12 +98,12 @@ impl S3KeyFilter {
let mut has_suffix = false;
for rule in &self.filter_rule_list {
rule.validate()?;
if rule.name == "prefix" {
if rule.name.eq_ignore_ascii_case("prefix") {
if has_prefix {
return Err(ParseConfigError::DuplicatePrefixFilter);
}
has_prefix = true;
} else if rule.name == "suffix" {
} else if rule.name.eq_ignore_ascii_case("suffix") {
if has_suffix {
return Err(ParseConfigError::DuplicateSuffixFilter);
}
@@ -126,9 +126,9 @@ impl S3KeyFilter {
let mut suffix_val: Option<&str> = None;
for rule in &self.filter_rule_list {
if rule.name == "prefix" {
if rule.name.eq_ignore_ascii_case("prefix") {
prefix_val = Some(&rule.value);
} else if rule.name == "suffix" {
} else if rule.name.eq_ignore_ascii_case("suffix") {
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)));
}
match name.as_str() {
"prefix" => {
if has_prefix {
return Err(s3_error!(InvalidArgument, "duplicate notification filter name 'prefix' ({cfg_scope})"));
}
has_prefix = true;
if name.as_str().eq_ignore_ascii_case("prefix") {
if has_prefix {
return Err(s3_error!(InvalidArgument, "duplicate notification filter name 'prefix' ({cfg_scope})"));
}
"suffix" => {
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_prefix = true;
} else if name.as_str().eq_ignore_ascii_case("suffix") {
if has_suffix {
return Err(s3_error!(InvalidArgument, "duplicate notification filter name 'suffix' ({cfg_scope})"));
}
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]
fn validate_notification_configuration_filters_rejects_invalid_filter_name() {
let raw_name = "Prefix".repeat(100);
let raw_name = "unsupported".repeat(100);
let cfg = NotificationConfiguration {
queue_configurations: Some(vec![QueueConfiguration {
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");
}
#[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]
fn validate_notification_configuration_filters_rejects_duplicate_prefix_rules() {
let cfg = NotificationConfiguration {
@@ -3229,7 +3253,7 @@ mod tests {
filter: Some(NotificationConfigurationFilter {
key: Some(S3KeyFilter {
filter_rules: Some(vec![FilterRule {
name: Some(FilterRuleName::from("Prefix".to_string())),
name: Some(FilterRuleName::from("unsupported".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 {
for rule in rules {
if let (Some(name), Some(value)) = (rule.name.as_ref(), rule.value.as_ref()) {
match name.as_str() {
"prefix" => prefix = value.clone(),
"suffix" => suffix = value.clone(),
_ => {}
if name.as_str().eq_ignore_ascii_case("prefix") {
prefix = 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 s3s::dto::{
CORSConfiguration, CORSRule, DefaultRetention, DeleteObjectTaggingInput, Delimiter, GetBucketAclInput, GetObjectAclInput,
GetObjectLegalHoldInput, GetObjectRetentionInput, GetObjectTaggingInput, LambdaFunctionConfiguration,
ObjectLockConfiguration, ObjectLockEnabled, ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention,
ObjectLockRetentionMode, ObjectLockRule, PutBucketAclInput, PutObjectAclInput, PutObjectLegalHoldInput,
PutObjectLockConfigurationInput, PutObjectRetentionInput, PutObjectTaggingInput, QueueConfiguration, Tag, Tagging,
TopicConfiguration,
CORSConfiguration, CORSRule, DefaultRetention, DeleteObjectTaggingInput, Delimiter, FilterRule, FilterRuleName,
GetBucketAclInput, GetObjectAclInput, GetObjectLegalHoldInput, GetObjectRetentionInput, GetObjectTaggingInput,
LambdaFunctionConfiguration, NotificationConfigurationFilter, ObjectLockConfiguration, ObjectLockEnabled,
ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention, ObjectLockRetentionMode, ObjectLockRule,
PutBucketAclInput, PutObjectAclInput, PutObjectLegalHoldInput, PutObjectLockConfigurationInput, PutObjectRetentionInput,
PutObjectTaggingInput, QueueConfiguration, S3KeyFilter, Tag, Tagging, TopicConfiguration,
};
use s3s::{S3, S3Error, S3ErrorCode, S3Request, s3_error};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
@@ -1734,6 +1734,47 @@ mod tests {
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) ---
/// Verifies that object tags are formatted as ExistingObjectTag/<key> condition keys