mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-28 09:08:58 +00:00
fix: object lock compliance mode allows deletion (#1687)
Co-authored-by: loverustfs <hello@rustfs.com>
This commit is contained in:
@@ -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(|_| {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// =======================
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user