fix(authz): fail closed on policy load errors (#5359)

* fix(authz): fail closed on policy load errors

* test(authz): cover policy failure precedence

* test: align policy failure expectations

* test: align upload part copy fail-closed expectation
This commit is contained in:
cxymds
2026-07-28 17:04:35 +08:00
committed by GitHub
parent 2216f00cfd
commit 4fb9b0dc7f
5 changed files with 163 additions and 36 deletions
+6 -4
View File
@@ -1810,7 +1810,7 @@ impl DefaultBucketUsecase {
client_info,
);
let read_allowed = PolicySys::is_allowed(&BucketPolicyArgs {
let read_allowed = PolicySys::try_is_allowed(&BucketPolicyArgs {
bucket: &bucket,
action: Action::S3Action(S3Action::ListBucketAction),
is_owner: false,
@@ -1819,9 +1819,10 @@ impl DefaultBucketUsecase {
conditions: &conditions,
object: "",
})
.await;
.await
.map_err(ApiError::from)?;
let write_allowed = PolicySys::is_allowed(&BucketPolicyArgs {
let write_allowed = PolicySys::try_is_allowed(&BucketPolicyArgs {
bucket: &bucket,
action: Action::S3Action(S3Action::PutObjectAction),
is_owner: false,
@@ -1830,7 +1831,8 @@ impl DefaultBucketUsecase {
conditions: &conditions,
object: "",
})
.await;
.await
.map_err(ApiError::from)?;
let mut is_public = read_allowed || write_allowed;
let ignore_public_acls = match metadata_sys::get_public_access_block_config(&bucket).await {
+8
View File
@@ -494,6 +494,14 @@ mod tests {
assert!(downcast_storage_error.is_some());
}
#[test]
fn policy_metadata_read_failure_maps_to_internal_error() {
let api_error = ApiError::from(StorageError::Io(IoError::other("policy read failed")));
assert_eq!(api_error.code, S3ErrorCode::InternalError);
assert!(api_error.source.is_some());
}
#[test]
fn test_kms_service_unavailable_maps_to_retryable_error() {
let api_error = ApiError::from(StorageError::other(KmsUnavailableError));
+29 -21
View File
@@ -505,7 +505,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
let owner_can_bypass_deny = owner_can_bypass_policy_deny(is_owner, &action);
if !bucket_name.is_empty()
&& !owner_can_bypass_deny
&& !PolicySys::is_allowed(&BucketPolicyArgs {
&& !PolicySys::try_is_allowed(&BucketPolicyArgs {
bucket: bucket_name,
action,
// Early explicit-deny gate for bucket policy: use owner short-circuit path so
@@ -517,6 +517,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
object: object.as_str(),
})
.await
.map_err(ApiError::from)?
{
return Err(s3_error!(AccessDenied, "Access Denied"));
}
@@ -535,7 +536,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
};
let delete_version_allowed = iam_store.eval_prepared(&prepared, &delete_version_args).await;
if !delete_version_allowed
&& !PolicySys::is_allowed(&BucketPolicyArgs {
&& !PolicySys::try_is_allowed(&BucketPolicyArgs {
bucket: bucket.as_str(),
action: Action::S3Action(S3Action::DeleteObjectVersionAction),
is_owner,
@@ -545,6 +546,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
object: object.as_str(),
})
.await
.map_err(ApiError::from)?
{
return Err(s3_error!(AccessDenied, "Access Denied"));
}
@@ -571,7 +573,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
return Ok(());
}
let policy_allowed_fallback = PolicySys::is_allowed(&BucketPolicyArgs {
let policy_allowed_fallback = PolicySys::try_is_allowed(&BucketPolicyArgs {
bucket: bucket.as_str(),
action,
is_owner,
@@ -580,7 +582,8 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
conditions: &conditions,
object: object.as_str(),
})
.await;
.await
.map_err(ApiError::from)?;
if policy_allowed_fallback {
authorize_table_data_plane_if_needed(action, bucket.as_str(), object.as_str(), cred, is_owner, &conditions, claims)
@@ -605,7 +608,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
return Ok(());
}
if PolicySys::is_allowed(&BucketPolicyArgs {
if PolicySys::try_is_allowed(&BucketPolicyArgs {
bucket: bucket.as_str(),
action: Action::S3Action(S3Action::ListBucketAction),
is_owner,
@@ -615,6 +618,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
object: object.as_str(),
})
.await
.map_err(ApiError::from)?
{
return Ok(());
}
@@ -688,7 +692,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
let bucket_name = bucket.as_str();
if !bucket_name.is_empty()
&& !PolicySys::is_allowed(&BucketPolicyArgs {
&& !PolicySys::try_is_allowed(&BucketPolicyArgs {
bucket: bucket_name,
action,
// Early explicit-deny gate for bucket policy in anonymous path.
@@ -699,13 +703,14 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
object: object.as_str(),
})
.await
.map_err(ApiError::from)?
{
return Err(s3_error!(AccessDenied, "Access Denied"));
}
if action != Action::S3Action(S3Action::ListAllMyBucketsAction) {
if action == Action::S3Action(S3Action::DeleteObjectAction) && version_id.is_some() {
let delete_version_allowed = PolicySys::is_allowed(&BucketPolicyArgs {
let delete_version_allowed = PolicySys::try_is_allowed(&BucketPolicyArgs {
bucket: bucket.as_str(),
action: Action::S3Action(S3Action::DeleteObjectVersionAction),
is_owner: false,
@@ -714,13 +719,14 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
conditions: &conditions,
object: object.as_str(),
})
.await;
.await
.map_err(ApiError::from)?;
if !delete_version_allowed {
return Err(s3_error!(AccessDenied, "Access Denied"));
}
}
let policy_allowed = PolicySys::is_allowed(&BucketPolicyArgs {
let policy_allowed = PolicySys::try_is_allowed(&BucketPolicyArgs {
bucket: bucket.as_str(),
action,
is_owner: false,
@@ -729,7 +735,8 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
conditions: &conditions,
object: object.as_str(),
})
.await;
.await
.map_err(ApiError::from)?;
// A bucket policy granting s3:ListBucket also covers listing versions. This
// fallback has to feed the same post-authorization gates as the direct grant
@@ -737,7 +744,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
// ListObjectVersions after RestrictPublicBuckets is turned on.
let policy_allowed = policy_allowed
|| (action == Action::S3Action(S3Action::ListBucketVersionsAction)
&& PolicySys::is_allowed(&BucketPolicyArgs {
&& PolicySys::try_is_allowed(&BucketPolicyArgs {
bucket: bucket.as_str(),
action: Action::S3Action(S3Action::ListBucketAction),
is_owner: false,
@@ -746,7 +753,8 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
conditions: &conditions,
object: "",
})
.await);
.await
.map_err(ApiError::from)?);
if policy_allowed {
deny_anonymous_table_data_plane_if_needed(action, bucket.as_str(), object.as_str()).await?;
@@ -2763,7 +2771,7 @@ mod tests {
}
#[tokio::test]
async fn abort_multipart_upload_rejects_unauthorized_request() {
async fn abort_multipart_upload_fails_closed_when_policy_metadata_is_unavailable() {
let fs = FS::new();
let mut req = build_request(
AbortMultipartUploadInput::builder()
@@ -2779,12 +2787,12 @@ mod tests {
let err = fs
.abort_multipart_upload(&mut req)
.await
.expect_err("missing credentials should reject access");
assert_eq!(err.code(), &S3ErrorCode::AccessDenied);
.expect_err("unavailable policy metadata must fail closed");
assert_eq!(err.code(), &S3ErrorCode::InternalError);
}
#[tokio::test]
async fn complete_multipart_upload_rejects_unauthorized_request() {
async fn complete_multipart_upload_fails_closed_when_policy_metadata_is_unavailable() {
let fs = FS::new();
let mut req = build_request(
CompleteMultipartUploadInput::builder()
@@ -2801,12 +2809,12 @@ mod tests {
let err = fs
.complete_multipart_upload(&mut req)
.await
.expect_err("missing credentials should reject access");
assert_eq!(err.code(), &S3ErrorCode::AccessDenied);
.expect_err("unavailable policy metadata must fail closed");
assert_eq!(err.code(), &S3ErrorCode::InternalError);
}
#[tokio::test]
async fn upload_part_copy_rejects_unauthorized_request() {
async fn upload_part_copy_fails_closed_when_policy_metadata_is_unavailable() {
let fs = FS::new();
let mut req = build_request(
UploadPartCopyInput::builder()
@@ -2828,7 +2836,7 @@ mod tests {
let err = fs
.upload_part_copy(&mut req)
.await
.expect_err("missing credentials should reject access");
assert_eq!(err.code(), &S3ErrorCode::AccessDenied);
.expect_err("unavailable policy metadata must fail closed");
assert_eq!(err.code(), &S3ErrorCode::InternalError);
}
}