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),