diff --git a/crates/e2e_test/src/multipart_auth_test.rs b/crates/e2e_test/src/multipart_auth_test.rs index 81613bf97..8764d90c5 100644 --- a/crates/e2e_test/src/multipart_auth_test.rs +++ b/crates/e2e_test/src/multipart_auth_test.rs @@ -17,6 +17,7 @@ use crate::common::{RustFSTestEnvironment, init_logging, local_http_client}; use async_compression::tokio::write::{BzEncoder, XzEncoder}; use aws_sdk_s3::error::{ProvideErrorMetadata, SdkError}; +use aws_sdk_s3::operation::head_object::HeadObjectOutput; use aws_sdk_s3::primitives::ByteStream; use aws_sdk_s3::types::{ ServerSideEncryption, ServerSideEncryptionByDefault, ServerSideEncryptionConfiguration, ServerSideEncryptionRule, @@ -349,6 +350,71 @@ async fn run_post_object_policy_case( Ok(()) } +/// One accepted POST Object upload driven end-to-end (backlog#1838): starts a +/// fresh server, allows anonymous PutObject on `bucket`, posts an anonymous +/// POST Object form whose policy carries `policy_conditions` and whose form +/// carries `form_field` on top of the mandatory key+policy fields, then asserts +/// 204 with an empty body, that `read_stored` observes the submitted value on +/// the stored object, and that the object body round-tripped unchanged. +/// `case` prefixes every assertion message so a failing table row is +/// identifiable at a glance. +#[allow(clippy::too_many_arguments)] +async fn run_post_object_accept_case( + bucket: &str, + object_key: &str, + policy_conditions: Vec, + form_field: (&str, &str), + file_mime: &str, + file_body: &[u8], + read_stored: fn(&HeadObjectOutput) -> Option<&str>, + case: &str, +) -> Result<(), Box> { + let mut env = RustFSTestEnvironment::new().await?; + env.start_rustfs_server(vec![]).await?; + + let admin_client = env.create_s3_client(); + admin_client.create_bucket().bucket(bucket).send().await?; + allow_anonymous_put_object(&admin_client, bucket).await?; + + let policy = encode_post_policy(policy_conditions); + + let (field_name, field_value) = form_field; + let post_form = reqwest::multipart::Form::new() + .text("key", object_key.to_string()) + .text("policy", policy) + .text(field_name.to_string(), field_value.to_string()) + .part( + "file", + reqwest::multipart::Part::bytes(file_body.to_vec()) + .file_name("upload.txt") + .mime_str(file_mime)?, + ); + + let post_resp = local_http_client() + .post(format!("{}/{}", env.url, bucket)) + .multipart(post_form) + .send() + .await?; + + let status = post_resp.status(); + let response_body = post_resp.text().await?; + + assert_eq!(status, reqwest::StatusCode::NO_CONTENT, "[{case}] unexpected status"); + assert!( + response_body.is_empty(), + "[{case}] 204 response should not contain a body, got: {response_body}" + ); + + let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; + assert_eq!(read_stored(&head), Some(field_value), "[{case}] stored {field_name} mismatch"); + + 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(), file_body, "[{case}] uploaded body mismatch"); + + Ok(()) +} + /// Table-driven fold of the nine `*_missing_from_policy_conditions` POST /// Object tests (backlog#1838 PR1). Every row keeps its original test's exact /// bucket, key, form field, file body, and expected error strings; the shared @@ -1551,60 +1617,6 @@ async fn test_anonymous_post_object_accepts_sse_s3_missing_from_policy_condition Ok(()) } -#[tokio::test] -#[serial] -async fn test_anonymous_post_object_accepts_storage_class_exact_policy_match() --> Result<(), Box> { - init_logging(); - - let mut env = RustFSTestEnvironment::new().await?; - env.start_rustfs_server(vec![]).await?; - - let bucket = "anon-post-storage-class"; - let object_key = "post-storage-class-object.txt"; - let expected_body = b"post-storage-class-body".to_vec(); - let storage_class = "REDUCED_REDUNDANCY"; - - let admin_client = env.create_s3_client(); - admin_client.create_bucket().bucket(bucket).send().await?; - allow_anonymous_put_object(&admin_client, bucket).await?; - - let policy = encode_post_policy(vec![ - serde_json::json!({ "bucket": bucket }), - serde_json::json!({ "key": object_key }), - serde_json::json!({ "x-amz-storage-class": storage_class }), - serde_json::json!(["content-length-range", 0, 1024]), - ]); - - let post_form = reqwest::multipart::Form::new() - .text("key", object_key.to_string()) - .text("policy", policy) - .text("x-amz-storage-class", storage_class) - .part( - "file", - reqwest::multipart::Part::bytes(expected_body.clone()) - .file_name("upload.txt") - .mime_str("text/plain")?, - ); - - let post_resp = local_http_client() - .post(format!("{}/{}", env.url, bucket)) - .multipart(post_form) - .send() - .await?; - - assert_eq!(post_resp.status(), reqwest::StatusCode::NO_CONTENT); - - let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; - assert_eq!(head.storage_class().map(|value| value.as_str()), Some(storage_class)); - - let uploaded = admin_client.get_object().bucket(bucket).key(object_key).send().await?; - let uploaded = uploaded.body.collect().await?.into_bytes(); - assert_eq!(uploaded.as_ref(), expected_body.as_slice()); - - Ok(()) -} - #[tokio::test] #[serial] async fn test_anonymous_post_object_rejects_storage_class_missing_from_policy_conditions() @@ -2620,521 +2632,183 @@ async fn test_anonymous_post_object_rejects_success_action_redirect_missing_from Ok(()) } +/// Table-driven fold of the eleven accepted POST Object form-field tests +/// (backlog#1838 PR4). Every row keeps its original test's exact bucket, key, +/// form field, submitted value, policy condition, file MIME type, and file +/// body; the shared shape is: the policy covers the field (exact condition or +/// `starts-with` prefix), the form submits it, the upload returns 204 with an +/// empty body, and the stored object echoes the submitted value back. #[tokio::test] #[serial] -async fn test_anonymous_post_object_accepts_metadata_field_covered_by_starts_with() +async fn test_anonymous_post_object_accepts_fields_covered_by_policy_conditions() -> Result<(), Box> { init_logging(); - let mut env = RustFSTestEnvironment::new().await?; - env.start_rustfs_server(vec![]).await?; + // (case, bucket, object_key, field, submitted value, `starts-with` prefix + // (`None` pins the field to an exact policy condition), file part MIME type, + // file body, stored-value accessor) + type Case = ( + &'static str, + &'static str, + &'static str, + &'static str, + &'static str, + Option<&'static str>, + &'static str, + &'static [u8], + fn(&HeadObjectOutput) -> Option<&str>, + ); + let cases: &[Case] = &[ + ( + "storage-class", + "anon-post-storage-class", + "post-storage-class-object.txt", + "x-amz-storage-class", + "REDUCED_REDUNDANCY", + None, + "text/plain", + b"post-storage-class-body", + |head: &HeadObjectOutput| head.storage_class().map(|value| value.as_str()), + ), + ( + "metadata-starts-with", + "anon-post-policy-meta-accept", + "uploads/meta-object.txt", + "x-amz-meta-project", + "alpha-demo", + Some("alpha-"), + "text/plain", + b"post-policy-meta-body", + |head: &HeadObjectOutput| head.metadata().and_then(|meta| meta.get("project")).map(String::as_str), + ), + ( + "content-type", + "anon-post-policy-content-type-accept", + "uploads/content-type-accept.txt", + "Content-Type", + "text/plain", + None, + "text/plain", + b"post-policy-content-type-accept", + |head: &HeadObjectOutput| head.content_type(), + ), + ( + "content-type-starts-with", + "anon-post-policy-content-type-accept", + "uploads/content-type-object.txt", + "Content-Type", + "image/png", + Some("image/"), + "image/png", + b"post-policy-content-type-body", + |head: &HeadObjectOutput| head.content_type(), + ), + ( + "content-disposition", + "anon-post-policy-disposition-accept", + "uploads/disposition-object.txt", + "Content-Disposition", + "attachment; filename=\"upload.txt\"", + None, + "text/plain", + b"post-policy-disposition-body", + |head: &HeadObjectOutput| head.content_disposition(), + ), + ( + "cache-control", + "anon-post-policy-cache-control-accept", + "uploads/cache-control-object.txt", + "Cache-Control", + "max-age=60", + None, + "text/plain", + b"post-policy-cache-control-body", + |head: &HeadObjectOutput| head.cache_control(), + ), + ( + "content-language", + "anon-post-policy-content-language-accept", + "uploads/content-language-object.txt", + "Content-Language", + "en-US", + None, + "text/plain", + b"post-policy-content-language-body", + |head: &HeadObjectOutput| head.content_language(), + ), + ( + "content-encoding", + "anon-post-policy-content-encoding-accept", + "uploads/content-encoding-object.txt", + "Content-Encoding", + "gzip", + None, + "text/plain", + b"post-policy-content-encoding-body", + |head: &HeadObjectOutput| head.content_encoding(), + ), + ( + "website-redirect-location", + "anon-post-policy-website-redirect-accept", + "uploads/website-redirect-object.txt", + "x-amz-website-redirect-location", + "/docs/landing.html", + None, + "text/plain", + b"post-policy-website-redirect-body", + |head: &HeadObjectOutput| head.website_redirect_location(), + ), + ( + "expires", + "anon-post-policy-expires-accept", + "uploads/expires-object.txt", + "Expires", + "Wed, 21 Oct 2037 07:28:00 GMT", + None, + "text/plain", + b"post-policy-expires-body", + |head: &HeadObjectOutput| head.expires_string(), + ), + ( + "metadata-exact", + "anon-post-policy-meta-exact-accept", + "uploads/meta-exact-accept-object.txt", + "x-amz-meta-project", + "alpha-demo", + None, + "text/plain", + b"post-policy-meta-exact-body", + |head: &HeadObjectOutput| head.metadata().and_then(|meta| meta.get("project")).map(String::as_str), + ), + ]; - let bucket = "anon-post-policy-meta-accept"; - let object_key = "uploads/meta-object.txt"; - let metadata_value = "alpha-demo"; - let expected_body = b"post-policy-meta-body".to_vec(); + for (case, bucket, object_key, field, value, starts_with_prefix, file_mime, file_body, read_stored) in cases { + let condition = match starts_with_prefix { + Some(prefix) => serde_json::json!(["starts-with", format!("${field}"), prefix]), + None => { + let mut exact = serde_json::Map::new(); + exact.insert((*field).to_string(), serde_json::Value::String((*value).to_string())); + serde_json::Value::Object(exact) + } + }; - let admin_client = env.create_s3_client(); - admin_client.create_bucket().bucket(bucket).send().await?; - allow_anonymous_put_object(&admin_client, bucket).await?; - - let policy = encode_post_policy(vec![ - serde_json::json!({ "bucket": bucket }), - serde_json::json!({ "key": object_key }), - serde_json::json!(["starts-with", "$x-amz-meta-project", "alpha-"]), - serde_json::json!(["content-length-range", 0, 1024]), - ]); - - let post_form = reqwest::multipart::Form::new() - .text("key", object_key.to_string()) - .text("policy", policy) - .text("x-amz-meta-project", metadata_value) - .part( - "file", - reqwest::multipart::Part::bytes(expected_body.clone()) - .file_name("upload.txt") - .mime_str("text/plain")?, - ); - - let post_resp = local_http_client() - .post(format!("{}/{}", env.url, bucket)) - .multipart(post_form) - .send() + run_post_object_accept_case( + bucket, + object_key, + vec![ + serde_json::json!({ "bucket": bucket }), + serde_json::json!({ "key": object_key }), + condition, + serde_json::json!(["content-length-range", 0, 1024]), + ], + (field, value), + file_mime, + file_body, + *read_stored, + case, + ) .await?; - - 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 head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; - let metadata = head.metadata().expect("head_object should expose uploaded metadata"); - assert_eq!(metadata.get("project").map(String::as_str), Some(metadata_value)); - - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_anonymous_post_object_accepts_content_type_field_exact_policy_match() --> Result<(), Box> { - init_logging(); - - let mut env = RustFSTestEnvironment::new().await?; - env.start_rustfs_server(vec![]).await?; - - let bucket = "anon-post-policy-content-type-accept"; - let object_key = "uploads/content-type-accept.txt"; - let content_type = "text/plain"; - let expected_body = b"post-policy-content-type-accept".to_vec(); - - let admin_client = env.create_s3_client(); - admin_client.create_bucket().bucket(bucket).send().await?; - allow_anonymous_put_object(&admin_client, bucket).await?; - - let policy = encode_post_policy(vec![ - serde_json::json!({ "bucket": bucket }), - serde_json::json!({ "key": object_key }), - serde_json::json!({ "Content-Type": content_type }), - serde_json::json!(["content-length-range", 0, 1024]), - ]); - - let post_form = reqwest::multipart::Form::new() - .text("key", object_key.to_string()) - .text("policy", policy) - .text("Content-Type", content_type) - .part( - "file", - reqwest::multipart::Part::bytes(expected_body.clone()) - .file_name("upload.txt") - .mime_str(content_type)?, - ); - - let post_resp = local_http_client() - .post(format!("{}/{}", env.url, bucket)) - .multipart(post_form) - .send() - .await?; - - 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 head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; - assert_eq!(head.content_type(), Some(content_type)); - - 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()); - - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_anonymous_post_object_accepts_content_type_field_covered_by_starts_with() --> Result<(), Box> { - init_logging(); - - let mut env = RustFSTestEnvironment::new().await?; - env.start_rustfs_server(vec![]).await?; - - let bucket = "anon-post-policy-content-type-accept"; - let object_key = "uploads/content-type-object.txt"; - let content_type = "image/png"; - let expected_body = b"post-policy-content-type-body".to_vec(); - - let admin_client = env.create_s3_client(); - admin_client.create_bucket().bucket(bucket).send().await?; - allow_anonymous_put_object(&admin_client, bucket).await?; - - let policy = encode_post_policy(vec![ - serde_json::json!({ "bucket": bucket }), - serde_json::json!({ "key": object_key }), - serde_json::json!(["starts-with", "$Content-Type", "image/"]), - serde_json::json!(["content-length-range", 0, 1024]), - ]); - - let post_form = reqwest::multipart::Form::new() - .text("key", object_key.to_string()) - .text("policy", policy) - .text("Content-Type", content_type) - .part( - "file", - reqwest::multipart::Part::bytes(expected_body.clone()) - .file_name("upload.txt") - .mime_str(content_type)?, - ); - - let post_resp = local_http_client() - .post(format!("{}/{}", env.url, bucket)) - .multipart(post_form) - .send() - .await?; - - 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 head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; - assert_eq!(head.content_type(), Some(content_type)); - - 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()); - - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_anonymous_post_object_accepts_content_disposition_field_exact_policy_match() --> Result<(), Box> { - init_logging(); - - let mut env = RustFSTestEnvironment::new().await?; - env.start_rustfs_server(vec![]).await?; - - let bucket = "anon-post-policy-disposition-accept"; - let object_key = "uploads/disposition-object.txt"; - let content_disposition = "attachment; filename=\"upload.txt\""; - let expected_body = b"post-policy-disposition-body".to_vec(); - - let admin_client = env.create_s3_client(); - admin_client.create_bucket().bucket(bucket).send().await?; - allow_anonymous_put_object(&admin_client, bucket).await?; - - let policy = encode_post_policy(vec![ - serde_json::json!({ "bucket": bucket }), - serde_json::json!({ "key": object_key }), - serde_json::json!({ "Content-Disposition": content_disposition }), - serde_json::json!(["content-length-range", 0, 1024]), - ]); - - let post_form = reqwest::multipart::Form::new() - .text("key", object_key.to_string()) - .text("policy", policy) - .text("Content-Disposition", content_disposition) - .part( - "file", - reqwest::multipart::Part::bytes(expected_body.clone()) - .file_name("upload.txt") - .mime_str("text/plain")?, - ); - - let post_resp = local_http_client() - .post(format!("{}/{}", env.url, bucket)) - .multipart(post_form) - .send() - .await?; - - 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 head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; - assert_eq!(head.content_disposition(), Some(content_disposition)); - - 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()); - - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_anonymous_post_object_accepts_cache_control_field_exact_policy_match() --> Result<(), Box> { - init_logging(); - - let mut env = RustFSTestEnvironment::new().await?; - env.start_rustfs_server(vec![]).await?; - - let bucket = "anon-post-policy-cache-control-accept"; - let object_key = "uploads/cache-control-object.txt"; - let cache_control = "max-age=60"; - let expected_body = b"post-policy-cache-control-body".to_vec(); - - let admin_client = env.create_s3_client(); - admin_client.create_bucket().bucket(bucket).send().await?; - allow_anonymous_put_object(&admin_client, bucket).await?; - - let policy = encode_post_policy(vec![ - serde_json::json!({ "bucket": bucket }), - serde_json::json!({ "key": object_key }), - serde_json::json!({ "Cache-Control": cache_control }), - serde_json::json!(["content-length-range", 0, 1024]), - ]); - - let post_form = reqwest::multipart::Form::new() - .text("key", object_key.to_string()) - .text("policy", policy) - .text("Cache-Control", cache_control) - .part( - "file", - reqwest::multipart::Part::bytes(expected_body.clone()) - .file_name("upload.txt") - .mime_str("text/plain")?, - ); - - let post_resp = local_http_client() - .post(format!("{}/{}", env.url, bucket)) - .multipart(post_form) - .send() - .await?; - - 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 head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; - assert_eq!(head.cache_control(), Some(cache_control)); - - 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()); - - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_anonymous_post_object_accepts_content_language_field_exact_policy_match() --> Result<(), Box> { - init_logging(); - - let mut env = RustFSTestEnvironment::new().await?; - env.start_rustfs_server(vec![]).await?; - - let bucket = "anon-post-policy-content-language-accept"; - let object_key = "uploads/content-language-object.txt"; - let content_language = "en-US"; - let expected_body = b"post-policy-content-language-body".to_vec(); - - let admin_client = env.create_s3_client(); - admin_client.create_bucket().bucket(bucket).send().await?; - allow_anonymous_put_object(&admin_client, bucket).await?; - - let policy = encode_post_policy(vec![ - serde_json::json!({ "bucket": bucket }), - serde_json::json!({ "key": object_key }), - serde_json::json!({ "Content-Language": content_language }), - serde_json::json!(["content-length-range", 0, 1024]), - ]); - - let post_form = reqwest::multipart::Form::new() - .text("key", object_key.to_string()) - .text("policy", policy) - .text("Content-Language", content_language) - .part( - "file", - reqwest::multipart::Part::bytes(expected_body.clone()) - .file_name("upload.txt") - .mime_str("text/plain")?, - ); - - let post_resp = local_http_client() - .post(format!("{}/{}", env.url, bucket)) - .multipart(post_form) - .send() - .await?; - - 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 head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; - assert_eq!(head.content_language(), Some(content_language)); - - 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()); - - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_anonymous_post_object_accepts_content_encoding_field_exact_policy_match() --> Result<(), Box> { - init_logging(); - - let mut env = RustFSTestEnvironment::new().await?; - env.start_rustfs_server(vec![]).await?; - - let bucket = "anon-post-policy-content-encoding-accept"; - let object_key = "uploads/content-encoding-object.txt"; - let content_encoding = "gzip"; - let expected_body = b"post-policy-content-encoding-body".to_vec(); - - let admin_client = env.create_s3_client(); - admin_client.create_bucket().bucket(bucket).send().await?; - allow_anonymous_put_object(&admin_client, bucket).await?; - - let policy = encode_post_policy(vec![ - serde_json::json!({ "bucket": bucket }), - serde_json::json!({ "key": object_key }), - serde_json::json!({ "Content-Encoding": content_encoding }), - serde_json::json!(["content-length-range", 0, 1024]), - ]); - - let post_form = reqwest::multipart::Form::new() - .text("key", object_key.to_string()) - .text("policy", policy) - .text("Content-Encoding", content_encoding) - .part( - "file", - reqwest::multipart::Part::bytes(expected_body.clone()) - .file_name("upload.txt") - .mime_str("text/plain")?, - ); - - let post_resp = local_http_client() - .post(format!("{}/{}", env.url, bucket)) - .multipart(post_form) - .send() - .await?; - - 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 head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; - assert_eq!(head.content_encoding(), Some(content_encoding)); - - 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()); - - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_anonymous_post_object_accepts_website_redirect_location_exact_policy_match() --> Result<(), Box> { - init_logging(); - - let mut env = RustFSTestEnvironment::new().await?; - env.start_rustfs_server(vec![]).await?; - - let bucket = "anon-post-policy-website-redirect-accept"; - let object_key = "uploads/website-redirect-object.txt"; - let website_redirect_location = "/docs/landing.html"; - let expected_body = b"post-policy-website-redirect-body".to_vec(); - - let admin_client = env.create_s3_client(); - admin_client.create_bucket().bucket(bucket).send().await?; - allow_anonymous_put_object(&admin_client, bucket).await?; - - let policy = encode_post_policy(vec![ - serde_json::json!({ "bucket": bucket }), - serde_json::json!({ "key": object_key }), - serde_json::json!({ "x-amz-website-redirect-location": website_redirect_location }), - serde_json::json!(["content-length-range", 0, 1024]), - ]); - - let post_form = reqwest::multipart::Form::new() - .text("key", object_key.to_string()) - .text("policy", policy) - .text("x-amz-website-redirect-location", website_redirect_location) - .part( - "file", - reqwest::multipart::Part::bytes(expected_body.clone()) - .file_name("upload.txt") - .mime_str("text/plain")?, - ); - - let post_resp = local_http_client() - .post(format!("{}/{}", env.url, bucket)) - .multipart(post_form) - .send() - .await?; - - 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 head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; - assert_eq!(head.website_redirect_location(), Some(website_redirect_location)); - - 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()); - - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_anonymous_post_object_accepts_expires_field_exact_policy_match() --> Result<(), Box> { - init_logging(); - - let mut env = RustFSTestEnvironment::new().await?; - env.start_rustfs_server(vec![]).await?; - - let bucket = "anon-post-policy-expires-accept"; - let object_key = "uploads/expires-object.txt"; - let expires = "Wed, 21 Oct 2037 07:28:00 GMT"; - let expected_body = b"post-policy-expires-body".to_vec(); - - let admin_client = env.create_s3_client(); - admin_client.create_bucket().bucket(bucket).send().await?; - allow_anonymous_put_object(&admin_client, bucket).await?; - - let policy = encode_post_policy(vec![ - serde_json::json!({ "bucket": bucket }), - serde_json::json!({ "key": object_key }), - serde_json::json!({ "Expires": expires }), - serde_json::json!(["content-length-range", 0, 1024]), - ]); - - let post_form = reqwest::multipart::Form::new() - .text("key", object_key.to_string()) - .text("policy", policy) - .text("Expires", expires) - .part( - "file", - reqwest::multipart::Part::bytes(expected_body.clone()) - .file_name("upload.txt") - .mime_str("text/plain")?, - ); - - let post_resp = local_http_client() - .post(format!("{}/{}", env.url, bucket)) - .multipart(post_form) - .send() - .await?; - - 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 head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; - assert_eq!(head.expires_string(), Some(expires)); - - 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()); + } Ok(()) } @@ -3491,65 +3165,6 @@ async fn test_anonymous_post_object_accepts_tagging_field_exact_policy_match() Ok(()) } -#[tokio::test] -#[serial] -async fn test_anonymous_post_object_accepts_metadata_field_exact_policy_match() --> Result<(), Box> { - init_logging(); - - let mut env = RustFSTestEnvironment::new().await?; - env.start_rustfs_server(vec![]).await?; - - let bucket = "anon-post-policy-meta-exact-accept"; - let object_key = "uploads/meta-exact-accept-object.txt"; - let metadata_value = "alpha-demo"; - let expected_body = b"post-policy-meta-exact-body".to_vec(); - - let admin_client = env.create_s3_client(); - admin_client.create_bucket().bucket(bucket).send().await?; - allow_anonymous_put_object(&admin_client, bucket).await?; - - let policy = encode_post_policy(vec![ - serde_json::json!({ "bucket": bucket }), - serde_json::json!({ "key": object_key }), - serde_json::json!({ "x-amz-meta-project": metadata_value }), - serde_json::json!(["content-length-range", 0, 1024]), - ]); - - let post_form = reqwest::multipart::Form::new() - .text("key", object_key.to_string()) - .text("policy", policy) - .text("x-amz-meta-project", metadata_value) - .part( - "file", - reqwest::multipart::Part::bytes(expected_body.clone()) - .file_name("upload.txt") - .mime_str("text/plain")?, - ); - - let post_resp = local_http_client() - .post(format!("{}/{}", env.url, bucket)) - .multipart(post_form) - .send() - .await?; - - 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 head = admin_client.head_object().bucket(bucket).key(object_key).send().await?; - let metadata = head.metadata().expect("head_object should expose uploaded metadata"); - assert_eq!(metadata.get("project").map(String::as_str), Some(metadata_value)); - - 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()); - - Ok(()) -} - #[tokio::test] #[serial] async fn test_anonymous_post_object_allows_x_ignore_fields_outside_policy_conditions() diff --git a/docs/testing/e2e-suite-inventory.md b/docs/testing/e2e-suite-inventory.md index 160941978..0885c32e9 100644 --- a/docs/testing/e2e-suite-inventory.md +++ b/docs/testing/e2e-suite-inventory.md @@ -63,7 +63,7 @@ | list_objects_v2_metadata_extension_test | 1 | | | list_objects_v2_pagination_test | 12 | ✅ | | mc_mirror_small_bucket_test | 1 | | -| multipart_auth_test | 85 | | +| multipart_auth_test | 75 | | | multipart_storage_class_test | 3 | ✅ | | namespace_lock_quorum_test | 2 | | | negative_sigv4_test | 6 | ✅ |