mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-12 23:52:45 +00:00
feat: support object lock remaining retention days (#8407)
This commit is contained in:
committed by
GitHub
parent
3e87004032
commit
89a277c3d9
@@ -198,6 +198,9 @@ pub enum S3KeyName {
|
||||
#[strum(serialize = "s3:object-lock-retain-until-date")]
|
||||
S3ObjectLockRetainUntilDate,
|
||||
|
||||
#[strum(serialize = "s3:object-lock-remaining-retention-days")]
|
||||
S3ObjectLockRemainingRetentionDays,
|
||||
|
||||
#[strum(serialize = "s3:object-lock-legal-hold")]
|
||||
S3ObjectLockLegalHold,
|
||||
|
||||
@@ -393,6 +396,10 @@ mod tests {
|
||||
#[test_case("s3:versionid", KeyName::S3(S3KeyName::S3VersionId) ; "minio_version_id")]
|
||||
#[test_case("s3:object-lock-mode", KeyName::S3(S3KeyName::S3ObjectLockMode))]
|
||||
#[test_case("s3:object-lock-legal-hold", KeyName::S3(S3KeyName::S3ObjectLockLegalHold))]
|
||||
#[test_case(
|
||||
"s3:object-lock-remaining-retention-days",
|
||||
KeyName::S3(S3KeyName::S3ObjectLockRemainingRetentionDays)
|
||||
)]
|
||||
#[test_case("aws:SecureTransport", KeyName::Aws(AwsKeyName::AWSSecureTransport))]
|
||||
#[test_case("jwt:sub", KeyName::Jwt(JwtKeyName::JWTSub))]
|
||||
#[test_case("ldap:user", KeyName::Ldap(LdapKeyName::User))]
|
||||
@@ -417,6 +424,10 @@ mod tests {
|
||||
#[test_case("s3:versionid", KeyName::S3(S3KeyName::S3VersionId) ; "minio_version_id")]
|
||||
#[test_case("s3:object-lock-mode", KeyName::S3(S3KeyName::S3ObjectLockMode))]
|
||||
#[test_case("s3:object-lock-legal-hold", KeyName::S3(S3KeyName::S3ObjectLockLegalHold))]
|
||||
#[test_case(
|
||||
"s3:object-lock-remaining-retention-days",
|
||||
KeyName::S3(S3KeyName::S3ObjectLockRemainingRetentionDays)
|
||||
)]
|
||||
#[test_case("aws:SecureTransport", KeyName::Aws(AwsKeyName::AWSSecureTransport))]
|
||||
#[test_case("jwt:sub", KeyName::Jwt(JwtKeyName::JWTSub))]
|
||||
#[test_case("ldap:user", KeyName::Ldap(LdapKeyName::User))]
|
||||
@@ -437,6 +448,10 @@ mod tests {
|
||||
#[test_case("s3:versionid", KeyName::S3(S3KeyName::S3VersionId))]
|
||||
#[test_case("s3:object-lock-mode", KeyName::S3(S3KeyName::S3ObjectLockMode))]
|
||||
#[test_case("s3:object-lock-legal-hold", KeyName::S3(S3KeyName::S3ObjectLockLegalHold))]
|
||||
#[test_case(
|
||||
"s3:object-lock-remaining-retention-days",
|
||||
KeyName::S3(S3KeyName::S3ObjectLockRemainingRetentionDays)
|
||||
)]
|
||||
#[test_case("aws:SecureTransport", KeyName::Aws(AwsKeyName::AWSSecureTransport))]
|
||||
#[test_case("jwt:sub", KeyName::Jwt(JwtKeyName::JWTSub))]
|
||||
#[test_case("ldap:user", KeyName::Ldap(LdapKeyName::User))]
|
||||
|
||||
@@ -104,6 +104,7 @@ mod tests {
|
||||
key_name::KeyName::{self, *},
|
||||
key_name::S3KeyName::*,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use test_case::test_case;
|
||||
|
||||
fn new_func(name: KeyName, variable: Option<String>, value: i64) -> NumberFunc {
|
||||
@@ -125,6 +126,35 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(r#"{"s3:object-lock-remaining-retention-days": 30}"#; "number")]
|
||||
#[test_case(r#"{"s3:object-lock-remaining-retention-days": "30"}"#; "string")]
|
||||
fn remaining_retention_days_deserializes(input: &str) -> Result<(), serde_json::Error> {
|
||||
let v: NumberFunc = serde_json::from_str(input)?;
|
||||
assert_eq!(v, new_func(S3(S3ObjectLockRemainingRetentionDays), None, 30));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case("29", true, false, false; "below limit")]
|
||||
#[test_case("30", true, false, true; "at limit")]
|
||||
#[test_case("31", false, true, false; "above limit")]
|
||||
fn remaining_retention_days_numeric_comparisons(days: &str, less_than_equals: bool, greater_than: bool, equals: bool) {
|
||||
let limit = new_func(S3(S3ObjectLockRemainingRetentionDays), None, 30);
|
||||
let values = HashMap::from([("object-lock-remaining-retention-days".to_string(), vec![days.to_string()])]);
|
||||
|
||||
assert_eq!(limit.evaluate(i64::le, false, &values), less_than_equals);
|
||||
assert_eq!(limit.evaluate(i64::gt, false, &values), greater_than);
|
||||
assert_eq!(limit.evaluate(i64::eq, false, &values), equals);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remaining_retention_days_numeric_condition_without_value_uses_if_exists() {
|
||||
let limit = new_func(S3(S3ObjectLockRemainingRetentionDays), None, 30);
|
||||
let values = HashMap::new();
|
||||
|
||||
assert!(!limit.evaluate(i64::le, false, &values));
|
||||
assert!(limit.evaluate(i64::le, true, &values));
|
||||
}
|
||||
|
||||
#[test_case(r#"{"s3:max-keys":"1"}"#, new_func(S3(S3MaxKeys), None, 1); "1")]
|
||||
#[test_case(r#"{"s3:max-keys/a":"1"}"#, new_func(S3(S3MaxKeys), Some("a".into()), 1); "2")]
|
||||
fn test_ser(expect: &str, input: NumberFunc) -> Result<(), serde_json::Error> {
|
||||
|
||||
@@ -2452,6 +2452,8 @@ impl DefaultObjectUsecase {
|
||||
explicit_version_id,
|
||||
opts.delete_marker_replication_status() == ReplicationStatusType::Replica,
|
||||
);
|
||||
// auth_req inherits the outer request's date; a PAX override replaces it.
|
||||
set_requested_object_lock_retain_until(&mut auth_req, effective_object_lock_retain_until_date.as_ref());
|
||||
extract_try!(authorize_request(&mut auth_req, Action::S3Action(S3Action::PutObjectAction)).await);
|
||||
if iam_requirements.tagging {
|
||||
extract_try!(authorize_request(&mut auth_req, Action::S3Action(S3Action::PutObjectTaggingAction)).await);
|
||||
|
||||
@@ -24,7 +24,7 @@ use super::storage_api::object_usecase::access::{
|
||||
apply_copy_source_bucket_generation_guard, authorize_request, delete_object_authorize_action, has_bypass_governance_header,
|
||||
load_bucket_generation_from_store, odm_read_generation, prepare_odm_read_generation,
|
||||
recursive_force_delete_has_authenticated_caller, replication_request_authorized, req_info_mut, req_info_ref,
|
||||
retained_table_data_plane_publication_guards,
|
||||
retained_table_data_plane_publication_guards, set_requested_object_lock_retain_until,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use super::storage_api::object_usecase::bucket::quota::BucketQuota;
|
||||
|
||||
@@ -277,6 +277,7 @@ pub(crate) mod access {
|
||||
load_bucket_generation_from_store, log_list_buckets_iam_implicit_deny, odm_read_generation,
|
||||
prepare_list_buckets_iam_authorization, prepare_odm_read_generation, recursive_force_delete_has_authenticated_caller,
|
||||
replication_request_authorized, req_info_mut, req_info_ref, retained_table_data_plane_publication_guards,
|
||||
set_requested_object_lock_retain_until,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -824,6 +824,10 @@ pub fn get_condition_values_with_query_and_client_info(
|
||||
args
|
||||
}
|
||||
|
||||
/// Condition name for `s3:object-lock-remaining-retention-days`. Storage access
|
||||
/// derives it from the requested retain-until date, so no header may set it.
|
||||
pub(crate) const OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION: &str = "object-lock-remaining-retention-days";
|
||||
|
||||
/// Whether a request header is forbidden from contributing to policy condition key
|
||||
/// `key`, either because the server already derived that key from verified state or
|
||||
/// because it is a well-known identity/context key that only the server may populate.
|
||||
@@ -836,6 +840,7 @@ fn is_reserved_condition_key(key: &str, server_derived: &HashMap<String, Vec<Str
|
||||
]
|
||||
.iter()
|
||||
.any(|header| key.eq_ignore_ascii_case(header.trim_start_matches("x-amz-")))
|
||||
|| key.eq_ignore_ascii_case(OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION)
|
||||
|| is_server_derived_condition_key(key)
|
||||
}
|
||||
|
||||
@@ -2004,6 +2009,17 @@ mod tests {
|
||||
assert_eq!(conditions.get("object-lock-retain-until-date"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_lock_remaining_retention_days_header_is_ignored() {
|
||||
let cred = create_test_credentials();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION, HeaderValue::from_static("1"));
|
||||
|
||||
let conditions = get_condition_values(&headers, &cred, None, None, None);
|
||||
|
||||
assert_eq!(conditions.get(OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_condition_values_with_grant_headers() {
|
||||
let cred = create_test_credentials();
|
||||
|
||||
@@ -16,10 +16,10 @@ use super::ObjectOptions;
|
||||
use super::ecfs::FS;
|
||||
use super::{ECStore, PolicySys, ReplicationStatusType, StorageError, get_lock_acquire_timeout, is_err_bucket_not_found};
|
||||
use crate::auth::{
|
||||
AuthType, RUSTFS_MAX_CONTENT_LENGTH_QUERY, RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY, VerifiedPresignedRequest,
|
||||
VerifiedSigV4Request, check_key_valid_with_context, get_condition_values_with_client_info,
|
||||
get_condition_values_with_query_and_client_info, get_request_auth_type_with_query, get_session_token,
|
||||
parse_presigned_multipart_max_total_object_size, parse_presigned_put_max_content_length,
|
||||
AuthType, OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION, RUSTFS_MAX_CONTENT_LENGTH_QUERY,
|
||||
RUSTFS_MAX_TOTAL_OBJECT_SIZE_QUERY, VerifiedPresignedRequest, VerifiedSigV4Request, check_key_valid_with_context,
|
||||
get_condition_values_with_client_info, get_condition_values_with_query_and_client_info, get_request_auth_type_with_query,
|
||||
get_session_token, parse_presigned_multipart_max_total_object_size, parse_presigned_put_max_content_length,
|
||||
reject_unsigned_amz_headers_on_sigv4_request,
|
||||
};
|
||||
use crate::error::ApiError;
|
||||
@@ -61,6 +61,7 @@ use std::collections::{BTreeSet, HashMap};
|
||||
use std::sync::Arc;
|
||||
#[cfg(test)]
|
||||
use std::sync::OnceLock;
|
||||
use time::OffsetDateTime;
|
||||
use url::{Url, form_urlencoded};
|
||||
|
||||
const EVENT_OBJECT_TAG_AUTHORIZATION: &str = "object_tag_authorization";
|
||||
@@ -134,6 +135,11 @@ pub(crate) struct PostObjectRequestMarker;
|
||||
#[derive(Clone, Debug)]
|
||||
struct InternalObjectAuthorization;
|
||||
|
||||
/// Retain-until date the request asks to set. `authorization_conditions` turns it
|
||||
/// into `object-lock-remaining-retention-days`.
|
||||
#[derive(Clone, Debug)]
|
||||
struct RequestedObjectLockRetainUntil(OffsetDateTime);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct StagedMultipartPartAuthorization;
|
||||
|
||||
@@ -879,9 +885,58 @@ 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)?;
|
||||
merge_object_lock_remaining_retention_days_condition(
|
||||
req.extensions.get::<RequestedObjectLockRetainUntil>(),
|
||||
OffsetDateTime::now_utc(),
|
||||
&mut conditions,
|
||||
);
|
||||
Ok(conditions)
|
||||
}
|
||||
|
||||
/// Records the retain-until date a request asks to set, or clears it when the
|
||||
/// request sets none. Call it before `authorize_request` with the parsed date from
|
||||
/// headers, form fields, the PutObjectRetention XML body, or an archive member.
|
||||
pub(crate) fn set_requested_object_lock_retain_until<T>(req: &mut S3Request<T>, retain_until: Option<&Timestamp>) {
|
||||
match retain_until {
|
||||
Some(retain_until) => {
|
||||
req.extensions
|
||||
.insert(RequestedObjectLockRetainUntil(OffsetDateTime::from(retain_until.clone())));
|
||||
}
|
||||
None => {
|
||||
req.extensions.remove::<RequestedObjectLockRetainUntil>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_object_lock_remaining_retention_days_condition(
|
||||
requested: Option<&RequestedObjectLockRetainUntil>,
|
||||
now: OffsetDateTime,
|
||||
conditions: &mut HashMap<String, Vec<String>>,
|
||||
) {
|
||||
if let Some(RequestedObjectLockRetainUntil(retain_until)) = requested {
|
||||
conditions.insert(
|
||||
OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION.to_string(),
|
||||
vec![remaining_retention_days(*retain_until, now).to_string()],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whole days from `now` to `retain_until`, rounded up as MinIO does, so a policy
|
||||
/// limit of N days rejects a date even one second past day N. A date that is not
|
||||
/// in the future yields 0.
|
||||
fn remaining_retention_days(retain_until: OffsetDateTime, now: OffsetDateTime) -> i64 {
|
||||
let remaining = retain_until - now;
|
||||
if remaining <= time::Duration::ZERO {
|
||||
return 0;
|
||||
}
|
||||
let days = remaining.whole_days();
|
||||
if remaining > time::Duration::days(days) {
|
||||
days + 1
|
||||
} else {
|
||||
days
|
||||
}
|
||||
}
|
||||
|
||||
fn retain_internal_object_authorization_conditions(
|
||||
conditions: &mut HashMap<String, Vec<String>>,
|
||||
cred: &rustfs_credentials::Credentials,
|
||||
@@ -2256,6 +2311,8 @@ impl S3Access for FS {
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
req_info.version_id = req.input.version_id.clone();
|
||||
|
||||
let requested_retain_until = req.input.object_lock_retain_until_date.clone();
|
||||
set_requested_object_lock_retain_until(req, requested_retain_until.as_ref());
|
||||
authorize_request(req, Action::S3Action(S3Action::PutObjectAction)).await?;
|
||||
|
||||
authorize_replication_only_put_headers(req).await?;
|
||||
@@ -2280,6 +2337,8 @@ impl S3Access for FS {
|
||||
req_info.bucket = Some(req.input.bucket.clone());
|
||||
req_info.object = Some(req.input.key.clone());
|
||||
|
||||
let requested_retain_until = req.input.object_lock_retain_until_date.clone();
|
||||
set_requested_object_lock_retain_until(req, requested_retain_until.as_ref());
|
||||
authorize_request(req, Action::S3Action(S3Action::PutObjectAction)).await?;
|
||||
|
||||
authorize_replication_only_put_headers(req).await?;
|
||||
@@ -3210,6 +3269,8 @@ impl S3Access for FS {
|
||||
// Snapshot before authorization, but preserve AccessDenied precedence
|
||||
// by exposing any bucket-state error only after authorization succeeds.
|
||||
let bucket_generation = load_bucket_generation(self, req, &bucket).await;
|
||||
let requested_retain_until = req.input.object_lock_retain_until_date.clone();
|
||||
set_requested_object_lock_retain_until(req, requested_retain_until.as_ref());
|
||||
authorize_request(req, Action::S3Action(S3Action::PutObjectAction)).await?;
|
||||
req.extensions.insert(bucket_generation?);
|
||||
|
||||
@@ -3275,6 +3336,12 @@ 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());
|
||||
authorize_request(req, Action::S3Action(S3Action::PutObjectRetentionAction)).await?;
|
||||
|
||||
// S3 Standard: When bypass_governance header is set, must have s3:BypassGovernanceRetention permission
|
||||
@@ -3416,6 +3483,8 @@ 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,
|
||||
};
|
||||
use super::{remaining_retention_days, set_requested_object_lock_retain_until};
|
||||
use crate::auth::OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION;
|
||||
use crate::error::ApiError;
|
||||
use crate::storage::storage_api::contract::bucket::{BucketOperations as _, DeleteBucketOptions, MakeBucketOptions};
|
||||
use crate::storage::storage_api::contract::multipart::MultipartOperations as _;
|
||||
@@ -4216,6 +4285,164 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn remaining_retention_days_conditions(req: &S3Request<()>, 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")
|
||||
}
|
||||
|
||||
fn request_retaining_for(retain_for: time::Duration) -> S3Request<()> {
|
||||
let mut req = build_request((), Method::PUT);
|
||||
let retain_until = OffsetDateTime::now_utc() + retain_for;
|
||||
set_requested_object_lock_retain_until(&mut req, Some(&retain_until.into()));
|
||||
req
|
||||
}
|
||||
|
||||
/// AWS does not document how it rounds partial days. MinIO computes
|
||||
/// `ceil(hours / 24)` in `enforceRetentionBypassForPut`
|
||||
/// (cmd/bucket-object-lock.go), and RustFS keeps that rounding so a policy
|
||||
/// moved from MinIO evaluates the same way.
|
||||
#[test]
|
||||
fn remaining_retention_days_matches_s3_ceil_rounding() {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let cases = [
|
||||
(time::Duration::NANOSECOND, 1),
|
||||
(time::Duration::HOUR, 1),
|
||||
(time::Duration::DAY - time::Duration::SECOND, 1),
|
||||
(time::Duration::DAY, 1),
|
||||
(time::Duration::DAY + time::Duration::SECOND, 2),
|
||||
(time::Duration::days(29) + time::Duration::hours(12), 30),
|
||||
(time::Duration::days(30), 30),
|
||||
(time::Duration::days(30) + time::Duration::SECOND, 31),
|
||||
];
|
||||
|
||||
for (retain_for, expected) in cases {
|
||||
let minio_days = (retain_for.as_seconds_f64() / 3600.0 / 24.0).ceil() as i64;
|
||||
assert_eq!(minio_days, expected, "MinIO reference for {retain_for}");
|
||||
assert_eq!(remaining_retention_days(now + retain_for, now), expected, "retain for {retain_for}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remaining_retention_days_is_zero_for_dates_not_in_future() {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
|
||||
assert_eq!(remaining_retention_days(now, now), 0);
|
||||
assert_eq!(remaining_retention_days(now - time::Duration::NANOSECOND, now), 0);
|
||||
assert_eq!(remaining_retention_days(now - time::Duration::days(3), now), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requested_retain_until_sets_remaining_retention_days_condition() {
|
||||
let req = request_retaining_for(time::Duration::days(10) - time::Duration::HOUR);
|
||||
|
||||
let conditions = remaining_retention_days_conditions(&req, Action::S3Action(S3Action::PutObjectAction));
|
||||
|
||||
assert_eq!(
|
||||
conditions.get(OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION),
|
||||
Some(&vec!["10".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_without_retention_has_no_remaining_retention_days_condition() {
|
||||
let action = Action::S3Action(S3Action::PutObjectAction);
|
||||
let req = build_request((), Method::PUT);
|
||||
assert_eq!(
|
||||
remaining_retention_days_conditions(&req, action).get(OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION),
|
||||
None
|
||||
);
|
||||
|
||||
let mut req = request_retaining_for(time::Duration::days(5));
|
||||
set_requested_object_lock_retain_until(&mut req, None);
|
||||
assert_eq!(
|
||||
remaining_retention_days_conditions(&req, action).get(OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remaining_retention_days_condition_ignores_client_header() {
|
||||
let action = Action::S3Action(S3Action::PutObjectRetentionAction);
|
||||
let mut req = build_request((), Method::PUT);
|
||||
req.headers
|
||||
.insert(OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION, HeaderValue::from_static("1"));
|
||||
assert_eq!(
|
||||
remaining_retention_days_conditions(&req, action).get(OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION),
|
||||
None
|
||||
);
|
||||
|
||||
let retain_until = OffsetDateTime::now_utc() + time::Duration::days(400);
|
||||
set_requested_object_lock_retain_until(&mut req, Some(&retain_until.into()));
|
||||
assert_eq!(
|
||||
remaining_retention_days_conditions(&req, action).get(OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION),
|
||||
Some(&vec!["400".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bucket_policy_limits_retention_to_thirty_days() {
|
||||
let policy: BucketPolicy = serde_json::from_str(
|
||||
r#"{
|
||||
"Version":"2012-10-17",
|
||||
"Statement":[
|
||||
{
|
||||
"Effect":"Allow",
|
||||
"Principal":{"AWS":"*"},
|
||||
"Action":["s3:PutObjectRetention"],
|
||||
"Resource":["arn:aws:s3:::bucket/*"]
|
||||
},
|
||||
{
|
||||
"Effect":"Deny",
|
||||
"Principal":{"AWS":"*"},
|
||||
"Action":["s3:PutObjectRetention"],
|
||||
"Resource":["arn:aws:s3:::bucket/*"],
|
||||
"Condition":{"NumericGreaterThan":{"s3:object-lock-remaining-retention-days":"30"}}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.expect("bucket policy should parse");
|
||||
let no_groups: Option<Vec<String>> = None;
|
||||
let action = Action::S3Action(S3Action::PutObjectRetentionAction);
|
||||
let spoofed_header = |mut req: S3Request<()>| {
|
||||
req.headers
|
||||
.insert(OBJECT_LOCK_REMAINING_RETENTION_DAYS_CONDITION, HeaderValue::from_static("1"));
|
||||
req
|
||||
};
|
||||
|
||||
let cases = [
|
||||
("29 days", request_retaining_for(time::Duration::days(29)), true),
|
||||
("30 days", request_retaining_for(time::Duration::days(30)), true),
|
||||
("31 days", request_retaining_for(time::Duration::days(31)), false),
|
||||
(
|
||||
"30 days and a minute",
|
||||
request_retaining_for(time::Duration::days(30) + time::Duration::MINUTE),
|
||||
false,
|
||||
),
|
||||
("no retention", build_request((), Method::PUT), true),
|
||||
(
|
||||
"31 days with spoofed header",
|
||||
spoofed_header(request_retaining_for(time::Duration::days(31))),
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
for (name, req, allowed) in cases {
|
||||
let conditions = remaining_retention_days_conditions(&req, action);
|
||||
let args = BucketPolicyArgs {
|
||||
bucket: "bucket",
|
||||
action,
|
||||
is_owner: false,
|
||||
account: "",
|
||||
groups: &no_groups,
|
||||
conditions: &conditions,
|
||||
object: "obj",
|
||||
};
|
||||
|
||||
assert_eq!(policy.is_allowed(&args).await, allowed, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_object_authorization_ignores_unapplied_request_options() {
|
||||
let mut req = build_request((), Method::POST);
|
||||
|
||||
@@ -131,6 +131,7 @@ pub(crate) mod access_consumer {
|
||||
load_bucket_generation_from_store, log_list_buckets_iam_implicit_deny, odm_read_generation,
|
||||
prepare_list_buckets_iam_authorization, prepare_odm_read_generation, recursive_force_delete_has_authenticated_caller,
|
||||
replication_request_authorized, req_info_mut, req_info_ref, retained_table_data_plane_publication_guards,
|
||||
set_requested_object_lock_retain_until,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user