mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-21 03:46:37 +00:00
fix: policy StringNotEquals double negation and delete_objects version mapping (#2015)
This commit is contained in:
@@ -148,7 +148,9 @@ impl Condition {
|
||||
#[inline]
|
||||
pub fn is_negate(&self) -> bool {
|
||||
use Condition::*;
|
||||
matches!(self, StringNotEquals(_) | StringNotEqualsIgnoreCase(_) | NotIpAddress(_))
|
||||
// StringNotEquals/StringNotEqualsIgnoreCase handle negation via the
|
||||
// `negate` parameter in `evaluate_with_resolver`; do NOT negate again here.
|
||||
matches!(self, NotIpAddress(_))
|
||||
}
|
||||
|
||||
pub fn serialize_map<T: SerializeMap>(&self, se: &mut T) -> Result<(), T::Error> {
|
||||
@@ -212,3 +214,90 @@ impl PartialEq for Condition {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::policy::function::{
|
||||
func::{FuncKeyValue, InnerFunc},
|
||||
key::Key,
|
||||
string::StringFuncValue,
|
||||
};
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
fn make_string_condition(condition_type: &str, key: &str, value: &str) -> Condition {
|
||||
let func: StringFunc = InnerFunc(vec![FuncKeyValue {
|
||||
key: Key {
|
||||
name: key.try_into().unwrap(),
|
||||
variable: None,
|
||||
},
|
||||
values: StringFuncValue({
|
||||
let mut s = BTreeSet::new();
|
||||
s.insert(value.to_string());
|
||||
s
|
||||
}),
|
||||
}]);
|
||||
match condition_type {
|
||||
"StringEquals" => Condition::StringEquals(func),
|
||||
"StringNotEquals" => Condition::StringNotEquals(func),
|
||||
"StringNotEqualsIgnoreCase" => Condition::StringNotEqualsIgnoreCase(func),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_string_not_equals_no_double_negation() {
|
||||
let cond = make_string_condition("StringNotEquals", "s3:x-amz-server-side-encryption", "aws:kms");
|
||||
|
||||
let mut values = HashMap::new();
|
||||
values.insert("x-amz-server-side-encryption".to_string(), vec!["AES256".to_string()]);
|
||||
|
||||
// "AES256" != "aws:kms" is true, so StringNotEquals should evaluate to true
|
||||
assert!(
|
||||
cond.evaluate_with_resolver(false, &values, None).await,
|
||||
"StringNotEquals should be true when values differ"
|
||||
);
|
||||
|
||||
values.insert("x-amz-server-side-encryption".to_string(), vec!["aws:kms".to_string()]);
|
||||
|
||||
// "aws:kms" != "aws:kms" is false, so StringNotEquals should evaluate to false
|
||||
assert!(
|
||||
!cond.evaluate_with_resolver(false, &values, None).await,
|
||||
"StringNotEquals should be false when values match"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_string_equals_condition() {
|
||||
let cond = make_string_condition("StringEquals", "s3:x-amz-server-side-encryption", "aws:kms");
|
||||
|
||||
let mut values = HashMap::new();
|
||||
values.insert("x-amz-server-side-encryption".to_string(), vec!["aws:kms".to_string()]);
|
||||
|
||||
assert!(
|
||||
cond.evaluate_with_resolver(false, &values, None).await,
|
||||
"StringEquals should be true when values match"
|
||||
);
|
||||
|
||||
values.insert("x-amz-server-side-encryption".to_string(), vec!["AES256".to_string()]);
|
||||
|
||||
assert!(
|
||||
!cond.evaluate_with_resolver(false, &values, None).await,
|
||||
"StringEquals should be false when values differ"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_string_not_equals_absent_key() {
|
||||
let cond = make_string_condition("StringNotEquals", "s3:x-amz-server-side-encryption", "aws:kms");
|
||||
|
||||
let values = HashMap::new();
|
||||
|
||||
// Key absent: rvalues is empty, intersection is empty.
|
||||
// for_all=false: ivalues.count() > 0 → false. Negated → true.
|
||||
assert!(
|
||||
cond.evaluate_with_resolver(false, &values, None).await,
|
||||
"StringNotEquals should be true when key is absent"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,21 @@ pub enum S3KeyName {
|
||||
#[strum(serialize = "s3:delimiter")]
|
||||
S3Delimiter,
|
||||
|
||||
#[strum(serialize = "s3:x-amz-grant-full-control")]
|
||||
S3XAmzGrantFullControl,
|
||||
|
||||
#[strum(serialize = "s3:x-amz-grant-read")]
|
||||
S3XAmzGrantRead,
|
||||
|
||||
#[strum(serialize = "s3:x-amz-grant-write")]
|
||||
S3XAmzGrantWrite,
|
||||
|
||||
#[strum(serialize = "s3:x-amz-grant-read-acp")]
|
||||
S3XAmzGrantReadAcp,
|
||||
|
||||
#[strum(serialize = "s3:x-amz-grant-write-acp")]
|
||||
S3XAmzGrantWriteAcp,
|
||||
|
||||
#[strum(serialize = "s3:ExistingObjectTag")]
|
||||
S3ExistingObjectTag,
|
||||
#[strum(serialize = "s3:RequestObjectTagKeys")]
|
||||
|
||||
@@ -1244,4 +1244,91 @@ mod test {
|
||||
assert_eq!(arr.len(), 1);
|
||||
assert_eq!(arr[0].as_str().unwrap(), "s3:ListBucket");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bucket_policy_deny_with_string_not_equals() -> Result<()> {
|
||||
let data = r#"
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Deny",
|
||||
"Action": "s3:PutObject",
|
||||
"Principal": {"AWS": "*"},
|
||||
"Resource": "arn:aws:s3:::mybucket/*",
|
||||
"Condition": {
|
||||
"StringNotEquals": {
|
||||
"s3:x-amz-server-side-encryption": "aws:kms"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Effect": "Deny",
|
||||
"Action": "s3:PutObject",
|
||||
"Principal": {"AWS": "*"},
|
||||
"Resource": "arn:aws:s3:::mybucket/*",
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
|
||||
let bp: BucketPolicy = serde_json::from_slice(data.as_bytes())?;
|
||||
|
||||
// Request with wrong encryption → should be DENIED (StringNotEquals matches)
|
||||
let mut cond_wrong_enc = HashMap::new();
|
||||
cond_wrong_enc.insert("x-amz-server-side-encryption".to_string(), vec!["AES256".to_string()]);
|
||||
|
||||
let args_wrong = BucketPolicyArgs {
|
||||
account: "testowner",
|
||||
groups: &None,
|
||||
action: Action::S3Action(crate::policy::action::S3Action::PutObjectAction),
|
||||
bucket: "mybucket",
|
||||
conditions: &cond_wrong_enc,
|
||||
is_owner: true,
|
||||
object: "testobj",
|
||||
};
|
||||
assert!(
|
||||
!bp.is_allowed(&args_wrong).await,
|
||||
"Should deny PutObject with AES256 when policy requires aws:kms"
|
||||
);
|
||||
|
||||
// Request with correct encryption → should be ALLOWED
|
||||
let mut cond_correct_enc = HashMap::new();
|
||||
cond_correct_enc.insert("x-amz-server-side-encryption".to_string(), vec!["aws:kms".to_string()]);
|
||||
|
||||
let args_correct = BucketPolicyArgs {
|
||||
account: "testowner",
|
||||
groups: &None,
|
||||
action: Action::S3Action(crate::policy::action::S3Action::PutObjectAction),
|
||||
bucket: "mybucket",
|
||||
conditions: &cond_correct_enc,
|
||||
is_owner: true,
|
||||
object: "testobj",
|
||||
};
|
||||
assert!(
|
||||
bp.is_allowed(&args_correct).await,
|
||||
"Should allow PutObject with aws:kms matching the policy"
|
||||
);
|
||||
|
||||
// Request with no encryption header → should be DENIED (Null condition matches)
|
||||
let cond_no_enc = HashMap::new();
|
||||
|
||||
let args_no_enc = BucketPolicyArgs {
|
||||
account: "testowner",
|
||||
groups: &None,
|
||||
action: Action::S3Action(crate::policy::action::S3Action::PutObjectAction),
|
||||
bucket: "mybucket",
|
||||
conditions: &cond_no_enc,
|
||||
is_owner: true,
|
||||
object: "testobj",
|
||||
};
|
||||
assert!(!bp.is_allowed(&args_no_enc).await, "Should deny PutObject with no encryption header");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user