package s3api import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/aws/aws-sdk-go/service/s3" "github.com/gorilla/mux" "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" ) func newMiscTestServer(t *testing.T, bucket string) *S3ApiServer { t.Helper() s3a := &S3ApiServer{ iam: &IdentityAccessManagement{isAuthEnabled: true}, bucketConfigCache: NewBucketConfigCache(time.Minute), } s3a.bucketConfigCache.Set(bucket, &BucketConfig{Name: bucket}) return s3a } func newBucketRequest(method, bucket, query, body string) *http.Request { req := httptest.NewRequest(method, "/"+bucket+"?"+query, strings.NewReader(body)) req = mux.SetURLVars(req, map[string]string{"bucket": bucket}) return req } func TestHasExplicitBucketACL(t *testing.T) { cases := []struct { name string headers map[string]string want bool }{ {name: "none", headers: nil, want: false}, {name: "private is default", headers: map[string]string{s3_constants.AmzCannedAcl: "private"}, want: false}, {name: "canned public-read", headers: map[string]string{s3_constants.AmzCannedAcl: "public-read"}, want: true}, {name: "canned case-insensitive private", headers: map[string]string{s3_constants.AmzCannedAcl: "PRIVATE"}, want: false}, {name: "grant read", headers: map[string]string{s3_constants.AmzAclRead: `id="x"`}, want: true}, {name: "grant full control", headers: map[string]string{s3_constants.AmzAclFullControl: `id="x"`}, want: true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { req := newBucketRequest(http.MethodPut, "b", "", "") for k, v := range tc.headers { req.Header.Set(k, v) } if got := hasExplicitBucketACL(req); got != tc.want { t.Fatalf("hasExplicitBucketACL = %v, want %v", got, tc.want) } }) } } func TestGetBucketPolicyStatusIsPublic(t *testing.T) { cases := []struct { name string raw string want bool }{ { name: "public allow star", raw: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::b/*"}]}`, want: true, }, { name: "deny is not public", raw: `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::b/*"}]}`, want: false, }, { name: "condition makes it non-public", raw: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::b/*","Condition":{"IpAddress":{"aws:SourceIp":"10.0.0.0/8"}}}]}`, want: false, }, { name: "specific principal is not public", raw: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"arn:aws:iam::1:user/a","Action":"s3:GetObject","Resource":"arn:aws:s3:::b/*"}]}`, want: false, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var doc policy_engine.PolicyDocument if err := json.Unmarshal([]byte(tc.raw), &doc); err != nil { t.Fatalf("unmarshal: %v", err) } if got := isPolicyPublic(&doc); got != tc.want { t.Fatalf("isPolicyPublic = %v, want %v", got, tc.want) } }) } } func TestPutBucketRequestPaymentBucketOwner(t *testing.T) { s3a := newMiscTestServer(t, "b") body := `BucketOwner` req := newBucketRequest(http.MethodPut, "b", "requestPayment=", body) rec := httptest.NewRecorder() s3a.PutBucketRequestPaymentHandler(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) } } func TestPutBucketRequestPaymentRequesterRejected(t *testing.T) { s3a := newMiscTestServer(t, "b") body := `Requester` req := newBucketRequest(http.MethodPut, "b", "requestPayment=", body) rec := httptest.NewRecorder() s3a.PutBucketRequestPaymentHandler(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusBadRequest, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "MalformedXML") { t.Fatalf("body missing MalformedXML: %s", rec.Body.String()) } } func TestPutBucketOwnershipControlsRejectsRuleWithoutObjectOwnership(t *testing.T) { ownerID := AccountAdmin.Id s3a := &S3ApiServer{ bucketRegistry: NewBucketRegistry(nil), } s3a.bucketRegistry.setMetadataCache(&BucketMetaData{ Name: "b", Owner: &s3.Owner{ ID: &ownerID, }, }) body := `` req := newBucketRequest(http.MethodPut, "b", "ownershipControls=", body) req.Header.Set(s3_constants.AmzAccountId, AccountAdmin.Id) rec := httptest.NewRecorder() s3a.PutBucketOwnershipControls(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusBadRequest, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "InvalidRequest") { t.Fatalf("body missing InvalidRequest: %s", rec.Body.String()) } } func TestGetBucketOwnershipControlsDefaultsToBucketOwnerEnforced(t *testing.T) { ownerID := AccountAdmin.Id s3a := newMiscTestServer(t, "b") s3a.bucketRegistry = NewBucketRegistry(nil) s3a.bucketRegistry.setMetadataCache(&BucketMetaData{ Name: "b", Owner: &s3.Owner{ID: &ownerID}, }) req := newBucketRequest(http.MethodGet, "b", "ownershipControls=", "") req.Header.Set(s3_constants.AmzAccountId, AccountAdmin.Id) rec := httptest.NewRecorder() s3a.GetBucketOwnershipControls(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) } if !strings.Contains(rec.Body.String(), ""+s3_constants.OwnershipBucketOwnerEnforced+"") { t.Fatalf("body missing default ownership: %s", rec.Body.String()) } } func TestGetBucketAccelerateConfiguration(t *testing.T) { s3a := newMiscTestServer(t, "b") req := newBucketRequest(http.MethodGet, "b", "accelerate=", "") rec := httptest.NewRecorder() s3a.GetBucketAccelerateConfigurationHandler(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } body, err := io.ReadAll(rec.Body) if err != nil { t.Fatalf("read body: %v", err) } got := string(body) if !strings.Contains(got, "Suspended") { t.Fatalf("missing Suspended status: %s", got) } if !strings.Contains(got, `xmlns="http://s3.amazonaws.com/doc/2006-03-01/"`) { t.Fatalf("missing xmlns: %s", got) } } func TestGetBucketLogging(t *testing.T) { s3a := newMiscTestServer(t, "b") req := newBucketRequest(http.MethodGet, "b", "logging=", "") rec := httptest.NewRecorder() s3a.GetBucketLoggingHandler(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } got := rec.Body.String() if !strings.Contains(got, "