diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index 9fb00df081..ee9a48698b 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -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 { diff --git a/weed/s3api/policy/post-policy_test.go b/weed/s3api/policy/post-policy_test.go index 0856681550..ec56acb4cb 100644 --- a/weed/s3api/policy/post-policy_test.go +++ b/weed/s3api/policy/post-policy_test.go @@ -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") + } +} diff --git a/weed/s3api/policy/postpolicyform.go b/weed/s3api/policy/postpolicyform.go index 46bf380751..5e2071255b 100644 --- a/weed/s3api/policy/postpolicyform.go +++ b/weed/s3api/policy/postpolicyform.go @@ -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] { diff --git a/weed/s3api/s3api_object_handlers_postpolicy.go b/weed/s3api/s3api_object_handlers_postpolicy.go index b95f18fe6f..4c18e34ca1 100644 --- a/weed/s3api/s3api_object_handlers_postpolicy.go +++ b/weed/s3api/s3api_object_handlers_postpolicy.go @@ -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 } diff --git a/weed/s3api/s3api_object_handlers_postpolicy_authz_test.go b/weed/s3api/s3api_object_handlers_postpolicy_authz_test.go index 0206710067..b60fbe4f33 100644 --- a/weed/s3api/s3api_object_handlers_postpolicy_authz_test.go +++ b/weed/s3api/s3api_object_handlers_postpolicy_authz_test.go @@ -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() + } +} diff --git a/weed/s3api/s3api_object_upload_acl.go b/weed/s3api/s3api_object_upload_acl.go index b9dd038fe7..6bb52a781d 100644 --- a/weed/s3api/s3api_object_upload_acl.go +++ b/weed/s3api/s3api_object_upload_acl.go @@ -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 { diff --git a/weed/s3api/s3api_server.go b/weed/s3api/s3api_server.go index f054e32efc..abd5403bf3 100644 --- a/weed/s3api/s3api_server.go +++ b/weed/s3api/s3api_server.go @@ -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