fix(object-lock): recover remaining s3 tests (#2294)

This commit is contained in:
weisd
2026-03-26 12:11:34 +08:00
committed by GitHub
parent a236b0d01d
commit d637c4d342
5 changed files with 273 additions and 30 deletions
+40
View File
@@ -35,8 +35,10 @@ use rustfs_ecstore::bucket::{
BUCKET_VERSIONING_CONFIG,
},
metadata_sys,
object_lock::ObjectLockApi,
policy_sys::PolicySys,
utils::serialize,
versioning::VersioningApi,
versioning_sys::BucketVersioningSys,
};
use rustfs_ecstore::client::object_api_utils::to_s3s_etag;
@@ -69,6 +71,32 @@ fn to_internal_error(err: impl Display) -> S3Error {
S3Error::with_message(S3ErrorCode::InternalError, format!("{err}"))
}
fn versioning_configuration_has_object_lock_incompatible_settings(config: &VersioningConfiguration) -> bool {
config.suspended()
|| config.exclude_folders.unwrap_or(false)
|| config
.excluded_prefixes
.as_ref()
.is_some_and(|excluded_prefixes| !excluded_prefixes.is_empty())
}
async fn validate_bucket_versioning_update(bucket: &str, config: &VersioningConfiguration) -> S3Result<()> {
match metadata_sys::get_object_lock_config(bucket).await {
Ok((object_lock_config, _)) => {
if object_lock_config.enabled() && versioning_configuration_has_object_lock_incompatible_settings(config) {
return Err(S3Error::with_message(
S3ErrorCode::InvalidBucketState,
"An Object Lock configuration is present on this bucket, versioning cannot be suspended.".to_string(),
));
}
}
Err(StorageError::ConfigNotFound) => {}
Err(err) => return Err(ApiError::from(err).into()),
}
Ok(())
}
fn create_bucket_exists_response(is_owner: bool) -> S3Result<S3Response<CreateBucketOutput>> {
if is_owner {
return Ok(S3Response::new(CreateBucketOutput::default()));
@@ -1357,6 +1385,8 @@ impl DefaultBucketUsecase {
..
} = req.input;
validate_bucket_versioning_update(&bucket, &versioning_configuration).await?;
let data = serialize_config(&versioning_configuration)?;
metadata_sys::update(&bucket, BUCKET_VERSIONING_CONFIG, data)
@@ -1629,6 +1659,16 @@ mod tests {
req
}
#[test]
fn versioning_configuration_has_object_lock_incompatible_settings_rejects_suspended() {
let config = VersioningConfiguration {
status: Some(BucketVersioningStatus::from_static(BucketVersioningStatus::SUSPENDED)),
..Default::default()
};
assert!(versioning_configuration_has_object_lock_incompatible_settings(&config));
}
#[test]
fn resolve_notification_region_prefers_global_region() {
let binding = resolve_notification_region(Some("us-east-1".parse().unwrap()), Some("ap-southeast-1".parse().unwrap()));
+126 -1
View File
@@ -554,6 +554,83 @@ 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
@@ -1164,6 +1241,8 @@ impl DefaultObjectUsecase {
.await
.map_err(ApiError::from)?;
validate_object_lock_configuration_input(&input_cfg)?;
match metadata_sys::get_object_lock_config(&bucket).await {
Ok(_) => {}
Err(err) => {
@@ -1237,6 +1316,7 @@ impl DefaultObjectUsecase {
.as_ref()
.and_then(|r| r.retain_until_date.as_ref())
.map(|d| OffsetDateTime::from(d.clone()));
let new_mode = retention.as_ref().and_then(|r| r.mode.as_ref()).map(|mode| mode.as_str());
// TODO(security): Known TOCTOU race condition (fix in future PR).
//
@@ -1270,7 +1350,7 @@ impl DefaultObjectUsecase {
if let Ok(existing_obj_info) = store.get_object_info(&bucket, &key, &check_opts).await {
let bypass_governance = has_bypass_governance_header(&req.headers);
if let Some(block_reason) =
check_retention_for_modification(&existing_obj_info.user_defined, new_retain_until, bypass_governance)
check_retention_for_modification(&existing_obj_info.user_defined, new_mode, new_retain_until, bypass_governance)
{
return Err(S3Error::with_message(S3ErrorCode::AccessDenied, block_reason.error_message()));
}
@@ -5010,6 +5090,51 @@ mod tests {
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);
}
#[tokio::test]
async fn execute_put_object_retention_returns_internal_error_when_store_uninitialized() {
let input = PutObjectRetentionInput::builder()