Files
seaweedfs/weed/s3api/s3api_bucket_handlers_misc_test.go
T
Chris Lu f09bc14165 s3: report the effective ownership when a bucket has none stored (#10591)
* s3: report the effective ownership when a bucket has none stored

GetBucketOwnershipControls read Seaweed-X-Amz-Ownership straight out of the
bucket entry, so a bucket that never had one written reported an empty
ObjectOwnership. The object write path defaults the same missing attribute to
BucketOwnerEnforced, so the API contradicted the behavior it describes.

Resolve the stored value through one helper both readers share, and let
PutBucketOwnershipControls persist unconditionally so setting the default
value still gives DeleteBucketOwnershipControls something to remove.

* test: cover the bucket ownership controls round trip

Pins the behaviors the ownership default fix depends on: a bucket that never
had ownership controls written reports BucketOwnerEnforced, and putting that
same value on such a bucket still persists it, so the delete that follows has
something to remove. The put-then-delete case gets its own bucket -- run after
an ObjectWriter put, it would pass against an implementation that skips only
the initial write.

The acl workflow already runs this package against a live weed mini, so it
needs no wiring.
2026-08-05 13:15:28 -07:00

224 lines
7.3 KiB
Go

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 := `<RequestPaymentConfiguration><Payer>BucketOwner</Payer></RequestPaymentConfiguration>`
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 := `<RequestPaymentConfiguration><Payer>Requester</Payer></RequestPaymentConfiguration>`
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 := `<OwnershipControls><Rule></Rule></OwnershipControls>`
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(), "<ObjectOwnership>"+s3_constants.OwnershipBucketOwnerEnforced+"</ObjectOwnership>") {
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, "<AccelerateConfiguration") {
t.Fatalf("missing root element: %s", got)
}
if !strings.Contains(got, "<Status>Suspended</Status>") {
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, "<BucketLoggingStatus") {
t.Fatalf("missing root element: %s", got)
}
if strings.Contains(got, "<LoggingEnabled") {
t.Fatalf("unexpected LoggingEnabled element: %s", got)
}
if !strings.Contains(got, `xmlns="http://s3.amazonaws.com/doc/2006-03-01/"`) {
t.Fatalf("missing xmlns: %s", got)
}
}