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:
Zhengchao An
2026-08-26 21:25:35 +08:00
committed by GitHub
parent ba15588ce8
commit 9f245e3fd4
13 changed files with 462 additions and 304 deletions
+5 -8
View File
@@ -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,
+10 -3
View File
@@ -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,
+4
View File
@@ -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,