Files
seaweedfs/weed/s3api/s3api_bucket_handlers_misc_test.go
T
48782e2b30 s3: allow PutBucketAcl when auth is off (#12180)
* s3: allow PutBucketAcl when auth is off

* s3: resolve the PutBucketAcl owner against the recorded owner

The unauthenticated path now resolves the account as anonymous, but XML
ACLs were still checked against it while GetBucketAcl reported an empty
owner for ownerless buckets, so a read-modify-write ACL update failed
with AccessDenied. GetBucketAcl and the XML check now both use the
recorded owner when the bucket has one and the resolved account
otherwise. The canned-ACL grantee moves to the same owner, and a request
carrying no account no longer erases a stored owner.

* s3: cover PutBucketAcl without auth for XML bodies and stored owners

The XML case posts the anonymous owner GetBucketAcl reports for an
ownerless bucket; the canned case asserts a recorded owner survives an
unauthenticated update and receives the full-control grant. Ambient AWS
credentials are cleared so the memory store never turns auth on.

* s3: keep the recorded owner through a PutBucketAcl update

The grantee and the stored owner were resolved differently: grants
named the recorded owner while config.Owner still took the caller's
account, so an admin updating another account's bucket rewrote the
owner and broke an XML retry naming the original one. An ownerless
bucket adopts the ACL's owner; an owned bucket keeps it.

* s3: fall back to the admin owner for an ownerless bucket in ACL handlers

An unauthenticated PutBucketAcl resolved the owner as anonymous and stored
it on the bucket, but the registry reports an ownerless bucket as
admin-owned: BucketOwnerEnforced uploads then stamped anonymous instead of
admin, and ownership checks after enabling auth compared against the wrong
owner. Both ACL handlers now resolve an anonymous fallback to the admin
account, so GetBucketAcl reports the same effective owner the registry
does and the stored owner matches it.

* s3: resolve the admin owner for bucket grants when the caller is anonymous or untrusted

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: skip recording anonymous identity or account as bucket owner

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: report stored bucket ACL grants verbatim

Explicitly saved anonymous full-control grants must survive a
GET-to-PUT round trip unchanged, so the read path no longer rewrites
them to the reported owner.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* s3: expect the stored anonymous grant verbatim in GetBucketAcl

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: user.email <chrislusf@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 14:45:23 -07:00

495 lines
19 KiB
Go

package s3api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/gorilla/mux"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/stretchr/testify/require"
)
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)
req = req.WithContext(s3_constants.SetIdentityInContext(req.Context(), &Identity{
Name: "admin",
Account: &AccountAdmin,
Actions: []Action{s3_constants.ACTION_ADMIN},
}))
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)
}
}
func TestGetBucketLocationInvalidBucketName(t *testing.T) {
// AWS answers 400 InvalidBucketName for a malformed bucket name rather than
// the 404 NoSuchBucket an unknown-but-valid name gets.
s3a := &S3ApiServer{}
req := httptest.NewRequest(http.MethodGet, "/invalid%20bucket%20name?location=", nil)
req = mux.SetURLVars(req, map[string]string{"bucket": "invalid bucket name"})
rec := httptest.NewRecorder()
s3a.GetBucketLocationHandler(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(), "InvalidBucketName") {
t.Fatalf("body = %s, want InvalidBucketName", rec.Body.String())
}
}
func TestHandleAutoCreateBucketDisabled(t *testing.T) {
s3a := &S3ApiServer{option: &S3ApiServerOption{}}
req := newBucketRequest(http.MethodPut, "test-bucket", "", "")
rec := httptest.NewRecorder()
if s3a.handleAutoCreateBucket(rec, req, "test-bucket", "PutObjectHandler") {
t.Fatal("expected auto-create to be rejected")
}
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
}
if !strings.Contains(rec.Body.String(), "NoSuchBucket") {
t.Fatalf("body = %s, want NoSuchBucket", rec.Body.String())
}
}
func TestHandleAutoCreateBucketNonAdmin(t *testing.T) {
s3a := &S3ApiServer{option: &S3ApiServerOption{AutoCreateBucket: true}}
req := newBucketRequest(http.MethodPut, "test-bucket", "", "")
rec := httptest.NewRecorder()
if s3a.handleAutoCreateBucket(rec, req, "test-bucket", "PutObjectHandler") {
t.Fatal("expected auto-create to be rejected")
}
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
}
}
func TestUploadMissingBucketAutoCreateDisabled(t *testing.T) {
cases := []struct {
name string
method string
object string
handler func(*S3ApiServer, http.ResponseWriter, *http.Request)
}{
{"put object", http.MethodPut, "/key", (*S3ApiServer).PutObjectHandler},
{"put directory marker", http.MethodPut, "/dir/", (*S3ApiServer).PutObjectHandler},
{"new multipart upload", http.MethodPost, "/key", (*S3ApiServer).NewMultipartUploadHandler},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
s3a := &S3ApiServer{
option: &S3ApiServerOption{},
iam: &IdentityAccessManagement{},
bucketConfigCache: NewBucketConfigCache(time.Minute),
}
s3a.bucketConfigCache.SetNegativeCache("missing")
req := httptest.NewRequest(tc.method, "/missing"+tc.object, strings.NewReader(""))
req = mux.SetURLVars(req, map[string]string{"bucket": "missing", "object": tc.object})
rec := httptest.NewRecorder()
tc.handler(s3a, rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
}
if !strings.Contains(rec.Body.String(), "NoSuchBucket") {
t.Fatalf("body = %s, want NoSuchBucket", rec.Body.String())
}
})
}
}
func TestAuthWithPublicReadHonorsPolicyDeny(t *testing.T) {
const bucket = "public-deny"
s3a := newMiscTestServer(t, bucket)
s3a.bucketConfigCache.Set(bucket, &BucketConfig{Name: bucket, IsPublicRead: true})
s3a.policyEngine = NewBucketPolicyEngine()
called := false
handler := s3a.AuthWithPublicRead(func(w http.ResponseWriter, r *http.Request) { called = true }, s3_constants.ACTION_LIST)
serve := func() *httptest.ResponseRecorder {
called = false
rr := httptest.NewRecorder()
handler(rr, newBucketRequest(http.MethodGet, bucket, "list-type=2", ""))
return rr
}
setPolicy := func(statements ...string) {
doc := `{"Version":"2012-10-17","Statement":[` + strings.Join(statements, ",") + `]}`
require.NoError(t, s3a.policyEngine.engine.SetBucketPolicy(bucket, doc))
}
arn := fmt.Sprintf("arn:aws:s3:::%s", bucket)
denyList := fmt.Sprintf(`{"Effect":"Deny","Principal":"*","Action":"s3:ListBucket","Resource":"%s"}`, arn)
denyGet := fmt.Sprintf(`{"Effect":"Deny","Principal":"*","Action":"s3:GetObject","Resource":"%s/*"}`, arn)
allowList := fmt.Sprintf(`{"Effect":"Allow","Principal":"*","Action":"s3:ListBucket","Resource":"%s"}`, arn)
setPolicy(denyList)
rr := serve()
require.False(t, called, "anonymous ListObjectsV2 reached the handler despite an explicit ListBucket Deny")
require.Equal(t, http.StatusForbidden, rr.Code)
setPolicy(denyGet)
rr = serve()
require.True(t, called, "an unrelated Deny must not block the ACL public-read grant")
setPolicy(denyList, allowList)
rr = serve()
require.False(t, called, "explicit Deny must beat a matching Allow")
setPolicy(allowList)
rr = serve()
require.True(t, called, "explicit Allow must still permit anonymous listing")
// Object requests defer to the handler's phase-2 recheck, which evaluates
// tag conditions against the fetched entry — a tag-conditional Deny must
// not terminate them here on a missing value.
tagDeny := fmt.Sprintf(`{"Effect":"Deny","Principal":"*","Action":"s3:GetObject","Resource":"%s/*","Condition":{"StringNotEquals":{"s3:ExistingObjectTag/classification":"public"}}}`, arn)
setPolicy(tagDeny)
called = false
rr = httptest.NewRecorder()
objectReq := newBucketRequest(http.MethodGet, bucket, "", "")
objectReq = mux.SetURLVars(objectReq, map[string]string{"bucket": bucket, "object": "secret/x"})
s3a.AuthWithPublicRead(func(w http.ResponseWriter, r *http.Request) { called = true }, s3_constants.ACTION_READ)(rr, objectReq)
require.True(t, called, "anonymous object request on a public bucket must reach the handler's tag-aware recheck")
}
// With auth off the request has no account, so the canned ACL's owner grant
// named an empty account and PutBucketAcl answered InvalidRequest.
func TestPutBucketAclWithoutAuth(t *testing.T) {
t.Setenv("AWS_ACCESS_KEY_ID", "")
t.Setenv("AWS_SECRET_ACCESS_KEY", "")
const bucket = "acl-bucket"
filer := &fakeLookupFiler{entry: &filer_pb.Entry{Name: bucket, IsDirectory: true}}
s3a := newHeadBucketTestServer(t, filer)
s3a.bucketConfigCache = NewBucketConfigCache(time.Minute)
s3a.bucketConfigCache.Set(bucket, &BucketConfig{Name: bucket})
s3a.iam = NewIdentityAccessManagementWithStore(s3a.option, nil, "memory")
t.Cleanup(s3a.iam.Shutdown)
require.False(t, s3a.iam.isEnabled())
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, "public-read")
rr := httptest.NewRecorder()
s3a.iam.Auth(s3a.PutBucketAclHandler, s3_constants.ACTION_WRITE_ACP)(rr, req)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
require.Len(t, filer.txnSeen, 1)
var grants []*s3.Grant
require.NoError(t, json.Unmarshal(filer.txnSeen[0].Mutations[0].SetExtended[s3_constants.ExtAmzAclKey], &grants))
require.Len(t, grants, 2)
require.Equal(t, s3_constants.AccountAdminId, aws.StringValue(grants[0].Grantee.ID))
require.Equal(t, s3_constants.GranteeGroupAllUsers, aws.StringValue(grants[1].Grantee.URI))
require.Equal(t, s3_constants.AccountAdminId, string(filer.txnSeen[0].Mutations[0].SetExtended[s3_constants.ExtAmzOwnerKey]))
}
// GetBucketAcl reports the admin account for an ownerless bucket without
// auth, so an XML ACL written back with that owner must validate.
func TestPutBucketAclXmlWithoutAuth(t *testing.T) {
t.Setenv("AWS_ACCESS_KEY_ID", "")
t.Setenv("AWS_SECRET_ACCESS_KEY", "")
const bucket = "acl-xml-bucket"
filer := &fakeLookupFiler{entry: &filer_pb.Entry{Name: bucket, IsDirectory: true}}
s3a := newHeadBucketTestServer(t, filer)
s3a.bucketConfigCache = NewBucketConfigCache(time.Minute)
s3a.bucketConfigCache.Set(bucket, &BucketConfig{Name: bucket})
s3a.iam = NewIdentityAccessManagementWithStore(s3a.option, nil, "memory")
t.Cleanup(s3a.iam.Shutdown)
body := `<AccessControlPolicy xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Owner><ID>admin</ID></Owner><AccessControlList>` +
`<Grant><Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="CanonicalUser"><ID>admin</ID></Grantee><Permission>FULL_CONTROL</Permission></Grant>` +
`</AccessControlList></AccessControlPolicy>`
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.iam.Auth(s3a.PutBucketAclHandler, s3_constants.ACTION_WRITE_ACP)(rr, req)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
}
// An ownerless bucket created before the admin fallback stored an anonymous
// full-control grantee; GetBucketAcl reports it verbatim so a GET-to-PUT
// round trip keeps the stored grant. A client-supplied account header with
// auth off cannot claim ownership either.
func TestGetBucketAclReportsAnonymousGrantVerbatim(t *testing.T) {
t.Setenv("AWS_ACCESS_KEY_ID", "")
t.Setenv("AWS_SECRET_ACCESS_KEY", "")
const bucket = "acl-legacy-bucket"
filer := &fakeLookupFiler{entry: &filer_pb.Entry{Name: bucket, IsDirectory: true}}
s3a := newHeadBucketTestServer(t, filer)
s3a.bucketConfigCache = NewBucketConfigCache(time.Minute)
s3a.iam = NewIdentityAccessManagementWithStore(s3a.option, nil, "memory")
t.Cleanup(s3a.iam.Shutdown)
require.False(t, s3a.iam.isEnabled())
fullControl := s3_constants.PermissionFullControl
grants := []*s3.Grant{{
Grantee: &s3.Grantee{
Type: aws.String("CanonicalUser"),
ID: aws.String(s3_constants.AccountAnonymousId),
},
Permission: &fullControl,
}}
raw, err := json.Marshal(grants)
require.NoError(t, err)
s3a.bucketConfigCache.Set(bucket, &BucketConfig{Name: bucket, ACL: raw})
req := httptest.NewRequest(http.MethodGet, "http://s3/"+bucket+"?acl", http.NoBody)
req = mux.SetURLVars(req, map[string]string{"bucket": bucket})
req.Header.Set(s3_constants.AmzAccountId, "spoofed-owner")
rr := httptest.NewRecorder()
s3a.iam.Auth(s3a.GetBucketAclHandler, s3_constants.ACTION_READ_ACP)(rr, req)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
require.Contains(t, rr.Body.String(), "<ID>"+s3_constants.AccountAdminId+"</ID>")
require.NotContains(t, rr.Body.String(), "spoofed-owner")
require.Contains(t, rr.Body.String(), s3_constants.AccountAnonymousId)
}
// A bucket with a recorded owner keeps it through an ACL update: the stored
// owner and the full-control grantee stay the same account, whether the
// request carries an account or none.
func TestPutBucketAclKeepsOwner(t *testing.T) {
t.Setenv("AWS_ACCESS_KEY_ID", "")
t.Setenv("AWS_SECRET_ACCESS_KEY", "")
const bucket = "acl-owned-bucket"
for _, account := range []string{"", "admin"} {
filer := &fakeLookupFiler{entry: &filer_pb.Entry{
Name: bucket,
IsDirectory: true,
Extended: map[string][]byte{s3_constants.ExtAmzOwnerKey: []byte("team-a")},
}}
s3a := newHeadBucketTestServer(t, filer)
s3a.bucketConfigCache = NewBucketConfigCache(time.Minute)
s3a.iam = NewIdentityAccessManagementWithStore(s3a.option, nil, "memory")
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, "private")
if account != "" {
req.Header.Set(s3_constants.AmzAccountId, account)
}
rr := httptest.NewRecorder()
s3a.iam.Auth(s3a.PutBucketAclHandler, s3_constants.ACTION_WRITE_ACP)(rr, req)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
s3a.iam.Shutdown()
require.Len(t, filer.txnSeen, 1)
require.NotContains(t, filer.txnSeen[0].Mutations[0].DeleteExtended, s3_constants.ExtAmzOwnerKey)
var grants []*s3.Grant
require.NoError(t, json.Unmarshal(filer.txnSeen[0].Mutations[0].SetExtended[s3_constants.ExtAmzAclKey], &grants))
require.Equal(t, "team-a", aws.StringValue(grants[0].Grantee.ID))
}
}