mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-24 21:26:28 +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");
|
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]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_put_copy_and_multipart_reject_incomplete_retention_headers() {
|
async fn test_put_copy_and_multipart_reject_incomplete_retention_headers() {
|
||||||
|
|||||||
@@ -1203,6 +1203,9 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use http::{Extensions, HeaderMap, Method, Uri, header::HeaderValue};
|
use http::{Extensions, HeaderMap, Method, Uri, header::HeaderValue};
|
||||||
use rustfs_filemeta::ObjectPartInfo;
|
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 std::{collections::HashMap, io::Cursor};
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
@@ -1548,9 +1551,9 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (header_name, header_value) in [
|
for (header_name, header_value) in [
|
||||||
("x-amz-object-lock-mode", "GOVERNANCE"),
|
(AMZ_OBJECT_LOCK_MODE_LOWER, "GOVERNANCE"),
|
||||||
("x-amz-object-lock-retain-until-date", "2030-01-01T00:00:00Z"),
|
(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER, "2030-01-01T00:00:00Z"),
|
||||||
("x-amz-object-lock-legal-hold", "ON"),
|
(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, "ON"),
|
||||||
("x-amz-bypass-governance-retention", "true"),
|
("x-amz-bypass-governance-retention", "true"),
|
||||||
] {
|
] {
|
||||||
let input = CompleteMultipartUploadInput::builder()
|
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,
|
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_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::S3Error;
|
||||||
use s3s::S3ErrorCode;
|
use s3s::S3ErrorCode;
|
||||||
use s3s::S3Result;
|
use s3s::S3Result;
|
||||||
@@ -571,9 +573,9 @@ pub fn get_condition_values_with_query(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for obj_lock in &[
|
for obj_lock in &[
|
||||||
"x-amz-object-lock-mode",
|
AMZ_OBJECT_LOCK_MODE_LOWER,
|
||||||
"x-amz-object-lock-legal-hold",
|
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER,
|
||||||
"x-amz-object-lock-retain-until-date",
|
AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
|
||||||
] {
|
] {
|
||||||
let values = clone_header
|
let values = clone_header
|
||||||
.get_all(*obj_lock)
|
.get_all(*obj_lock)
|
||||||
@@ -1173,8 +1175,8 @@ mod tests {
|
|||||||
fn test_get_condition_values_with_object_lock_headers() {
|
fn test_get_condition_values_with_object_lock_headers() {
|
||||||
let cred = create_test_credentials();
|
let cred = create_test_credentials();
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert("x-amz-object-lock-mode", HeaderValue::from_static("GOVERNANCE"));
|
headers.insert(AMZ_OBJECT_LOCK_MODE_LOWER, 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_RETAIN_UNTIL_DATE_LOWER, HeaderValue::from_static("2024-12-31T23:59:59Z"));
|
||||||
|
|
||||||
let conditions = get_condition_values(&headers, &cred, None, None, None);
|
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_s3_common::{S3Operation, record_s3_op};
|
||||||
use rustfs_targets::EventName;
|
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 s3s::{S3, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, dto::*, s3_error};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||||
@@ -780,12 +782,12 @@ impl S3 for FS {
|
|||||||
|
|
||||||
let mode = object_info
|
let mode = object_info
|
||||||
.user_defined
|
.user_defined
|
||||||
.get("x-amz-object-lock-mode")
|
.get(AMZ_OBJECT_LOCK_MODE_LOWER)
|
||||||
.map(|v| ObjectLockRetentionMode::from(v.as_str().to_string()));
|
.map(|v| ObjectLockRetentionMode::from(v.as_str().to_string()));
|
||||||
|
|
||||||
let retain_until_date = object_info
|
let retain_until_date = object_info
|
||||||
.user_defined
|
.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())
|
.and_then(|v| OffsetDateTime::parse(v.as_str(), &Rfc3339).ok())
|
||||||
.map(Timestamp::from);
|
.map(Timestamp::from);
|
||||||
|
|
||||||
|
|||||||
@@ -841,9 +841,9 @@ mod tests {
|
|||||||
retain_until_date: Some(datetime!(2030-01-01 00:00:00 UTC).into()),
|
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();
|
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!(
|
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"
|
"2030-01-01T00:00:00Z"
|
||||||
);
|
);
|
||||||
assert!(contains_key_str(&compliance_metadata, SUFFIX_OBJECTLOCK_RETENTION_TIMESTAMP));
|
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()),
|
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();
|
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)
|
// [4] Normal case: Retention with None mode (empty string for mode, date not validated)
|
||||||
let none_mode_retention = ObjectLockRetention {
|
let none_mode_retention = ObjectLockRetention {
|
||||||
@@ -866,7 +866,7 @@ mod tests {
|
|||||||
retain_until_date: Some(datetime!(2030-01-01 00:00:00 UTC).into()),
|
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();
|
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)
|
// [5] Normal case: Retention with None retain_until_date (empty string for date)
|
||||||
let none_date_retention = ObjectLockRetention {
|
let none_date_retention = ObjectLockRetention {
|
||||||
@@ -874,7 +874,7 @@ mod tests {
|
|||||||
retain_until_date: None,
|
retain_until_date: None,
|
||||||
};
|
};
|
||||||
let none_date_metadata = parse_object_lock_retention(Some(none_date_retention)).unwrap();
|
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)
|
// [6] Error case: Retention with invalid mode (non COMPLIANCE/GOVERNANCE)
|
||||||
let invalid_mode_retention = ObjectLockRetention {
|
let invalid_mode_retention = ObjectLockRetention {
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ use http::{HeaderMap, HeaderValue};
|
|||||||
use rustfs_ecstore::bucket::versioning_sys::BucketVersioningSys;
|
use rustfs_ecstore::bucket::versioning_sys::BucketVersioningSys;
|
||||||
use rustfs_ecstore::error::Result;
|
use rustfs_ecstore::error::Result;
|
||||||
use rustfs_ecstore::error::StorageError;
|
use rustfs_ecstore::error::StorageError;
|
||||||
use rustfs_utils::http::AMZ_META_UNENCRYPTED_CONTENT_LENGTH;
|
use rustfs_utils::http::{
|
||||||
use rustfs_utils::http::AMZ_META_UNENCRYPTED_CONTENT_MD5;
|
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::{
|
use rustfs_utils::http::{
|
||||||
SUFFIX_FORCE_DELETE, SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE, SUFFIX_REPLICATION_SSEC_CRC, SUFFIX_SOURCE_DELETEMARKER,
|
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,
|
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",
|
"expires",
|
||||||
"x-amz-replication-status",
|
"x-amz-replication-status",
|
||||||
// Object Lock headers - required for S3 Object Lock functionality
|
// Object Lock headers - required for S3 Object Lock functionality
|
||||||
"x-amz-object-lock-mode",
|
AMZ_OBJECT_LOCK_MODE_LOWER,
|
||||||
"x-amz-object-lock-retain-until-date",
|
AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
|
||||||
"x-amz-object-lock-legal-hold",
|
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER,
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1325,9 +1327,9 @@ mod tests {
|
|||||||
"x-amz-tagging",
|
"x-amz-tagging",
|
||||||
"expires",
|
"expires",
|
||||||
"x-amz-replication-status",
|
"x-amz-replication-status",
|
||||||
"x-amz-object-lock-mode",
|
AMZ_OBJECT_LOCK_MODE_LOWER,
|
||||||
"x-amz-object-lock-retain-until-date",
|
AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
|
||||||
"x-amz-object-lock-legal-hold",
|
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER,
|
||||||
];
|
];
|
||||||
|
|
||||||
assert_eq!(*SUPPORTED_HEADERS, expected_headers);
|
assert_eq!(*SUPPORTED_HEADERS, expected_headers);
|
||||||
|
|||||||
Reference in New Issue
Block a user