Merge commit from fork

This commit is contained in:
Chris
2026-10-09 06:43:53 -07:00
committed by GitHub
parent b77bd26874
commit 99af59a914
3 changed files with 151 additions and 21 deletions
+91 -8
View File
@@ -140,6 +140,9 @@ struct InternalObjectAuthorization;
#[derive(Clone, Debug)]
struct RequestedObjectLockRetainUntil(OffsetDateTime);
#[derive(Clone, Debug)]
struct RequestedObjectLockRetention(ObjectLockRetention);
#[derive(Clone, Debug)]
struct StagedMultipartPartAuthorization;
@@ -885,6 +888,21 @@ fn authorization_conditions<T>(
}
merge_list_bucket_query_conditions(action, req.uri.query(), &mut conditions);
merge_request_object_tag_conditions(action, &req.headers, &mut conditions)?;
if let Some(RequestedObjectLockRetention(retention)) = req.extensions.get::<RequestedObjectLockRetention>() {
// Standalone retention writes apply the XML body, so headers must not
// supply a different value to the policy decision.
conditions.remove("object-lock-mode");
conditions.remove("object-lock-retain-until-date");
if let Some(mode) = &retention.mode {
conditions.insert("object-lock-mode".to_string(), vec![mode.as_str().to_string()]);
}
if let Some(date) = &retention.retain_until_date {
let date = OffsetDateTime::from(date.clone())
.format(&time::format_description::well_known::Rfc3339)
.map_err(|_| S3Error::with_message(S3ErrorCode::InvalidArgument, "Invalid retain-until date"))?;
conditions.insert("object-lock-retain-until-date".to_string(), vec![date]);
}
}
merge_object_lock_remaining_retention_days_condition(
req.extensions.get::<RequestedObjectLockRetainUntil>(),
OffsetDateTime::now_utc(),
@@ -908,6 +926,12 @@ pub(crate) fn set_requested_object_lock_retain_until<T>(req: &mut S3Request<T>,
}
}
fn set_requested_object_lock_retention(req: &mut S3Request<PutObjectRetentionInput>) {
let retention = req.input.retention.clone().unwrap_or_default();
set_requested_object_lock_retain_until(req, retention.retain_until_date.as_ref());
req.extensions.insert(RequestedObjectLockRetention(retention));
}
fn merge_object_lock_remaining_retention_days_condition(
requested: Option<&RequestedObjectLockRetainUntil>,
now: OffsetDateTime,
@@ -3355,12 +3379,7 @@ impl S3Access for FS {
let bucket = req.input.bucket.clone();
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
let requested_retain_until = req
.input
.retention
.as_ref()
.and_then(|retention| retention.retain_until_date.clone());
set_requested_object_lock_retain_until(req, requested_retain_until.as_ref());
set_requested_object_lock_retention(req);
authorize_request(req, Action::S3Action(S3Action::PutObjectRetentionAction)).await?;
// S3 Standard: When bypass_governance header is set, must have s3:BypassGovernanceRetention permission
@@ -3506,7 +3525,7 @@ mod tests {
table_data_plane_content_mutation, table_data_plane_resource_for_request, table_publication_guard_error,
validate_post_object_success_controls, versioned_read_action, versioned_tagging_action,
};
use super::{remaining_retention_days, set_requested_object_lock_retain_until};
use super::{remaining_retention_days, set_requested_object_lock_retain_until, set_requested_object_lock_retention};
use crate::auth::OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION;
use crate::error::ApiError;
use crate::storage::storage_api::contract::bucket::{BucketOperations as _, DeleteBucketOptions, MakeBucketOptions};
@@ -4349,11 +4368,75 @@ mod tests {
}
}
fn remaining_retention_days_conditions(req: &S3Request<()>, action: Action) -> HashMap<String, Vec<String>> {
fn remaining_retention_days_conditions<T>(req: &S3Request<T>, action: Action) -> HashMap<String, Vec<String>> {
let credentials = rustfs_credentials::Credentials::default();
authorization_conditions(req, &credentials, None, None, None, None, action).expect("conditions should build")
}
#[tokio::test]
async fn retention_policy_checks_body_instead_of_conflicting_headers() {
use rustfs_policy::policy::{Args, Policy};
let mut req = build_request(
PutObjectRetentionInput {
retention: Some(ObjectLockRetention {
mode: Some(ObjectLockRetentionMode::from_static("COMPLIANCE")),
retain_until_date: Some(
Timestamp::parse(TimestampFormat::DateTime, "2035-01-02T00:00:00+23:59").expect("body date should parse"),
),
}),
..Default::default()
},
Method::PUT,
);
req.headers
.insert("x-amz-object-lock-mode", HeaderValue::from_static("GOVERNANCE"));
req.headers
.insert("x-amz-object-lock-retain-until-date", HeaderValue::from_static("2029-01-01T00:00:00Z"));
set_requested_object_lock_retention(&mut req);
let action = Action::S3Action(S3Action::PutObjectRetentionAction);
let conditions = remaining_retention_days_conditions(&req, action);
assert_eq!(conditions.get("object-lock-mode"), Some(&vec!["COMPLIANCE".to_string()]));
let applied = OffsetDateTime::from(
req.input
.retention
.as_ref()
.expect("body retention")
.retain_until_date
.clone()
.expect("body date"),
);
let authorized = OffsetDateTime::parse(
&conditions["object-lock-retain-until-date"][0],
&time::format_description::well_known::Rfc3339,
)
.expect("condition date should parse");
assert_eq!(authorized.unix_timestamp(), applied.unix_timestamp());
let policy = Policy::parse_config(br#"{
"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutObjectRetention","Resource":"arn:aws:s3:::bucket/*",
"Condition":{"DateLessThan":{"s3:object-lock-retain-until-date":"2030-01-01T00:00:00Z"}}}]
}"#).expect("bounded retention policy should parse");
let args = Args {
account: "writer",
groups: &None,
action,
bucket: "bucket",
conditions: &conditions,
is_owner: false,
object: "object",
claims: &HashMap::new(),
deny_only: false,
};
assert!(
!policy.is_allowed(&args).await,
"an in-range header cannot authorize an out-of-range body"
);
req.input.retention = None;
set_requested_object_lock_retention(&mut req);
let conditions = remaining_retention_days_conditions(&req, action);
assert!(!conditions.contains_key("object-lock-mode"));
assert!(!conditions.contains_key("object-lock-retain-until-date"));
}
fn request_retaining_for(retain_for: time::Duration) -> S3Request<()> {
let mut req = build_request((), Method::PUT);
let retain_until = OffsetDateTime::now_utc() + retain_for;
+20 -12
View File
@@ -402,6 +402,25 @@ async fn validate_table_catalog_object_mutation(bucket: &str, key: &str) -> S3Re
const MAXIMUM_RETENTION_DAYS: i32 = 36_500;
const MAXIMUM_RETENTION_YEARS: i32 = 100;
pub(crate) fn object_lock_retention_from_metadata(metadata: &HashMap<String, String>) -> S3Result<ObjectLockRetention> {
let mode = metadata
.get(AMZ_OBJECT_LOCK_MODE_LOWER)
.map(|value| ObjectLockRetentionMode::from(value.as_str().to_string()));
let retain_until_date = metadata
.get(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER)
.and_then(|value| OffsetDateTime::parse(value, &Rfc3339).ok())
// The S3 timestamp serializer emits a literal Z, so its wall clock
// must be UTC before it is serialized.
.map(|date| {
date.checked_to_offset(time::UtcOffset::UTC)
.filter(|date| (0..=9999).contains(&date.year()))
.map(Timestamp::from)
.ok_or_else(|| s3_error!(InternalError, "Stored retention date is outside the supported UTC range"))
})
.transpose()?;
Ok(ObjectLockRetention { mode, retain_until_date })
}
fn invalid_object_lock_configuration(message: impl Into<String>) -> S3Error {
S3Error::with_message(S3ErrorCode::MalformedXML, message.into())
}
@@ -1112,19 +1131,8 @@ impl S3 for FS {
s3_error!(InternalError, "{}", e.to_string())
})?;
let mode = object_info
.user_defined
.get(AMZ_OBJECT_LOCK_MODE_LOWER)
.map(|v| ObjectLockRetentionMode::from(v.as_str().to_string()));
let retain_until_date = object_info
.user_defined
.get(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER)
.and_then(|v| OffsetDateTime::parse(v.as_str(), &Rfc3339).ok())
.map(Timestamp::from);
let output = GetObjectRetentionOutput {
retention: Some(ObjectLockRetention { mode, retain_until_date }),
retention: Some(object_lock_retention_from_metadata(&object_info.user_defined)?),
};
let version_id = req.input.version_id.clone().unwrap_or_default();
helper = helper.object(object_info).version_id(version_id);
+40 -1
View File
@@ -18,7 +18,8 @@ mod tests {
use crate::server::cors;
use crate::storage::StorageError;
use crate::storage::ecfs::{
FS, parse_object_version_id, propagate_object_lock_peer_reload, validate_object_lock_configuration_input,
FS, object_lock_retention_from_metadata, parse_object_version_id, propagate_object_lock_peer_reload,
validate_object_lock_configuration_input,
};
use crate::storage::ecfs_extend::{apply_bucket_default_lock_retention, map_bucket_object_lock_config_state};
use crate::storage::s3_api::common::{rustfs_initiator, rustfs_owner};
@@ -988,6 +989,44 @@ mod tests {
assert!(validate_list_object_unordered_with_delimiter(delimiter_some, complex_query_without_unordered).is_ok());
}
#[test]
fn retention_response_serialization_preserves_offset_metadata_instant() {
for value in [
"2035-01-02T00:00:00+23:59",
"2035-01-02T00:00:00-23:59",
"2035-01-02T00:00:00Z",
] {
let metadata = HashMap::from([
(AMZ_OBJECT_LOCK_MODE_LOWER.to_owned(), "COMPLIANCE".to_owned()),
(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER.to_owned(), value.to_owned()),
]);
let retention = object_lock_retention_from_metadata(&metadata).expect("representable retention instant");
assert_eq!(retention.mode.as_ref().expect("mode").as_str(), "COMPLIANCE");
let mut serialized = Vec::new();
retention
.retain_until_date
.expect("stored retention date")
.format(s3s::dto::TimestampFormat::DateTime, &mut serialized)
.expect("serialize the response timestamp");
let serialized = String::from_utf8(serialized).expect("UTF-8 timestamp");
let returned = OffsetDateTime::parse(&serialized, &Rfc3339).expect("parse wire timestamp");
let stored = OffsetDateTime::parse(value, &Rfc3339).expect("parse stored timestamp");
assert_eq!(returned.unix_timestamp(), stored.unix_timestamp(), "stored date {value}");
}
}
#[test]
fn retention_response_rejects_instants_outside_the_utc_wire_range() {
for value in ["9999-12-31T23:59:59-23:59", "0000-01-01T00:00:00+23:59"] {
let metadata = HashMap::from([
(AMZ_OBJECT_LOCK_MODE_LOWER.to_owned(), "COMPLIANCE".to_owned()),
(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER.to_owned(), value.to_owned()),
]);
let error = object_lock_retention_from_metadata(&metadata).expect_err("unrepresentable UTC timestamp");
assert_eq!(error.code(), &S3ErrorCode::InternalError);
}
}
#[test]
fn test_parse_object_lock_retention() {
use time::macros::datetime;