mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-18 18:46:17 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e2f98037e |
@@ -17,6 +17,7 @@
|
|||||||
use crate::common::{RustFSTestEnvironment, init_logging, local_http_client};
|
use crate::common::{RustFSTestEnvironment, init_logging, local_http_client};
|
||||||
use async_compression::tokio::write::{BzEncoder, XzEncoder};
|
use async_compression::tokio::write::{BzEncoder, XzEncoder};
|
||||||
use aws_sdk_s3::error::{ProvideErrorMetadata, SdkError};
|
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::primitives::ByteStream;
|
||||||
use aws_sdk_s3::types::{
|
use aws_sdk_s3::types::{
|
||||||
ServerSideEncryption, ServerSideEncryptionByDefault, ServerSideEncryptionConfiguration, ServerSideEncryptionRule,
|
ServerSideEncryption, ServerSideEncryptionByDefault, ServerSideEncryptionConfiguration, ServerSideEncryptionRule,
|
||||||
@@ -349,6 +350,71 @@ async fn run_post_object_policy_case(
|
|||||||
Ok(())
|
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<serde_json::Value>,
|
||||||
|
form_field: (&str, &str),
|
||||||
|
file_mime: &str,
|
||||||
|
file_body: &[u8],
|
||||||
|
read_stored: fn(&HeadObjectOutput) -> Option<&str>,
|
||||||
|
case: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
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
|
/// Table-driven fold of the nine `*_missing_from_policy_conditions` POST
|
||||||
/// Object tests (backlog#1838 PR1). Every row keeps its original test's exact
|
/// 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
|
/// 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_anonymous_post_object_accepts_storage_class_exact_policy_match()
|
|
||||||
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
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]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_anonymous_post_object_rejects_storage_class_missing_from_policy_conditions()
|
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(())
|
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]
|
#[tokio::test]
|
||||||
#[serial]
|
#[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<dyn std::error::Error + Send + Sync>> {
|
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
init_logging();
|
init_logging();
|
||||||
|
|
||||||
let mut env = RustFSTestEnvironment::new().await?;
|
// (case, bucket, object_key, field, submitted value, `starts-with` prefix
|
||||||
env.start_rustfs_server(vec![]).await?;
|
// (`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";
|
for (case, bucket, object_key, field, value, starts_with_prefix, file_mime, file_body, read_stored) in cases {
|
||||||
let object_key = "uploads/meta-object.txt";
|
let condition = match starts_with_prefix {
|
||||||
let metadata_value = "alpha-demo";
|
Some(prefix) => serde_json::json!(["starts-with", format!("${field}"), prefix]),
|
||||||
let expected_body = b"post-policy-meta-body".to_vec();
|
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();
|
run_post_object_accept_case(
|
||||||
admin_client.create_bucket().bucket(bucket).send().await?;
|
bucket,
|
||||||
allow_anonymous_put_object(&admin_client, bucket).await?;
|
object_key,
|
||||||
|
vec![
|
||||||
let policy = encode_post_policy(vec![
|
serde_json::json!({ "bucket": bucket }),
|
||||||
serde_json::json!({ "bucket": bucket }),
|
serde_json::json!({ "key": object_key }),
|
||||||
serde_json::json!({ "key": object_key }),
|
condition,
|
||||||
serde_json::json!(["starts-with", "$x-amz-meta-project", "alpha-"]),
|
serde_json::json!(["content-length-range", 0, 1024]),
|
||||||
serde_json::json!(["content-length-range", 0, 1024]),
|
],
|
||||||
]);
|
(field, value),
|
||||||
|
file_mime,
|
||||||
let post_form = reqwest::multipart::Form::new()
|
file_body,
|
||||||
.text("key", object_key.to_string())
|
*read_stored,
|
||||||
.text("policy", policy)
|
case,
|
||||||
.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?;
|
.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<dyn std::error::Error + Send + Sync>> {
|
|
||||||
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<dyn std::error::Error + Send + Sync>> {
|
|
||||||
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<dyn std::error::Error + Send + Sync>> {
|
|
||||||
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<dyn std::error::Error + Send + Sync>> {
|
|
||||||
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<dyn std::error::Error + Send + Sync>> {
|
|
||||||
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<dyn std::error::Error + Send + Sync>> {
|
|
||||||
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<dyn std::error::Error + Send + Sync>> {
|
|
||||||
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<dyn std::error::Error + Send + Sync>> {
|
|
||||||
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -3491,65 +3165,6 @@ async fn test_anonymous_post_object_accepts_tagging_field_exact_policy_match()
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_anonymous_post_object_accepts_metadata_field_exact_policy_match()
|
|
||||||
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
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]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_anonymous_post_object_allows_x_ignore_fields_outside_policy_conditions()
|
async fn test_anonymous_post_object_allows_x_ignore_fields_outside_policy_conditions()
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
| list_objects_v2_metadata_extension_test | 1 | |
|
| list_objects_v2_metadata_extension_test | 1 | |
|
||||||
| list_objects_v2_pagination_test | 12 | ✅ |
|
| list_objects_v2_pagination_test | 12 | ✅ |
|
||||||
| mc_mirror_small_bucket_test | 1 | |
|
| mc_mirror_small_bucket_test | 1 | |
|
||||||
| multipart_auth_test | 85 | |
|
| multipart_auth_test | 75 | |
|
||||||
| multipart_storage_class_test | 3 | ✅ |
|
| multipart_storage_class_test | 3 | ✅ |
|
||||||
| namespace_lock_quorum_test | 2 | |
|
| namespace_lock_quorum_test | 2 | |
|
||||||
| negative_sigv4_test | 6 | ✅ |
|
| negative_sigv4_test | 6 | ✅ |
|
||||||
|
|||||||
Reference in New Issue
Block a user