//! 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> { 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>) -> 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"); }