fix: object lock compliance mode allows deletion (#1687)

Co-authored-by: loverustfs <hello@rustfs.com>
This commit is contained in:
LeonWang0735
2026-02-03 17:06:24 +08:00
committed by GitHub
parent 2f66f15524
commit 36f14acbe9
4 changed files with 188 additions and 9 deletions
@@ -106,7 +106,7 @@ pub fn check_retention_for_modification(
None
}
pub(crate) fn add_years(dt: OffsetDateTime, years: i32) -> OffsetDateTime {
pub fn add_years(dt: OffsetDateTime, years: i32) -> OffsetDateTime {
let target_year = dt.year() + years;
dt.replace_year(target_year)
.or_else(|_| {
+23 -5
View File
@@ -31,11 +31,12 @@ use crate::storage::{
},
};
use crate::storage::{
check_preconditions, create_managed_encryption_material, decrypt_managed_encryption_key, decrypt_multipart_managed_stream,
derive_part_nonce, get_buffer_size_opt_in, get_validated_store, has_replication_rules, is_managed_sse,
parse_object_lock_legal_hold, parse_object_lock_retention, process_lambda_configurations, process_queue_configurations,
process_topic_configurations, strip_managed_encryption_metadata, validate_bucket_object_lock_enabled,
validate_list_object_unordered_with_delimiter, validate_object_key, wrap_response_with_cors,
apply_lock_retention, check_preconditions, create_managed_encryption_material, decrypt_managed_encryption_key,
decrypt_multipart_managed_stream, derive_part_nonce, get_buffer_size_opt_in, get_validated_store, has_replication_rules,
is_managed_sse, parse_object_lock_legal_hold, parse_object_lock_retention, process_lambda_configurations,
process_queue_configurations, process_topic_configurations, strip_managed_encryption_metadata,
validate_bucket_object_lock_enabled, validate_list_object_unordered_with_delimiter, validate_object_key,
wrap_response_with_cors,
};
use crate::storage::{entity, parse_part_number_i32_to_usize};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD};
@@ -4666,6 +4667,23 @@ impl S3 for FS {
let mut metadata = metadata.unwrap_or_default();
let object_lock_configuration = match metadata_sys::get_object_lock_config(&bucket).await {
Ok((cfg, _created)) => Some(cfg),
Err(err) => {
if err == StorageError::ConfigNotFound {
None
} else {
warn!("get_object_lock_config err {:?}", err);
return Err(S3Error::with_message(
S3ErrorCode::InternalError,
"Failed to load Object Lock configuration".to_string(),
));
}
}
};
apply_lock_retention(object_lock_configuration, &mut metadata);
if let Some(content_type) = content_type {
metadata.insert("content-type".to_string(), content_type.to_string());
}
+43 -3
View File
@@ -23,6 +23,7 @@ use http::{HeaderMap, HeaderValue, StatusCode};
use metrics::counter;
use rustfs_ecstore::bucket::metadata_sys;
use rustfs_ecstore::bucket::metadata_sys::get_replication_config;
use rustfs_ecstore::bucket::object_lock::objectlock_sys;
use rustfs_ecstore::bucket::replication::ReplicationConfigurationExt;
use rustfs_ecstore::error::StorageError;
use rustfs_ecstore::store_api::{BucketOptions, ObjectInfo, ObjectToDelete};
@@ -37,9 +38,9 @@ use rustfs_utils::http::{
RESERVED_METADATA_PREFIX_LOWER,
};
use s3s::dto::{
Delimiter, LambdaFunctionConfiguration, NotificationConfigurationFilter, ObjectLockEnabled, ObjectLockLegalHold,
ObjectLockLegalHoldStatus, ObjectLockRetention, ObjectLockRetentionMode, QueueConfiguration, ServerSideEncryption,
TopicConfiguration,
Delimiter, LambdaFunctionConfiguration, NotificationConfigurationFilter, ObjectLockConfiguration, ObjectLockEnabled,
ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention, ObjectLockRetentionMode, QueueConfiguration,
ServerSideEncryption, TopicConfiguration,
};
use s3s::{S3Error, S3ErrorCode, S3Response, S3Result};
use serde_urlencoded::from_bytes;
@@ -55,6 +56,45 @@ use tracing::{debug, warn};
pub const RFC1123: &[FormatItem<'_>] =
format_description!("[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT");
/// Apply bucket default Object Lock retention to object metadata if no explicit retention is set.
///
/// This function implements S3-compatible behavior where objects uploaded to a bucket with
/// default retention configuration automatically inherit the bucket's default retention policy.
/// The retention is only applied if:
/// 1. The bucket has Object Lock enabled
/// 2. The bucket has a default retention rule configured
/// 3. The object metadata does not already contain explicit retention headers
///
/// # Arguments
/// * `object_lock_config` - Optional bucket Object Lock configuration. If None, no retention is applied.
/// * `metadata` - Mutable reference to object metadata HashMap. Retention headers are inserted here.
pub(crate) fn apply_lock_retention(object_lock_config: Option<ObjectLockConfiguration>, metadata: &mut HashMap<String, String>) {
if metadata.contains_key(AMZ_OBJECT_LOCK_MODE_LOWER) || metadata.contains_key(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER) {
return;
}
let Some(config) = object_lock_config else { return };
if config.object_lock_enabled.as_ref().map(|e| e.as_str()) != Some(ObjectLockEnabled::ENABLED) {
return;
}
let Some(default_retention) = config.rule.and_then(|r| r.default_retention) else { return };
let Some(mode) = default_retention.mode else { return };
let now = OffsetDateTime::now_utc();
let retain_until = match (default_retention.days, default_retention.years) {
(Some(days), _) => now.saturating_add(time::Duration::days(days as i64)),
(None, Some(years)) => objectlock_sys::add_years(now, years),
_ => return,
};
if let Ok(date_str) = retain_until.format(&Rfc3339) {
metadata.insert(AMZ_OBJECT_LOCK_MODE_LOWER.to_string(), mode.as_str().to_string());
metadata.insert(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER.to_string(), date_str);
}
}
/// =======================
/// Presigned POST helpers
/// =======================
+121
View File
@@ -798,6 +798,127 @@ mod tests {
assert_eq!(result13.unwrap_err().code(), &S3ErrorCode::InvalidArgument);
}
#[test]
fn test_apply_lock_retention() {
use crate::storage::ecfs_extend::apply_lock_retention;
use s3s::dto::{DefaultRetention, ObjectLockConfiguration, ObjectLockEnabled, ObjectLockRetentionMode, ObjectLockRule};
use std::collections::HashMap;
// [1] Normal case: Apply default retention with COMPLIANCE mode and days
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::COMPLIANCE)),
days: Some(30),
years: None,
}),
}),
});
apply_lock_retention(config, &mut metadata);
assert_eq!(metadata.get("x-amz-object-lock-mode"), Some(&"COMPLIANCE".to_string()));
assert!(metadata.contains_key("x-amz-object-lock-retain-until-date"));
// [2] Normal case: Apply default retention with GOVERNANCE mode and years
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::GOVERNANCE)),
days: None,
years: Some(1),
}),
}),
});
apply_lock_retention(config, &mut metadata);
assert_eq!(metadata.get("x-amz-object-lock-mode"), Some(&"GOVERNANCE".to_string()));
assert!(metadata.contains_key("x-amz-object-lock-retain-until-date"));
// [3] Skip case: No configuration provided
let mut metadata = HashMap::new();
apply_lock_retention(None, &mut metadata);
assert!(!metadata.contains_key("x-amz-object-lock-mode"));
assert!(!metadata.contains_key("x-amz-object-lock-retain-until-date"));
// [4] Skip case: Object Lock not enabled
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: None,
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::COMPLIANCE)),
days: Some(30),
years: None,
}),
}),
});
apply_lock_retention(config, &mut metadata);
assert!(!metadata.contains_key("x-amz-object-lock-mode"));
// [5] Skip case: Explicit retention already set (explicit takes precedence)
let mut metadata = HashMap::new();
metadata.insert("x-amz-object-lock-mode".to_string(), "GOVERNANCE".to_string());
metadata.insert("x-amz-object-lock-retain-until-date".to_string(), "2030-01-01T00:00:00Z".to_string());
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::COMPLIANCE)),
days: Some(30),
years: None,
}),
}),
});
apply_lock_retention(config, &mut metadata);
// Explicit retention should remain unchanged
assert_eq!(metadata.get("x-amz-object-lock-mode"), Some(&"GOVERNANCE".to_string()));
assert_eq!(
metadata.get("x-amz-object-lock-retain-until-date"),
Some(&"2030-01-01T00:00:00Z".to_string())
);
// [6] Skip case: No default retention configured
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule { default_retention: None }),
});
apply_lock_retention(config, &mut metadata);
assert!(!metadata.contains_key("x-amz-object-lock-mode"));
// [7] Skip case: No retention mode specified
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: None,
days: Some(30),
years: None,
}),
}),
});
apply_lock_retention(config, &mut metadata);
assert!(!metadata.contains_key("x-amz-object-lock-mode"));
// [8] Skip case: No retention period specified (neither days nor years)
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::COMPLIANCE)),
days: None,
years: None,
}),
}),
});
apply_lock_retention(config, &mut metadata);
assert!(!metadata.contains_key("x-amz-object-lock-mode"));
}
// Note: S3Request structure is complex and requires many fields.
// For real testing, we would need proper integration test setup.
// Removing this test as it requires too much S3 infrastructure setup.