mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 08:27:06 +00:00
fix(notify): accept case-insensitive filter rule names (#2990)
This commit is contained in:
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()),
|
||||||
}]),
|
}]),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user