Files
rustfs/crates/policy/tests/quantified_negation.rs
T
Zhengchao An 7f19e9a465 Merge commit from fork
docs/testing/security-regressions.md requires every fixed advisory to map to a
named, greppable regression test. Rename the tests added with the fixes to carry
their advisory id so `rg -i ghsa` finds them, and add the four rows to the
advisory -> test map.

Adds the FTPS MKD regression test that was missing. It primes the dummy backend
with a successful create_bucket, so the assertion distinguishes "denied at the
authorization boundary" from "backend refused" — without the queued success an
unconfigured create_bucket fails on its own and the test would pass even with
the authorization check removed. Verified it fails when the check is reverted.

DummyBackend gains a Debug impl (FtpsDriver's trait bounds require it) and a
queue_create_bucket_ok helper.

Records in the CI-execution map why ghsa_g3vq_* runs in the default pass despite
sitting behind the ftps feature: the rustfs crate defaults to ["ftps", "webdav"]
and cargo unifies features across the workspace build, so the test executes
there even though `cargo test -p rustfs-protocols` alone would skip it.
2026-07-26 07:10:50 +08:00

101 lines
4.3 KiB
Rust

//! Regression tests for quantified negated string conditions.
//!
//! AWS semantics:
//! ForAllValues:StringNotEquals -> true iff EVERY request value is absent from the policy set.
//! ForAnyValue:StringNotEquals -> true iff AT LEAST ONE request value is absent from the policy set.
//!
//! Negating the aggregate result of the positive quantified match instead of the
//! per-value predicate yields NOT(all match) and NOT(any match) respectively,
//! which transposes the two quantifiers. The discriminating case is a request
//! whose values partially overlap the policy set; a set that is fully contained
//! or fully disjoint cannot tell the two apart.
use std::collections::HashMap;
use rustfs_policy::policy::Functions;
fn ctx(key: &str, vals: &[&str]) -> HashMap<String, Vec<String>> {
let mut m = HashMap::new();
m.insert(key.to_owned(), vals.iter().map(|s| (*s).to_owned()).collect());
m
}
fn eval(json: &str, values: &HashMap<String, Vec<String>>) -> bool {
let f: Functions = serde_json::from_str(json).expect("parse condition");
pollster::block_on(f.evaluate(values))
}
#[test]
fn ghsa_v9cp_for_all_values_not_equals_partial_overlap() {
// groups = {art, hr}; policy set = {prod, art}. "art" IS in the set,
// so not every value is "not equal" -> AWS says false.
let v = ctx("groups", &["art", "hr"]);
let got = eval(r#"{"ForAllValues:StringNotEquals":{"jwt:groups":["prod","art"]}}"#, &v);
assert!(
!got,
"ForAllValues:StringNotEquals must be false when a request value matches the policy set"
);
}
#[test]
fn ghsa_v9cp_for_any_value_not_equals_partial_overlap() {
// groups = {art, hr}; policy set = {prod, art}. "hr" is NOT in the set,
// so at least one value is "not equal" -> AWS says true.
let v = ctx("groups", &["art", "hr"]);
let got = eval(r#"{"ForAnyValue:StringNotEquals":{"jwt:groups":["prod","art"]}}"#, &v);
assert!(
got,
"ForAnyValue:StringNotEquals must be true when some request value is outside the policy set"
);
}
#[test]
fn for_all_values_string_not_equals_missing_key() {
// ForAllValues is vacuously true when the key is absent from the request.
let v = HashMap::new();
let got = eval(r#"{"ForAllValues:StringNotEquals":{"jwt:groups":["prod","art"]}}"#, &v);
assert!(got, "ForAllValues:* is vacuously true when the context key is absent");
}
#[test]
fn for_any_value_string_not_equals_missing_key() {
// ForAnyValue is false when the key is absent -- there is no value to satisfy it.
let v = HashMap::new();
let got = eval(r#"{"ForAnyValue:StringNotEquals":{"jwt:groups":["prod","art"]}}"#, &v);
assert!(!got, "ForAnyValue:* is false when the context key is absent");
}
#[test]
fn for_all_values_string_not_like_partial_overlap() {
// Same inversion for the wildcard family (R03-CAN-042).
let v = ctx("groups", &["art-team", "hr"]);
let got = eval(r#"{"ForAllValues:StringNotLike":{"jwt:groups":["art-*"]}}"#, &v);
assert!(!got, "ForAllValues:StringNotLike must be false when a request value matches the pattern");
}
#[test]
fn for_any_value_string_not_like_partial_overlap() {
let v = ctx("groups", &["art-team", "hr"]);
let got = eval(r#"{"ForAnyValue:StringNotLike":{"jwt:groups":["art-*"]}}"#, &v);
assert!(got, "ForAnyValue:StringNotLike must be true when some request value does not match");
}
/// Positive (non-negated) quantifiers must keep working -- guards against a fix
/// that flips these instead.
#[test]
fn positive_quantifiers_unchanged() {
let partial = ctx("groups", &["art", "hr"]);
let contained = ctx("groups", &["art"]);
let empty = HashMap::new();
let all_eq = r#"{"ForAllValues:StringEquals":{"jwt:groups":["prod","art"]}}"#;
let any_eq = r#"{"ForAnyValue:StringEquals":{"jwt:groups":["prod","art"]}}"#;
assert!(!eval(all_eq, &partial), "ForAllValues:StringEquals false when a value is outside the set");
assert!(eval(all_eq, &contained), "ForAllValues:StringEquals true when all values are in the set");
assert!(eval(all_eq, &empty), "ForAllValues:StringEquals vacuously true on absent key");
assert!(eval(any_eq, &partial), "ForAnyValue:StringEquals true when some value is in the set");
assert!(!eval(any_eq, &empty), "ForAnyValue:StringEquals false on absent key");
}