mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-07 05:43:14 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user