s3: store the POST Object acl on the uploaded object (#12567)

* s3api: split upload ACL assembly out of preparePutObjectACL

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: store POST Object acl and grant fields on the object

The form acl and x-amz-grant-* fields were authorized but never assembled,
so the object kept no ACL: acl=private on a public-read bucket stayed
anonymously readable and invalid values were accepted. Build them with the
same rules as PutObject, which also covers the Block Public Access check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: test POST Object stores its upload ACL

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: take the POST Object ACL from the signed form only

ACL request headers are not covered by the POST policy, so a holder of a
signed form could add one and have it stored with the upload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: reject a POST acl field the policy does not cover

The acl form field now sets the stored object ACL, so like the x-amz-*
fields it needs a policy condition, as AWS requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: test unsigned ACL headers and uncovered acl fields on POST

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: evaluate POST upload policies on the canonical ACL

Policy conditions on the grant headers saw the raw form value, so an
equivalent spelling of a denied grantee passed. Use the PutObject request
shape that the PUT path evaluates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: check bucket policy for POST ACLs with auth disabled

PutObject applies the bucket policy to an explicit upload ACL when IAM is
off; POST now does the same before storing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: reject differing acl and x-amz-acl POST fields

Both promote to the canned ACL header, so map order picked the stored ACL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: test POST grant canonicalization, auth-off ACL policy, acl conflicts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: check the auth-off POST ACL policy after loading the bucket

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: reject an empty acl or x-amz-acl POST field beside a differing one

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: test the auth-off POST ACL policy on a cold bucket cache and empty acl aliases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: load the bucket before per-key object authorization

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: test the POST ACL policy on a cold bucket cache with auth on

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: stop per-key object authorization when the bucket fails to load

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: test the cold-bucket POST ACL allow path and a failed bucket load

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris LuandClaude Opus 5.5 authored and GitHub committed 2026-11-11 10:36:51 -08:00
1 parent eb159f3181
commit 6c1eec8703
7 files changed
+325 -33

No files matched your search

+6 -1
View File
@@ -85,7 +85,7 @@ type IdentityAccessManagement struct {
// primeBucketForIAM loads a bucket's config (and with it the advanced-IAM
// bucket-policy mirror backfill) before an IAM authorization that will
// evaluate that mirror. Set by NewS3ApiServer; nil in tests.
primeBucketForIAM func(bucket string)
primeBucketForIAM func(bucket string) s3err.ErrorCode
// Cached policy engine for IAM policy fallback evaluation.
// Keyed by policy name, kept in sync by PutPolicy/DeletePolicy.
@@ -3134,6 +3134,11 @@ func (iam *IdentityAccessManagement) authorizeObjectKeyAction(r *http.Request, i
keyReq := objectKeyPolicyRequest(r, method, bucket, objectKey, versionId)
if iam.policyEngine != nil {
if iam.primeBucketForIAM != nil {
if code := iam.primeBucketForIAM(bucket); code != s3err.ErrNone && code != s3err.ErrNoSuchBucket {
return code
}
}
principal := buildPrincipalARN(identity, keyReq)
allowed, evaluated, err := iam.policyEngine.EvaluatePolicy(bucket, objectKey, string(action), principal, keyReq, identity.Claims, nil)
if err != nil {
+22
View File
@@ -347,3 +347,25 @@ func TestCheckPostPolicy_UnknownKeyErrorIncludesPolicyValue(t *testing.T) {
t.Fatalf("expected 'unknown condition key' suffix, got: %v", err)
}
}
// An acl form field sets the stored object ACL, so it needs a policy
// condition like the X-Amz-* fields do.
func TestCheckPostPolicy_AclFieldNeedsCondition(t *testing.T) {
form := http.Header{}
form.Set("Bucket", "mybucket")
form.Set("Acl", "public-read")
err := CheckPostPolicy(form, buildParsedPolicy(t, `["eq","$bucket","mybucket"]`))
if err == nil || !strings.Contains(err.Error(), "Extra input fields: Acl") {
t.Fatalf("expected 'Extra input fields: Acl' error, got: %v", err)
}
if err := CheckPostPolicy(form, buildParsedPolicy(t, `["eq","$bucket","mybucket"],["eq","$acl","public-read"]`)); err != nil {
t.Fatalf("expected an eq $acl condition to cover the field, got: %v", err)
}
if err := CheckPostPolicy(form, buildParsedPolicy(t, `["eq","$bucket","mybucket"],["starts-with","$acl","public-"]`)); err != nil {
t.Fatalf("expected a starts-with $acl condition to cover the field, got: %v", err)
}
if err := CheckPostPolicy(form, buildParsedPolicy(t, `["eq","$bucket","mybucket"],["eq","$acl","private"]`)); err == nil {
t.Fatalf("expected a mismatched $acl condition to fail")
}
}
+4 -4
View File
@@ -252,7 +252,7 @@ func CheckPostPolicy(formValues http.Header, postPolicyForm PostPolicyForm) erro
policyXAmzKeys := make(map[string]bool)
var policyXAmzPrefixes []xAmzPrefixRule
for _, policy := range postPolicyForm.Conditions.Policies {
if !strings.HasPrefix(policy.Key, "$x-amz-") {
if policy.Key != "$acl" && !strings.HasPrefix(policy.Key, "$x-amz-") {
continue
}
formCanonicalName := http.CanonicalHeaderKey(strings.TrimPrefix(policy.Key, "$"))
@@ -266,8 +266,8 @@ func CheckPostPolicy(formValues http.Header, postPolicyForm PostPolicyForm) erro
}
policyXAmzKeys[formCanonicalName] = true
}
// Reject any X-Amz-* form field that has no matching policy condition,
// except for the reserved auth/signing fields clients must always send.
// Reject any acl or X-Amz-* form field that has no matching policy
// condition, except for the reserved auth/signing fields clients must always send.
// A field may be covered by an exact-key condition, by any number of
// prefix-stem rules, or both. Check every applicable rule: AWS requires
// all matching conditions to be satisfied, so exact-match coverage does
@@ -276,7 +276,7 @@ func CheckPostPolicy(formValues http.Header, postPolicyForm PostPolicyForm) erro
// Prefix-stem conditions are then skipped in the main loop below because
// no single form field corresponds to the prefix itself.
for key := range formValues {
if !strings.HasPrefix(key, "X-Amz-") {
if key != "Acl" && !strings.HasPrefix(key, "X-Amz-") {
continue
}
if postPolicyAuthFields[key] {
+35 -21
View File
@@ -144,6 +144,13 @@ func (s3a *S3ApiServer) PostPolicyBucketHandler(w http.ResponseWriter, r *http.R
}
r.Header.Set("Content-Type", contentType)
// acl and x-amz-acl both set the canned ACL; differing values are ambiguous.
_, hasAcl := formValues["Acl"]
_, hasAmzAcl := formValues[s3_constants.AmzCannedAcl]
if hasAcl && hasAmzAcl && formValues.Get("Acl") != formValues.Get(s3_constants.AmzCannedAcl) {
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
return
}
// Forward validated POST form fields to the underlying PUT as headers.
applyPostPolicyFormHeaders(r, formValues)
if !validHeaderValues(r.Header) {
@@ -151,33 +158,23 @@ func (s3a *S3ApiServer) PostPolicyBucketHandler(w http.ResponseWriter, r *http.R
return
}
// Authorize the object like the PUT path.
if errCode := s3a.iam.AuthorizeObjectWrite(r, identity, bucket, object); errCode != s3err.ErrNone {
// Authorize the object like the PUT path, with policies seeing the ACL as PutObject presents it.
policyReq, errCode := putObjectACLPolicyRequest(objectKeyPolicyRequest(r, http.MethodPut, bucket, object, ""), s3_constants.ACTION_WRITE, bucket, object)
if errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
if errCode := s3a.iam.AuthorizeObjectWrite(policyReq, identity, bucket, object); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, refineDenialForBucketName(r, identity, errCode))
return
}
// An acl or x-amz-grant-* form field rides onto the upload as ACL headers,
// which the PUT path only applies under s3:PutObjectAcl.
if postRequestsAcl(r) {
if errCode := s3a.iam.AuthorizeObjectAclWrite(r, identity, bucket, object); errCode != s3err.ErrNone {
if errCode := s3a.iam.AuthorizeObjectAclWrite(policyReq, identity, bucket, object); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, refineDenialForBucketName(r, identity, errCode))
return
}
config, errCode := s3a.getBucketConfig(bucket)
if errCode != s3err.ErrNone && errCode != s3err.ErrNoSuchBucket {
s3err.WriteErrorResponse(w, r, errCode)
return
}
if errCode == s3err.ErrNone && config.PublicAccessBlock.blocksPublicAcls() {
_, grants, aclCode := ParseAclHeaders(r, config.Ownership, config.Owner, "", false)
if aclCode == s3err.ErrNone {
aclCode = rejectPublicGrants(config, grants)
}
if aclCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, aclCode)
return
}
}
}
versioningState, err := s3a.getVersioningState(bucket)
@@ -197,6 +194,18 @@ func (s3a *S3ApiServer) PostPolicyBucketHandler(w http.ResponseWriter, r *http.R
return
}
}
// The bucket load above brings its policy into the engine.
if !s3a.iam.isEnabled() && postRequestsAcl(r) {
if errCode := s3a.checkPutObjectACLPolicy(policyReq, bucket, object); errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
}
r, errCode = s3a.preparePostObjectACL(r, bucket)
if errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, errCode)
return
}
objectLockEnabled, err := s3a.isObjectLockEnabled(bucket)
if err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
glog.Errorf("PostPolicyBucketHandler: object lock state for bucket %s: %v", bucket, err)
@@ -298,6 +307,10 @@ var postPolicyReservedFormFields = map[string]struct{}{
// skipped. The acl form field is translated to the X-Amz-Acl header to match
// how AWS promotes the form value to the underlying PUT.
func applyPostPolicyFormHeaders(r *http.Request, formValues http.Header) {
// The policy signs form fields only, so the upload ACL comes from the form.
for _, header := range postAclHeaders {
r.Header.Del(header)
}
for k := range formValues {
if _, reserved := postPolicyReservedFormFields[k]; reserved {
continue
@@ -330,10 +343,11 @@ func validHeaderValues(h http.Header) bool {
return true
}
// postRequestsAcl reports whether the request carries ACL headers, whether
// promoted from the POST form fields or supplied on the request itself.
var postAclHeaders = []string{s3_constants.AmzCannedAcl, s3_constants.AmzAclFullControl, s3_constants.AmzAclRead, s3_constants.AmzAclReadAcp, s3_constants.AmzAclWrite, s3_constants.AmzAclWriteAcp}
// postRequestsAcl reports whether the POST form fields promoted an ACL.
func postRequestsAcl(r *http.Request) bool {
for _, header := range []string{s3_constants.AmzCannedAcl, s3_constants.AmzAclFullControl, s3_constants.AmzAclRead, s3_constants.AmzAclReadAcp, s3_constants.AmzAclWrite, s3_constants.AmzAclWriteAcp} {
for _, header := range postAclHeaders {
if r.Header.Get(header) != "" {
return true
}
@@ -540,3 +540,220 @@ func TestPostPolicyBucketHandlerBlocksPublicAcls(t *testing.T) {
defer filer.mu.Unlock()
assert.NotNil(t, filer.entries["/buckets/"+postAuthzBucket+"/"+key])
}
// A POST Object acl field is stored on the object like PUT's x-amz-acl, so a
// private upload into a public-read bucket is not anonymously readable.
func TestPostPolicyBucketHandlerStoresObjectAcl(t *testing.T) {
const key = "uploads/pic.png"
upload := func(t *testing.T, publicBucket bool, fields, headers map[string]string) (*httptest.ResponseRecorder, *S3ApiServer, *filer_pb.Entry) {
bucketEntry := &filer_pb.Entry{
Name: postAuthzBucket,
IsDirectory: true,
Attributes: &filer_pb.FuseAttributes{},
Extended: map[string][]byte{s3_constants.ExtAmzOwnerKey: []byte("000000000000")},
}
if publicBucket {
bucketEntry.Extended[s3_constants.ExtAmzAclKey] = anonymousReadGrant(s3_constants.PermissionRead)
}
filer := &ambiguousPutFiler{
volume: startFakeVolumeServer(t),
entries: map[string]*filer_pb.Entry{"/buckets/" + postAuthzBucket: bucketEntry},
apply: true,
}
s3a := newPostAutoCreateServer(t, filer)
req := newSignedPostRequestFor(t, key, postAdminAccessKey, postAdminSecretKey, fields)
for name, value := range headers {
req.Header.Set(name, value)
}
rec := httptest.NewRecorder()
s3a.PostPolicyBucketHandler(rec, req)
filer.mu.Lock()
defer filer.mu.Unlock()
return rec, s3a, filer.entries["/buckets/"+postAuthzBucket+"/"+key]
}
anonymousRead := func(s3a *S3ApiServer, entry *filer_pb.Entry) s3err.ErrorCode {
req := httptest.NewRequest(http.MethodGet, "/"+postAuthzBucket+"/"+key, nil)
return s3a.authorizeAnonymousObjectRead(req, postAuthzBucket, key, entry.Extended)
}
for _, tc := range []struct {
name string
publicBucket bool
fields map[string]string
headers map[string]string
want s3err.ErrorCode
}{
{"private on public-read bucket", true, map[string]string{"acl": "private"}, nil, s3err.ErrAccessDenied},
{"no acl on public-read bucket", true, nil, nil, s3err.ErrAccessDenied},
{"public-read on private bucket", false, map[string]string{"acl": "public-read"}, nil, s3err.ErrNone},
{"grant-read AllUsers on private bucket", false, map[string]string{"x-amz-grant-read": `uri="` + s3_constants.GranteeGroupAllUsers + `"`}, nil, s3err.ErrNone},
{"unsigned acl header", false, nil, map[string]string{s3_constants.AmzCannedAcl: "public-read"}, s3err.ErrAccessDenied},
{"unsigned grant header", false, nil, map[string]string{s3_constants.AmzAclRead: `uri="` + s3_constants.GranteeGroupAllUsers + `"`}, s3err.ErrAccessDenied},
{"unsigned header beside signed acl", false, map[string]string{"acl": "private"}, map[string]string{s3_constants.AmzAclRead: `uri="` + s3_constants.GranteeGroupAllUsers + `"`}, s3err.ErrAccessDenied},
} {
t.Run(tc.name, func(t *testing.T) {
rec, s3a, entry := upload(t, tc.publicBucket, tc.fields, tc.headers)
require.Equal(t, http.StatusNoContent, rec.Code, "body: %s", rec.Body.String())
require.NotNil(t, entry)
assert.Equal(t, []byte("000000000000"), entry.Extended[s3_constants.ExtAmzOwnerKey])
assert.NotEmpty(t, entry.Extended[s3_constants.ExtAmzAclKey], "the upload ACL must be stored")
assert.Equal(t, tc.want, anonymousRead(s3a, entry))
})
}
t.Run("invalid canned acl", func(t *testing.T) {
rec, _, entry := upload(t, false, map[string]string{"acl": "bogus-acl"}, nil)
assert.Equal(t, http.StatusBadRequest, rec.Code, "body: %s", rec.Body.String())
assert.Nil(t, entry, "a rejected ACL must not write the object")
})
t.Run("canned and grant together", func(t *testing.T) {
rec, _, entry := upload(t, false, map[string]string{"acl": "private", "x-amz-grant-read": `uri="` + s3_constants.GranteeGroupAllUsers + `"`}, nil)
assert.Equal(t, http.StatusBadRequest, rec.Code, "body: %s", rec.Body.String())
assert.Nil(t, entry)
})
}
// newPostAclWriterServer serves the policy-confined identities of
// newPostAuthzServer against a fake filer holding the test bucket.
func newPostAclWriterServer(t *testing.T) (*S3ApiServer, *ambiguousPutFiler) {
filer := &ambiguousPutFiler{
volume: startFakeVolumeServer(t),
entries: map[string]*filer_pb.Entry{"/buckets/" + postAuthzBucket: {
Name: postAuthzBucket,
IsDirectory: true,
Attributes: &filer_pb.FuseAttributes{},
Extended: map[string][]byte{s3_constants.ExtAmzOwnerKey: []byte("000000000000")},
}},
apply: true,
}
s3a := newPostAutoCreateServer(t, filer)
authz := newPostAuthzServer(t)
s3a.iam, s3a.policyEngine = authz.iam, authz.policyEngine
return s3a, filer
}
// Policy conditions on a POST grant see the canonical grant list, as on PUT,
// so a respelled grantee does not slip past a deny.
func TestPostPolicyBucketHandlerCanonicalGrantCondition(t *testing.T) {
for _, grant := range []string{`id="000000000000"`, `id = "000000000000"`, `id="00000000000\u0030"`} {
s3a, filer := newPostAclWriterServer(t)
require.NoError(t, s3a.policyEngine.engine.SetBucketPolicy(postAuthzBucket, fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Principal": {"AWS": "%s"},
"Action": ["s3:PutObjectAcl"],
"Resource": ["arn:aws:s3:::%s/*"],
"Condition": {"StringEquals": {"s3:x-amz-grant-read": "id=\"000000000000\""}}
}]
}`, postAclPrincipal, postAuthzBucket)))
rec := httptest.NewRecorder()
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, "allowed/report.txt",
postAclAccessKey, postAclSecretKey, map[string]string{"x-amz-grant-read": grant}))
assert.Equal(t, http.StatusForbidden, rec.Code, "grant %s, body: %s", grant, rec.Body.String())
filer.mu.Lock()
assert.Nil(t, filer.entries["/buckets/"+postAuthzBucket+"/allowed/report.txt"], "grant %s", grant)
filer.mu.Unlock()
}
}
// With authentication off, a bucket policy still governs a POST upload ACL,
// as it does a PutObject ACL, including on the first request after start.
func TestPostPolicyBucketHandlerAclPolicyWithAuthDisabled(t *testing.T) {
const key = "uploads/pic.png"
s3a, filer := newPostAclWriterServer(t)
s3a.iam = &IdentityAccessManagement{}
s3a.policyEngine = NewBucketPolicyEngine()
filer.mu.Lock()
filer.entries["/buckets/"+postAuthzBucket].Extended[BUCKET_POLICY_METADATA_KEY] = []byte(fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [{"Effect": "Deny", "Principal": "*", "Action": ["s3:PutObjectAcl"], "Resource": ["arn:aws:s3:::%s/*"]}]
}`, postAuthzBucket))
filer.mu.Unlock()
rec := httptest.NewRecorder()
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAdminAccessKey, postAdminSecretKey, map[string]string{"acl": "public-read"}))
assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body.String())
filer.mu.Lock()
assert.Nil(t, filer.entries["/buckets/"+postAuthzBucket+"/"+key])
filer.mu.Unlock()
rec = httptest.NewRecorder()
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAdminAccessKey, postAdminSecretKey, nil))
assert.Equal(t, http.StatusNoContent, rec.Code, "an upload without an ACL is not a PutObjectAcl, body: %s", rec.Body.String())
}
// newColdPostAclWriterServer is newPostAclWriterServer with an empty policy
// engine that only learns the bucket policy when the bucket loads.
func newColdPostAclWriterServer(t *testing.T, policy string) (*S3ApiServer, *ambiguousPutFiler) {
s3a, filer := newPostAclWriterServer(t)
s3a.policyEngine = NewBucketPolicyEngine()
s3a.iam.policyEngine = s3a.policyEngine
s3a.iam.primeBucketForIAM = func(bucket string) s3err.ErrorCode {
_, code := s3a.getBucketConfig(bucket)
return code
}
if policy != "" {
filer.mu.Lock()
filer.entries["/buckets/"+postAuthzBucket].Extended[BUCKET_POLICY_METADATA_KEY] = []byte(policy)
filer.mu.Unlock()
}
return s3a, filer
}
// With authentication on, a persisted bucket policy also governs a POST
// upload ACL on the first request after start.
func TestPostPolicyBucketHandlerAclPolicyOnColdBucket(t *testing.T) {
const key = "allowed/report.txt"
aclField := map[string]string{"acl": "public-read"}
s3a, _ := newColdPostAclWriterServer(t, "")
rec := httptest.NewRecorder()
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAclAccessKey, postAclSecretKey, aclField))
assert.Equal(t, http.StatusNoContent, rec.Code, "body: %s", rec.Body.String())
s3a, filer := newColdPostAclWriterServer(t, fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [{"Effect": "Deny", "Principal": {"AWS": "%s"}, "Action": ["s3:PutObjectAcl"], "Resource": ["arn:aws:s3:::%s/*"]}]
}`, postAclPrincipal, postAuthzBucket))
rec = httptest.NewRecorder()
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAclAccessKey, postAclSecretKey, aclField))
assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body.String())
filer.mu.Lock()
assert.Nil(t, filer.entries["/buckets/"+postAuthzBucket+"/"+key])
filer.mu.Unlock()
rec = httptest.NewRecorder()
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAclAccessKey, postAclSecretKey, nil))
assert.Equal(t, http.StatusNoContent, rec.Code, "an upload without an ACL is not a PutObjectAcl, body: %s", rec.Body.String())
}
// A bucket that fails to load cannot have its policy evaluated, so the
// upload stops instead of falling back to the identity's grants.
func TestPostPolicyBucketHandlerStopsWhenBucketLoadFails(t *testing.T) {
const key = "allowed/report.txt"
s3a, filer := newColdPostAclWriterServer(t, "")
s3a.iam.primeBucketForIAM = func(string) s3err.ErrorCode { return s3err.ErrInternalError }
rec := httptest.NewRecorder()
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, key, postAclAccessKey, postAclSecretKey, nil))
assert.Equal(t, http.StatusInternalServerError, rec.Code, "body: %s", rec.Body.String())
filer.mu.Lock()
assert.Nil(t, filer.entries["/buckets/"+postAuthzBucket+"/"+key])
filer.mu.Unlock()
}
func TestPostPolicyBucketHandlerRejectsConflictingAclFields(t *testing.T) {
for _, fields := range []map[string]string{
{"acl": "private", "x-amz-acl": "public-read"},
{"acl": "public-read", "x-amz-acl": ""},
{"acl": "", "x-amz-acl": "public-read"},
} {
s3a, filer := newPostAclWriterServer(t)
rec := httptest.NewRecorder()
s3a.PostPolicyBucketHandler(rec, newSignedPostRequestFor(t, "allowed/report.txt", postAclAccessKey, postAclSecretKey, fields))
assert.Equal(t, http.StatusBadRequest, rec.Code, "fields %v, body: %s", fields, rec.Body.String())
filer.mu.Lock()
assert.Nil(t, filer.entries["/buckets/"+postAuthzBucket+"/allowed/report.txt"], "fields %v", fields)
filer.mu.Unlock()
}
}
+38 -5
View File
@@ -176,12 +176,45 @@ func (s3a *S3ApiServer) preparePutObjectACL(r *http.Request, bucket string) (*ht
if policyCode != s3err.ErrNone {
return r, policyCode
}
for _, action := range []Action{s3_constants.ACTION_WRITE, s3_constants.ACTION_WRITE_ACP} {
if policyCode, _ := s3a.checkPolicyWithEntry(policyRequest, bucket, object, string(action), "", nil); policyCode != s3err.ErrNone {
return r, policyCode
}
if policyCode = s3a.checkPutObjectACLPolicy(policyRequest, bucket, object); policyCode != s3err.ErrNone {
return r, policyCode
}
}
return s3a.attachPutObjectACL(r, aclRequest, metadata, accountID, canned, custom)
}
// checkPutObjectACLPolicy applies bucket policies to an upload ACL when
// authentication is off and no IAM check covered it.
func (s3a *S3ApiServer) checkPutObjectACLPolicy(policyRequest *http.Request, bucket, object string) s3err.ErrorCode {
for _, action := range []Action{s3_constants.ACTION_WRITE, s3_constants.ACTION_WRITE_ACP} {
if code, _ := s3a.checkPolicyWithEntry(policyRequest, bucket, object, string(action), "", nil); code != s3err.ErrNone {
return code
}
}
return s3err.ErrNone
}
// preparePostObjectACL builds the upload ACL from the acl and x-amz-grant-*
// form fields promoted onto the request. PostPolicyBucketHandler has already
// authorized them for the policy signer.
func (s3a *S3ApiServer) preparePostObjectACL(r *http.Request, bucket string) (*http.Request, s3err.ErrorCode) {
metadata, code := s3a.getBucketConfig(bucket)
if code != s3err.ErrNone {
return r, code
}
if metadata == nil || s3a.iam == nil {
return r, s3err.ErrInternalError
}
custom := false
for _, header := range []string{s3_constants.AmzAclFullControl, s3_constants.AmzAclRead, s3_constants.AmzAclReadAcp, s3_constants.AmzAclWrite, s3_constants.AmzAclWriteAcp} {
if r.Header.Get(header) != "" {
custom = true
}
}
return s3a.attachPutObjectACL(r, r.Clone(r.Context()), metadata, s3a.requestAccountId(r), r.Header.Get(s3_constants.AmzCannedAcl), custom)
}
func (s3a *S3ApiServer) attachPutObjectACL(r, aclRequest *http.Request, metadata *BucketConfig, accountID, canned string, custom bool) (*http.Request, s3err.ErrorCode) {
if accountID == "" {
return r, s3err.ErrAccessDenied
}
@@ -253,7 +286,7 @@ func (s3a *S3ApiServer) preparePutObjectACL(r *http.Request, bucket string) (*ht
}
// applyPutObjectACL adds prevalidated ownership and grants before CreateEntry.
// Multipart parts and POST form uploads do not carry this PutObject context.
// Multipart parts do not carry this PutObject context.
func applyPutObjectACL(r *http.Request, entry *filer_pb.Entry) {
metadata, _ := r.Context().Value(putObjectACLContextKey{}).(putObjectACLMetadata)
for key, value := range metadata.extended {
+3 -2
View File
@@ -399,8 +399,9 @@ func NewS3ApiServerWithStore(router *mux.Router, option *S3ApiServerOption, expl
// lazy bucket load (and with it the mirror backfill): a grant carried
// only by a not-yet-mirrored policy would otherwise deny forever, and
// the denied request never reaches the handlers that load the bucket.
iam.primeBucketForIAM = func(bucket string) {
s3ApiServer.getBucketConfig(bucket)
iam.primeBucketForIAM = func(bucket string) s3err.ErrorCode {
_, code := s3ApiServer.getBucketConfig(bucket)
return code
}
// Initialize advanced IAM system if config is provided or explicitly enabled