From 40c1797f8e55a2b4baf38e5774f9c418c348ef0b Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Tue, 14 Apr 2026 10:52:00 -0700 Subject: [PATCH] fix(s3): allow anonymous ListBuckets with prefix-scoped List action (#9073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(s3): allow anonymous ListBuckets with prefix-scoped List action An anonymous identity holding a prefix-scoped action such as "List:prefix-*" was denied at the auth middleware before ListBucketsHandler could apply the per-bucket visibility check. The middleware called CanDo with an empty bucket, which never matches a scoped action, so every anonymous ListBuckets request returned 403 even though matching buckets should have been visible. Defer ListBuckets authorization to the handler for the anonymous identity when it actually carries a List action, mirroring the existing behavior for authenticated users. Anonymous identities with no List action continue to be rejected at the global layer, preserving the secure-by-default posture. Fixes #9072 * refactor(s3): make hasListAction a method on Identity Addresses PR review — consistent with existing CanDo/isAdmin methods and also treats Admin identities as implicitly having List permission. --- weed/s3api/auth_credentials.go | 33 ++++++++++++++++++++++++++-- weed/s3api/auto_signature_v4_test.go | 27 +++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index 3a924c44d..aae2bfd4d 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -1413,8 +1413,14 @@ func (iam *IdentityAccessManagement) authRequestWithAuthType(r *http.Request, ac // through buckets and checking permissions for each. Skip the global check here. policyAllows := false - if action == s3_constants.ACTION_LIST && bucket == "" && identity.Name != s3_constants.AccountAnonymousId { - // ListBuckets operation for authenticated users - authorization handled per-bucket in the handler + if action == s3_constants.ACTION_LIST && bucket == "" && + (identity.Name != s3_constants.AccountAnonymousId || identity.hasListAction()) { + // ListBuckets operation - authorization handled per-bucket in the handler. + // For authenticated users this is always deferred to the handler. For the + // anonymous identity we only defer when it actually carries a List action + // (e.g. "List:prefix-*"); otherwise fall through to the global check so + // an anonymous caller with no permissions is rejected outright rather + // than receiving an empty ListAllMyBuckets result. } else { // First check bucket policy if one exists // Bucket policies can grant or deny access to specific users/principals @@ -1590,6 +1596,29 @@ func (identity *Identity) CanDo(action Action, bucket string, objectKey string) return false } +// hasListAction reports whether the identity carries any List-scoped legacy +// action (e.g. "List", "List:*", "List:prefix-*") or has administrative +// privileges. Used to decide whether a ListBuckets request from an anonymous +// identity should be deferred to the per-bucket check in the handler or +// denied at the global auth layer. +func (identity *Identity) hasListAction() bool { + if identity == nil { + return false + } + if identity.isAdmin() { + return true + } + listPrefix := string(s3_constants.ACTION_LIST) + listPrefixWithColon := listPrefix + ":" + for _, a := range identity.Actions { + act := string(a) + if act == listPrefix || strings.HasPrefix(act, listPrefixWithColon) { + return true + } + } + return false +} + func (identity *Identity) isAdmin() bool { if identity == nil { return false diff --git a/weed/s3api/auto_signature_v4_test.go b/weed/s3api/auto_signature_v4_test.go index 228de4d6f..8d8ba104f 100644 --- a/weed/s3api/auto_signature_v4_test.go +++ b/weed/s3api/auto_signature_v4_test.go @@ -132,6 +132,33 @@ func TestCheckaAnonymousRequestAuthType(t *testing.T) { } +// TestAnonymousListBucketsWithPrefixAction verifies that an anonymous identity +// holding a prefix-scoped List action (e.g. "List:prefix-*") is not denied at +// the global auth layer when issuing ListBuckets. The per-bucket permission +// check happens inside ListBucketsHandler, so the global auth step must let +// the request through. +// +// Regression test for https://github.com/seaweedfs/seaweedfs/issues/9072 +func TestAnonymousListBucketsWithPrefixAction(t *testing.T) { + iam := &IdentityAccessManagement{ + hashes: make(map[string]*sync.Pool), + hashCounters: make(map[string]*int32), + } + _ = iam.loadS3ApiConfiguration(&iam_pb.S3ApiConfiguration{ + Identities: []*iam_pb.Identity{ + { + Name: s3_constants.AccountAnonymousId, + Actions: []string{"Read:prefix-*", "List:prefix-*"}, + }, + }, + }) + + req := mustNewRequest(http.MethodGet, "http://127.0.0.1:9000/", 0, nil, t) + if _, s3Error := iam.authRequest(req, s3_constants.ACTION_LIST); s3Error != s3err.ErrNone { + t.Errorf("anonymous ListBuckets with prefix-scoped List action: want ErrNone, got %d", s3Error) + } +} + func TestCheckAdminRequestAuthType(t *testing.T) { iam := &IdentityAccessManagement{ hashes: make(map[string]*sync.Pool),