fix: policy StringNotEquals double negation and delete_objects version mapping (#2015)

This commit is contained in:
安正超
2026-03-01 03:13:52 +08:00
committed by GitHub
parent fd1b903531
commit e7466eb1cc
6 changed files with 208 additions and 19 deletions
+90 -1
View File
@@ -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")]
+87
View File
@@ -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(())
}
}