use crate::S3Error;
use crate::S3ErrorCode;
use crate::S3Result;
use crate::dto::Timestamp;
use crate::dto::TimestampFormat;
use crate::http::Multipart;
use std::borrow::Cow;
use std::collections::HashMap;
use serde::Deserialize;
use serde::de::{Deserializer, MapAccess, SeqAccess, Visitor};
#[derive(Debug, Clone)]
pub struct PostPolicy {
pub expiration: Timestamp,
pub conditions: Vec<PostPolicyCondition>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PostPolicyCondition {
Eq {
field: String,
value: String,
},
StartsWith {
field: String,
prefix: String,
},
ContentLengthRange {
min: u64,
max: u64,
},
}
#[derive(Debug, thiserror::Error)]
pub enum PostPolicyError {
#[error("invalid base64 encoding: {0}")]
Base64(#[from] base64_simd::Error),
#[error("invalid UTF-8 encoding: {0}")]
Utf8(#[from] std::str::Utf8Error),
#[error("invalid JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("missing required field: {0}")]
MissingField(&'static str),
#[error("invalid condition format")]
InvalidCondition,
#[error("invalid expiration format: {0}")]
InvalidExpiration(String),
}
impl PostPolicy {
pub fn from_base64(encoded: &str) -> Result<Self, PostPolicyError> {
let decoded = base64_simd::STANDARD.decode_to_vec(encoded)?;
let json_str = std::str::from_utf8(&decoded)?;
Self::from_json(json_str)
}
pub fn from_json(json: &str) -> Result<Self, PostPolicyError> {
let raw: RawPostPolicy = serde_json::from_str(json)?;
let expiration = Timestamp::parse(TimestampFormat::DateTime, &raw.expiration)
.map_err(|_| PostPolicyError::InvalidExpiration(raw.expiration.clone()))?;
let conditions = raw
.conditions
.into_iter()
.map(RawCondition::into_condition)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { expiration, conditions })
}
pub(crate) fn validate_conditions_only(
&self,
multipart: &Multipart,
file_size: u64,
url_bucket: Option<&str>,
) -> S3Result<()> {
for condition in &self.conditions {
Self::validate_condition(condition, multipart, file_size, url_bucket)?;
}
for (name, _) in multipart.fields() {
if Self::is_field_exempt_from_policy(name) {
continue;
}
if !self.has_condition_for_field(name) {
return Err(S3Error::with_message(
S3ErrorCode::AccessDenied,
format!(
"Each form field that you specify in a form must appear in the list \
of policy conditions. \"{name}\" not specified in the policy."
),
));
}
}
Ok(())
}
fn is_field_exempt_from_policy(name: &str) -> bool {
matches!(name, "x-amz-signature" | "signature" | "awsaccesskeyid" | "file" | "submit" | "policy")
|| name.starts_with("x-ignore-")
}
fn has_condition_for_field(&self, name: &str) -> bool {
self.conditions.iter().any(|c| match c {
PostPolicyCondition::Eq { field, .. } | PostPolicyCondition::StartsWith { field, .. } => field == name,
PostPolicyCondition::ContentLengthRange { .. } => false,
})
}
fn validate_condition(
condition: &PostPolicyCondition,
multipart: &Multipart,
file_size: u64,
url_bucket: Option<&str>,
) -> S3Result<()> {
match condition {
PostPolicyCondition::Eq { field, value } => {
let actual = Self::get_field_value(field, multipart, url_bucket)?;
if actual.as_deref() != Some(value.as_str()) {
return Err(S3Error::with_message(
S3ErrorCode::InvalidPolicyDocument,
format!(
"Policy condition 'eq' for field '{field}' failed: expected '{value}', got '{}'",
actual.as_deref().unwrap_or_default()
),
));
}
}
PostPolicyCondition::StartsWith { field, prefix } => {
let actual = Self::get_field_value(field, multipart, url_bucket)?;
let actual_str = actual.as_deref().unwrap_or("");
if !actual_str.starts_with(prefix.as_str()) {
return Err(S3Error::with_message(
S3ErrorCode::InvalidPolicyDocument,
format!(
"Policy condition 'starts-with' for field '{field}' failed: expected prefix '{prefix}', got '{actual_str}'"
),
));
}
}
PostPolicyCondition::ContentLengthRange { min, max } => {
if file_size > *max {
return Err(s3_error!(EntityTooLarge, "Your proposed upload exceeds the maximum allowed object size."));
}
if file_size < *min {
return Err(s3_error!(
EntityTooSmall,
"Your proposed upload is smaller than the minimum allowed object size."
));
}
}
}
Ok(())
}
fn get_field_value<'a>(field: &str, multipart: &'a Multipart, url_bucket: Option<&'a str>) -> S3Result<Option<Cow<'a, str>>> {
if field == "bucket" {
let form_bucket = multipart.find_field_value(field);
if let Some(ub) = url_bucket {
if let Some(fb) = form_bucket
&& fb != ub
{
return Err(s3_error!(
InvalidPolicyDocument,
"Bucket in form field '{fb}' does not match bucket in URL '{ub}'"
));
}
return Ok(Some(Cow::Borrowed(ub)));
}
return Ok(form_bucket.map(Cow::Borrowed));
}
Ok(multipart.find_field_value(field).map(Cow::Borrowed))
}
#[must_use]
pub fn content_length_range(&self) -> Option<(u64, u64)> {
for condition in &self.conditions {
if let PostPolicyCondition::ContentLengthRange { min, max } = condition {
return Some((*min, *max));
}
}
None
}
}
#[derive(Debug, Deserialize)]
struct RawPostPolicy {
expiration: String,
conditions: Vec<RawCondition>,
}
#[derive(Debug)]
enum RawCondition {
Array(Vec<serde_json::Value>),
Object(HashMap<String, String>),
}
impl<'de> Deserialize<'de> for RawCondition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct RawConditionVisitor;
impl<'de> Visitor<'de> for RawConditionVisitor {
type Value = RawCondition;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("an array or object")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut items = Vec::new();
while let Some(item) = seq.next_element()? {
items.push(item);
}
Ok(RawCondition::Array(items))
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut items = HashMap::new();
while let Some((key, value)) = map.next_entry::<String, String>()? {
items.insert(key, value);
}
Ok(RawCondition::Object(items))
}
}
deserializer.deserialize_any(RawConditionVisitor)
}
}
impl RawCondition {
fn into_condition(self) -> Result<PostPolicyCondition, PostPolicyError> {
match self {
RawCondition::Array(items) => Self::parse_array_condition(&items),
RawCondition::Object(map) => Self::parse_object_condition(map),
}
}
fn parse_array_condition(items: &[serde_json::Value]) -> Result<PostPolicyCondition, PostPolicyError> {
if items.is_empty() {
return Err(PostPolicyError::InvalidCondition);
}
let operator = items[0].as_str().ok_or(PostPolicyError::InvalidCondition)?;
match operator.to_ascii_lowercase().as_str() {
"eq" => {
if items.len() != 3 {
return Err(PostPolicyError::InvalidCondition);
}
let field = items[1].as_str().ok_or(PostPolicyError::InvalidCondition)?;
let value = items[2].as_str().ok_or(PostPolicyError::InvalidCondition)?;
Ok(PostPolicyCondition::Eq {
field: normalize_field_name(field),
value: value.to_owned(),
})
}
"starts-with" => {
if items.len() != 3 {
return Err(PostPolicyError::InvalidCondition);
}
let field = items[1].as_str().ok_or(PostPolicyError::InvalidCondition)?;
let prefix = items[2].as_str().ok_or(PostPolicyError::InvalidCondition)?;
Ok(PostPolicyCondition::StartsWith {
field: normalize_field_name(field),
prefix: prefix.to_owned(),
})
}
"content-length-range" => {
if items.len() != 3 {
return Err(PostPolicyError::InvalidCondition);
}
let min = items[1].as_u64().ok_or(PostPolicyError::InvalidCondition)?;
let max = items[2].as_u64().ok_or(PostPolicyError::InvalidCondition)?;
if min > max {
return Err(PostPolicyError::InvalidCondition);
}
Ok(PostPolicyCondition::ContentLengthRange { min, max })
}
_ => Err(PostPolicyError::InvalidCondition),
}
}
fn parse_object_condition(map: HashMap<String, String>) -> Result<PostPolicyCondition, PostPolicyError> {
if map.len() != 1 {
return Err(PostPolicyError::InvalidCondition);
}
let (field, value) = map.into_iter().next().ok_or(PostPolicyError::InvalidCondition)?;
Ok(PostPolicyCondition::Eq {
field: normalize_field_name(&field),
value,
})
}
}
fn normalize_field_name(field: &str) -> String {
let field = field.strip_prefix('$').unwrap_or(field);
field.to_ascii_lowercase()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_timestamp(s: &str) -> Timestamp {
Timestamp::parse(TimestampFormat::DateTime, s).unwrap()
}
#[test]
fn test_parse_policy_json() {
let json = r#"{
"expiration": "2030-01-01T00:00:00.000Z",
"conditions": [
["eq", "$bucket", "mybucket"],
["starts-with", "$key", "user/"],
["content-length-range", 0, 10485760],
{"acl": "public-read"}
]
}"#;
let policy = PostPolicy::from_json(json).unwrap();
assert_eq!(policy.expiration, make_timestamp("2030-01-01T00:00:00.000Z"));
assert_eq!(policy.conditions.len(), 4);
assert_eq!(
policy.conditions[0],
PostPolicyCondition::Eq {
field: "bucket".to_owned(),
value: "mybucket".to_owned()
}
);
assert_eq!(
policy.conditions[1],
PostPolicyCondition::StartsWith {
field: "key".to_owned(),
prefix: "user/".to_owned()
}
);
assert_eq!(policy.conditions[2], PostPolicyCondition::ContentLengthRange { min: 0, max: 10_485_760 });
assert_eq!(
policy.conditions[3],
PostPolicyCondition::Eq {
field: "acl".to_owned(),
value: "public-read".to_owned()
}
);
}
#[test]
fn test_parse_policy_base64() {
let json = r#"{"expiration":"2030-01-01T00:00:00.000Z","conditions":[["eq","$bucket","test"]]}"#;
let encoded = base64_simd::STANDARD.encode_to_string(json);
let policy = PostPolicy::from_base64(&encoded).unwrap();
assert_eq!(policy.conditions.len(), 1);
}
#[test]
fn test_parse_invalid_base64() {
PostPolicy::from_base64("not-valid-base64!!!").unwrap_err();
}
#[test]
fn test_parse_invalid_json() {
let encoded = base64_simd::STANDARD.encode_to_string("{invalid json}");
let error = PostPolicy::from_base64(&encoded).unwrap_err();
assert!(matches!(error, PostPolicyError::Json(_)));
}
#[test]
fn test_parse_invalid_expiration() {
let json = r#"{"expiration":"not-a-date","conditions":[]}"#;
let result = PostPolicy::from_json(json);
assert!(matches!(result, Err(PostPolicyError::InvalidExpiration(_))));
}
#[test]
fn test_parse_invalid_condition_format() {
let json = r#"{"expiration":"2030-01-01T00:00:00.000Z","conditions":[["unknown-op","$key","value"]]}"#;
let result = PostPolicy::from_json(json);
assert!(matches!(result, Err(PostPolicyError::InvalidCondition)));
}
#[test]
fn test_content_length_range() {
let json = r#"{
"expiration": "2030-01-01T00:00:00.000Z",
"conditions": [
["content-length-range", 100, 1000]
]
}"#;
let policy = PostPolicy::from_json(json).unwrap();
assert_eq!(policy.content_length_range(), Some((100, 1000)));
}
#[test]
fn test_no_content_length_range() {
let json = r#"{
"expiration": "2030-01-01T00:00:00.000Z",
"conditions": [
["eq", "$bucket", "test"]
]
}"#;
let policy = PostPolicy::from_json(json).unwrap();
assert_eq!(policy.content_length_range(), None);
}
#[test]
fn test_normalize_field_name() {
assert_eq!(normalize_field_name("$bucket"), "bucket");
assert_eq!(normalize_field_name("$Key"), "key");
assert_eq!(normalize_field_name("bucket"), "bucket");
assert_eq!(normalize_field_name("X-Amz-Meta-Custom"), "x-amz-meta-custom");
}
#[test]
fn test_invalid_content_length_range_min_greater_than_max() {
let json = r#"{
"expiration": "2030-01-01T00:00:00.000Z",
"conditions": [
["content-length-range", 1000, 100]
]
}"#;
let result = PostPolicy::from_json(json);
assert!(matches!(result, Err(PostPolicyError::InvalidCondition)));
}
#[test]
fn test_valid_content_length_range_equal() {
let json = r#"{
"expiration": "2030-01-01T00:00:00.000Z",
"conditions": [
["content-length-range", 100, 100]
]
}"#;
let result = PostPolicy::from_json(json);
assert!(result.is_ok());
let policy = result.unwrap();
assert_eq!(policy.content_length_range(), Some((100, 100)));
}
fn create_test_multipart(fields: Vec<(&str, &str)>, content_type: Option<&str>) -> Multipart {
use crate::http::File;
let fields: Vec<(String, String)> = fields.into_iter().map(|(k, v)| (k.to_owned(), v.to_owned())).collect();
let file = File {
name: "test.txt".to_owned(),
content_type: content_type.map(String::from),
stream: None,
};
Multipart::new_for_test(fields, file)
}
#[test]
fn test_validate_condition_eq_success() {
let multipart = create_test_multipart(vec![("bucket", "mybucket"), ("key", "mykey")], None);
let condition = PostPolicyCondition::Eq {
field: "bucket".to_owned(),
value: "mybucket".to_owned(),
};
let result = PostPolicy::validate_condition(&condition, &multipart, 0, None);
assert!(result.is_ok());
}
#[test]
fn test_validate_condition_eq_failure() {
let multipart = create_test_multipart(vec![("bucket", "mybucket"), ("key", "mykey")], None);
let condition = PostPolicyCondition::Eq {
field: "bucket".to_owned(),
value: "wrongbucket".to_owned(),
};
let e = PostPolicy::validate_condition(&condition, &multipart, 0, None).unwrap_err();
assert!(matches!(e.code(), S3ErrorCode::InvalidPolicyDocument));
}
#[test]
fn test_validate_condition_starts_with_success() {
let multipart = create_test_multipart(vec![("key", "user/alice/file.txt")], None);
let condition = PostPolicyCondition::StartsWith {
field: "key".to_owned(),
prefix: "user/".to_owned(),
};
let result = PostPolicy::validate_condition(&condition, &multipart, 0, None);
assert!(result.is_ok());
}
#[test]
fn test_validate_condition_starts_with_empty_prefix() {
let multipart = create_test_multipart(vec![("key", "anyvalue")], None);
let condition = PostPolicyCondition::StartsWith {
field: "key".to_owned(),
prefix: String::new(),
};
let result = PostPolicy::validate_condition(&condition, &multipart, 0, None);
assert!(result.is_ok());
}
#[test]
fn test_validate_condition_starts_with_failure() {
let multipart = create_test_multipart(vec![("key", "public/file.txt")], None);
let condition = PostPolicyCondition::StartsWith {
field: "key".to_owned(),
prefix: "user/".to_owned(),
};
let e = PostPolicy::validate_condition(&condition, &multipart, 0, None).unwrap_err();
assert!(matches!(e.code(), S3ErrorCode::InvalidPolicyDocument));
}
#[test]
fn test_validate_condition_content_length_range_success() {
let multipart = create_test_multipart(vec![], None);
let condition = PostPolicyCondition::ContentLengthRange { min: 100, max: 1000 };
let result = PostPolicy::validate_condition(&condition, &multipart, 500, None);
assert!(result.is_ok());
}
#[test]
fn test_validate_condition_content_length_range_at_min() {
let multipart = create_test_multipart(vec![], None);
let condition = PostPolicyCondition::ContentLengthRange { min: 100, max: 1000 };
let result = PostPolicy::validate_condition(&condition, &multipart, 100, None);
assert!(result.is_ok());
}
#[test]
fn test_validate_condition_content_length_range_at_max() {
let multipart = create_test_multipart(vec![], None);
let condition = PostPolicyCondition::ContentLengthRange { min: 100, max: 1000 };
let result = PostPolicy::validate_condition(&condition, &multipart, 1000, None);
assert!(result.is_ok());
}
#[test]
fn test_validate_condition_content_length_range_too_small() {
let multipart = create_test_multipart(vec![], None);
let condition = PostPolicyCondition::ContentLengthRange { min: 100, max: 1000 };
let e = PostPolicy::validate_condition(&condition, &multipart, 99, None).unwrap_err();
assert!(matches!(e.code(), S3ErrorCode::EntityTooSmall));
}
#[test]
fn test_validate_condition_content_length_range_too_large() {
let multipart = create_test_multipart(vec![], None);
let condition = PostPolicyCondition::ContentLengthRange { min: 100, max: 1000 };
let e = PostPolicy::validate_condition(&condition, &multipart, 1001, None).unwrap_err();
assert!(matches!(e.code(), S3ErrorCode::EntityTooLarge));
}
#[test]
fn test_validate_condition_file_content_type() {
let multipart = create_test_multipart(vec![], Some("image/jpeg"));
let condition = PostPolicyCondition::Eq {
field: "content-type".to_owned(),
value: "image/jpeg".to_owned(),
};
let e = PostPolicy::validate_condition(&condition, &multipart, 0, None).unwrap_err();
assert!(matches!(e.code(), S3ErrorCode::InvalidPolicyDocument));
}
#[test]
fn test_validate_condition_field_content_type() {
let multipart = create_test_multipart(vec![("Content-Type", "image/jpg")], Some("image/jpeg"));
let condition = PostPolicyCondition::Eq {
field: "content-type".to_owned(),
value: "image/jpeg".to_owned(),
};
let e = PostPolicy::validate_condition(&condition, &multipart, 0, None).unwrap_err();
assert!(matches!(e.code(), S3ErrorCode::InvalidPolicyDocument));
}
#[test]
fn test_validate_condition_field_content_type_right() {
let multipart = create_test_multipart(vec![("Content-Type", "image/jpeg")], Some("image/jpg"));
let condition = PostPolicyCondition::Eq {
field: "content-type".to_owned(),
value: "image/jpeg".to_owned(),
};
let result = PostPolicy::validate_condition(&condition, &multipart, 0, None);
assert!(result.is_ok());
}
#[test]
fn test_validate_condition_missing_field() {
let multipart = create_test_multipart(vec![], None);
let condition = PostPolicyCondition::Eq {
field: "bucket".to_owned(),
value: "mybucket".to_owned(),
};
let e = PostPolicy::validate_condition(&condition, &multipart, 0, None).unwrap_err();
assert!(matches!(e.code(), S3ErrorCode::InvalidPolicyDocument));
}
#[test]
fn test_validate_bucket_condition_from_url() {
let multipart = create_test_multipart(vec![("key", "mykey")], None);
let condition = PostPolicyCondition::Eq {
field: "bucket".to_owned(),
value: "mybucket".to_owned(),
};
let result = PostPolicy::validate_condition(&condition, &multipart, 0, Some("mybucket"));
assert!(result.is_ok());
}
#[test]
fn test_validate_bucket_condition_from_url_mismatch() {
let multipart = create_test_multipart(vec![("key", "mykey")], None);
let condition = PostPolicyCondition::Eq {
field: "bucket".to_owned(),
value: "mybucket".to_owned(),
};
let e = PostPolicy::validate_condition(&condition, &multipart, 0, Some("wrongbucket")).unwrap_err();
assert!(matches!(e.code(), S3ErrorCode::InvalidPolicyDocument));
}
#[test]
fn test_validate_bucket_form_field_conflicts_with_url_bucket() {
let multipart = create_test_multipart(vec![("bucket", "form-bucket"), ("key", "mykey")], None);
let condition = PostPolicyCondition::Eq {
field: "bucket".to_owned(),
value: "form-bucket".to_owned(),
};
let e = PostPolicy::validate_condition(&condition, &multipart, 0, Some("url-bucket")).unwrap_err();
assert!(matches!(e.code(), S3ErrorCode::InvalidPolicyDocument));
}
#[test]
fn test_validate_conditions_only_with_url_bucket() {
let json = r#"{
"expiration": "2030-01-01T00:00:00.000Z",
"conditions": [
{"bucket": "mybucket"},
["eq", "$key", "mykey"]
]
}"#;
let policy = PostPolicy::from_json(json).unwrap();
let multipart = create_test_multipart(vec![("key", "mykey")], None);
let result = policy.validate_conditions_only(&multipart, 0, Some("mybucket"));
assert!(result.is_ok());
}
fn assert_invalid_condition(json_samples: &[&str]) {
for json in json_samples {
let error = PostPolicy::from_json(json).unwrap_err();
assert!(
matches!(error, PostPolicyError::InvalidCondition),
"Expected InvalidCondition for: {json}"
);
}
}
fn assert_json_error(json_samples: &[&str]) {
for json in json_samples {
let error = PostPolicy::from_json(json).unwrap_err();
assert!(matches!(error, PostPolicyError::Json(_)), "Expected Json error for: {json}");
}
}
#[test]
fn test_parse_array_condition_errors() {
let invalid_jsons = vec![
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [[]]}"#,
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [[123, "$key", "value"]]}"#,
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [["unknown-op", "$key", "value"]]}"#,
];
assert_invalid_condition(&invalid_jsons);
}
#[test]
fn test_parse_eq_condition_errors() {
let invalid_jsons = vec![
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [["eq", "$key"]]}"#,
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [["eq", 123, "value"]]}"#,
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [["eq", "$key", 123]]}"#,
];
assert_invalid_condition(&invalid_jsons);
}
#[test]
fn test_parse_starts_with_condition_errors() {
let invalid_jsons = vec![
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [["starts-with", "$key"]]}"#,
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [["starts-with", 123, "prefix"]]}"#,
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [["starts-with", "$key", 123]]}"#,
];
assert_invalid_condition(&invalid_jsons);
}
#[test]
fn test_parse_content_length_range_condition_errors() {
let invalid_jsons = vec![
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [["content-length-range", 100]]}"#,
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [["content-length-range", "100", 1000]]}"#,
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [["content-length-range", 100, "1000"]]}"#,
];
assert_invalid_condition(&invalid_jsons);
}
#[test]
fn test_parse_object_condition_errors() {
let invalid_jsons = vec![
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [{"bucket": "mybucket", "key": "mykey"}]}"#,
];
assert_invalid_condition(&invalid_jsons);
}
#[test]
fn test_parse_object_condition_value_type_error() {
let invalid_jsons = vec![
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [{"bucket": 123}]}"#,
];
assert_json_error(&invalid_jsons);
}
#[test]
fn test_parse_condition_invalid_types() {
let invalid_jsons = vec![
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": ["invalid string"]}"#,
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [123]}"#,
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [true]}"#,
r#"{"expiration": "2030-01-01T00:00:00.000Z", "conditions": [null]}"#,
];
assert_json_error(&invalid_jsons);
}
#[test]
fn test_form_field_not_in_policy_is_rejected() {
let json = r#"{
"expiration": "2030-01-01T00:00:00.000Z",
"conditions": [
{"bucket": "mybucket"},
["eq", "$key", "mykey"]
]
}"#;
let policy = PostPolicy::from_json(json).unwrap();
let multipart = create_test_multipart(vec![("key", "mykey"), ("success_action_status", "200")], None);
let err = policy.validate_conditions_only(&multipart, 0, Some("mybucket")).unwrap_err();
assert!(matches!(err.code(), S3ErrorCode::AccessDenied));
assert!(
err.message().unwrap_or("").contains("success_action_status"),
"error message should mention the undeclared field"
);
}
#[test]
fn test_exempt_fields_not_in_policy_are_allowed() {
let json = r#"{
"expiration": "2030-01-01T00:00:00.000Z",
"conditions": [
{"bucket": "mybucket"},
["eq", "$key", "mykey"]
]
}"#;
let policy = PostPolicy::from_json(json).unwrap();
let multipart = create_test_multipart(vec![("key", "mykey"), ("policy", "abc"), ("x-amz-signature", "sig123")], None);
let result = policy.validate_conditions_only(&multipart, 0, Some("mybucket"));
assert!(result.is_ok(), "SigV4 exempt fields should not be rejected");
let multipart_v2 = create_test_multipart(
vec![
("awsaccesskeyid", "AKID"),
("key", "mykey"),
("policy", "abc"),
("signature", "sig456"),
],
None,
);
let result_v2 = policy.validate_conditions_only(&multipart_v2, 0, Some("mybucket"));
assert!(result_v2.is_ok(), "SigV2 exempt fields should not be rejected");
}
#[test]
fn test_starts_with_condition_covers_field() {
let json = r#"{
"expiration": "2030-01-01T00:00:00.000Z",
"conditions": [
{"bucket": "mybucket"},
["eq", "$key", "mykey"],
["starts-with", "$success_action_status", ""]
]
}"#;
let policy = PostPolicy::from_json(json).unwrap();
let multipart = create_test_multipart(vec![("key", "mykey"), ("success_action_status", "200")], None);
let result = policy.validate_conditions_only(&multipart, 0, Some("mybucket"));
assert!(result.is_ok(), "field covered by starts-with should be accepted");
}
#[test]
fn test_x_ignore_prefixed_fields_are_allowed() {
let json = r#"{
"expiration": "2030-01-01T00:00:00.000Z",
"conditions": [
{"bucket": "mybucket"},
["eq", "$key", "mykey"]
]
}"#;
let policy = PostPolicy::from_json(json).unwrap();
let multipart = create_test_multipart(vec![("key", "mykey"), ("x-ignore-custom", "anything")], None);
let result = policy.validate_conditions_only(&multipart, 0, Some("mybucket"));
assert!(result.is_ok(), "x-ignore- fields should not be rejected");
}
}