package s3api import ( "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gorilla/mux" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" "github.com/stretchr/testify/require" ) const testPublicAccessBlockXML = `` + `truetrue` + `falsetrue` + `` func newPublicAccessBlockTestServer(t *testing.T, entry *filer_pb.Entry) (*S3ApiServer, *fakeLookupFiler) { t.Helper() t.Setenv("AWS_ACCESS_KEY_ID", "") t.Setenv("AWS_SECRET_ACCESS_KEY", "") filer := &fakeLookupFiler{entry: entry} s3a := newHeadBucketTestServer(t, filer) s3a.bucketConfigCache = NewBucketConfigCache(time.Minute) s3a.iam = NewIdentityAccessManagementWithStore(s3a.option, nil, "memory") t.Cleanup(s3a.iam.Shutdown) return s3a, filer } func publicAccessBlockRequest(method, bucket, body string) *http.Request { req := httptest.NewRequest(method, "http://s3/"+bucket+"?publicAccessBlock", strings.NewReader(body)) return mux.SetURLVars(req, map[string]string{"bucket": bucket}) } func TestPutPublicAccessBlockStoresConfiguration(t *testing.T) { const bucket = "pab-put" s3a, filer := newPublicAccessBlockTestServer(t, &filer_pb.Entry{Name: bucket, IsDirectory: true}) rr := httptest.NewRecorder() s3a.PutPublicAccessBlockHandler(rr, publicAccessBlockRequest(http.MethodPut, bucket, testPublicAccessBlockXML)) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Len(t, filer.txnSeen, 1) stored := filer.txnSeen[0].Mutations[0].SetExtended[s3_constants.ExtPublicAccessBlockKey] config, err := parsePublicAccessBlock(stored) require.NoError(t, err) require.Equal(t, PublicAccessBlockConfiguration{ BlockPublicAcls: true, IgnorePublicAcls: true, RestrictPublicBuckets: true, }, *config) // The PUT loaded the bucket into the cache; a GET must not serve that copy. filer.entry.Extended = filer.txnSeen[0].Mutations[0].SetExtended rr = httptest.NewRecorder() s3a.GetPublicAccessBlockHandler(rr, publicAccessBlockRequest(http.MethodGet, bucket, "")) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Contains(t, rr.Body.String(), "true") } func TestUnreadablePublicAccessBlockBlocksPublicAccess(t *testing.T) { s3a, _ := newPublicAccessBlockTestServer(t, nil) config := s3a.newBucketConfigFromEntry("b", &filer_pb.Entry{Name: "b", IsDirectory: true, Extended: map[string][]byte{ s3_constants.ExtPublicAccessBlockKey: []byte(""), s3_constants.ExtAmzAclKey: []byte(`[{"Grantee":{"Type":"Group","URI":"` + s3_constants.GranteeGroupAllUsers + `"},"Permission":"READ"}]`), }}) require.True(t, config.PublicAccessBlock.blocksPublicAcls()) require.True(t, config.PublicAccessBlock.ignoresPublicAcls()) require.False(t, config.IsPublicRead) } func TestBucketUpdateKeepsStoredPublicAccessBlock(t *testing.T) { const bucket = "pab-keep" unreadable := []byte("") for _, stored := range [][]byte{unreadable, []byte(testPublicAccessBlockXML)} { s3a, filer := newPublicAccessBlockTestServer(t, &filer_pb.Entry{ Name: bucket, IsDirectory: true, Extended: map[string][]byte{s3_constants.ExtPublicAccessBlockKey: stored}, }) require.Equal(t, s3err.ErrNone, s3a.updateBucketConfig(bucket, func(config *BucketConfig) error { config.Versioning = s3_constants.VersioningEnabled return nil })) require.Len(t, filer.txnSeen, 1) require.NotContains(t, filer.txnSeen[0].Mutations[0].SetExtended, s3_constants.ExtPublicAccessBlockKey) require.NotContains(t, filer.txnSeen[0].Mutations[0].DeleteExtended, s3_constants.ExtPublicAccessBlockKey) } s3a, filer := newPublicAccessBlockTestServer(t, &filer_pb.Entry{ Name: bucket, IsDirectory: true, Extended: map[string][]byte{s3_constants.ExtPublicAccessBlockKey: unreadable}, }) all := `truetrue` + `truetrue` rr := httptest.NewRecorder() s3a.PutPublicAccessBlockHandler(rr, publicAccessBlockRequest(http.MethodPut, bucket, all)) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Len(t, filer.txnSeen, 1) written, err := parsePublicAccessBlock(filer.txnSeen[0].Mutations[0].SetExtended[s3_constants.ExtPublicAccessBlockKey]) require.NoError(t, err) require.True(t, written.BlockPublicAcls && written.IgnorePublicAcls && written.BlockPublicPolicy && written.RestrictPublicBuckets) } func TestPutPublicAccessBlockRejectsMalformedXML(t *testing.T) { const bucket = "pab-malformed" s3a, filer := newPublicAccessBlockTestServer(t, &filer_pb.Entry{Name: bucket, IsDirectory: true}) for _, body := range []string{ "", "yes", "true", "true", } { rr := httptest.NewRecorder() s3a.PutPublicAccessBlockHandler(rr, publicAccessBlockRequest(http.MethodPut, bucket, body)) require.Equal(t, http.StatusBadRequest, rr.Code, rr.Body.String()) require.Contains(t, rr.Body.String(), "MalformedXML") } require.Empty(t, filer.txnSeen) } func TestGetPublicAccessBlock(t *testing.T) { const bucket = "pab-get" s3a, _ := newPublicAccessBlockTestServer(t, &filer_pb.Entry{Name: bucket, IsDirectory: true}) rr := httptest.NewRecorder() s3a.GetPublicAccessBlockHandler(rr, publicAccessBlockRequest(http.MethodGet, bucket, "")) require.Equal(t, http.StatusNotFound, rr.Code, rr.Body.String()) require.Contains(t, rr.Body.String(), "NoSuchPublicAccessBlockConfiguration") s3a, _ = newPublicAccessBlockTestServer(t, &filer_pb.Entry{ Name: bucket, IsDirectory: true, Extended: map[string][]byte{s3_constants.ExtPublicAccessBlockKey: []byte(testPublicAccessBlockXML)}, }) rr = httptest.NewRecorder() s3a.GetPublicAccessBlockHandler(rr, publicAccessBlockRequest(http.MethodGet, bucket, "")) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) body := rr.Body.String() require.Equal(t, 1, strings.Count(body, "xmlns="), body) require.Contains(t, body, ``) require.Contains(t, body, "true") require.Contains(t, body, "true") require.Contains(t, body, "false") require.Contains(t, body, "true") } func TestDeletePublicAccessBlockRemovesConfiguration(t *testing.T) { const bucket = "pab-delete" s3a, filer := newPublicAccessBlockTestServer(t, &filer_pb.Entry{ Name: bucket, IsDirectory: true, Extended: map[string][]byte{s3_constants.ExtPublicAccessBlockKey: []byte(testPublicAccessBlockXML)}, }) rr := httptest.NewRecorder() s3a.DeletePublicAccessBlockHandler(rr, publicAccessBlockRequest(http.MethodDelete, bucket, "")) require.Equal(t, http.StatusNoContent, rr.Code, rr.Body.String()) require.Len(t, filer.txnSeen, 1) require.Contains(t, filer.txnSeen[0].Mutations[0].DeleteExtended, s3_constants.ExtPublicAccessBlockKey) } func TestPublicAccessBlockMissingBucket(t *testing.T) { s3a, _ := newPublicAccessBlockTestServer(t, nil) for _, tc := range []struct { method string body string handler http.HandlerFunc }{ {http.MethodGet, "", s3a.GetPublicAccessBlockHandler}, {http.MethodPut, testPublicAccessBlockXML, s3a.PutPublicAccessBlockHandler}, {http.MethodDelete, "", s3a.DeletePublicAccessBlockHandler}, } { rr := httptest.NewRecorder() tc.handler(rr, publicAccessBlockRequest(tc.method, "pab-missing", tc.body)) require.Equal(t, http.StatusNotFound, rr.Code, tc.method) require.Contains(t, rr.Body.String(), "NoSuchBucket", tc.method) } } func TestBlockPublicAclsRejectsPublicBucketAndObjectAcls(t *testing.T) { const bucket, object = "pab-acl", "doc.txt" newServer := func(t *testing.T) (*S3ApiServer, *fakeLookupFiler) { // The fake filer answers every lookup with this entry, so it serves // as both the bucket and the object. return newPublicAccessBlockTestServer(t, &filer_pb.Entry{ Name: object, Attributes: &filer_pb.FuseAttributes{}, Extended: map[string][]byte{ s3_constants.ExtPublicAccessBlockKey: []byte(`true`), s3_constants.ExtAmzOwnerKey: []byte(s3_constants.AccountAdminId), }, }) } for _, tc := range []struct { acl string status int }{ {s3_constants.CannedAclPrivate, http.StatusOK}, {s3_constants.CannedAclPublicRead, http.StatusForbidden}, {s3_constants.CannedAclPublicReadWrite, http.StatusForbidden}, {s3_constants.CannedAclAuthenticatedRead, http.StatusForbidden}, } { t.Run("bucket "+tc.acl, func(t *testing.T) { s3a, filer := newServer(t) req := httptest.NewRequest(http.MethodPut, "http://s3/"+bucket+"?acl", http.NoBody) req = mux.SetURLVars(req, map[string]string{"bucket": bucket}) req.Header.Set(s3_constants.AmzCannedAcl, tc.acl) rr := httptest.NewRecorder() s3a.PutBucketAclHandler(rr, req) require.Equal(t, tc.status, rr.Code, rr.Body.String()) if tc.status == http.StatusForbidden { require.Contains(t, rr.Body.String(), "AccessDenied") require.Empty(t, filer.txnSeen) } }) t.Run("object "+tc.acl, func(t *testing.T) { s3a, filer := newServer(t) req := httptest.NewRequest(http.MethodPut, "http://s3/"+bucket+"/"+object+"?acl", http.NoBody) req = mux.SetURLVars(req, map[string]string{"bucket": bucket, "object": object}) req.Header.Set(s3_constants.AmzCannedAcl, tc.acl) rr := httptest.NewRecorder() s3a.PutObjectAclHandler(rr, req) require.Equal(t, tc.status, rr.Code, rr.Body.String()) if tc.status == http.StatusForbidden { require.Contains(t, rr.Body.String(), "AccessDenied") require.Equal(t, s3_constants.AccountAdminId, string(filer.entry.Extended[s3_constants.ExtAmzOwnerKey])) require.NotContains(t, filer.entry.Extended, s3_constants.ExtAmzAclKey) } }) } t.Run("bucket xml all users grant", func(t *testing.T) { s3a, filer := newServer(t) body := `admin` + `` + s3_constants.GranteeGroupAllUsers + `READ` + `` req := httptest.NewRequest(http.MethodPut, "http://s3/"+bucket+"?acl", strings.NewReader(body)) req = mux.SetURLVars(req, map[string]string{"bucket": bucket}) rr := httptest.NewRecorder() s3a.PutBucketAclHandler(rr, req) require.Equal(t, http.StatusForbidden, rr.Code, rr.Body.String()) require.Empty(t, filer.txnSeen) }) }