mirror of
https://github.com/versity/versitygw.git
synced 2026-07-19 22:42:20 +00:00
feat: implement x-amz-source-expected-bucket-owner for CopyObject and UploadPartCopy
Closes #1897 Extract the `X-Amz-Source-Expected-Bucket-Owner` header for CopyObject and UploadPartCopy. Verify the source bucket owner in the backend and if the provided access key id doesn't match, return an `AccessDenied` error.
This commit is contained in:
@@ -956,6 +956,20 @@ func (az *Azure) CopyObject(ctx context.Context, input s3response.CopyObjectInpu
|
||||
return s3response.CopyObjectOutput{}, err
|
||||
}
|
||||
|
||||
if input.ExpectedSourceBucketOwner != nil && *input.ExpectedSourceBucketOwner != "" {
|
||||
aclData, err := az.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: &srcBucket})
|
||||
if err != nil {
|
||||
return s3response.CopyObjectOutput{}, err
|
||||
}
|
||||
srcAcl, err := auth.ParseACL(aclData)
|
||||
if err != nil {
|
||||
return s3response.CopyObjectOutput{}, err
|
||||
}
|
||||
if srcAcl.Owner != *input.ExpectedSourceBucketOwner {
|
||||
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
}
|
||||
|
||||
if !areNils(input.CopySourceIfMatch, input.CopySourceIfNoneMatch) || !areNils(input.CopySourceIfModifiedSince, input.CopySourceIfUnmodifiedSince) {
|
||||
_, err = az.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
Bucket: &srcBucket,
|
||||
|
||||
@@ -3053,6 +3053,20 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput)
|
||||
return s3response.CopyPartResult{}, fmt.Errorf("stat bucket: %w", err)
|
||||
}
|
||||
|
||||
if upi.ExpectedSourceBucketOwner != nil && *upi.ExpectedSourceBucketOwner != "" {
|
||||
aclData, err := p.meta.RetrieveAttribute(nil, srcBucket, "", aclkey)
|
||||
if err != nil {
|
||||
return s3response.CopyPartResult{}, fmt.Errorf("get src bucket acl: %w", err)
|
||||
}
|
||||
srcAcl, err := auth.ParseACL(aclData)
|
||||
if err != nil {
|
||||
return s3response.CopyPartResult{}, err
|
||||
}
|
||||
if srcAcl.Owner != *upi.ExpectedSourceBucketOwner {
|
||||
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
}
|
||||
|
||||
vStatus, err := p.getBucketVersioningStatus(ctx, srcBucket)
|
||||
if err != nil {
|
||||
return s3response.CopyPartResult{}, err
|
||||
@@ -4674,6 +4688,20 @@ func (p *Posix) CopyObject(ctx context.Context, input s3response.CopyObjectInput
|
||||
return s3response.CopyObjectOutput{}, fmt.Errorf("stat bucket: %w", err)
|
||||
}
|
||||
|
||||
if input.ExpectedSourceBucketOwner != nil && *input.ExpectedSourceBucketOwner != "" {
|
||||
aclData, err := p.meta.RetrieveAttribute(nil, srcBucket, "", aclkey)
|
||||
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
||||
return s3response.CopyObjectOutput{}, fmt.Errorf("get src bucket acl: %w", err)
|
||||
}
|
||||
srcAcl, err := auth.ParseACL(aclData)
|
||||
if err != nil {
|
||||
return s3response.CopyObjectOutput{}, err
|
||||
}
|
||||
if srcAcl.Owner != *input.ExpectedSourceBucketOwner {
|
||||
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
}
|
||||
}
|
||||
|
||||
vStatus, err := p.getBucketVersioningStatus(ctx, srcBucket)
|
||||
if err != nil {
|
||||
return s3response.CopyObjectOutput{}, err
|
||||
|
||||
@@ -360,6 +360,7 @@ func (c S3ApiController) UploadPartCopy(ctx *fiber.Ctx) (*Response, error) {
|
||||
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
|
||||
copySource := strings.TrimPrefix(ctx.Get("X-Amz-Copy-Source"), "/")
|
||||
copySrcRange := ctx.Get("X-Amz-Copy-Source-Range")
|
||||
expectedSrcBucketOwnerUPC := ctx.Get("X-Amz-Source-Expected-Bucket-Owner")
|
||||
partNumber := int32(ctx.QueryInt("partNumber", -1))
|
||||
uploadId := ctx.Query("uploadId")
|
||||
// context locals
|
||||
@@ -429,6 +430,7 @@ func (c S3ApiController) UploadPartCopy(ctx *fiber.Ctx) (*Response, error) {
|
||||
CopySourceIfNoneMatch: preconditionHdrs.IfNoneMatch,
|
||||
CopySourceIfModifiedSince: preconditionHdrs.IfModSince,
|
||||
CopySourceIfUnmodifiedSince: preconditionHdrs.IfUnmodeSince,
|
||||
ExpectedSourceBucketOwner: &expectedSrcBucketOwnerUPC,
|
||||
})
|
||||
var headers map[string]*string
|
||||
if err == nil && resp.CopySourceVersionId != "" {
|
||||
@@ -500,6 +502,7 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
key := strings.TrimPrefix(ctx.Path(), fmt.Sprintf("/%s/", bucket))
|
||||
copySource := strings.TrimPrefix(ctx.Get("X-Amz-Copy-Source"), "/")
|
||||
expectedSrcBucketOwner := ctx.Get("X-Amz-Source-Expected-Bucket-Owner")
|
||||
metaDirective := types.MetadataDirective(ctx.Get("X-Amz-Metadata-Directive", string(types.MetadataDirectiveCopy)))
|
||||
taggingDirective := types.TaggingDirective(ctx.Get("X-Amz-Tagging-Directive", string(types.TaggingDirectiveCopy)))
|
||||
contentType := ctx.Get("Content-Type", defaultContentType)
|
||||
@@ -626,6 +629,7 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
CopySourceIfModifiedSince: preconditionHdrs.IfModSince,
|
||||
CopySourceIfUnmodifiedSince: preconditionHdrs.IfUnmodeSince,
|
||||
ExpectedBucketOwner: &acct.Access,
|
||||
ExpectedSourceBucketOwner: &expectedSrcBucketOwner,
|
||||
Metadata: metadata,
|
||||
MetadataDirective: metaDirective,
|
||||
StorageClass: types.StorageClass(storageClass),
|
||||
|
||||
@@ -1703,3 +1703,33 @@ func CopyObject_object_acl_not_supported(s *S3Conf) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CopyObject_incorrect_source_bucket_expected_owner(s *S3Conf) error {
|
||||
testName := "CopyObject_incorrect_source_bucket_expected_owner"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
srcBucket := getBucketName()
|
||||
err := setup(s, srcBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = teardown(s, srcBucket)
|
||||
}()
|
||||
|
||||
srcObj := "my-obj"
|
||||
_, err = putObjects(s3client, []string{srcObj}, srcBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: getPtr("dst-obj"),
|
||||
CopySource: getPtr(fmt.Sprintf("%v/%v", srcBucket, srcObj)),
|
||||
ExpectedSourceBucketOwner: getPtr("incorrect-owner"),
|
||||
})
|
||||
cancel()
|
||||
return checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1020,3 +1020,41 @@ func UploadPartCopy_should_calculate_the_checksum(s *S3Conf) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func UploadPartCopy_incorrect_source_bucket_expected_owner(s *S3Conf) error {
|
||||
testName := "UploadPartCopy_incorrect_source_bucket_expected_owner"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
srcBucket := getBucketName()
|
||||
err := setup(s, srcBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = teardown(s, srcBucket)
|
||||
}()
|
||||
|
||||
srcObj := "src-obj"
|
||||
_, err = putObjects(s3client, []string{srcObj}, srcBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj := "my-obj"
|
||||
mp, err := createMp(s3client, bucket, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
CopySource: getPtr(fmt.Sprintf("%v/%v", srcBucket, srcObj)),
|
||||
UploadId: mp.UploadId,
|
||||
PartNumber: getPtr(int32(1)),
|
||||
ExpectedSourceBucketOwner: getPtr("incorrect-owner"),
|
||||
})
|
||||
cancel()
|
||||
return checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -362,6 +362,7 @@ func TestCopyObject(ts *TestState) {
|
||||
}
|
||||
ts.Run(CopyObject_with_special_characters)
|
||||
ts.Run(CopyObject_success)
|
||||
ts.Run(CopyObject_incorrect_source_bucket_expected_owner)
|
||||
}
|
||||
|
||||
func TestPutObjectTagging(ts *TestState) {
|
||||
@@ -451,6 +452,7 @@ func TestUploadPartCopy(ts *TestState) {
|
||||
ts.Run(UploadPartCopy_should_calculate_the_checksum)
|
||||
ts.Run(UploadPartCopy_conditional_reads)
|
||||
}
|
||||
ts.Run(UploadPartCopy_incorrect_source_bucket_expected_owner)
|
||||
}
|
||||
|
||||
func TestListParts(ts *TestState) {
|
||||
@@ -1489,6 +1491,7 @@ func GetIntTests() IntTests {
|
||||
"CopyObject_to_itself_by_replacing_the_checksum": CopyObject_to_itself_by_replacing_the_checksum,
|
||||
"CopyObject_with_special_characters": CopyObject_with_special_characters,
|
||||
"CopyObject_success": CopyObject_success,
|
||||
"CopyObject_incorrect_source_bucket_expected_owner": CopyObject_incorrect_source_bucket_expected_owner,
|
||||
"PutObjectTagging_non_existing_object": PutObjectTagging_non_existing_object,
|
||||
"PutObjectTagging_long_tags": PutObjectTagging_long_tags,
|
||||
"PutObjectTagging_duplicate_keys": PutObjectTagging_duplicate_keys,
|
||||
@@ -1548,6 +1551,7 @@ func GetIntTests() IntTests {
|
||||
"UploadPartCopy_greater_range_than_obj_size": UploadPartCopy_greater_range_than_obj_size,
|
||||
"UploadPartCopy_by_range_success": UploadPartCopy_by_range_success,
|
||||
"UploadPartCopy_conditional_reads": UploadPartCopy_conditional_reads,
|
||||
"UploadPartCopy_incorrect_source_bucket_expected_owner": UploadPartCopy_incorrect_source_bucket_expected_owner,
|
||||
"UploadPartCopy_should_copy_the_checksum": UploadPartCopy_should_copy_the_checksum,
|
||||
"UploadPartCopy_should_not_copy_the_checksum": UploadPartCopy_should_not_copy_the_checksum,
|
||||
"UploadPartCopy_should_calculate_the_checksum": UploadPartCopy_should_calculate_the_checksum,
|
||||
|
||||
Reference in New Issue
Block a user