mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-05 02:23:56 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b821684caa | |||
| 7dde05d945 |
@@ -1,2 +1,2 @@
|
||||
sha256-darwin=7c705942d3a9e583ebaa85845daa21c4f81fdbf36def24db931aaef38636c53e
|
||||
sha256-linux=01e1f5569be60674e30d9e30a9961c5c53a6cea6ca1f7913218f2adf7291d574
|
||||
sha256-darwin=a3e166a140f3150218b4a857fedb8749940fad0c8b053d0e3c1b957321bf5beb
|
||||
sha256-linux=dce4a8eb634a27b4589bcf6d2ebf248ede97aa59701acc8696391d2812a3177d
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256=f794c004e5952147c79dadbe6b7c84804bc986f34a8142731fac8864004e5130
|
||||
sha256=5bdd4c2eadbc41625ae3184fff34062e000d328108436259b2b7c4fe2531ecaf
|
||||
|
||||
@@ -502,10 +502,163 @@ mod tests {
|
||||
assert!(response.contains(if expected == reqwest::StatusCode::FORBIDDEN {
|
||||
"<Code>AccessDenied</Code>"
|
||||
} else {
|
||||
"<Code>BucketAlreadyExists</Code>"
|
||||
"<Code>BucketAlreadyOwnedByYou</Code>"
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A deployment owns a single bucket namespace, so an IAM user that recreates
|
||||
/// an existing bucket is the owner, not another account. In the default region
|
||||
/// every such request returns the legacy idempotent `200 OK` (AWS CreateBucket
|
||||
/// semantics), including buckets created by root and by another IAM user.
|
||||
#[tokio::test]
|
||||
async fn test_create_bucket_duplicate_in_default_region_is_idempotent_for_iam_user()
|
||||
-> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
init_logging();
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
env.start_rustfs_server(vec!["--region", "us-east-1"]).await?;
|
||||
let root = env.create_s3_client();
|
||||
|
||||
admin_create_user(&env, "duplicate-iam", "duplicate-iam-secret").await?;
|
||||
admin_attach_user_policy_via(
|
||||
AdminTransport::Signed,
|
||||
&env.url,
|
||||
&env.access_key,
|
||||
&env.secret_key,
|
||||
"readwrite",
|
||||
"duplicate-iam",
|
||||
)
|
||||
.await?;
|
||||
let iam = Client::from_conf(build_test_s3_config(
|
||||
&env.url,
|
||||
"duplicate-iam",
|
||||
"duplicate-iam-secret",
|
||||
None,
|
||||
"duplicate-create",
|
||||
));
|
||||
|
||||
root.create_bucket()
|
||||
.bucket("duplicate-root-owned")
|
||||
.send()
|
||||
.await
|
||||
.expect("root creates the first bucket");
|
||||
iam.create_bucket()
|
||||
.bucket("duplicate-root-owned")
|
||||
.send()
|
||||
.await
|
||||
.expect("an IAM user recreating a root-owned bucket in us-east-1 gets 200 OK");
|
||||
|
||||
iam.create_bucket()
|
||||
.bucket("duplicate-iam-owned")
|
||||
.send()
|
||||
.await
|
||||
.expect("the IAM user creates its own bucket");
|
||||
iam.create_bucket()
|
||||
.bucket("duplicate-iam-owned")
|
||||
.send()
|
||||
.await
|
||||
.expect("the same IAM user recreating its bucket gets 200 OK");
|
||||
root.create_bucket()
|
||||
.bucket("duplicate-iam-owned")
|
||||
.send()
|
||||
.await
|
||||
.expect("root recreating an IAM-created bucket gets 200 OK");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Outside the default region the same ownership answer becomes
|
||||
/// `409 BucketAlreadyOwnedByYou` for every authorized requester. Requests that
|
||||
/// lack `s3:CreateBucket` still fail authorization before the duplicate check.
|
||||
#[tokio::test]
|
||||
async fn test_create_bucket_duplicate_outside_default_region_reports_owned_by_you() -> Result<(), Box<dyn Error + Send + Sync>>
|
||||
{
|
||||
init_logging();
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
env.start_rustfs_server(vec!["--region", "eu-west-1"]).await?;
|
||||
let region = "eu-west-1";
|
||||
let root = Client::from_conf(
|
||||
env.create_s3_client()
|
||||
.config()
|
||||
.to_builder()
|
||||
.region(Region::new(region))
|
||||
.build(),
|
||||
);
|
||||
|
||||
admin_create_user(&env, "duplicate-eu-iam", "duplicate-eu-iam-secret").await?;
|
||||
admin_attach_user_policy_via(
|
||||
AdminTransport::Signed,
|
||||
&env.url,
|
||||
&env.access_key,
|
||||
&env.secret_key,
|
||||
"readwrite",
|
||||
"duplicate-eu-iam",
|
||||
)
|
||||
.await?;
|
||||
let iam = Client::from_conf(
|
||||
build_test_s3_config(&env.url, "duplicate-eu-iam", "duplicate-eu-iam-secret", None, "duplicate-create")
|
||||
.to_builder()
|
||||
.region(Region::new(region))
|
||||
.build(),
|
||||
);
|
||||
|
||||
admin_create_user(&env, "duplicate-eu-readonly", "duplicate-eu-readonly-secret").await?;
|
||||
admin_attach_user_policy_via(
|
||||
AdminTransport::Signed,
|
||||
&env.url,
|
||||
&env.access_key,
|
||||
&env.secret_key,
|
||||
"readonly",
|
||||
"duplicate-eu-readonly",
|
||||
)
|
||||
.await?;
|
||||
let readonly = Client::from_conf(
|
||||
build_test_s3_config(
|
||||
&env.url,
|
||||
"duplicate-eu-readonly",
|
||||
"duplicate-eu-readonly-secret",
|
||||
None,
|
||||
"duplicate-create",
|
||||
)
|
||||
.to_builder()
|
||||
.region(Region::new(region))
|
||||
.build(),
|
||||
);
|
||||
|
||||
for (client, bucket) in [(&root, "duplicate-eu-root"), (&iam, "duplicate-eu-iam")] {
|
||||
client
|
||||
.create_bucket()
|
||||
.bucket(bucket)
|
||||
.send()
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("{bucket} must be created: {error:?}"));
|
||||
// Both the creator and the other authenticated identity own the namespace.
|
||||
for creator in [&root, &iam] {
|
||||
let error = creator
|
||||
.create_bucket()
|
||||
.bucket(bucket)
|
||||
.send()
|
||||
.await
|
||||
.expect_err("a duplicate create outside us-east-1 must report ownership");
|
||||
assert_eq!(
|
||||
error.as_service_error().and_then(ProvideErrorMetadata::code),
|
||||
Some("BucketAlreadyOwnedByYou"),
|
||||
"{bucket}"
|
||||
);
|
||||
assert_eq!(error.raw_response().map(|response| response.status().as_u16()), Some(409), "{bucket}");
|
||||
}
|
||||
}
|
||||
|
||||
let denied = readonly
|
||||
.create_bucket()
|
||||
.bucket("duplicate-eu-root")
|
||||
.send()
|
||||
.await
|
||||
.expect_err("a requester without s3:CreateBucket must be denied before the duplicate check");
|
||||
assert_eq!(denied.as_service_error().and_then(ProvideErrorMetadata::code), Some("AccessDenied"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1267,19 +1267,22 @@ fn build_list_objects_v2_metadata_output(
|
||||
}
|
||||
}
|
||||
|
||||
fn create_bucket_exists_response(is_owner: bool, region: &str) -> S3Result<S3Response<CreateBucketOutput>> {
|
||||
if is_owner && region == RUSTFS_REGION {
|
||||
// AWS preserves the legacy idempotent response for buckets recreated in
|
||||
// us-east-1 (the default RustFS region).
|
||||
/// Answer a `CreateBucket` for a bucket that already exists.
|
||||
///
|
||||
/// A deployment owns one bucket namespace, so every requester that passed
|
||||
/// authorization belongs to the same owner as the bucket's creator; there is no
|
||||
/// second account that could trigger `BucketAlreadyExists`. That leaves the AWS
|
||||
/// account-owner semantics: the default region returns the legacy idempotent
|
||||
/// `200 OK`, any other region returns `BucketAlreadyOwnedByYou`.
|
||||
fn create_bucket_exists_response(region: &str) -> S3Result<S3Response<CreateBucketOutput>> {
|
||||
if region == RUSTFS_REGION {
|
||||
return Ok(S3Response::new(CreateBucketOutput::default()));
|
||||
}
|
||||
|
||||
let error_code = if is_owner {
|
||||
S3ErrorCode::BucketAlreadyOwnedByYou
|
||||
} else {
|
||||
S3ErrorCode::BucketAlreadyExists
|
||||
};
|
||||
Err(S3Error::with_message(error_code, "The requested bucket already exists."))
|
||||
Err(S3Error::with_message(
|
||||
S3ErrorCode::BucketAlreadyOwnedByYou,
|
||||
"The requested bucket already exists.",
|
||||
))
|
||||
}
|
||||
|
||||
fn resolve_notification_region(global_region: Option<Region>, request_region: Option<Region>) -> String {
|
||||
@@ -1460,14 +1463,12 @@ impl DefaultBucketUsecase {
|
||||
current_object_store_handle_for_context(self.context.as_deref())
|
||||
}
|
||||
|
||||
fn create_bucket_exists_result(&self, requester_is_owner: bool) -> S3Result<S3Response<CreateBucketOutput>> {
|
||||
// Match AWS CreateBucket semantics: an owned bucket is idempotent in
|
||||
// us-east-1 and returns BucketAlreadyOwnedByYou elsewhere.
|
||||
fn create_bucket_exists_result(&self) -> S3Result<S3Response<CreateBucketOutput>> {
|
||||
let region = self
|
||||
.global_region()
|
||||
.map(|region| region.to_string())
|
||||
.unwrap_or_else(|| RUSTFS_REGION.to_string());
|
||||
create_bucket_exists_response(requester_is_owner, ®ion)
|
||||
create_bucket_exists_response(®ion)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
@@ -1497,7 +1498,7 @@ impl DefaultBucketUsecase {
|
||||
|
||||
// An existing bucket needs no mutation. Answer before the mutation queue and the
|
||||
// bucket's exclusive locks, which wait for every in-flight object write.
|
||||
if let Ok(req_info) = req_info_ref(&req)
|
||||
if req_info_ref(&req).is_ok()
|
||||
&& let Some(store) = self.object_store()
|
||||
&& store
|
||||
.get_bucket_info(&req.input.bucket, &BucketOptions::default())
|
||||
@@ -1505,7 +1506,7 @@ impl DefaultBucketUsecase {
|
||||
.is_ok()
|
||||
{
|
||||
let helper = OperationHelper::new(&req, EventName::BucketCreated, S3Operation::CreateBucket);
|
||||
let result = self.create_bucket_exists_result(req_info.is_owner);
|
||||
let result = self.create_bucket_exists_result();
|
||||
let _ = helper.complete(&result);
|
||||
return result;
|
||||
}
|
||||
@@ -1523,12 +1524,9 @@ impl DefaultBucketUsecase {
|
||||
) -> S3Result<S3Response<CreateBucketOutput>> {
|
||||
let helper = OperationHelper::new(&req, EventName::BucketCreated, S3Operation::CreateBucket);
|
||||
let request_context = req.extensions.get::<request_context::RequestContext>().cloned();
|
||||
let requester_is_owner = match req_info_ref(&req) {
|
||||
Ok(r) => r.is_owner,
|
||||
Err(_) => {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Missing request info".to_string()));
|
||||
}
|
||||
};
|
||||
if req_info_ref(&req).is_err() {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Missing request info".to_string()));
|
||||
}
|
||||
let CreateBucketInput {
|
||||
bucket,
|
||||
object_lock_enabled_for_bucket,
|
||||
@@ -1588,7 +1586,7 @@ impl DefaultBucketUsecase {
|
||||
.await;
|
||||
}
|
||||
Err(StorageError::BucketExists(_)) => {
|
||||
let result = self.create_bucket_exists_result(requester_is_owner);
|
||||
let result = self.create_bucket_exists_result();
|
||||
let _ = helper.complete(&result);
|
||||
return result;
|
||||
}
|
||||
@@ -4477,20 +4475,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_bucket_exists_response_returns_ok_for_owner() {
|
||||
let response = create_bucket_exists_response(true, RUSTFS_REGION).expect("owner recreate in us-east-1 should succeed");
|
||||
fn create_bucket_exists_response_returns_ok_in_default_region() {
|
||||
let response = create_bucket_exists_response(RUSTFS_REGION).expect("recreate in us-east-1 should succeed");
|
||||
assert_eq!(response.output.location, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_bucket_exists_response_returns_bucket_already_exists_for_non_owner() {
|
||||
let err = create_bucket_exists_response(false, RUSTFS_REGION).expect_err("non-owner recreate should fail");
|
||||
assert_eq!(err.code(), &S3ErrorCode::BucketAlreadyExists);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_bucket_exists_response_returns_owned_by_you_outside_us_east_1() {
|
||||
let err = create_bucket_exists_response(true, "eu-west-1").expect_err("owner recreate outside us-east-1 should fail");
|
||||
fn create_bucket_exists_response_returns_owned_by_you_outside_default_region() {
|
||||
let err = create_bucket_exists_response("eu-west-1").expect_err("recreate outside us-east-1 should report ownership");
|
||||
assert_eq!(err.code(), &S3ErrorCode::BucketAlreadyOwnedByYou);
|
||||
}
|
||||
|
||||
@@ -4553,9 +4545,11 @@ mod tests {
|
||||
.expect("hold the bucket lifecycle lock");
|
||||
let usecase = DefaultBucketUsecase::with_context(Some(context));
|
||||
|
||||
for (is_owner, location, expected_error) in [
|
||||
// The requester's owner flag must not change the duplicate response:
|
||||
// one deployment has a single bucket owner.
|
||||
for (requester_is_owner, location, expected_error) in [
|
||||
(true, None, None),
|
||||
(false, None, Some(S3ErrorCode::BucketAlreadyExists)),
|
||||
(false, None, None),
|
||||
(true, Some("ap-south-1"), Some(S3ErrorCode::InvalidLocationConstraint)),
|
||||
] {
|
||||
let input = CreateBucketInput {
|
||||
@@ -4570,7 +4564,7 @@ mod tests {
|
||||
input,
|
||||
Method::PUT,
|
||||
ReqInfo {
|
||||
is_owner,
|
||||
is_owner: requester_is_owner,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -4578,9 +4572,9 @@ mod tests {
|
||||
.await
|
||||
.expect("CreateBucket on an existing bucket must not wait for the bucket lifecycle lock");
|
||||
match expected_error {
|
||||
Some(code) => assert_eq!(result.expect_err("duplicate or invalid request must fail").code(), &code),
|
||||
Some(code) => assert_eq!(result.expect_err("invalid request must fail").code(), &code),
|
||||
None => {
|
||||
result.expect("the owner recreating a bucket in us-east-1 gets the idempotent success");
|
||||
result.expect("recreating a bucket in us-east-1 returns the idempotent success");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,3 +315,11 @@ test_lifecycle_cloud_transition_target_by_bucket
|
||||
test_lifecycle_cloud_transition_target_by_bucket_multiple_buckets
|
||||
test_list_object_versions_restore_status
|
||||
test_list_objects_restore_status
|
||||
|
||||
# Require per-account bucket ownership across distinct S3 accounts
|
||||
# A RustFS deployment owns one bucket namespace and records no bucket owner, so every
|
||||
# authorized requester is the bucket's owner: a duplicate CreateBucket returns the
|
||||
# legacy 200 in the default region and BucketAlreadyOwnedByYou elsewhere, never
|
||||
# BucketAlreadyExists for a second account. The deployment-level behavior is covered by
|
||||
# crates/e2e_test/src/create_bucket_region_test.rs.
|
||||
test_bucket_create_exists_nonowner
|
||||
|
||||
@@ -205,7 +205,6 @@ test_put_public_block
|
||||
test_block_public_policy
|
||||
test_block_public_policy_with_principal
|
||||
test_block_public_restrict_public_buckets
|
||||
test_bucket_create_exists_nonowner
|
||||
test_get_public_block_deny_bucket_policy
|
||||
test_get_undefined_public_block
|
||||
|
||||
|
||||
Reference in New Issue
Block a user