mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 16:28:15 +00:00
fix(object-lock): prevent locked version deletes (#4297)
This commit is contained in:
@@ -61,6 +61,7 @@ pub struct ObjectOptions {
|
||||
|
||||
pub eval_metadata: Option<HashMap<String, String>>,
|
||||
pub object_lock_retention: Option<ObjectLockRetentionOptions>,
|
||||
pub object_lock_delete: Option<crate::storage_api_contracts::object::ObjectLockDeleteOptions>,
|
||||
|
||||
pub want_checksum: Option<Checksum>,
|
||||
pub skip_verify_bitrot: bool,
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
use crate::bucket::lifecycle::lifecycle::TRANSITION_COMPLETE;
|
||||
use crate::bucket::metadata_sys;
|
||||
use crate::bucket::object_lock::objectlock_sys::check_retention_for_modification;
|
||||
use crate::bucket::object_lock::objectlock_sys::{check_object_lock_for_deletion, check_retention_for_modification};
|
||||
use crate::bucket::replication::{
|
||||
ReplicateDecision, ReplicationObjectBridge, ReplicationState, ReplicationStatusType, VersionPurgeStatusType,
|
||||
replication_state_to_filemeta,
|
||||
@@ -2528,6 +2528,29 @@ fn check_object_lock_retention_update(bucket: &str, object: &str, obj_info: &Obj
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_object_lock_delete(bucket: &str, object: &str, obj_info: &ObjectInfo, opts: &ObjectOptions) -> Result<()> {
|
||||
if set_disk_delete_creates_delete_marker(opts) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let bypass_governance = opts
|
||||
.object_lock_delete
|
||||
.as_ref()
|
||||
.is_some_and(|delete_opts| delete_opts.bypass_governance);
|
||||
if check_object_lock_for_deletion(bucket, obj_info, bypass_governance)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
return Err(StorageError::PrefixAccessDenied(bucket.to_string(), object.to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_disk_delete_creates_delete_marker(opts: &ObjectOptions) -> bool {
|
||||
opts.version_id.is_none() && opts.versioned && !opts.version_suspended
|
||||
}
|
||||
|
||||
fn should_preserve_delete_replication_state(opts: &ObjectOptions) -> bool {
|
||||
opts.delete_replication.as_ref().is_some_and(|state| {
|
||||
state.replica_status == ReplicationStatusType::Replica
|
||||
@@ -5972,6 +5995,65 @@ mod tests {
|
||||
.expect("GOVERNANCE shortening with bypass should remain allowed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_object_lock_delete_blocks_compliance_version_delete() {
|
||||
let retain_until = OffsetDateTime::now_utc() + Duration::from_secs(60 * 60 * 24 * 60);
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
);
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
retain_until.format(&time::format_description::well_known::Rfc3339).unwrap(),
|
||||
);
|
||||
|
||||
let obj_info = ObjectInfo {
|
||||
user_defined: Arc::new(user_defined),
|
||||
..Default::default()
|
||||
};
|
||||
let opts = ObjectOptions {
|
||||
version_id: Some(Uuid::new_v4().to_string()),
|
||||
versioned: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let err = check_object_lock_delete("bucket", "object", &obj_info, &opts)
|
||||
.await
|
||||
.expect_err("COMPLIANCE retention must block explicit version deletion");
|
||||
|
||||
assert!(matches!(err, StorageError::PrefixAccessDenied(_, _)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_object_lock_delete_allows_versioned_delete_marker_creation() {
|
||||
let retain_until = OffsetDateTime::now_utc() + Duration::from_secs(60 * 60 * 24 * 60);
|
||||
let mut user_defined = HashMap::new();
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_MODE.as_str().to_string(),
|
||||
s3s::dto::ObjectLockRetentionMode::COMPLIANCE.to_string(),
|
||||
);
|
||||
user_defined.insert(
|
||||
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str().to_string(),
|
||||
retain_until.format(&time::format_description::well_known::Rfc3339).unwrap(),
|
||||
);
|
||||
|
||||
let obj_info = ObjectInfo {
|
||||
user_defined: Arc::new(user_defined),
|
||||
..Default::default()
|
||||
};
|
||||
let opts = ObjectOptions {
|
||||
version_id: None,
|
||||
versioned: true,
|
||||
version_suspended: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
check_object_lock_delete("bucket", "object", &obj_info, &opts)
|
||||
.await
|
||||
.expect("versioned delete marker creation should not delete the locked version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_persist_encryption_original_size_rejects_plain_metadata() {
|
||||
let metadata = HashMap::from([("content-type".to_string(), "application/octet-stream".to_string())]);
|
||||
|
||||
@@ -1327,10 +1327,31 @@ impl crate::storage_api_contracts::object::ObjectOperations for SetDisks {
|
||||
let mut vers_map: HashMap<&String, FileInfoVersions> = HashMap::new();
|
||||
|
||||
for (i, dobj) in objects.iter().enumerate() {
|
||||
if del_errs[i].is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let explicit_null_version = is_explicit_null_version(dobj.version_id);
|
||||
let version_id = delete_file_info_version_id(dobj.version_id);
|
||||
let check_opts = ObjectOptions {
|
||||
version_id: version_id.map(|version_id| version_id.to_string()),
|
||||
versioned: ver_cfg.prefix_enabled(dobj.object_name.as_str()),
|
||||
version_suspended: ver_cfg.suspended(),
|
||||
object_lock_delete: opts.object_lock_delete.clone(),
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
};
|
||||
let (goi, _write_quorum, gerr) = self.get_object_info_and_quorum(bucket, &dobj.object_name, &check_opts).await;
|
||||
if gerr.is_none()
|
||||
&& let Err(err) = check_object_lock_delete(bucket, &dobj.object_name, &goi, &check_opts).await
|
||||
{
|
||||
del_errs[i] = Some(err);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut vr = FileInfo {
|
||||
name: dobj.object_name.clone(),
|
||||
version_id: delete_file_info_version_id(dobj.version_id),
|
||||
version_id,
|
||||
idx: i,
|
||||
replication_state_internal: Some(dobj.replication_state()),
|
||||
..Default::default()
|
||||
@@ -1531,6 +1552,10 @@ impl crate::storage_api_contracts::object::ObjectOperations for SetDisks {
|
||||
}
|
||||
}
|
||||
|
||||
if version_found {
|
||||
check_object_lock_delete(bucket, object, &goi, &opts).await?;
|
||||
}
|
||||
|
||||
let otd = ObjectToDelete {
|
||||
object_name: object.to_string(),
|
||||
version_id: opts
|
||||
|
||||
@@ -45,8 +45,8 @@ pub(crate) mod object {
|
||||
use super::{Debug, Error, FileInfo, GetObjectReader, ObjectInfo, ObjectOptions, PutObjReader};
|
||||
use crate::storage_api_contracts::range::HTTPRangeSpec;
|
||||
pub(crate) use rustfs_storage_api::{
|
||||
DeletedObject, HTTPPreconditions, ObjectIO, ObjectLockRetentionOptions, ObjectOperations, ObjectPreconditionError,
|
||||
ObjectPreconditionPart, ObjectPreconditionState, ObjectToDelete,
|
||||
DeletedObject, HTTPPreconditions, ObjectIO, ObjectLockDeleteOptions, ObjectLockRetentionOptions, ObjectOperations,
|
||||
ObjectPreconditionError, ObjectPreconditionPart, ObjectPreconditionState, ObjectToDelete,
|
||||
};
|
||||
|
||||
pub(crate) trait EcstoreObjectIO:
|
||||
|
||||
@@ -30,6 +30,7 @@ pub use bucket::{BucketInfo, BucketOperations, BucketOptions, DeleteBucketOption
|
||||
pub use capability::{CapabilitySnapshotError, CapabilityState, CapabilityStatus};
|
||||
pub use error::{StorageErrorCode, StorageResult};
|
||||
pub use multipart::{CompletePart, ListMultipartsInfo, ListPartsInfo, MultipartInfo, MultipartUploadResult, PartInfo};
|
||||
pub use object::ObjectLockDeleteOptions;
|
||||
pub use object::{DeletedObject, ObjectToDelete};
|
||||
pub use object::{ExpirationOptions, TransitionedObject};
|
||||
pub use object::{HTTPPreconditions, HTTPRangeError, HTTPRangeSpec, ObjectLockRetentionOptions};
|
||||
|
||||
@@ -53,6 +53,11 @@ pub struct ExpirationOptions {
|
||||
pub expire: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ObjectLockDeleteOptions {
|
||||
pub bypass_governance: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TransitionedObject {
|
||||
pub name: String,
|
||||
|
||||
@@ -188,7 +188,7 @@ use tracing::{debug, error, instrument, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::storage_api::object_usecase::{
|
||||
StorageDeletedObject, StorageObjectInfo as ObjectInfo, StorageObjectOptions as ObjectOptions,
|
||||
StorageDeletedObject, StorageObjectInfo as ObjectInfo, StorageObjectLockDeleteOptions, StorageObjectOptions as ObjectOptions,
|
||||
StorageObjectToDelete as ObjectToDelete, StoragePutObjReader as PutObjReader,
|
||||
};
|
||||
use crate::app::object_data_cache::{
|
||||
@@ -4847,6 +4847,7 @@ impl DefaultObjectUsecase {
|
||||
ObjectOptions {
|
||||
versioned: version_cfg.enabled(),
|
||||
version_suspended: version_cfg.suspended(),
|
||||
object_lock_delete: Some(StorageObjectLockDeleteOptions { bypass_governance }),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -5047,6 +5048,9 @@ impl DefaultObjectUsecase {
|
||||
let mut opts: ObjectOptions = del_opts(&bucket, &key, version_id, &req.headers, metadata)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
opts.object_lock_delete = Some(StorageObjectLockDeleteOptions {
|
||||
bypass_governance: has_bypass_governance_header(&req.headers),
|
||||
});
|
||||
let force_delete = opts.delete_prefix;
|
||||
|
||||
let lock_cfg = BucketObjectLockSys::get(&bucket).await;
|
||||
|
||||
@@ -967,11 +967,12 @@ pub(crate) mod object_usecase {
|
||||
object_utils, options, request_context, s3_api, set_disk, sse, storage_class, timeout_wrapper,
|
||||
};
|
||||
pub(crate) use crate::storage::storage_api::{
|
||||
ECStore, RFC1123, StorageDeletedObject, StorageObjectInfo, StorageObjectOptions, StorageObjectToDelete,
|
||||
StoragePutObjReader, check_preconditions, get_validated_store, has_replication_rules, parse_object_lock_legal_hold,
|
||||
parse_object_lock_retention, parse_part_number_i32_to_usize, remove_object_lock_metadata_for_copy,
|
||||
strip_managed_encryption_metadata, validate_bucket_object_lock_enabled, validate_object_key,
|
||||
validate_sse_headers_for_read, validate_sse_headers_for_write, validate_ssec_for_read, wrap_response_with_cors,
|
||||
ECStore, RFC1123, StorageDeletedObject, StorageObjectInfo, StorageObjectLockDeleteOptions, StorageObjectOptions,
|
||||
StorageObjectToDelete, StoragePutObjReader, check_preconditions, get_validated_store, has_replication_rules,
|
||||
parse_object_lock_legal_hold, parse_object_lock_retention, parse_part_number_i32_to_usize,
|
||||
remove_object_lock_metadata_for_copy, strip_managed_encryption_metadata, validate_bucket_object_lock_enabled,
|
||||
validate_object_key, validate_sse_headers_for_read, validate_sse_headers_for_write, validate_ssec_for_read,
|
||||
wrap_response_with_cors,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ pub(crate) mod contract {
|
||||
|
||||
pub(crate) mod object {
|
||||
pub(crate) use super::super::storage_contracts::{
|
||||
DeletedObject, HTTPPreconditions, ObjectIO, ObjectLockRetentionOptions, ObjectOperations, ObjectToDelete,
|
||||
DeletedObject, HTTPPreconditions, ObjectIO, ObjectLockDeleteOptions, ObjectLockRetentionOptions, ObjectOperations,
|
||||
ObjectToDelete,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,6 +60,7 @@ pub(crate) mod contract {
|
||||
pub(crate) type StorageDeletedObject = contract::object::DeletedObject;
|
||||
pub(crate) type StorageGetObjectReader = super::GetObjectReader;
|
||||
pub(crate) type StorageObjectInfo = super::ObjectInfo;
|
||||
pub(crate) type StorageObjectLockDeleteOptions = contract::object::ObjectLockDeleteOptions;
|
||||
pub(crate) type StorageObjectOptions = super::ObjectOptions;
|
||||
pub(crate) type StorageObjectToDelete = contract::object::ObjectToDelete;
|
||||
pub(crate) type StoragePutObjReader = super::PutObjReader;
|
||||
|
||||
Reference in New Issue
Block a user