From 759ade47705748a02e72965a0f4eec62924528d7 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Wed, 5 Aug 2026 15:21:51 +0800 Subject: [PATCH] fix(auth): restore filtered ListBuckets fallback (#5726) --- .config/nextest.toml | 2 +- crates/e2e_test/src/common.rs | 7 +- crates/e2e_test/src/lib.rs | 4 + crates/e2e_test/src/list_buckets_auth_test.rs | 88 +++++++++++++++++++ rustfs/src/storage/access.rs | 4 + 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 crates/e2e_test/src/list_buckets_auth_test.rs diff --git a/.config/nextest.toml b/.config/nextest.toml index 0ef3e198c..584c1b216 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -222,7 +222,7 @@ test-group = 'ecstore-serial-flaky' [profile.e2e-smoke] default-filter = """ package(e2e_test) & ( - test(/^(delete_marker_migration_semantics|version_id_regression|list_objects_v2_pagination|list_object_versions_regression|list_objects_duplicates|list_buckets_double_slash|leading_slash_key|special_chars|create_bucket_region|delete_objects_versioning|head_object_consistency|head_object_range|copy_object_metadata|copy_object_tagging|copy_source_invalid_date|content_encoding|multipart_storage_class|storage_class_capability|ssec_copy|anonymous_access|bucket_policy_check|presigned_negative|negative_sigv4|admin_auth|notification_webhook|tls_hot_reload|console_smoke|admin_iam_crud|admin_pools|sts_query_compat)_test::|^fake_s3_target::/) + test(/^(delete_marker_migration_semantics|version_id_regression|list_objects_v2_pagination|list_object_versions_regression|list_objects_duplicates|list_buckets_double_slash|list_buckets_auth|leading_slash_key|special_chars|create_bucket_region|delete_objects_versioning|head_object_consistency|head_object_range|copy_object_metadata|copy_object_tagging|copy_source_invalid_date|content_encoding|multipart_storage_class|storage_class_capability|ssec_copy|anonymous_access|bucket_policy_check|presigned_negative|negative_sigv4|admin_auth|notification_webhook|tls_hot_reload|console_smoke|admin_iam_crud|admin_pools|sts_query_compat)_test::|^fake_s3_target::/) | test(/^replication_extension_test::(test_replication_check_succeeds_with_remote_target|test_replication_check_rejects_target_without_object_lock|test_set_remote_target_rejects_unversioned_source_bucket|test_replication_check_rejects_unversioned_source_bucket|test_replication_check_rejects_missing_replication_config|test_replication_check_rejects_invalid_bucket|test_set_remote_target_rejects_same_bucket_on_same_deployment|test_set_remote_target_rejects_unversioned_target_bucket|test_set_remote_target_update_requires_arn|test_set_remote_target_update_rejects_missing_target|test_set_remote_target_rejects_invalid_target_url|test_set_remote_target_rejects_self_signed_https_target_without_skip_tls_verify|test_set_remote_target_rejects_private_ca_https_target_without_ca_cert_pem|test_list_remote_targets_rejects_empty_bucket|test_list_remote_targets_rejects_invalid_bucket|test_remove_remote_target_rejects_missing_target|test_remove_remote_target_rejects_missing_arn|test_remove_remote_target_rejects_invalid_bucket|test_remove_remote_target_rejects_target_used_by_replication|test_delete_bucket_replication_removes_remote_target)$/) | test(/^reliant::lifecycle::/) | test(/^reliant::tiering::/) diff --git a/crates/e2e_test/src/common.rs b/crates/e2e_test/src/common.rs index 92b5510eb..7a6196bca 100644 --- a/crates/e2e_test/src/common.rs +++ b/crates/e2e_test/src/common.rs @@ -564,7 +564,12 @@ impl RustFSTestEnvironment { /// Create an AWS S3 client configured for this RustFS instance pub fn create_s3_client(&self) -> Client { - Client::from_conf(build_test_s3_config(&self.url, &self.access_key, &self.secret_key, "e2e-test")) + self.create_s3_client_with_credentials(&self.access_key, &self.secret_key) + } + + /// Create an AWS S3 client with explicit credentials for this RustFS instance. + pub fn create_s3_client_with_credentials(&self, access_key: &str, secret_key: &str) -> Client { + Client::from_conf(build_test_s3_config(&self.url, access_key, secret_key, "e2e-test")) } /// Create test bucket diff --git a/crates/e2e_test/src/lib.rs b/crates/e2e_test/src/lib.rs index 86c4ea4e6..a6580debd 100644 --- a/crates/e2e_test/src/lib.rs +++ b/crates/e2e_test/src/lib.rs @@ -290,6 +290,10 @@ mod overwrite_cleanup_regression_test; #[cfg(test)] mod list_buckets_double_slash_test; +// Regression coverage for bucket-scoped ListBuckets authorization fallback. +#[cfg(test)] +mod list_buckets_auth_test; + // Regression test for backlog#629(b): region-aware CreateBucket SigV4. #[cfg(test)] mod create_bucket_region_test; diff --git a/crates/e2e_test/src/list_buckets_auth_test.rs b/crates/e2e_test/src/list_buckets_auth_test.rs new file mode 100644 index 000000000..24085ccae --- /dev/null +++ b/crates/e2e_test/src/list_buckets_auth_test.rs @@ -0,0 +1,88 @@ +// Copyright 2026 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Regression coverage for the MinIO-compatible filtered ListBuckets fallback. + +use crate::common::{RustFSTestEnvironment, admin_ok, init_logging}; +use std::error::Error; + +type TestResult = Result<(), Box>; + +#[tokio::test] +async fn bucket_scoped_policy_returns_only_authorized_bucket() -> TestResult { + init_logging(); + + let mut env = RustFSTestEnvironment::new().await?; + env.start_rustfs_server(vec![]).await?; + + let root_client = env.create_s3_client(); + let allowed_bucket = "list-buckets-authorized"; + let hidden_bucket = "list-buckets-hidden"; + let user = "listbucketsuser"; + let secret = "listbucketssecret"; + let policy = "list-buckets-scoped"; + + root_client.create_bucket().bucket(allowed_bucket).send().await?; + root_client.create_bucket().bucket(hidden_bucket).send().await?; + + admin_ok( + &env, + http::Method::PUT, + &format!("/rustfs/admin/v3/add-canned-policy?name={policy}"), + Some( + serde_json::json!({ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": ["s3:*"], + "Resource": [ + format!("arn:aws:s3:::{allowed_bucket}"), + format!("arn:aws:s3:::{allowed_bucket}/*") + ] + }] + }) + .to_string(), + ), + ) + .await?; + admin_ok( + &env, + http::Method::PUT, + &format!("/rustfs/admin/v3/add-user?accessKey={user}"), + Some(serde_json::json!({ "secretKey": secret, "status": "enabled" }).to_string()), + ) + .await?; + admin_ok( + &env, + http::Method::POST, + "/rustfs/admin/v3/idp/builtin/policy/attach", + Some(serde_json::json!({ "policies": [policy], "user": user }).to_string()), + ) + .await?; + + let client = env.create_s3_client_with_credentials(user, secret); + // Capture ListBuckets first so the direct-access control cannot warm bucket metadata and mask the regression. + let listed = client.list_buckets().send().await; + client.list_objects_v2().bucket(allowed_bucket).send().await?; + + let listed = listed?; + let names = listed + .buckets() + .iter() + .filter_map(|bucket| bucket.name().map(ToOwned::to_owned)) + .collect::>(); + assert_eq!(names, vec![allowed_bucket]); + + Ok(()) +} diff --git a/rustfs/src/storage/access.rs b/rustfs/src/storage/access.rs index 06fe3bd69..4b4674869 100644 --- a/rustfs/src/storage/access.rs +++ b/rustfs/src/storage/access.rs @@ -858,6 +858,10 @@ pub async fn authorize_request(req: &mut S3Request, action: Action) -> S3R return Ok(()); } + if action == Action::S3Action(S3Action::ListAllMyBucketsAction) { + return Err(s3_error!(AccessDenied, "Access Denied")); + } + let policy_allowed_fallback = PolicySys::try_is_allowed(&BucketPolicyArgs { bucket: bucket.as_str(), action,