mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-30 08:50:48 +01:00
* 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>
495 lines
19 KiB
Go
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))
|
|
}
|
|
}
|