mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-17 18:27:49 +00:00
feat(object-lock): complete legal hold enforcement (#2293)
This commit is contained in:
@@ -51,7 +51,12 @@ use rustfs_ecstore::bucket::{
|
||||
},
|
||||
metadata::{BUCKET_VERSIONING_CONFIG, OBJECT_LOCK_CONFIG},
|
||||
metadata_sys,
|
||||
object_lock::objectlock_sys::{BucketObjectLockSys, check_object_lock_for_deletion, check_retention_for_modification},
|
||||
object_lock::{
|
||||
objectlock::{get_object_legalhold_meta, get_object_retention_meta},
|
||||
objectlock_sys::{
|
||||
BucketObjectLockSys, check_object_lock_for_deletion, check_retention_for_modification, is_retention_active,
|
||||
},
|
||||
},
|
||||
quota::QuotaOperation,
|
||||
replication::{
|
||||
DeletedObjectReplicationInfo, check_replicate_delete, get_must_replicate_options, must_replicate, schedule_replication,
|
||||
@@ -499,8 +504,28 @@ async fn apply_put_request_object_lock_opts(
|
||||
object_lock_retain_until_date: Option<Timestamp>,
|
||||
opts: &mut ObjectOptions,
|
||||
) -> S3Result<()> {
|
||||
if let Some(eval_metadata) = build_put_like_object_lock_metadata(
|
||||
bucket,
|
||||
object_lock_legal_hold_status,
|
||||
object_lock_mode,
|
||||
object_lock_retain_until_date,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
opts.eval_metadata = Some(eval_metadata);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn build_put_like_object_lock_metadata(
|
||||
bucket: &str,
|
||||
object_lock_legal_hold_status: Option<ObjectLockLegalHoldStatus>,
|
||||
object_lock_mode: Option<ObjectLockMode>,
|
||||
object_lock_retain_until_date: Option<Timestamp>,
|
||||
) -> S3Result<Option<HashMap<String, String>>> {
|
||||
if object_lock_legal_hold_status.is_none() && object_lock_mode.is_none() && object_lock_retain_until_date.is_none() {
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
validate_bucket_object_lock_enabled(bucket).await?;
|
||||
@@ -522,13 +547,44 @@ async fn apply_put_request_object_lock_opts(
|
||||
object_lock_legal_hold_status.map(|status| ObjectLockLegalHold { status: Some(status) }),
|
||||
)?);
|
||||
|
||||
if !eval_metadata.is_empty() {
|
||||
opts.eval_metadata = Some(eval_metadata);
|
||||
if eval_metadata.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(eval_metadata))
|
||||
}
|
||||
|
||||
pub(crate) fn validate_existing_object_lock_for_write(existing_obj_info: &ObjectInfo) -> S3Result<()> {
|
||||
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)
|
||||
{
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::AccessDenied,
|
||||
"Object has a legal hold and cannot be overwritten. Remove the legal hold first.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::AccessDenied,
|
||||
"Object is under COMPLIANCE retention and cannot be overwritten.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_creates_delete_marker(opts: &ObjectOptions) -> bool {
|
||||
opts.version_id.is_none() && opts.versioned && !opts.version_suspended
|
||||
}
|
||||
|
||||
fn resolve_put_object_extract_options(headers: &HeaderMap) -> PutObjectExtractOptions {
|
||||
let prefix = snowball_meta_value_by_suffix(headers, AMZ_SNOWBALL_PREFIX_INTERNAL, SNOWBALL_PREFIX_SUFFIX_LOWER)
|
||||
.and_then(|value| normalize_snowball_prefix(&value));
|
||||
@@ -811,6 +867,18 @@ impl DefaultObjectUsecase {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let current_opts: ObjectOptions = get_opts(&bucket, &key, version_id.clone(), None, &req.headers)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
match store.get_object_info(&bucket, &key, ¤t_opts).await {
|
||||
Ok(existing_obj_info) => validate_existing_object_lock_for_write(&existing_obj_info)?,
|
||||
Err(err) => {
|
||||
if !is_err_object_not_found(&err) && !is_err_version_not_found(&err) {
|
||||
return Err(ApiError::from(err).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut reader: Box<dyn Reader> = Box::new(WarpReader::new(body));
|
||||
|
||||
let actual_size = size;
|
||||
@@ -2504,6 +2572,7 @@ impl DefaultObjectUsecase {
|
||||
copy_source,
|
||||
bucket,
|
||||
key,
|
||||
version_id: dest_version_id,
|
||||
server_side_encryption: requested_sse,
|
||||
ssekms_key_id: requested_kms_key_id,
|
||||
sse_customer_algorithm,
|
||||
@@ -2516,6 +2585,9 @@ impl DefaultObjectUsecase {
|
||||
copy_source_if_match,
|
||||
copy_source_if_none_match,
|
||||
content_type,
|
||||
object_lock_legal_hold_status,
|
||||
object_lock_mode,
|
||||
object_lock_retain_until_date,
|
||||
..
|
||||
} = req.input.clone();
|
||||
let (src_bucket, src_key, version_id) = match copy_source {
|
||||
@@ -2551,7 +2623,7 @@ impl DefaultObjectUsecase {
|
||||
|
||||
src_opts.version_id = version_id.clone();
|
||||
|
||||
let mut get_opts = ObjectOptions {
|
||||
let mut src_get_opts = ObjectOptions {
|
||||
version_id: src_opts.version_id.clone(),
|
||||
versioned: src_opts.versioned,
|
||||
version_suspended: src_opts.version_suspended,
|
||||
@@ -2565,13 +2637,25 @@ impl DefaultObjectUsecase {
|
||||
let cp_src_dst_same = path_join_buf(&[&src_bucket, &src_key]) == path_join_buf(&[&bucket, &key]);
|
||||
|
||||
if cp_src_dst_same {
|
||||
get_opts.no_lock = true;
|
||||
src_get_opts.no_lock = true;
|
||||
}
|
||||
|
||||
let Some(store) = new_object_layer_fn() else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||
};
|
||||
|
||||
let current_opts: ObjectOptions = get_opts(&bucket, &key, dest_version_id.clone(), None, &req.headers)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
match store.get_object_info(&bucket, &key, ¤t_opts).await {
|
||||
Ok(existing_obj_info) => validate_existing_object_lock_for_write(&existing_obj_info)?,
|
||||
Err(err) => {
|
||||
if !is_err_object_not_found(&err) && !is_err_version_not_found(&err) {
|
||||
return Err(ApiError::from(err).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let bucket_sse_config = metadata_sys::get_sse_config(&bucket).await.ok();
|
||||
let mut effective_sse = requested_sse.or_else(|| {
|
||||
bucket_sse_config.as_ref().and_then(|(config, _)| {
|
||||
@@ -2599,7 +2683,7 @@ impl DefaultObjectUsecase {
|
||||
let h = HeaderMap::new();
|
||||
|
||||
let gr = store
|
||||
.get_object_reader(&src_bucket, &src_key, None, h, &get_opts)
|
||||
.get_object_reader(&src_bucket, &src_key, None, h, &src_get_opts)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
@@ -2691,6 +2775,17 @@ impl DefaultObjectUsecase {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(object_lock_metadata) = build_put_like_object_lock_metadata(
|
||||
&bucket,
|
||||
object_lock_legal_hold_status,
|
||||
object_lock_mode,
|
||||
object_lock_retain_until_date,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
src_info.user_defined.extend(object_lock_metadata);
|
||||
}
|
||||
|
||||
let mut reader = HashReader::new(reader, length, actual_size, None, None, false).map_err(ApiError::from)?;
|
||||
|
||||
let encryption_request = EncryptionRequest {
|
||||
@@ -2887,6 +2982,7 @@ impl DefaultObjectUsecase {
|
||||
};
|
||||
|
||||
if gerr.is_none()
|
||||
&& !delete_creates_delete_marker(&opts)
|
||||
&& let Some(block_reason) = check_object_lock_for_deletion(&bucket, &goi, bypass_governance).await
|
||||
{
|
||||
delete_results[idx].error = Some(Error {
|
||||
@@ -3176,7 +3272,9 @@ impl DefaultObjectUsecase {
|
||||
// Check for bypass governance retention header (permission already verified in access.rs)
|
||||
let bypass_governance = has_bypass_governance_header(&req.headers);
|
||||
|
||||
if let Some(block_reason) = check_object_lock_for_deletion(&bucket, &obj_info, bypass_governance).await {
|
||||
if !delete_creates_delete_marker(&opts)
|
||||
&& let Some(block_reason) = check_object_lock_for_deletion(&bucket, &obj_info, bypass_governance).await
|
||||
{
|
||||
return Err(S3Error::with_message(S3ErrorCode::AccessDenied, block_reason.error_message()));
|
||||
}
|
||||
Some(obj_info)
|
||||
|
||||
Reference in New Issue
Block a user