mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-01 09:48:20 +00:00
fix(s3): add x-amz-grant-* headers to policy condition values (#2031)
This commit is contained in:
@@ -508,6 +508,25 @@ pub fn get_condition_values(
|
|||||||
clone_header.remove(*obj_lock);
|
clone_header.remove(*obj_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// S3 policy condition keys use "x-amz-grant-*" (policy key s3:x-amz-grant-* -> name() returns x-amz-grant-*)
|
||||||
|
for grant_header in &[
|
||||||
|
"x-amz-grant-full-control",
|
||||||
|
"x-amz-grant-read",
|
||||||
|
"x-amz-grant-write",
|
||||||
|
"x-amz-grant-read-acp",
|
||||||
|
"x-amz-grant-write-acp",
|
||||||
|
] {
|
||||||
|
let values = clone_header
|
||||||
|
.get_all(*grant_header)
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.to_str().unwrap_or("").to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
if !values.is_empty() {
|
||||||
|
args.insert((*grant_header).to_string(), values);
|
||||||
|
}
|
||||||
|
clone_header.remove(*grant_header);
|
||||||
|
}
|
||||||
|
|
||||||
for (key, _values) in clone_header.iter() {
|
for (key, _values) in clone_header.iter() {
|
||||||
if key.as_str().eq_ignore_ascii_case("x-amz-tagging") {
|
if key.as_str().eq_ignore_ascii_case("x-amz-tagging") {
|
||||||
continue;
|
continue;
|
||||||
@@ -1038,6 +1057,25 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_condition_values_with_grant_headers() {
|
||||||
|
let cred = create_test_credentials();
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("x-amz-grant-full-control", HeaderValue::from_static("id=owner-123"));
|
||||||
|
headers.insert(
|
||||||
|
"x-amz-grant-read",
|
||||||
|
HeaderValue::from_static("uri=http://acs.amazonaws.com/groups/global/AllUsers"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let conditions = get_condition_values(&headers, &cred, None, None, None);
|
||||||
|
|
||||||
|
assert_eq!(conditions.get("x-amz-grant-full-control"), Some(&vec!["id=owner-123".to_string()]));
|
||||||
|
assert_eq!(
|
||||||
|
conditions.get("x-amz-grant-read"),
|
||||||
|
Some(&vec!["uri=http://acs.amazonaws.com/groups/global/AllUsers".to_string()])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_condition_values_with_signature_age() {
|
fn test_get_condition_values_with_signature_age() {
|
||||||
let cred = create_test_credentials();
|
let cred = create_test_credentials();
|
||||||
|
|||||||
@@ -166,14 +166,20 @@ async fn load_object_acl(bucket: &str, object: &str, version_id: Option<&str>, h
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn check_acl_access<T>(req: &S3Request<T>, req_info: &ReqInfo, action: &Action, policy_allowed: bool) -> S3Result<bool> {
|
async fn check_acl_access<T>(req: &S3Request<T>, req_info: &ReqInfo, action: &Action, policy_allowed: bool) -> S3Result<bool> {
|
||||||
if req_info.is_owner || policy_allowed {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some((target, permission)) = acl_permission_for_action(action) else {
|
let Some((target, permission)) = acl_permission_for_action(action) else {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// For object-level operations, do not bypass on account-level is_owner.
|
||||||
|
// The bucket/account owner must have explicit ACL grant to access objects owned by others.
|
||||||
|
let bypass_owner = match target {
|
||||||
|
AclTarget::Bucket => req_info.is_owner,
|
||||||
|
AclTarget::Object => false,
|
||||||
|
};
|
||||||
|
if bypass_owner || policy_allowed {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
let bucket = req_info.bucket.as_deref().unwrap_or("");
|
let bucket = req_info.bucket.as_deref().unwrap_or("");
|
||||||
if bucket.is_empty() {
|
if bucket.is_empty() {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
@@ -364,7 +370,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
|
|||||||
return Err(s3_error!(AccessDenied, "Access Denied"));
|
return Err(s3_error!(AccessDenied, "Access Denied"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if PolicySys::is_allowed(&BucketPolicyArgs {
|
let policy_allowed_fallback = PolicySys::is_allowed(&BucketPolicyArgs {
|
||||||
bucket: req_info.bucket.as_deref().unwrap_or(""),
|
bucket: req_info.bucket.as_deref().unwrap_or(""),
|
||||||
action,
|
action,
|
||||||
is_owner: req_info.is_owner,
|
is_owner: req_info.is_owner,
|
||||||
@@ -373,8 +379,16 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
|
|||||||
conditions: &conditions,
|
conditions: &conditions,
|
||||||
object: req_info.object.as_deref().unwrap_or(""),
|
object: req_info.object.as_deref().unwrap_or(""),
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
{
|
|
||||||
|
if policy_allowed_fallback {
|
||||||
|
// For object-level operations, bucket owner (is_owner) must still pass ACL check.
|
||||||
|
// Policy may have allowed due to is_owner, but object owner is authoritative.
|
||||||
|
if let Some((AclTarget::Object, _)) = acl_permission_for_action(&action)
|
||||||
|
&& !check_acl_access(req, req_info, &action, false).await?
|
||||||
|
{
|
||||||
|
return Err(s3_error!(AccessDenied, "Access Denied"));
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
# - Object ownership: Bucket ownership controls
|
# - Object ownership: Bucket ownership controls
|
||||||
#
|
#
|
||||||
# - SSE-KMS: KMS-related edge cases
|
# - SSE-KMS: KMS-related edge cases
|
||||||
# - Bucket Policy: Multipart upload authorization, SSE condition keys
|
# - Bucket Policy: Multipart upload authorization, SSE condition keys, grant header conditions
|
||||||
# - Versioning: Concurrent multi-object delete
|
# - Versioning: Concurrent multi-object delete
|
||||||
# - ACL: Bucket and Object ACL
|
# - ACL: Bucket and Object ACL
|
||||||
# - Object Copy: Same/cross bucket, metadata, versioning
|
# - Object Copy: Same/cross bucket, metadata, versioning
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
# - DeleteObject: Proper NoSuchBucket for deleted buckets
|
# - DeleteObject: Proper NoSuchBucket for deleted buckets
|
||||||
# - Multipart Copy: InvalidRange when CopySourceRange exceeds source size
|
# - Multipart Copy: InvalidRange when CopySourceRange exceeds source size
|
||||||
#
|
#
|
||||||
# Total: 394 tests
|
# Total: 395 tests
|
||||||
|
|
||||||
test_basic_key_count
|
test_basic_key_count
|
||||||
test_bucket_create_naming_bad_short_one
|
test_bucket_create_naming_bad_short_one
|
||||||
@@ -227,6 +227,7 @@ test_bucket_policy_another_bucket
|
|||||||
test_bucket_policy_allow_notprincipal
|
test_bucket_policy_allow_notprincipal
|
||||||
test_bucket_policy_multipart
|
test_bucket_policy_multipart
|
||||||
test_bucket_policy_put_obj_acl
|
test_bucket_policy_put_obj_acl
|
||||||
|
test_bucket_policy_put_obj_grant
|
||||||
test_bucket_policy_put_obj_kms_s3
|
test_bucket_policy_put_obj_kms_s3
|
||||||
test_bucket_policy_put_obj_s3_kms
|
test_bucket_policy_put_obj_s3_kms
|
||||||
test_object_presigned_put_object_with_acl
|
test_object_presigned_put_object_with_acl
|
||||||
|
|||||||
@@ -28,5 +28,4 @@ test_lifecycle_transition_encrypted
|
|||||||
|
|
||||||
# Tests with known issues (need further investigation)
|
# Tests with known issues (need further investigation)
|
||||||
test_bucket_policy_different_tenant
|
test_bucket_policy_different_tenant
|
||||||
test_bucket_policy_put_obj_grant
|
|
||||||
test_bucket_policy_tenanted_bucket
|
test_bucket_policy_tenanted_bucket
|
||||||
|
|||||||
Reference in New Issue
Block a user