mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-12 23:09:33 +01:00
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:
1 parent
eb159f3181
commit
6c1eec8703
7 files changed
+325
-33
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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] {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user