mirror of
https://github.com/versity/versitygw.git
synced 2026-09-25 09:24:22 +00:00
Merge pull request #2434 from versity/sis/post-object-lock
fix: enforce object lock on browser-based `POST` object uploads
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -3437,9 +3439,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user