diff --git a/s3api/controllers/bucket-post.go b/s3api/controllers/bucket-post.go index 721ad7cf..2c293725 100644 --- a/s3api/controllers/bucket-post.go +++ b/s3api/controllers/bucket-post.go @@ -220,6 +220,15 @@ func (c S3ApiController) POSTObject(ctx fiber.Ctx) (*Response, error) { }, err } + err = auth.CheckObjectAccess(ctx, bucket, acct, []types.ObjectIdentifier{{Key: &key}}, auth.BypassOverwrite, IsBucketPublic, c.be, c.iam, true) + if err != nil { + return &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: parsedAcl.Owner, + }, + }, err + } + res, err := c.be.PutObject(ctx.RequestCtx(), s3response.PutObjectInput{ Bucket: &bucket, Key: &key, diff --git a/s3api/controllers/bucket-post_test.go b/s3api/controllers/bucket-post_test.go index eba1683b..c8fe4039 100644 --- a/s3api/controllers/bucket-post_test.go +++ b/s3api/controllers/bucket-post_test.go @@ -484,6 +484,27 @@ func TestS3ApiController_POSTObject(t *testing.T) { err: s3err.GetMetadataTooLargeErr(2053, 2048), }, }, + { + name: "overwriting a locked object", + input: testInput{ + // object lock enabled on the bucket; the mock reports a + // legal hold on every object + extraMockResp: []byte(`{"Enabled":true}`), + locals: postObjectLocalsForTest(middlewares.PostObjectResult{ + Fields: baseFields, + FileRdr: newMockFileReader("payload"), + ContentLength: int64(len("payload")), + }), + }, + output: testOutput{ + response: &Response{ + MetaOpts: &MetaOptions{ + BucketOwner: "root", + }, + }, + err: s3err.GetAPIError(s3err.ErrObjectLocked), + }, + }, { name: "backend returns error", input: testInput{ @@ -734,6 +755,22 @@ func TestS3ApiController_POSTObject(t *testing.T) { GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { return nil, s3err.GetAPIError(s3err.ErrAccessDenied) }, + GetBucketVersioningFunc: func(contextMoqParam context.Context, bucket string) (s3response.GetBucketVersioningOutput, error) { + return s3response.GetBucketVersioningOutput{}, s3err.GetAPIError(s3err.ErrNotImplemented) + }, + GetObjectLockConfigurationFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) { + if tt.input.extraMockResp != nil { + return tt.input.extraMockResp.([]byte), nil + } + return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) + }, + GetObjectRetentionFunc: func(contextMoqParam context.Context, bucket, object, versionId string) ([]byte, error) { + return nil, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration) + }, + GetObjectLegalHoldFunc: func(contextMoqParam context.Context, bucket, object, versionId string) (*bool, error) { + legalHold := true + return &legalHold, nil + }, } ctrl := S3ApiController{ diff --git a/tests/integration/WORM_protection.go b/tests/integration/WORM_protection.go index 72b12fd0..a4b6f78a 100644 --- a/tests/integration/WORM_protection.go +++ b/tests/integration/WORM_protection.go @@ -15,8 +15,11 @@ package integration import ( + "bytes" "context" "fmt" + "io" + "net/http" "time" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -52,7 +55,7 @@ func WORMProtection_bucket_object_lock_configuration_compliance_mode(s *S3Conf) return err } - if err := checkWORMProtection(s3client, bucket, object); err != nil { + if err := checkWORMProtection(s, s3client, bucket, object); err != nil { return err } return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: object, isCompliance: true}}) @@ -87,7 +90,7 @@ func WORMProtection_bucket_object_lock_configuration_governance_mode(s *S3Conf) return err } - if err := checkWORMProtection(s3client, bucket, object); err != nil { + if err := checkWORMProtection(s, s3client, bucket, object); err != nil { return err } return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: object}}) @@ -287,7 +290,7 @@ func WORMProtection_object_lock_retention_compliance_locked(s *S3Conf) error { return err } - if err := checkWORMProtection(s3client, bucket, object); err != nil { + if err := checkWORMProtection(s, s3client, bucket, object); err != nil { return err } @@ -320,7 +323,7 @@ func WORMProtection_object_lock_retention_governance_locked(s *S3Conf) error { return err } - if err := checkWORMProtection(s3client, bucket, object); err != nil { + if err := checkWORMProtection(s, s3client, bucket, object); err != nil { return err } return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: object}}) @@ -473,6 +476,77 @@ func WORMProtection_object_lock_retention_governance_bypass_overwrite_copy(s *S3 }, withLock()) } +func WORMProtection_object_lock_retention_governance_bypass_overwrite_post(s *S3Conf) error { + testName := "WORMProtection_object_lock_retention_governance_bypass_overwrite_post" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + object := "my-obj" + + _, err := putObjects(s3client, []string{object}, bucket) + if err != nil { + return err + } + + err = lockObject(s3client, objectLockModeGovernance, bucket, object, "") + if err != nil { + return err + } + + policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, s.awsID), `["s3:BypassGovernanceRetention"]`, fmt.Sprintf(`"arn:aws:s3:::%v/*"`, bucket)) + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{ + Bucket: &bucket, + Policy: &policy, + }) + cancel() + if err != nil { + return err + } + + // overwrite the locked object with a new object with POST object + data := []byte("new object data") + resp, err := sendPostObject(PostRequestConfig{ + bucket: bucket, + key: object, + s3Conf: s, + fileContent: data, + }) + if err != nil { + return err + } + resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("expected status 204, instead got %d", resp.StatusCode) + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + defer cancel() + out, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &object, + }) + if err != nil { + return err + } + defer out.Body.Close() + + gotData, err := io.ReadAll(out.Body) + if err != nil { + return err + } + + if getString(out.ETag) != resp.Header.Get("ETag") { + return fmt.Errorf("expected the object ETag to be %s, instead got %s", resp.Header.Get("ETag"), getString(out.ETag)) + } + if !bytes.Equal(gotData, data) { + return fmt.Errorf("expected the object data to be %q, instead got %q", data, gotData) + } + + return nil + }, withLock()) +} + func WORMProtection_unable_to_overwrite_locked_object_put(s *S3Conf) error { testName := "WORMProtection_unable_to_overwrite_locked_object_put" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { @@ -597,6 +671,43 @@ func WORMProtection_unable_to_overwrite_locked_object_mp(s *S3Conf) error { }, withLock()) } +func WORMProtection_unable_to_overwrite_locked_object_post(s *S3Conf) error { + testName := "WORMProtection_unable_to_overwrite_locked_object_post" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + object := "my-obj" + + _, err := putObjects(s3client, []string{object}, bucket) + if err != nil { + return err + } + + err = lockObject(s3client, objectLockModeLegalHold, bucket, object, "") + if err != nil { + return err + } + + // overwrite the locked object with a new object with POST object + resp, err := sendPostObject(PostRequestConfig{ + bucket: bucket, + key: object, + s3Conf: s, + fileContent: []byte("new object data"), + }) + if err != nil { + return err + } + if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil { + return err + } + return cleanupLockedObjects(s3client, bucket, []objToDelete{ + { + key: object, + removeOnlyLeglHold: true, + }, + }) + }, withLock()) +} + func WORMProtection_object_lock_retention_governance_bypass_delete(s *S3Conf) error { testName := "WORMProtection_object_lock_retention_governance_bypass_delete" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { @@ -766,7 +877,7 @@ func WORMProtection_root_bypass_governance_retention_delete_object(s *S3Conf) er return err } - if err := checkWORMProtection(s3client, bucket, obj); err != nil { + if err := checkWORMProtection(s, s3client, bucket, obj); err != nil { return err } diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index cf3493c6..9e74c831 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -896,9 +896,11 @@ func TestWORMProtection(ts *TestState) { ts.Run(WORMProtection_object_lock_retention_governance_bypass_overwrite_put) ts.Run(WORMProtection_object_lock_retention_governance_bypass_overwrite_copy) ts.Run(WORMProtection_object_lock_retention_governance_bypass_overwrite_mp) + ts.Run(WORMProtection_object_lock_retention_governance_bypass_overwrite_post) ts.Run(WORMProtection_unable_to_overwrite_locked_object_put) ts.Run(WORMProtection_unable_to_overwrite_locked_object_copy) ts.Run(WORMProtection_unable_to_overwrite_locked_object_mp) + ts.Run(WORMProtection_unable_to_overwrite_locked_object_post) ts.Run(WORMProtection_object_lock_retention_governance_bypass_delete) ts.Run(WORMProtection_object_lock_retention_governance_bypass_delete_mul) ts.Run(WORMProtection_object_lock_legal_hold_locked) @@ -3435,9 +3437,11 @@ func GetIntTests() IntTests { "WORMProtection_object_lock_retention_governance_bypass_overwrite_put": WORMProtection_object_lock_retention_governance_bypass_overwrite_put, "WORMProtection_object_lock_retention_governance_bypass_overwrite_copy": WORMProtection_object_lock_retention_governance_bypass_overwrite_copy, "WORMProtection_object_lock_retention_governance_bypass_overwrite_mp": WORMProtection_object_lock_retention_governance_bypass_overwrite_mp, + "WORMProtection_object_lock_retention_governance_bypass_overwrite_post": WORMProtection_object_lock_retention_governance_bypass_overwrite_post, "WORMProtection_unable_to_overwrite_locked_object_put": WORMProtection_unable_to_overwrite_locked_object_put, "WORMProtection_unable_to_overwrite_locked_object_copy": WORMProtection_unable_to_overwrite_locked_object_copy, "WORMProtection_unable_to_overwrite_locked_object_mp": WORMProtection_unable_to_overwrite_locked_object_mp, + "WORMProtection_unable_to_overwrite_locked_object_post": WORMProtection_unable_to_overwrite_locked_object_post, "WORMProtection_object_lock_retention_governance_bypass_delete": WORMProtection_object_lock_retention_governance_bypass_delete, "WORMProtection_object_lock_retention_governance_bypass_delete_mul": WORMProtection_object_lock_retention_governance_bypass_delete_mul, "WORMProtection_object_lock_legal_hold_locked": WORMProtection_object_lock_legal_hold_locked, diff --git a/tests/integration/utils.go b/tests/integration/utils.go index e2a9650c..d7156c6b 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -2253,7 +2253,7 @@ func putBucketVersioningStatus(client *s3.Client, bucket string, status types.Bu return err } -func checkWORMProtection(client *s3.Client, bucket, object string) error { +func checkWORMProtection(s *S3Conf, client *s3.Client, bucket, object string) error { ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) _, err := client.PutObject(ctx, &s3.PutObjectInput{ Bucket: &bucket, @@ -2264,6 +2264,19 @@ func checkWORMProtection(client *s3.Client, bucket, object string) error { return err } + resp, err := sendPostObject(PostRequestConfig{ + bucket: bucket, + key: object, + s3Conf: s, + fileContent: []byte("overwrite"), + }) + if err != nil { + return err + } + if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil { + return fmt.Errorf("POST object overwrite: %w", err) + } + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) _, err = client.DeleteObject(ctx, &s3.DeleteObjectInput{ Bucket: &bucket,