mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-30 00:47:13 +00:00
refactor(ecstore): move object_lock WORM evaluation onto storage-level types (#6666)
The object_lock module evaluated WORM state through s3s wire DTOs (ObjectLockRetention, ObjectLockLegalHold, DefaultRetention, Date) and s3s header constants, keeping the storage engine coupled to the serving protocol (rustfs/backlog#1842, ARCHITECTURE.md invariant 4). This PR gives the module its own storage-level vocabulary and pushes the DTO conversions to the boundaries that already speak s3s. New crates/ecstore/src/bucket/object_lock/types.rs defines RetentionMode, LegalHoldStatus, ObjectRetention, ObjectLegalHold, and DefaultRetention with no s3s dependency. objectlock.rs parses persisted metadata into these types using the rustfs-utils lowercase header constants (the same literal keys as before, pinned by the existing g-key-002 test). objectlock_sys.rs evaluates retention/legal-hold/default-retention from them; the fail-closed error messages and decision logic are unchanged line for line where possible. Boundary conversions: - bucket/metadata_sys.rs gains default_retention_from_object_lock_config, converting the persisted s3s configuration into the storage-level DefaultRetention; a rule without a usable GOVERNANCE/COMPLIANCE mode converts to None exactly like the evaluation code always ignored it, and days/years pass through so an invalid period still fails closed at evaluation time. - check_object_lock_for_deletion_with_config becomes check_object_lock_for_deletion_with_default_retention (it only ever read the default retention); the lifecycle object_lock_boundary keeps the old s3s-typed signature and converts. - The ObjectLockApi / ObjectLockStatusExt trait impls for the s3s DTOs move next to the persisted configuration owner in bucket/metadata.rs; the traits stay in object_lock/mod.rs. - check_retention_for_modification now takes Option<RetentionMode>. The serving-layer wrappers (rustfs storage_api, set_disk options path) convert the request string with the new RetentionMode::parse_exact, which accepts only the canonical spelling — preserving the historical literal comparison where a non-canonical requested mode reads as a mode change and stays blocked. - rustfs app-layer wrappers return the storage types; the replication-overwrite gate in object_usecase.rs uses the typed API (legal_hold.is_on(), RetentionMode::Compliance). Ratchet: the ecstore-scoped s3s counter drops 42 -> 39 and the repo-wide file counter 211 -> 208 in scripts/check_s3s_footprint.sh. Verification: cargo check -p rustfs-ecstore --all-targets and -p rustfs (lib+bins); cargo clippy -p rustfs-ecstore --all-targets and -p rustfs --lib --bins (clean); cargo nextest run -p rustfs-ecstore --no-fail-fast (4534/4542; the 8 failures are the same store::rebalance / store::heal machine-baseline set that fails identically on pristine origin/main, plus one fencing flake that passes in isolation); all object_lock/retention/legal-hold tests pass; guard scripts (layer deps, migration rules, s3s footprint, logging, error-format ratchet, doc paths) pass.
This commit is contained in:
@@ -40,6 +40,7 @@ use super::storage_api::object_usecase::bucket::{
|
||||
object_lock::{
|
||||
objectlock::{get_object_legalhold_meta, get_object_retention_meta},
|
||||
objectlock_sys::{check_object_lock_for_deletion, is_retention_active, replication_write_may_pass_worm_gate},
|
||||
types::RetentionMode,
|
||||
},
|
||||
predict_lifecycle_expiration,
|
||||
quota::{QuotaCheckResult, QuotaError, QuotaOperation},
|
||||
@@ -4045,11 +4046,7 @@ pub(crate) fn validate_existing_object_lock_for_write(
|
||||
}
|
||||
|
||||
let legal_hold = get_object_legalhold_meta(&existing_obj_info.user_defined);
|
||||
if legal_hold
|
||||
.status
|
||||
.as_ref()
|
||||
.is_some_and(|status| status.as_str() == ObjectLockLegalHoldStatus::ON)
|
||||
{
|
||||
if legal_hold.is_on() {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::AccessDenied,
|
||||
"Object has a legal hold and cannot be overwritten. Remove the legal hold first.".to_string(),
|
||||
@@ -4057,9 +4054,9 @@ pub(crate) fn validate_existing_object_lock_for_write(
|
||||
}
|
||||
|
||||
let retention = get_object_retention_meta(&existing_obj_info.user_defined);
|
||||
if let Some(mode) = retention.mode.as_ref()
|
||||
&& mode.as_str() == ObjectLockRetentionMode::COMPLIANCE
|
||||
&& is_retention_active(mode.as_str(), retention.retain_until_date.as_ref())
|
||||
if let Some(mode) = retention.mode
|
||||
&& mode == RetentionMode::Compliance
|
||||
&& is_retention_active(mode, retention.retain_until_date)
|
||||
{
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::AccessDenied,
|
||||
|
||||
@@ -559,16 +559,20 @@ pub(crate) mod bucket {
|
||||
}
|
||||
|
||||
pub(crate) mod object_lock {
|
||||
pub(crate) mod types {
|
||||
pub(crate) use crate::storage::storage_api::ecstore_bucket::object_lock::types::RetentionMode;
|
||||
}
|
||||
|
||||
pub(crate) mod objectlock {
|
||||
pub(crate) fn get_object_legalhold_meta(
|
||||
meta: &std::collections::HashMap<String, String>,
|
||||
) -> s3s::dto::ObjectLockLegalHold {
|
||||
) -> crate::storage::storage_api::ecstore_bucket::object_lock::types::ObjectLegalHold {
|
||||
crate::storage::storage_api::ecstore_bucket::object_lock::objectlock::get_object_legalhold_meta(meta)
|
||||
}
|
||||
|
||||
pub(crate) fn get_object_retention_meta(
|
||||
meta: &std::collections::HashMap<String, String>,
|
||||
) -> s3s::dto::ObjectLockRetention {
|
||||
) -> crate::storage::storage_api::ecstore_bucket::object_lock::types::ObjectRetention {
|
||||
crate::storage::storage_api::ecstore_bucket::object_lock::objectlock::get_object_retention_meta(meta)
|
||||
}
|
||||
}
|
||||
@@ -587,7 +591,10 @@ pub(crate) mod bucket {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) fn is_retention_active(mode: &str, retain_until_date: Option<&s3s::dto::Date>) -> bool {
|
||||
pub(crate) fn is_retention_active(
|
||||
mode: crate::storage::storage_api::ecstore_bucket::object_lock::types::RetentionMode,
|
||||
retain_until_date: Option<time::OffsetDateTime>,
|
||||
) -> bool {
|
||||
crate::storage::storage_api::ecstore_bucket::object_lock::objectlock_sys::is_retention_active(
|
||||
mode,
|
||||
retain_until_date,
|
||||
|
||||
@@ -1745,6 +1745,10 @@ pub(crate) fn check_retention_for_modification(
|
||||
new_retain_until: Option<time::OffsetDateTime>,
|
||||
bypass_governance: bool,
|
||||
) -> Option<ObjectLockBlockReason> {
|
||||
// The gate compares the requested mode literally against the canonical
|
||||
// persisted mode, so only the exact canonical spelling maps to a typed
|
||||
// mode; anything else stays `None` and is judged as a mode change.
|
||||
let new_mode = new_mode.and_then(ecstore_bucket::object_lock::types::RetentionMode::parse_exact);
|
||||
ecstore_bucket::object_lock::objectlock_sys::check_retention_for_modification(
|
||||
user_defined,
|
||||
new_mode,
|
||||
|
||||
Reference in New Issue
Block a user