mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-22 12:26:37 +00:00
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:
committed by
GitHub
parent
8d4caeacad
commit
fefb308b35
@@ -14,19 +14,394 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||||
|
|
||||||
use super::func::InnerFunc;
|
use super::func::InnerFunc;
|
||||||
|
|
||||||
pub type BinaryFunc = InnerFunc<BinaryFuncValue>;
|
pub type BinaryFunc = InnerFunc<BinaryFuncValue>;
|
||||||
|
|
||||||
// todo implement it
|
#[derive(thiserror::Error, Clone, Debug, Eq, PartialEq)]
|
||||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
|
pub enum BinaryFuncValueError {
|
||||||
#[serde(transparent)]
|
#[error("invalid base64 for BinaryEquals")]
|
||||||
pub struct BinaryFuncValue(String);
|
InvalidBase64,
|
||||||
|
}
|
||||||
|
|
||||||
impl BinaryFunc {
|
/// Policy value for the AWS IAM `BinaryEquals` condition.
|
||||||
pub fn evaluate(&self, _values: &HashMap<String, Vec<String>>) -> bool {
|
///
|
||||||
todo!()
|
/// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user