test(object-lock): cover default retention delete marker (#2836)

This commit is contained in:
GatewayJ
2026-05-07 05:14:00 +08:00
committed by GitHub
parent 96d41b6349
commit 3130670157
6 changed files with 119 additions and 25 deletions
@@ -1536,6 +1536,91 @@ async fn test_default_retention_applied_to_new_objects() {
info!("✅ Test passed: Default retention is applied to new objects");
}
#[tokio::test]
#[serial]
async fn test_delete_object_creates_delete_marker_for_default_retained_current_version() {
init_logging();
info!("🧪 Test: DeleteObject creates delete marker for default-retained current version");
let mut env = ObjectLockTestEnvironment::new().await.unwrap();
env.start_rustfs().await.unwrap();
let bucket = "test-default-retention-delete-marker";
let key = "default-retained-object";
let data = b"test data for default-retained current version";
env.create_object_lock_bucket(bucket).await.unwrap();
let client = env.s3_client();
put_object_lock_configuration(&client, bucket, ObjectLockRetentionMode::Governance, Some(30), None)
.await
.unwrap();
let put_output = client
.put_object()
.bucket(bucket)
.key(key)
.body(ByteStream::from(data.to_vec()))
.send()
.await
.unwrap();
let retained_version_id = put_output
.version_id()
.expect("default-retained object should have a version id")
.to_string();
let retention = client
.get_object_retention()
.bucket(bucket)
.key(key)
.version_id(&retained_version_id)
.send()
.await
.unwrap();
let retention = retention.retention().expect("default retention should be readable");
assert_eq!(retention.mode().map(|value| value.as_str()), Some("GOVERNANCE"));
assert!(
retention.retain_until_date().is_some(),
"default retention should write a retain-until date"
);
let delete_marker_output = client.delete_object().bucket(bucket).key(key).send().await.unwrap();
assert_eq!(delete_marker_output.delete_marker(), Some(true));
let delete_marker_version_id = delete_marker_output
.version_id()
.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");
let retention_after_delete_marker = client
.get_object_retention()
.bucket(bucket)
.key(key)
.version_id(&retained_version_id)
.send()
.await
.unwrap();
let retention_after_delete_marker = retention_after_delete_marker
.retention()
.expect("default retention should remain readable by version id after delete marker creation");
assert_eq!(retention_after_delete_marker.mode().map(|value| value.as_str()), Some("GOVERNANCE"));
assert!(
retention_after_delete_marker.retain_until_date().is_some(),
"retained version should keep its retain-until date after delete marker creation"
);
delete_object_with_bypass(&client, bucket, key, Some(&delete_marker_version_id), false)
.await
.unwrap();
delete_object_with_bypass(&client, bucket, key, Some(&retained_version_id), true)
.await
.unwrap();
info!("✅ Test passed: Delete marker is allowed while default-retained version stays protected");
}
#[tokio::test]
#[serial]
async fn test_put_copy_and_multipart_reject_incomplete_retention_headers() {
+6 -3
View File
@@ -1203,6 +1203,9 @@ mod tests {
use super::*;
use http::{Extensions, HeaderMap, Method, Uri, header::HeaderValue};
use rustfs_filemeta::ObjectPartInfo;
use rustfs_utils::http::{
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
};
use std::{collections::HashMap, io::Cursor};
use tokio::io::AsyncReadExt;
@@ -1548,9 +1551,9 @@ mod tests {
};
for (header_name, header_value) in [
("x-amz-object-lock-mode", "GOVERNANCE"),
("x-amz-object-lock-retain-until-date", "2030-01-01T00:00:00Z"),
("x-amz-object-lock-legal-hold", "ON"),
(AMZ_OBJECT_LOCK_MODE_LOWER, "GOVERNANCE"),
(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER, "2030-01-01T00:00:00Z"),
(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, "ON"),
("x-amz-bypass-governance-retention", "true"),
] {
let input = CompleteMultipartUploadInput::builder()
+8 -6
View File
@@ -20,7 +20,9 @@ use rustfs_iam::sys::{
SESSION_POLICY_NAME, get_claims_from_token_with_secret, get_claims_from_token_with_secret_allow_missing_exp,
};
use rustfs_policy::policy::{ClaimLookup, get_claim_case_insensitive};
use rustfs_utils::http::ip::get_source_ip_raw;
use rustfs_utils::http::{
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER, ip::get_source_ip_raw,
};
use s3s::S3Error;
use s3s::S3ErrorCode;
use s3s::S3Result;
@@ -571,9 +573,9 @@ pub fn get_condition_values_with_query(
}
for obj_lock in &[
"x-amz-object-lock-mode",
"x-amz-object-lock-legal-hold",
"x-amz-object-lock-retain-until-date",
AMZ_OBJECT_LOCK_MODE_LOWER,
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER,
AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
] {
let values = clone_header
.get_all(*obj_lock)
@@ -1173,8 +1175,8 @@ mod tests {
fn test_get_condition_values_with_object_lock_headers() {
let cred = create_test_credentials();
let mut headers = HeaderMap::new();
headers.insert("x-amz-object-lock-mode", HeaderValue::from_static("GOVERNANCE"));
headers.insert("x-amz-object-lock-retain-until-date", HeaderValue::from_static("2024-12-31T23:59:59Z"));
headers.insert(AMZ_OBJECT_LOCK_MODE_LOWER, HeaderValue::from_static("GOVERNANCE"));
headers.insert(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER, HeaderValue::from_static("2024-12-31T23:59:59Z"));
let conditions = get_condition_values(&headers, &cred, None, None, None);
+5 -3
View File
@@ -43,7 +43,9 @@ use rustfs_ecstore::{
};
use rustfs_s3_common::{S3Operation, record_s3_op};
use rustfs_targets::EventName;
use rustfs_utils::http::headers::AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER;
use rustfs_utils::http::headers::{
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
};
use s3s::{S3, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, dto::*, s3_error};
use std::fmt::Debug;
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
@@ -780,12 +782,12 @@ impl S3 for FS {
let mode = object_info
.user_defined
.get("x-amz-object-lock-mode")
.get(AMZ_OBJECT_LOCK_MODE_LOWER)
.map(|v| ObjectLockRetentionMode::from(v.as_str().to_string()));
let retain_until_date = object_info
.user_defined
.get("x-amz-object-lock-retain-until-date")
.get(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER)
.and_then(|v| OffsetDateTime::parse(v.as_str(), &Rfc3339).ok())
.map(Timestamp::from);
+5 -5
View File
@@ -841,9 +841,9 @@ mod tests {
retain_until_date: Some(datetime!(2030-01-01 00:00:00 UTC).into()),
};
let compliance_metadata = parse_object_lock_retention(Some(valid_compliance_retention)).unwrap();
assert_eq!(compliance_metadata.get("x-amz-object-lock-mode").unwrap(), "COMPLIANCE");
assert_eq!(compliance_metadata.get(AMZ_OBJECT_LOCK_MODE_LOWER).unwrap(), "COMPLIANCE");
assert_eq!(
compliance_metadata.get("x-amz-object-lock-retain-until-date").unwrap(),
compliance_metadata.get(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER).unwrap(),
"2030-01-01T00:00:00Z"
);
assert!(contains_key_str(&compliance_metadata, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP));
@@ -858,7 +858,7 @@ mod tests {
retain_until_date: Some(datetime!(2030-01-01 00:00:00 UTC).into()),
};
let governance_metadata = parse_object_lock_retention(Some(valid_governance_retention)).unwrap();
assert_eq!(governance_metadata.get("x-amz-object-lock-mode").unwrap(), "GOVERNANCE");
assert_eq!(governance_metadata.get(AMZ_OBJECT_LOCK_MODE_LOWER).unwrap(), "GOVERNANCE");
// [4] Normal case: Retention with None mode (empty string for mode, date not validated)
let none_mode_retention = ObjectLockRetention {
@@ -866,7 +866,7 @@ mod tests {
retain_until_date: Some(datetime!(2030-01-01 00:00:00 UTC).into()),
};
let none_mode_metadata = parse_object_lock_retention(Some(none_mode_retention)).unwrap();
assert_eq!(none_mode_metadata.get("x-amz-object-lock-mode").unwrap(), "");
assert_eq!(none_mode_metadata.get(AMZ_OBJECT_LOCK_MODE_LOWER).unwrap(), "");
// [5] Normal case: Retention with None retain_until_date (empty string for date)
let none_date_retention = ObjectLockRetention {
@@ -874,7 +874,7 @@ mod tests {
retain_until_date: None,
};
let none_date_metadata = parse_object_lock_retention(Some(none_date_retention)).unwrap();
assert_eq!(none_date_metadata.get("x-amz-object-lock-retain-until-date").unwrap(), "");
assert_eq!(none_date_metadata.get(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER).unwrap(), "");
// [6] Error case: Retention with invalid mode (non COMPLIANCE/GOVERNANCE)
let invalid_mode_retention = ObjectLockRetention {
+10 -8
View File
@@ -17,8 +17,10 @@ use http::{HeaderMap, HeaderValue};
use rustfs_ecstore::bucket::versioning_sys::BucketVersioningSys;
use rustfs_ecstore::error::Result;
use rustfs_ecstore::error::StorageError;
use rustfs_utils::http::AMZ_META_UNENCRYPTED_CONTENT_LENGTH;
use rustfs_utils::http::AMZ_META_UNENCRYPTED_CONTENT_MD5;
use rustfs_utils::http::{
AMZ_META_UNENCRYPTED_CONTENT_LENGTH, AMZ_META_UNENCRYPTED_CONTENT_MD5, AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER,
AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
};
use rustfs_utils::http::{
SUFFIX_FORCE_DELETE, SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE, SUFFIX_REPLICATION_SSEC_CRC, SUFFIX_SOURCE_DELETEMARKER,
SUFFIX_SOURCE_MTIME, SUFFIX_SOURCE_REPLICATION_REQUEST, SUFFIX_SOURCE_VERSION_ID, get_header, insert_header_map,
@@ -586,9 +588,9 @@ static SUPPORTED_HEADERS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
"expires",
"x-amz-replication-status",
// Object Lock headers - required for S3 Object Lock functionality
"x-amz-object-lock-mode",
"x-amz-object-lock-retain-until-date",
"x-amz-object-lock-legal-hold",
AMZ_OBJECT_LOCK_MODE_LOWER,
AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER,
]
});
@@ -1325,9 +1327,9 @@ mod tests {
"x-amz-tagging",
"expires",
"x-amz-replication-status",
"x-amz-object-lock-mode",
"x-amz-object-lock-retain-until-date",
"x-amz-object-lock-legal-hold",
AMZ_OBJECT_LOCK_MODE_LOWER,
AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER,
];
assert_eq!(*SUPPORTED_HEADERS, expected_headers);