mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-27 08:38:58 +00:00
test(object-lock): cover default retention delete marker (#2836)
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user