From a43267160df8608b97e5581b0e556ef26c4f6fab Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Tue, 4 Aug 2026 23:20:24 +0800 Subject: [PATCH] fix(auth): enforce object-lock actions for POST uploads (#5701) --- crates/e2e_test/src/multipart_auth_test.rs | 60 ++++------------------ rustfs/src/storage/access.rs | 15 +----- 2 files changed, 12 insertions(+), 63 deletions(-) diff --git a/crates/e2e_test/src/multipart_auth_test.rs b/crates/e2e_test/src/multipart_auth_test.rs index 4ae950e76..28483524c 100644 --- a/crates/e2e_test/src/multipart_auth_test.rs +++ b/crates/e2e_test/src/multipart_auth_test.rs @@ -3584,8 +3584,8 @@ async fn test_anonymous_post_object_rejects_expires_field_missing_from_policy_co #[tokio::test] #[serial] -async fn test_anonymous_post_object_accepts_object_lock_retention_fields() -> Result<(), Box> -{ +async fn test_anonymous_post_object_rejects_object_lock_retention_without_permission() +-> Result<(), Box> { init_logging(); let mut env = RustFSTestEnvironment::new().await?; @@ -3594,8 +3594,6 @@ async fn test_anonymous_post_object_accepts_object_lock_retention_fields() -> Re let bucket = "anon-post-policy-object-lock-retention"; let object_key = "uploads/object-lock-retention.txt"; let retain_until = "2037-10-21T07:28:00Z"; - let expected_body = b"post-policy-object-lock-retention-body".to_vec(); - let admin_client = env.create_s3_client(); admin_client .create_bucket() @@ -3620,7 +3618,7 @@ async fn test_anonymous_post_object_accepts_object_lock_retention_fields() -> Re .text("x-amz-object-lock-retain-until-date", retain_until) .part( "file", - reqwest::multipart::Part::bytes(expected_body.clone()) + reqwest::multipart::Part::bytes(b"post-policy-object-lock-retention-body".to_vec()) .file_name("upload.txt") .mime_str("text/plain")?, ); @@ -3634,26 +3632,8 @@ async fn test_anonymous_post_object_accepts_object_lock_retention_fields() -> Re let status = post_resp.status(); let response_body = post_resp.text().await?; - assert_eq!(status, reqwest::StatusCode::NO_CONTENT); - assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}"); - - let retention = admin_client - .get_object_retention() - .bucket(bucket) - .key(object_key) - .send() - .await?; - let retention = retention.retention().expect("retention should be present"); - assert_eq!(retention.mode().map(|value| value.as_str()), Some("GOVERNANCE")); - let retain_until_out = retention - .retain_until_date() - .expect("retain_until_date should be present") - .fmt(aws_sdk_s3::primitives::DateTimeFormat::DateTime)?; - assert_eq!(retain_until_out, retain_until); - - let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?; - let uploaded = get_out.body.collect().await?.into_bytes(); - assert_eq!(uploaded.as_ref(), expected_body.as_slice()); + assert_eq!(status, reqwest::StatusCode::FORBIDDEN); + assert!(response_body.contains("AccessDenied")); Ok(()) } @@ -3842,8 +3822,8 @@ async fn test_anonymous_post_object_rejects_object_lock_retention_missing_from_p #[tokio::test] #[serial] -async fn test_anonymous_post_object_accepts_object_lock_legal_hold_field() -> Result<(), Box> -{ +async fn test_anonymous_post_object_rejects_object_lock_legal_hold_without_permission() +-> Result<(), Box> { init_logging(); let mut env = RustFSTestEnvironment::new().await?; @@ -3851,8 +3831,6 @@ async fn test_anonymous_post_object_accepts_object_lock_legal_hold_field() -> Re let bucket = "anon-post-policy-object-lock-legal-hold"; let object_key = "uploads/object-lock-legal-hold.txt"; - let expected_body = b"post-policy-object-lock-legal-hold-body".to_vec(); - let admin_client = env.create_s3_client(); admin_client .create_bucket() @@ -3875,7 +3853,7 @@ async fn test_anonymous_post_object_accepts_object_lock_legal_hold_field() -> Re .text("x-amz-object-lock-legal-hold", "ON") .part( "file", - reqwest::multipart::Part::bytes(expected_body.clone()) + reqwest::multipart::Part::bytes(b"post-policy-object-lock-legal-hold-body".to_vec()) .file_name("upload.txt") .mime_str("text/plain")?, ); @@ -3889,26 +3867,8 @@ async fn test_anonymous_post_object_accepts_object_lock_legal_hold_field() -> Re let status = post_resp.status(); let response_body = post_resp.text().await?; - assert_eq!(status, reqwest::StatusCode::NO_CONTENT); - assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}"); - - let legal_hold = admin_client - .get_object_legal_hold() - .bucket(bucket) - .key(object_key) - .send() - .await?; - assert_eq!( - legal_hold - .legal_hold() - .and_then(|value| value.status()) - .map(|value| value.as_str()), - Some("ON") - ); - - let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?; - let uploaded = get_out.body.collect().await?.into_bytes(); - assert_eq!(uploaded.as_ref(), expected_body.as_slice()); + assert_eq!(status, reqwest::StatusCode::FORBIDDEN); + assert!(response_body.contains("AccessDenied")); Ok(()) } diff --git a/rustfs/src/storage/access.rs b/rustfs/src/storage/access.rs index 9b567f0e8..06fe3bd69 100644 --- a/rustfs/src/storage/access.rs +++ b/rustfs/src/storage/access.rs @@ -2328,21 +2328,10 @@ impl S3Access for FS { authorize_request(req, Action::S3Action(S3Action::PutObjectAction)).await?; req.extensions.insert(bucket_generation?); - // POST-object form uploads (s3s routes them through this hook with the - // original POST method before the dedicated post_object hook) are - // governed by the POST policy document instead of the retention / - // legal-hold IAM actions: s3s validates every x-amz-object-lock-* form - // field against the policy conditions before dispatch, and signed POSTs - // sign the policy itself. Anonymous POSTs author their own policy, so - // requiring these PUT-header IAM actions here only breaks the - // policy-covered accept path (rustfs#4845) without adding a boundary — - // the same MinIO handler applies no per-field lock permission either. - if req.method == http::Method::POST { - return Ok(()); + if req.method != http::Method::POST { + authorize_replication_only_put_headers(req).await?; } - authorize_replication_only_put_headers(req).await?; - if legal_hold_write_requested(req.input.object_lock_legal_hold_status.as_ref()) { authorize_request(req, Action::S3Action(S3Action::PutObjectLegalHoldAction)).await?; }