diff --git a/weed/s3api/s3api_bucket_handlers.go b/weed/s3api/s3api_bucket_handlers.go index 0f74211e65..cd15cdd9ec 100644 --- a/weed/s3api/s3api_bucket_handlers.go +++ b/weed/s3api/s3api_bucket_handlers.go @@ -78,19 +78,22 @@ func (s3a *S3ApiServer) ListBucketsHandler(w http.ResponseWriter, r *http.Reques var buckets []ListAllMyBucketsEntry var nextToken string - // Unauthenticated users should not see any buckets + var err error + // Unauthenticated users should not see any buckets, unless auth is off: + // then every request may use every bucket, so all of them are listed. if identity != nil { - var err error if fromIndex, granted := s3a.iam.canListBucketsFromOwnerIndex(r, identity); fromIndex && s3a.bucketOwnerIndexReady() { buckets, nextToken, err = s3a.listBucketsFromOwnerIndex(r, identity, granted, prefix, startAfter, maxBuckets) } else { buckets, nextToken, err = s3a.scanVisibleBuckets(r, identity, prefix, startAfter, maxBuckets) } - if err != nil { - glog.Errorf("ListBucketsHandler: %v", err) - s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) - return - } + } else if !s3a.iam.isEnabled() { + buckets, nextToken, err = s3a.scanVisibleBuckets(r, nil, prefix, startAfter, maxBuckets) + } + if err != nil { + glog.Errorf("ListBucketsHandler: %v", err) + s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) + return } response := ListAllMyBucketsResult{ @@ -151,6 +154,9 @@ func decodeContinuationToken(token string) (string, error) { // bucketVisibleToIdentity reports whether ListBuckets should include the bucket: // the identity owns it or has explicit permission to list it. func (s3a *S3ApiServer) bucketVisibleToIdentity(r *http.Request, bucket, owner string, identity *Identity) bool { + if !s3a.iam.isEnabled() { + return true + } if isBucketOwnedByIdentity(owner, identity) { return true } diff --git a/weed/s3api/s3api_bucket_handlers_list_test.go b/weed/s3api/s3api_bucket_handlers_list_test.go index e037fe425b..695404ffd2 100644 --- a/weed/s3api/s3api_bucket_handlers_list_test.go +++ b/weed/s3api/s3api_bucket_handlers_list_test.go @@ -1,14 +1,19 @@ package s3api import ( + "encoding/xml" "net/http" + "net/http/httptest" "net/url" "strings" "testing" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetListBucketsArgs(t *testing.T) { @@ -220,3 +225,72 @@ func TestContinuationTokenRoundTrip(t *testing.T) { t.Error("non-base64 token should not decode") } } + +// With auth off every request may use every bucket, so ListBuckets lists them +// all instead of treating the caller as an unauthenticated user. +func TestListBucketsWithoutAuthListsEveryBucket(t *testing.T) { + f := &fakeBucketDeleteFiler{entriesByDir: map[string][]*filer_pb.Entry{ + "/buckets": { + {Name: ".system", IsDirectory: true, Attributes: &filer_pb.FuseAttributes{}}, + {Name: "alpha", IsDirectory: true, Attributes: &filer_pb.FuseAttributes{}}, + {Name: "beta", IsDirectory: true, Attributes: &filer_pb.FuseAttributes{}}, + }, + }} + s3a := newBucketDeleteTestServer(t, f, false) + + for _, tt := range []struct { + query string + want []string + }{ + {query: "", want: []string{"alpha", "beta"}}, + {query: "?prefix=b", want: []string{"beta"}}, + } { + rec := httptest.NewRecorder() + s3a.ListBucketsHandler(rec, httptest.NewRequest(http.MethodGet, "/"+tt.query, nil)) + require.Equal(t, http.StatusOK, rec.Code, tt.query) + var result ListAllMyBucketsResult + require.NoError(t, xml.Unmarshal(rec.Body.Bytes(), &result), tt.query) + var names []string + for _, b := range result.Buckets.Bucket { + names = append(names, b.Name) + } + assert.Equal(t, tt.want, names, tt.query) + } + + // A truncated page must hand back a token that reaches the next page. + rec := httptest.NewRecorder() + s3a.ListBucketsHandler(rec, httptest.NewRequest(http.MethodGet, "/?max-buckets=1", nil)) + require.Equal(t, http.StatusOK, rec.Code) + var first ListAllMyBucketsResult + require.NoError(t, xml.Unmarshal(rec.Body.Bytes(), &first)) + require.Len(t, first.Buckets.Bucket, 1) + assert.Equal(t, "alpha", first.Buckets.Bucket[0].Name) + require.NotEmpty(t, first.ContinuationToken) + + rec = httptest.NewRecorder() + s3a.ListBucketsHandler(rec, httptest.NewRequest(http.MethodGet, "/?max-buckets=1&continuation-token="+url.QueryEscape(first.ContinuationToken), nil)) + require.Equal(t, http.StatusOK, rec.Code) + var second ListAllMyBucketsResult + require.NoError(t, xml.Unmarshal(rec.Body.Bytes(), &second)) + require.Len(t, second.Buckets.Bucket, 1) + assert.Equal(t, "beta", second.Buckets.Bucket[0].Name) + assert.Empty(t, second.ContinuationToken) +} + +// With auth on, a request carrying no identity still lists no buckets. +func TestListBucketsWithAuthListsNothingForAnonymous(t *testing.T) { + f := &fakeBucketDeleteFiler{entriesByDir: map[string][]*filer_pb.Entry{ + "/buckets": { + {Name: "alpha", IsDirectory: true, Attributes: &filer_pb.FuseAttributes{}}, + }, + }} + s3a := newBucketDeleteTestServer(t, f, false) + s3a.iam.isAuthEnabled = true + + rec := httptest.NewRecorder() + s3a.ListBucketsHandler(rec, httptest.NewRequest(http.MethodGet, "/", nil)) + require.Equal(t, http.StatusOK, rec.Code) + var result ListAllMyBucketsResult + require.NoError(t, xml.Unmarshal(rec.Body.Bytes(), &result)) + assert.Empty(t, result.Buckets.Bucket) +}