feat(policy): implement BinaryEquals condition evaluation (#2626)

Signed-off-by: Ramakrishna Chilaka <49393831+RamakrishnaChilaka@users.noreply.github.com>
Co-authored-by: loverustfs <hello@rustfs.com>
Co-authored-by: cxymds <Cxymds@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
Ramakrishna Chilaka
2026-04-24 12:38:11 +05:30
committed by GitHub
parent 8d4caeacad
commit fefb308b35
+383 -8
View File
@@ -14,19 +14,394 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use super::func::InnerFunc;
pub type BinaryFunc = InnerFunc<BinaryFuncValue>;
// todo implement it
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
#[serde(transparent)]
pub struct BinaryFuncValue(String);
#[derive(thiserror::Error, Clone, Debug, Eq, PartialEq)]
pub enum BinaryFuncValueError {
#[error("invalid base64 for BinaryEquals")]
InvalidBase64,
}
impl BinaryFunc {
pub fn evaluate(&self, _values: &HashMap<String, Vec<String>>) -> bool {
todo!()
/// Policy value for the AWS IAM `BinaryEquals` condition.
///
/// Policies store the value as a base64-encoded string or array of strings.
/// During deserialization the values are validated and the raw bytes are
/// cached, so evaluation is a plain byte comparison and malformed policies
/// are rejected at parse time.
#[derive(Clone, Debug)]
pub struct BinaryFuncValue {
/// Original base64 forms, preserved for serialization round-trips.
encoded: Vec<String>,
/// Decoded bytes used for comparison during `evaluate`.
decoded: Vec<Vec<u8>>,
}
impl BinaryFuncValue {
/// Construct from a base64-encoded string, validating the encoding.
pub fn new(encoded: impl Into<String>) -> Result<Self, BinaryFuncValueError> {
Self::from_encoded_values(vec![encoded.into()])
}
fn from_encoded_values(encoded: Vec<String>) -> Result<Self, BinaryFuncValueError> {
let decoded = encoded
.iter()
.map(|value| {
base64_simd::STANDARD
.decode_to_vec(value.as_bytes())
.map_err(|_| BinaryFuncValueError::InvalidBase64)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { encoded, decoded })
}
}
impl TryFrom<String> for BinaryFuncValue {
type Error = BinaryFuncValueError;
fn try_from(encoded: String) -> Result<Self, Self::Error> {
Self::new(encoded)
}
}
impl TryFrom<&str> for BinaryFuncValue {
type Error = BinaryFuncValueError;
fn try_from(encoded: &str) -> Result<Self, Self::Error> {
Self::new(encoded)
}
}
// Equality is defined over decoded bytes so that semantically equal values
// compare equal regardless of incidental base64 formatting differences.
impl PartialEq for BinaryFuncValue {
fn eq(&self, other: &Self) -> bool {
self.decoded == other.decoded
}
}
impl Eq for BinaryFuncValue {}
impl Serialize for BinaryFuncValue {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if self.encoded.len() == 1 {
serializer.serialize_str(&self.encoded[0])
} else {
self.encoded.serialize(serializer)
}
}
}
impl<'de> Deserialize<'de> for BinaryFuncValue {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct StringOrVecVisitor;
impl<'de> de::Visitor<'de> for StringOrVecVisitor {
type Value = BinaryFuncValue;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a base64 string or an array of base64 strings")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
BinaryFuncValue::new(value).map_err(E::custom)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut values = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(value) = seq.next_element::<String>()? {
values.push(value);
}
if values.is_empty() {
return Err(de::Error::custom("empty"));
}
BinaryFuncValue::from_encoded_values(values).map_err(de::Error::custom)
}
}
deserializer.deserialize_any(StringOrVecVisitor)
}
}
impl BinaryFunc {
/// Evaluate an AWS IAM `BinaryEquals` condition.
///
/// AWS semantics compare the base64-decoded bytes of the policy value
/// against the base64-decoded bytes of the request context value. In this
/// codebase request context values come directly from HTTP header strings
/// (see `rustfs::auth::get_condition_values_with_query`), so for real
/// binary condition keys (e.g. SSE-C customer-key headers) the request
/// value is itself base64. Decoding both sides is therefore required for
/// the comparison to ever succeed.
///
/// All key/value pairs in the function must match (logical AND); for a
/// given key, any decoded request value that equals any expected decoded
/// policy value satisfies that pair (OR across request values and policy
/// values). A missing request key, or *any* request value that is not
/// valid base64, causes the condition to evaluate to false (fail-closed).
pub fn evaluate(&self, values: &HashMap<String, Vec<String>>) -> bool {
for inner in self.0.iter() {
let Some(rvalues) = values.get(inner.key.name().as_str()) else {
return false;
};
let mut matched = false;
for v in rvalues {
let Ok(decoded) = base64_simd::STANDARD.decode_to_vec(v.as_bytes()) else {
return false;
};
if inner
.values
.decoded
.iter()
.any(|expected| decoded.as_slice() == expected.as_slice())
{
matched = true;
}
}
if !matched {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::{BinaryFunc, BinaryFuncValue, BinaryFuncValueError};
use crate::policy::function::func::FuncKeyValue;
use crate::policy::function::{
key::Key,
key_name::AwsKeyName::*,
key_name::KeyName::{self, *},
};
use std::collections::HashMap;
fn new_func(name: KeyName, variable: Option<String>, value: &str) -> BinaryFunc {
BinaryFunc {
0: vec![FuncKeyValue {
key: Key { name, variable },
values: BinaryFuncValue::new(value).expect("valid base64 in test"),
}],
}
}
fn new_multi_func(name: KeyName, variable: Option<String>, values: &[&str]) -> BinaryFunc {
BinaryFunc {
0: vec![FuncKeyValue {
key: Key { name, variable },
values: BinaryFuncValue::from_encoded_values(values.iter().map(|value| (*value).to_string()).collect())
.expect("valid binary array in test"),
}],
}
}
#[test]
fn evaluate_matches_decoded_bytes() {
// base64("hello") = "aGVsbG8="
let f = new_func(Aws(AWSUsername), None, "aGVsbG8=");
let mut ctx = HashMap::new();
// Request value is itself base64 — BinaryEquals decodes both sides.
ctx.insert("username".to_string(), vec!["aGVsbG8=".to_string()]);
assert!(f.evaluate(&ctx));
}
#[test]
fn evaluate_rejects_non_matching_value() {
let f = new_func(Aws(AWSUsername), None, "aGVsbG8="); // "hello"
let mut ctx = HashMap::new();
ctx.insert("username".to_string(), vec!["d29ybGQ=".to_string()]); // "world"
assert!(!f.evaluate(&ctx));
}
#[test]
fn evaluate_matches_any_request_value() {
let f = new_func(Aws(AWSUsername), None, "aGVsbG8="); // "hello"
let mut ctx = HashMap::new();
ctx.insert("username".to_string(), vec!["d29ybGQ=".to_string(), "aGVsbG8=".to_string()]);
assert!(f.evaluate(&ctx));
}
#[test]
fn evaluate_missing_key_is_false() {
let f = new_func(Aws(AWSUsername), None, "aGVsbG8=");
let ctx = HashMap::new();
assert!(!f.evaluate(&ctx));
}
#[test]
fn evaluate_empty_request_values_is_false() {
let f = new_func(Aws(AWSUsername), None, "aGVsbG8=");
let mut ctx = HashMap::new();
ctx.insert("username".to_string(), vec![]);
assert!(!f.evaluate(&ctx));
}
#[test]
fn evaluate_matches_multibyte_utf8() {
// base64("café") = "Y2Fmw6k=" — exercises multi-byte UTF-8 round trip.
let f = new_func(Aws(AWSUsername), None, "Y2Fmw6k=");
let mut ctx = HashMap::new();
ctx.insert("username".to_string(), vec!["Y2Fmw6k=".to_string()]);
assert!(f.evaluate(&ctx));
}
#[test]
fn evaluate_matches_any_policy_value() {
let f = new_multi_func(Aws(AWSUsername), None, &["aGVsbG8=", "d29ybGQ="]);
let mut ctx = HashMap::new();
ctx.insert("username".to_string(), vec!["d29ybGQ=".to_string()]);
assert!(f.evaluate(&ctx));
}
#[test]
fn evaluate_invalid_base64_request_value_fails_closed() {
// Malformed base64 in the request must never match, regardless of policy value.
let f = new_func(Aws(AWSUsername), None, "aGVsbG8=");
let mut ctx = HashMap::new();
ctx.insert("username".to_string(), vec!["!!!not-base64!!!".to_string()]);
assert!(!f.evaluate(&ctx));
}
#[test]
fn evaluate_mixed_valid_and_invalid_request_values_fails_closed() {
// A valid matching value alongside an invalid base64 value must still
// fail closed — for BinaryEquals, any unparsable request value causes
// evaluation to return false even if another request value matches.
let f = new_func(Aws(AWSUsername), None, "aGVsbG8="); // "hello"
let mut ctx = HashMap::new();
ctx.insert("username".to_string(), vec!["aGVsbG8=".to_string(), "!!!not-base64!!!".to_string()]);
assert!(!f.evaluate(&ctx));
// Order-independent: invalid first, valid second — still false.
let mut ctx2 = HashMap::new();
ctx2.insert("username".to_string(), vec!["!!!not-base64!!!".to_string(), "aGVsbG8=".to_string()]);
assert!(!f.evaluate(&ctx2));
}
#[test]
fn evaluate_raw_request_value_does_not_match() {
// A raw (non-base64) request value that happens to equal the decoded
// policy bytes must NOT match — both sides are decoded first. "hello"
// is not valid standard base64 (length 5, not a multiple of 4), so
// decoding fails and the evaluation fails closed.
let f = new_func(Aws(AWSUsername), None, "aGVsbG8="); // decodes to "hello"
let mut ctx = HashMap::new();
ctx.insert("username".to_string(), vec!["hello".to_string()]);
assert!(!f.evaluate(&ctx));
}
#[test]
fn try_from_constructs_binary_func_value() {
// Ergonomic alternatives to BinaryFuncValue::new — parity with the
// prior public-struct API and idiomatic Rust conversion.
let from_str: BinaryFuncValue = "aGVsbG8=".try_into().unwrap();
let from_string: BinaryFuncValue = String::from("aGVsbG8=").try_into().unwrap();
assert_eq!(from_str, from_string);
assert_eq!(BinaryFuncValue::try_from("!!!bad!!!").unwrap_err(), BinaryFuncValueError::InvalidBase64,);
}
#[test]
fn evaluate_all_key_values_must_match() {
// Two key/value pairs — both must be satisfied.
let f = BinaryFunc {
0: vec![
FuncKeyValue {
key: Key {
name: Aws(AWSUsername),
variable: None,
},
values: BinaryFuncValue::new("aGVsbG8=").unwrap(), // "hello"
},
FuncKeyValue {
key: Key {
name: Aws(AWSPrincipalType),
variable: None,
},
values: BinaryFuncValue::new("d29ybGQ=").unwrap(), // "world"
},
],
};
let mut ctx = HashMap::new();
ctx.insert("username".to_string(), vec!["aGVsbG8=".to_string()]);
ctx.insert("principaltype".to_string(), vec!["d29ybGQ=".to_string()]);
assert!(f.evaluate(&ctx));
// Second key missing — must fail.
let mut ctx2 = HashMap::new();
ctx2.insert("username".to_string(), vec!["aGVsbG8=".to_string()]);
assert!(!f.evaluate(&ctx2));
}
#[test]
fn deserializes_from_policy_json() {
let json = r#"{"aws:username": "aGVsbG8="}"#;
let f: BinaryFunc = serde_json::from_str(json).unwrap();
let mut ctx = HashMap::new();
ctx.insert("username".to_string(), vec!["aGVsbG8=".to_string()]);
assert!(f.evaluate(&ctx));
}
#[test]
fn deserializes_array_from_policy_json() {
let json = r#"{"aws:username": ["aGVsbG8=", "d29ybGQ="]}"#;
let f: BinaryFunc = serde_json::from_str(json).unwrap();
let mut ctx = HashMap::new();
ctx.insert("username".to_string(), vec!["d29ybGQ=".to_string()]);
assert!(f.evaluate(&ctx));
}
#[test]
fn deserialize_rejects_invalid_base64_at_parse_time() {
// Malformed policies must be rejected eagerly, not silently fail at eval.
let json = r#"{"aws:username": "!!!not-base64!!!"}"#;
let err = serde_json::from_str::<BinaryFunc>(json).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("invalid base64"), "unexpected error message: {msg}");
}
#[test]
fn deserialize_rejects_invalid_base64_in_array_at_parse_time() {
let json = r#"{"aws:username": ["aGVsbG8=", "!!!not-base64!!!"]}"#;
let err = serde_json::from_str::<BinaryFunc>(json).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("invalid base64"), "unexpected error message: {msg}");
}
#[test]
fn deserialize_rejects_empty_array() {
let json = r#"{"aws:username": []}"#;
let err = serde_json::from_str::<BinaryFunc>(json).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("empty"), "unexpected error message: {msg}");
}
#[test]
fn serialize_round_trip_preserves_encoded_form() {
let json = r#"{"aws:username":"aGVsbG8="}"#;
let f: BinaryFunc = serde_json::from_str(json).unwrap();
let out = serde_json::to_string(&f).unwrap();
assert_eq!(out, json);
}
#[test]
fn serialize_round_trip_preserves_encoded_array_form() {
let json = r#"{"aws:username":["aGVsbG8=","d29ybGQ="]}"#;
let f: BinaryFunc = serde_json::from_str(json).unwrap();
let out = serde_json::to_string(&f).unwrap();
assert_eq!(out, json);
}
}