mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-27 16:48:58 +00:00
refactor(storage): inline object lock config entrypoints (#2494)
This commit is contained in:
@@ -54,7 +54,6 @@ use rustfs_ecstore::bucket::{
|
||||
bucket_lifecycle_ops::{RestoreRequestOps, enqueue_transition_immediate, post_restore_opts},
|
||||
lifecycle::{self, Lifecycle, TransitionOptions},
|
||||
},
|
||||
metadata::{BUCKET_VERSIONING_CONFIG, OBJECT_LOCK_CONFIG},
|
||||
metadata_sys,
|
||||
object_lock::{
|
||||
objectlock::{get_object_legalhold_meta, get_object_retention_meta},
|
||||
@@ -66,7 +65,6 @@ use rustfs_ecstore::bucket::{
|
||||
schedule_replication_delete,
|
||||
},
|
||||
tagging::decode_tags,
|
||||
utils::serialize,
|
||||
versioning::VersioningApi,
|
||||
versioning_sys::BucketVersioningSys,
|
||||
};
|
||||
@@ -77,8 +75,7 @@ use rustfs_ecstore::error::{StorageError, is_err_bucket_not_found, is_err_object
|
||||
use rustfs_ecstore::new_object_layer_fn;
|
||||
use rustfs_ecstore::set_disk::is_valid_storage_class;
|
||||
use rustfs_ecstore::store_api::{
|
||||
BucketOperations, BucketOptions, ChunkNativePutData, HTTPRangeSpec, ObjectIO, ObjectInfo, ObjectOperations, ObjectOptions,
|
||||
ObjectToDelete,
|
||||
ChunkNativePutData, HTTPRangeSpec, ObjectIO, ObjectInfo, ObjectOperations, ObjectOptions, ObjectToDelete,
|
||||
};
|
||||
use rustfs_filemeta::{
|
||||
REPLICATE_INCOMING_DELETE, ReplicationStatusType, ReplicationType, RestoreStatusOps, VersionPurgeStatusType,
|
||||
@@ -513,83 +510,6 @@ pub(crate) async fn build_put_like_object_lock_metadata(
|
||||
Ok(Some(eval_metadata))
|
||||
}
|
||||
|
||||
const MAXIMUM_RETENTION_DAYS: i32 = 36_500;
|
||||
const MAXIMUM_RETENTION_YEARS: i32 = 100;
|
||||
|
||||
fn invalid_object_lock_configuration(message: impl Into<String>) -> S3Error {
|
||||
S3Error::with_message(S3ErrorCode::MalformedXML, message.into())
|
||||
}
|
||||
|
||||
fn invalid_retention_period(message: impl Into<String>) -> S3Error {
|
||||
let mut err = S3Error::with_message(S3ErrorCode::Custom("InvalidRetentionPeriod".into()), message.into());
|
||||
err.set_status_code(StatusCode::BAD_REQUEST);
|
||||
err
|
||||
}
|
||||
|
||||
fn validate_default_retention_configuration(default_retention: &DefaultRetention) -> S3Result<()> {
|
||||
let Some(mode) = default_retention.mode.as_ref() else {
|
||||
return Err(invalid_object_lock_configuration("retention mode must be specified"));
|
||||
};
|
||||
|
||||
match mode.as_str() {
|
||||
ObjectLockRetentionMode::COMPLIANCE | ObjectLockRetentionMode::GOVERNANCE => {}
|
||||
_ => {
|
||||
return Err(invalid_object_lock_configuration(format!("unknown retention mode {}", mode.as_str())));
|
||||
}
|
||||
}
|
||||
|
||||
match (default_retention.days, default_retention.years) {
|
||||
(Some(days), None) => {
|
||||
if days <= 0 {
|
||||
return Err(invalid_retention_period(
|
||||
"Default retention period must be a positive integer value for 'Days'",
|
||||
));
|
||||
}
|
||||
if days > MAXIMUM_RETENTION_DAYS {
|
||||
return Err(invalid_retention_period(format!("Default retention period too large for 'Days' {days}",)));
|
||||
}
|
||||
}
|
||||
(None, Some(years)) => {
|
||||
if years <= 0 {
|
||||
return Err(invalid_retention_period(
|
||||
"Default retention period must be a positive integer value for 'Years'",
|
||||
));
|
||||
}
|
||||
if years > MAXIMUM_RETENTION_YEARS {
|
||||
return Err(invalid_retention_period(format!(
|
||||
"Default retention period too large for 'Years' {years}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(invalid_object_lock_configuration("either Days or Years must be specified, not both"));
|
||||
}
|
||||
(None, None) => {
|
||||
return Err(invalid_object_lock_configuration("either Days or Years must be specified"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_object_lock_configuration_input(input_cfg: &ObjectLockConfiguration) -> S3Result<()> {
|
||||
let enabled = input_cfg.object_lock_enabled.as_ref().map(ObjectLockEnabled::as_str);
|
||||
if enabled != Some(ObjectLockEnabled::ENABLED) {
|
||||
return Err(invalid_object_lock_configuration(
|
||||
"only 'Enabled' value is allowed to ObjectLockEnabled element",
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(rule) = input_cfg.rule.as_ref() {
|
||||
let Some(default_retention) = rule.default_retention.as_ref() else {
|
||||
return Err(invalid_object_lock_configuration("Rule must include DefaultRetention"));
|
||||
};
|
||||
validate_default_retention_configuration(default_retention)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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
|
||||
@@ -790,76 +710,6 @@ impl DefaultObjectUsecase {
|
||||
result
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn execute_put_object_lock_configuration(
|
||||
&self,
|
||||
req: S3Request<PutObjectLockConfigurationInput>,
|
||||
) -> S3Result<S3Response<PutObjectLockConfigurationOutput>> {
|
||||
let PutObjectLockConfigurationInput {
|
||||
bucket,
|
||||
object_lock_configuration,
|
||||
..
|
||||
} = req.input;
|
||||
|
||||
let Some(input_cfg) = object_lock_configuration else { return Err(s3_error!(InvalidArgument)) };
|
||||
|
||||
let Some(store) = new_object_layer_fn() else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||
};
|
||||
|
||||
store
|
||||
.get_bucket_info(&bucket, &BucketOptions::default())
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
validate_object_lock_configuration_input(&input_cfg)?;
|
||||
|
||||
match metadata_sys::get_object_lock_config(&bucket).await {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
if err == StorageError::ConfigNotFound {
|
||||
// AWS S3 allows enabling Object Lock on existing buckets if versioning
|
||||
// is already enabled. Reject only when versioning is not enabled.
|
||||
if !BucketVersioningSys::enabled(&bucket).await {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InvalidBucketState,
|
||||
"Object Lock configuration cannot be enabled on existing buckets".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
warn!("get_object_lock_config err {:?}", err);
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InternalError,
|
||||
"Failed to get bucket ObjectLockConfiguration".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let data = serialize(&input_cfg).map_err(|err| S3Error::with_message(S3ErrorCode::InternalError, format!("{}", err)))?;
|
||||
|
||||
metadata_sys::update(&bucket, OBJECT_LOCK_CONFIG, data)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
// When Object Lock is enabled, automatically enable versioning if not already enabled.
|
||||
// This matches S3-compatible behavior.
|
||||
let versioning_config = BucketVersioningSys::get(&bucket).await.map_err(ApiError::from)?;
|
||||
if !versioning_config.enabled() {
|
||||
let enable_versioning_config = VersioningConfiguration {
|
||||
status: Some(BucketVersioningStatus::from_static(BucketVersioningStatus::ENABLED)),
|
||||
..Default::default()
|
||||
};
|
||||
let versioning_data = serialize(&enable_versioning_config)
|
||||
.map_err(|err| S3Error::with_message(S3ErrorCode::InternalError, format!("{}", err)))?;
|
||||
metadata_sys::update(&bucket, BUCKET_VERSIONING_CONFIG, versioning_data)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
}
|
||||
|
||||
Ok(S3Response::new(PutObjectLockConfigurationOutput::default()))
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "debug",
|
||||
skip(self, req),
|
||||
@@ -1176,35 +1026,6 @@ impl DefaultObjectUsecase {
|
||||
result
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn execute_get_object_lock_configuration(
|
||||
&self,
|
||||
req: S3Request<GetObjectLockConfigurationInput>,
|
||||
) -> S3Result<S3Response<GetObjectLockConfigurationOutput>> {
|
||||
let GetObjectLockConfigurationInput { bucket, .. } = req.input;
|
||||
|
||||
let object_lock_configuration = match metadata_sys::get_object_lock_config(&bucket).await {
|
||||
Ok((cfg, _created)) => Some(cfg),
|
||||
Err(err) => {
|
||||
if err == StorageError::ConfigNotFound {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::ObjectLockConfigurationNotFoundError,
|
||||
"Object Lock configuration does not exist for this bucket".to_string(),
|
||||
));
|
||||
}
|
||||
warn!("get_object_lock_config err {:?}", err);
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InternalError,
|
||||
"Failed to load Object Lock configuration".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(S3Response::new(GetObjectLockConfigurationOutput {
|
||||
object_lock_configuration,
|
||||
}))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self, req))]
|
||||
pub async fn execute_copy_object(&self, req: S3Request<CopyObjectInput>) -> S3Result<S3Response<CopyObjectOutput>> {
|
||||
let mut helper = OperationHelper::new(&req, EventName::ObjectCreatedCopy, S3Operation::CopyObject);
|
||||
@@ -2860,114 +2681,6 @@ mod tests {
|
||||
assert_eq!(err.code(), &S3ErrorCode::InternalError);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_put_object_lock_configuration_returns_internal_error_when_store_uninitialized() {
|
||||
let input = PutObjectLockConfigurationInput::builder()
|
||||
.bucket("test-bucket".to_string())
|
||||
.object_lock_configuration(Some(ObjectLockConfiguration {
|
||||
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
|
||||
rule: None,
|
||||
}))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let req = build_request(input, Method::PUT);
|
||||
let usecase = DefaultObjectUsecase::without_context();
|
||||
|
||||
let err = usecase.execute_put_object_lock_configuration(req).await.unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::InternalError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_object_lock_configuration_rejects_disabled_status() {
|
||||
let cfg = ObjectLockConfiguration {
|
||||
object_lock_enabled: Some(ObjectLockEnabled::from("Disabled".to_string())),
|
||||
rule: None,
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::MalformedXML);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_object_lock_configuration_rejects_invalid_default_retention_mode() {
|
||||
let cfg = ObjectLockConfiguration {
|
||||
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
|
||||
rule: Some(ObjectLockRule {
|
||||
default_retention: Some(DefaultRetention {
|
||||
mode: Some(ObjectLockRetentionMode::from("abc".to_string())),
|
||||
days: Some(1),
|
||||
years: None,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::MalformedXML);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_object_lock_configuration_rejects_days_and_years_together() {
|
||||
let cfg = 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: Some(1),
|
||||
years: Some(1),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::MalformedXML);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_object_lock_configuration_rejects_missing_default_retention() {
|
||||
let cfg = ObjectLockConfiguration {
|
||||
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
|
||||
rule: Some(ObjectLockRule { default_retention: None }),
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::MalformedXML);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_object_lock_configuration_rejects_zero_days() {
|
||||
let cfg = 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: Some(0),
|
||||
years: None,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::Custom("InvalidRetentionPeriod".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_object_lock_configuration_rejects_too_many_years() {
|
||||
let cfg = 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: Some(MAXIMUM_RETENTION_YEARS + 1),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::Custom("InvalidRetentionPeriod".into()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_head_object_rejects_range_with_part_number() {
|
||||
let input = HeadObjectInput::builder()
|
||||
|
||||
@@ -23,7 +23,8 @@ use rustfs_ecstore::{
|
||||
endpoints::{EndpointServerPools, Endpoints, PoolEndpoints},
|
||||
store::ECStore,
|
||||
store_api::{
|
||||
BucketOperations, ChunkNativePutData, CompletePart, MakeBucketOptions, MultipartOperations, ObjectIO, ObjectOptions,
|
||||
BucketOperations, BucketOptions, ChunkNativePutData, CompletePart, MakeBucketOptions, MultipartOperations, ObjectIO,
|
||||
ObjectOptions,
|
||||
},
|
||||
};
|
||||
use rustfs_object_io::get::{GetObjectBodySource, GetObjectReadSetup};
|
||||
|
||||
+176
-5
@@ -21,14 +21,20 @@ use crate::storage::helper::OperationHelper;
|
||||
use crate::storage::options::get_opts;
|
||||
use crate::storage::s3_api::acl;
|
||||
use crate::storage::{parse_object_lock_legal_hold, parse_object_lock_retention, validate_bucket_object_lock_enabled};
|
||||
use http::StatusCode;
|
||||
use metrics::{counter, histogram};
|
||||
use rustfs_ecstore::{
|
||||
bucket::{
|
||||
metadata::{BUCKET_ACCELERATE_CONFIG, BUCKET_LOGGING_CONFIG, BUCKET_REQUEST_PAYMENT_CONFIG, BUCKET_WEBSITE_CONFIG},
|
||||
metadata::{
|
||||
BUCKET_ACCELERATE_CONFIG, BUCKET_LOGGING_CONFIG, BUCKET_REQUEST_PAYMENT_CONFIG, BUCKET_VERSIONING_CONFIG,
|
||||
BUCKET_WEBSITE_CONFIG, OBJECT_LOCK_CONFIG,
|
||||
},
|
||||
metadata_sys,
|
||||
object_lock::objectlock_sys::check_retention_for_modification,
|
||||
tagging::{decode_tags, decode_tags_to_map, encode_tags},
|
||||
utils::serialize,
|
||||
versioning::VersioningApi,
|
||||
versioning_sys::BucketVersioningSys,
|
||||
},
|
||||
error::{StorageError, is_err_bucket_not_found, is_err_object_not_found, is_err_version_not_found},
|
||||
new_object_layer_fn,
|
||||
@@ -128,6 +134,83 @@ pub(crate) fn parse_object_version_id(version_id: Option<String>) -> S3Result<Op
|
||||
}
|
||||
}
|
||||
|
||||
const MAXIMUM_RETENTION_DAYS: i32 = 36_500;
|
||||
const MAXIMUM_RETENTION_YEARS: i32 = 100;
|
||||
|
||||
fn invalid_object_lock_configuration(message: impl Into<String>) -> S3Error {
|
||||
S3Error::with_message(S3ErrorCode::MalformedXML, message.into())
|
||||
}
|
||||
|
||||
fn invalid_retention_period(message: impl Into<String>) -> S3Error {
|
||||
let mut err = S3Error::with_message(S3ErrorCode::Custom("InvalidRetentionPeriod".into()), message.into());
|
||||
err.set_status_code(StatusCode::BAD_REQUEST);
|
||||
err
|
||||
}
|
||||
|
||||
fn validate_default_retention_configuration(default_retention: &DefaultRetention) -> S3Result<()> {
|
||||
let Some(mode) = default_retention.mode.as_ref() else {
|
||||
return Err(invalid_object_lock_configuration("retention mode must be specified"));
|
||||
};
|
||||
|
||||
match mode.as_str() {
|
||||
ObjectLockRetentionMode::COMPLIANCE | ObjectLockRetentionMode::GOVERNANCE => {}
|
||||
_ => {
|
||||
return Err(invalid_object_lock_configuration(format!("unknown retention mode {}", mode.as_str())));
|
||||
}
|
||||
}
|
||||
|
||||
match (default_retention.days, default_retention.years) {
|
||||
(Some(days), None) => {
|
||||
if days <= 0 {
|
||||
return Err(invalid_retention_period(
|
||||
"Default retention period must be a positive integer value for 'Days'",
|
||||
));
|
||||
}
|
||||
if days > MAXIMUM_RETENTION_DAYS {
|
||||
return Err(invalid_retention_period(format!("Default retention period too large for 'Days' {days}",)));
|
||||
}
|
||||
}
|
||||
(None, Some(years)) => {
|
||||
if years <= 0 {
|
||||
return Err(invalid_retention_period(
|
||||
"Default retention period must be a positive integer value for 'Years'",
|
||||
));
|
||||
}
|
||||
if years > MAXIMUM_RETENTION_YEARS {
|
||||
return Err(invalid_retention_period(format!(
|
||||
"Default retention period too large for 'Years' {years}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(invalid_object_lock_configuration("either Days or Years must be specified, not both"));
|
||||
}
|
||||
(None, None) => {
|
||||
return Err(invalid_object_lock_configuration("either Days or Years must be specified"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn validate_object_lock_configuration_input(input_cfg: &ObjectLockConfiguration) -> S3Result<()> {
|
||||
let enabled = input_cfg.object_lock_enabled.as_ref().map(ObjectLockEnabled::as_str);
|
||||
if enabled != Some(ObjectLockEnabled::ENABLED) {
|
||||
return Err(invalid_object_lock_configuration(
|
||||
"only 'Enabled' value is allowed to ObjectLockEnabled element",
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(rule) = input_cfg.rule.as_ref() {
|
||||
let Some(default_retention) = rule.default_retention.as_ref() else {
|
||||
return Err(invalid_object_lock_configuration("Rule must include DefaultRetention"));
|
||||
};
|
||||
validate_default_retention_configuration(default_retention)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl S3 for FS {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
@@ -608,8 +691,37 @@ impl S3 for FS {
|
||||
req: S3Request<GetObjectLockConfigurationInput>,
|
||||
) -> S3Result<S3Response<GetObjectLockConfigurationOutput>> {
|
||||
record_s3_op(S3Operation::GetObjectLockConfiguration, &req.input.bucket);
|
||||
let usecase = DefaultObjectUsecase::from_global();
|
||||
usecase.execute_get_object_lock_configuration(req).await
|
||||
let GetObjectLockConfigurationInput { bucket, .. } = req.input;
|
||||
|
||||
let Some(store) = new_object_layer_fn() else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||
};
|
||||
|
||||
store
|
||||
.get_bucket_info(&bucket, &BucketOptions::default())
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let object_lock_configuration = match metadata_sys::get_object_lock_config(&bucket).await {
|
||||
Ok((cfg, _created)) => Some(cfg),
|
||||
Err(err) => {
|
||||
if err == StorageError::ConfigNotFound {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::ObjectLockConfigurationNotFoundError,
|
||||
"Object Lock configuration does not exist for this bucket".to_string(),
|
||||
));
|
||||
}
|
||||
warn!("get_object_lock_config err {:?}", err);
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InternalError,
|
||||
"Failed to load Object Lock configuration".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(S3Response::new(GetObjectLockConfigurationOutput {
|
||||
object_lock_configuration,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_object_retention(
|
||||
@@ -1026,8 +1138,67 @@ impl S3 for FS {
|
||||
&self,
|
||||
req: S3Request<PutObjectLockConfigurationInput>,
|
||||
) -> S3Result<S3Response<PutObjectLockConfigurationOutput>> {
|
||||
let usecase = DefaultObjectUsecase::from_global();
|
||||
usecase.execute_put_object_lock_configuration(req).await
|
||||
let PutObjectLockConfigurationInput {
|
||||
bucket,
|
||||
object_lock_configuration,
|
||||
..
|
||||
} = req.input;
|
||||
|
||||
let Some(input_cfg) = object_lock_configuration else { return Err(s3_error!(InvalidArgument)) };
|
||||
|
||||
let Some(store) = new_object_layer_fn() else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||
};
|
||||
|
||||
store
|
||||
.get_bucket_info(&bucket, &BucketOptions::default())
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
validate_object_lock_configuration_input(&input_cfg)?;
|
||||
|
||||
match metadata_sys::get_object_lock_config(&bucket).await {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
if err == StorageError::ConfigNotFound {
|
||||
if !BucketVersioningSys::enabled(&bucket).await {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InvalidBucketState,
|
||||
"Object Lock configuration cannot be enabled on existing buckets".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
warn!("get_object_lock_config err {:?}", err);
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InternalError,
|
||||
"Failed to get bucket ObjectLockConfiguration".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let data = serialize(&input_cfg).map_err(|err| S3Error::with_message(S3ErrorCode::InternalError, format!("{err}")))?;
|
||||
|
||||
metadata_sys::update(&bucket, OBJECT_LOCK_CONFIG, data)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
// When Object Lock is enabled, automatically enable versioning if not already enabled.
|
||||
// This matches S3-compatible behavior.
|
||||
let versioning_config = BucketVersioningSys::get(&bucket).await.map_err(ApiError::from)?;
|
||||
if !versioning_config.enabled() {
|
||||
let enable_versioning_config = VersioningConfiguration {
|
||||
status: Some(BucketVersioningStatus::from_static(BucketVersioningStatus::ENABLED)),
|
||||
..Default::default()
|
||||
};
|
||||
let versioning_data = serialize(&enable_versioning_config)
|
||||
.map_err(|err| S3Error::with_message(S3ErrorCode::InternalError, format!("{err}")))?;
|
||||
metadata_sys::update(&bucket, BUCKET_VERSIONING_CONFIG, versioning_data)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
}
|
||||
|
||||
Ok(S3Response::new(PutObjectLockConfigurationOutput::default()))
|
||||
}
|
||||
|
||||
async fn put_object_retention(
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
mod tests {
|
||||
use crate::config::WorkloadProfile;
|
||||
use crate::server::cors;
|
||||
use crate::storage::ecfs::FS;
|
||||
use crate::storage::ecfs::{FS, validate_object_lock_configuration_input};
|
||||
use crate::storage::s3_api::common::{rustfs_initiator, rustfs_owner};
|
||||
use crate::storage::{
|
||||
apply_cors_headers, check_preconditions, get_adaptive_buffer_size_with_profile, get_buffer_size_opt_in, is_etag_equal,
|
||||
@@ -35,9 +35,10 @@ mod tests {
|
||||
};
|
||||
use rustfs_zip::CompressionFormat;
|
||||
use s3s::dto::{
|
||||
CORSConfiguration, CORSRule, DeleteObjectTaggingInput, Delimiter, GetObjectAclInput, GetObjectLegalHoldInput,
|
||||
GetObjectRetentionInput, GetObjectTaggingInput, LambdaFunctionConfiguration, ObjectLockLegalHold,
|
||||
ObjectLockLegalHoldStatus, ObjectLockRetention, ObjectLockRetentionMode, PutObjectLegalHoldInput,
|
||||
CORSConfiguration, CORSRule, DefaultRetention, DeleteObjectTaggingInput, Delimiter, GetObjectAclInput,
|
||||
GetObjectLegalHoldInput, GetObjectRetentionInput, GetObjectTaggingInput, LambdaFunctionConfiguration,
|
||||
ObjectLockConfiguration, ObjectLockEnabled, ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention,
|
||||
ObjectLockRetentionMode, ObjectLockRule, PutObjectLegalHoldInput, PutObjectLockConfigurationInput,
|
||||
PutObjectRetentionInput, PutObjectTaggingInput, QueueConfiguration, Tag, Tagging, TopicConfiguration,
|
||||
};
|
||||
use s3s::{S3, S3Error, S3ErrorCode, S3Request, s3_error};
|
||||
@@ -251,6 +252,115 @@ mod tests {
|
||||
assert_eq!(err.code(), &S3ErrorCode::InternalError);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_put_object_lock_configuration_returns_internal_error_when_store_uninitialized() {
|
||||
let input = PutObjectLockConfigurationInput::builder()
|
||||
.bucket("test-bucket".to_string())
|
||||
.object_lock_configuration(Some(ObjectLockConfiguration {
|
||||
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
|
||||
rule: None,
|
||||
}))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let fs = FS::new();
|
||||
let err = fs
|
||||
.put_object_lock_configuration(build_request(input, Method::PUT))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::InternalError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_object_lock_configuration_rejects_disabled_status() {
|
||||
let cfg = ObjectLockConfiguration {
|
||||
object_lock_enabled: Some(ObjectLockEnabled::from("Disabled".to_string())),
|
||||
rule: None,
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::MalformedXML);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_object_lock_configuration_rejects_invalid_default_retention_mode() {
|
||||
let cfg = ObjectLockConfiguration {
|
||||
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
|
||||
rule: Some(ObjectLockRule {
|
||||
default_retention: Some(DefaultRetention {
|
||||
mode: Some(ObjectLockRetentionMode::from("abc".to_string())),
|
||||
days: Some(1),
|
||||
years: None,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::MalformedXML);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_object_lock_configuration_rejects_days_and_years_together() {
|
||||
let cfg = 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: Some(1),
|
||||
years: Some(1),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::MalformedXML);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_object_lock_configuration_rejects_missing_default_retention() {
|
||||
let cfg = ObjectLockConfiguration {
|
||||
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
|
||||
rule: Some(ObjectLockRule { default_retention: None }),
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::MalformedXML);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_object_lock_configuration_rejects_zero_days() {
|
||||
let cfg = 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: Some(0),
|
||||
years: None,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::Custom("InvalidRetentionPeriod".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_object_lock_configuration_rejects_too_many_years() {
|
||||
let cfg = 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: Some(101),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
let err = validate_object_lock_configuration_input(&cfg).unwrap_err();
|
||||
assert_eq!(err.code(), &S3ErrorCode::Custom("InvalidRetentionPeriod".into()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_object_tagging_returns_internal_error_when_store_uninitialized() {
|
||||
let input = GetObjectTaggingInput::builder()
|
||||
|
||||
Reference in New Issue
Block a user