mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
test(e2e): require exact object lock errors (#6532)
This commit is contained in:
@@ -21,6 +21,8 @@
|
||||
//! - Bypass governance retention header handling
|
||||
|
||||
use aws_sdk_s3::Client;
|
||||
use aws_sdk_s3::error::SdkError;
|
||||
use aws_sdk_s3::operation::delete_object::DeleteObjectError;
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use aws_sdk_s3::types::{
|
||||
DefaultRetention, ObjectLockConfiguration, ObjectLockEnabled, ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockMode,
|
||||
@@ -236,7 +238,7 @@ pub async fn delete_object_with_bypass(
|
||||
key: &str,
|
||||
version_id: Option<&str>,
|
||||
bypass_governance: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
) -> Result<(), SdkError<DeleteObjectError>> {
|
||||
let mut request = client
|
||||
.delete_object()
|
||||
.bucket(bucket)
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
use super::common::*;
|
||||
use aws_sdk_s3::Client;
|
||||
use aws_sdk_s3::error::ProvideErrorMetadata;
|
||||
use aws_sdk_s3::error::{ProvideErrorMetadata, SdkError};
|
||||
use aws_sdk_s3::primitives::{ByteStream, DateTimeFormat};
|
||||
use aws_sdk_s3::types::{
|
||||
CompletedMultipartUpload, CompletedPart, Delete, MetadataDirective, ObjectIdentifier, ObjectLockLegalHoldStatus,
|
||||
@@ -70,14 +70,21 @@ fn retention_timestamp(days: i64) -> aws_sdk_s3::primitives::DateTime {
|
||||
.expect("retention timestamp should parse")
|
||||
}
|
||||
|
||||
fn assert_access_denied<T, E: std::fmt::Debug>(result: Result<T, E>, context: &str) {
|
||||
let err = match result {
|
||||
Ok(_) => panic!("{context}"),
|
||||
Err(err) => format!("{err:?}"),
|
||||
};
|
||||
assert!(
|
||||
err.contains("AccessDenied") || err.to_lowercase().contains("access denied"),
|
||||
"{context}: expected AccessDenied, got: {err}"
|
||||
fn assert_access_denied<T, E>(result: Result<T, SdkError<E>>, context: &str)
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
E: ProvideErrorMetadata + std::fmt::Debug,
|
||||
{
|
||||
let error = result.expect_err(context);
|
||||
assert_eq!(
|
||||
error.raw_response().map(|response| response.status().as_u16()),
|
||||
Some(403),
|
||||
"{context}: expected HTTP 403, got: {error:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
error.as_service_error().and_then(ProvideErrorMetadata::code),
|
||||
Some("AccessDenied"),
|
||||
"{context}: expected AccessDenied, got: {error:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,14 +136,15 @@ async fn test_delete_object_blocked_by_compliance_retention() {
|
||||
.unwrap();
|
||||
|
||||
// Attempt to delete - should fail
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await;
|
||||
assert!(delete_result.is_err(), "Delete should fail for COMPLIANCE locked object");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await,
|
||||
"Delete should fail for COMPLIANCE locked object",
|
||||
);
|
||||
|
||||
// Even with bypass header, COMPLIANCE should not allow deletion
|
||||
let delete_with_bypass_result = delete_object_with_bypass(&client, bucket, key, Some(&version_id), true).await;
|
||||
assert!(
|
||||
delete_with_bypass_result.is_err(),
|
||||
"Delete with bypass should still fail for COMPLIANCE mode"
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&version_id), true).await,
|
||||
"Delete with bypass should still fail for COMPLIANCE mode",
|
||||
);
|
||||
|
||||
info!("✅ Test passed: COMPLIANCE retention blocks deletion");
|
||||
@@ -165,8 +173,10 @@ async fn test_delete_object_blocked_by_governance_without_bypass() {
|
||||
.unwrap();
|
||||
|
||||
// Attempt to delete without bypass - should fail
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await;
|
||||
assert!(delete_result.is_err(), "Delete without bypass should fail for GOVERNANCE locked object");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await,
|
||||
"Delete without bypass should fail for GOVERNANCE locked object",
|
||||
);
|
||||
|
||||
info!("✅ Test passed: GOVERNANCE retention blocks deletion without bypass");
|
||||
}
|
||||
@@ -198,14 +208,19 @@ async fn test_delete_object_allowed_by_governance_with_bypass() {
|
||||
assert!(delete_result.is_ok(), "Delete with bypass should succeed for GOVERNANCE mode");
|
||||
|
||||
// Verify object is deleted
|
||||
let head_result = client
|
||||
let head_error = client
|
||||
.head_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.version_id(&version_id)
|
||||
.send()
|
||||
.await;
|
||||
assert!(head_result.is_err(), "Object should be deleted");
|
||||
.await
|
||||
.expect_err("Object should be deleted");
|
||||
assert_eq!(
|
||||
head_error.raw_response().map(|response| response.status().as_u16()),
|
||||
Some(404),
|
||||
"deleted version should return HTTP 404: {head_error:?}"
|
||||
);
|
||||
|
||||
info!("✅ Test passed: GOVERNANCE retention allows deletion with bypass");
|
||||
}
|
||||
@@ -240,17 +255,18 @@ async fn test_delete_object_creates_delete_marker_for_retained_current_version()
|
||||
.expect("delete marker should have a version id")
|
||||
.to_string();
|
||||
|
||||
let protected_delete = delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), false).await;
|
||||
assert!(protected_delete.is_err(), "Retained version should still reject direct deletion");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), false).await,
|
||||
"Retained version should still reject direct deletion",
|
||||
);
|
||||
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&delete_marker_version_id), false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let still_protected = delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), false).await;
|
||||
assert!(
|
||||
still_protected.is_err(),
|
||||
"Retained version should remain protected after delete marker removal"
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), false).await,
|
||||
"Retained version should remain protected after delete marker removal",
|
||||
);
|
||||
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), true)
|
||||
@@ -282,12 +298,16 @@ async fn test_delete_object_blocked_by_legal_hold() {
|
||||
.unwrap();
|
||||
|
||||
// Attempt to delete - should fail (legal hold cannot be bypassed)
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await;
|
||||
assert!(delete_result.is_err(), "Delete should fail for legal hold object");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await,
|
||||
"Delete should fail for legal hold object",
|
||||
);
|
||||
|
||||
// Even with bypass header, legal hold should block deletion
|
||||
let delete_with_bypass_result = delete_object_with_bypass(&client, bucket, key, Some(&version_id), true).await;
|
||||
assert!(delete_with_bypass_result.is_err(), "Delete with bypass should still fail for legal hold");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&version_id), true).await,
|
||||
"Delete with bypass should still fail for legal hold",
|
||||
);
|
||||
|
||||
info!("✅ Test passed: Legal Hold blocks deletion");
|
||||
}
|
||||
@@ -315,14 +335,19 @@ async fn test_delete_object_allowed_with_legal_hold_off() {
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, key, Some(&version_id), false).await;
|
||||
assert!(delete_result.is_ok(), "Delete should succeed when legal hold is OFF");
|
||||
|
||||
let head_result = client
|
||||
let head_error = client
|
||||
.head_object()
|
||||
.bucket(bucket)
|
||||
.key(key)
|
||||
.version_id(&version_id)
|
||||
.send()
|
||||
.await;
|
||||
assert!(head_result.is_err(), "Object should be deleted when legal hold is OFF");
|
||||
.await
|
||||
.expect_err("Object should be deleted when legal hold is OFF");
|
||||
assert_eq!(
|
||||
head_error.raw_response().map(|response| response.status().as_u16()),
|
||||
Some(404),
|
||||
"deleted version should return HTTP 404: {head_error:?}"
|
||||
);
|
||||
|
||||
info!("✅ Test passed: Legal Hold OFF allows deletion");
|
||||
}
|
||||
@@ -545,8 +570,10 @@ async fn test_put_object_overwrite_creates_new_version_under_legal_hold() {
|
||||
"held version must keep its legal hold after the overwrite"
|
||||
);
|
||||
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, key, Some(&held_version_id), false).await;
|
||||
assert!(delete_result.is_err(), "held version must stay delete-protected after the overwrite");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&held_version_id), false).await,
|
||||
"held version must stay delete-protected after the overwrite",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -768,8 +795,10 @@ async fn test_copy_object_overwrite_creates_new_version_under_legal_hold() {
|
||||
"held destination version must keep its legal hold after the copy"
|
||||
);
|
||||
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, dst_key, Some(&held_version_id), false).await;
|
||||
assert!(delete_result.is_err(), "held destination version must stay delete-protected");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, dst_key, Some(&held_version_id), false).await,
|
||||
"held destination version must stay delete-protected",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -909,10 +938,9 @@ async fn test_create_multipart_upload_creates_new_version_under_compliance_reten
|
||||
|
||||
// COMPLIANCE retention on the previous version survives the overwrite and
|
||||
// cannot be bypassed.
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), true).await;
|
||||
assert!(
|
||||
delete_result.is_err(),
|
||||
"retained version must stay delete-protected even with governance bypass"
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), true).await,
|
||||
"retained version must stay delete-protected even with governance bypass",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -971,8 +999,10 @@ async fn test_delete_completed_multipart_object_blocked_by_legal_hold() {
|
||||
.unwrap();
|
||||
|
||||
let version_id = complete_output.version_id().expect("multipart object should be versioned");
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, key, Some(version_id), false).await;
|
||||
assert!(delete_result.is_err(), "Delete should fail for multipart object protected by legal hold");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(version_id), false).await,
|
||||
"Delete should fail for multipart object protected by legal hold",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1032,8 +1062,10 @@ async fn test_delete_completed_multipart_object_blocked_by_retention() {
|
||||
.unwrap();
|
||||
|
||||
let version_id = complete_output.version_id().expect("multipart object should be versioned");
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, key, Some(version_id), false).await;
|
||||
assert!(delete_result.is_err(), "Delete should fail for multipart object protected by retention");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(version_id), false).await,
|
||||
"Delete should fail for multipart object protected by retention",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1111,8 +1143,10 @@ async fn test_complete_multipart_upload_creates_new_version_under_legal_hold() {
|
||||
"held version must keep its legal hold after multipart completion"
|
||||
);
|
||||
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, key, Some(&held_version_id), false).await;
|
||||
assert!(delete_result.is_err(), "held version must stay delete-protected");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&held_version_id), false).await,
|
||||
"held version must stay delete-protected",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1181,10 +1215,9 @@ async fn test_complete_multipart_upload_creates_new_version_under_compliance_ret
|
||||
|
||||
// COMPLIANCE retention on the previous version survives the overwrite and
|
||||
// cannot be bypassed.
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), true).await;
|
||||
assert!(
|
||||
delete_result.is_err(),
|
||||
"retained version must stay delete-protected even with governance bypass"
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), true).await,
|
||||
"retained version must stay delete-protected even with governance bypass",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1621,8 +1654,10 @@ async fn test_default_retention_applied_to_new_objects() {
|
||||
let version_id = response.version_id().unwrap();
|
||||
|
||||
// Try to delete without bypass - should fail due to default retention
|
||||
let delete_result = delete_object_with_bypass(&client, bucket, key, Some(version_id), false).await;
|
||||
assert!(delete_result.is_err(), "Delete should fail for object with default retention applied");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(version_id), false).await,
|
||||
"Delete should fail for object with default retention applied",
|
||||
);
|
||||
|
||||
let retention = client
|
||||
.get_object_retention()
|
||||
@@ -1710,8 +1745,10 @@ async fn test_delete_object_creates_delete_marker_for_default_retained_current_v
|
||||
.expect("delete marker should have a version id")
|
||||
.to_string();
|
||||
|
||||
let protected_delete = delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), false).await;
|
||||
assert!(protected_delete.is_err(), "Default-retained version should still reject direct deletion");
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), false).await,
|
||||
"Default-retained version should still reject direct deletion",
|
||||
);
|
||||
|
||||
let retention_after_delete_marker = client
|
||||
.get_object_retention()
|
||||
@@ -2011,11 +2048,9 @@ async fn test_copy_object_retention_uses_destination_policy() {
|
||||
|
||||
// COMPLIANCE retention on the previous destination version survives the
|
||||
// overwrite and cannot be bypassed.
|
||||
let delete_result =
|
||||
delete_object_with_bypass(&client, dst_bucket, "locked-destination", Some(&retained_version_id), true).await;
|
||||
assert!(
|
||||
delete_result.is_err(),
|
||||
"retained destination version must stay delete-protected even with governance bypass"
|
||||
assert_access_denied(
|
||||
delete_object_with_bypass(&client, dst_bucket, "locked-destination", Some(&retained_version_id), true).await,
|
||||
"retained destination version must stay delete-protected even with governance bypass",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user