mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-27 16:48:58 +00:00
7f19e9a465
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.
101 lines
4.3 KiB
Rust
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");
|
|
}
|