mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-12 23:52:45 +00:00
Merge commit from fork
This commit is contained in:
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user