diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 1af92fb..f5d91d6 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -1238,6 +1238,7 @@ func (p *Posix) CopyObject(ctx context.Context, input *s3.CopyObjectInput) (*s3. } dstBucket := *input.Bucket dstObject := *input.Key + owner := *input.ExpectedBucketOwner if fmt.Sprintf("%v/%v", srcBucket, srcObject) == fmt.Sprintf("%v/%v", dstBucket, dstObject) { return &s3.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidCopyDest) @@ -1259,6 +1260,22 @@ func (p *Posix) CopyObject(ctx context.Context, input *s3.CopyObjectInput) (*s3. return nil, fmt.Errorf("stat bucket: %w", err) } + dstBucketACLBytes, err := xattr.Get(dstBucket, aclkey) + if err != nil { + return nil, fmt.Errorf("get dst bucket acl tag: %w", err) + } + + var dstBucketACL auth.ACL + err = json.Unmarshal(dstBucketACLBytes, &dstBucketACL) + if err != nil { + return nil, fmt.Errorf("parse dst bucket acl: %w", err) + } + + err = auth.VerifyACL(dstBucketACL, dstBucket, owner, types.PermissionWrite, false) + if err != nil { + return nil, err + } + objPath := filepath.Join(srcBucket, srcObject) f, err := os.Open(objPath) if errors.Is(err, fs.ErrNotExist) { diff --git a/integration/action-tests.go b/integration/action-tests.go index 128e092..482e358 100644 --- a/integration/action-tests.go +++ b/integration/action-tests.go @@ -88,6 +88,7 @@ func TestDeleteObjects(s *S3Conf) { func TestCopyObject(s *S3Conf) { CopyObject_non_existing_dst_bucket(s) + CopyObject_not_owned_source_bucket(s) CopyObject_copy_to_itself(s) CopyObject_success(s) } diff --git a/integration/tests.go b/integration/tests.go index 9f7865d..f2601a8 100644 --- a/integration/tests.go +++ b/integration/tests.go @@ -1676,6 +1676,50 @@ func CopyObject_non_existing_dst_bucket(s *S3Conf) { }) } +func CopyObject_not_owned_source_bucket(s *S3Conf) { + testName := "CopyObject_not_owned_source_bucket" + actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + srcObj := "my-obj" + err := putObjects(s3client, []string{srcObj}, bucket) + if err != nil { + return err + } + + usr := user{ + access: "admin1", + secret: "admin1secret", + role: "admin", + } + + cfg := *s + cfg.awsID = usr.access + cfg.awsSecret = usr.secret + + err = createUsers(s, []user{usr}) + if err != nil { + return err + } + + dstBucket := getBucketName() + err = setup(&cfg, dstBucket) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{ + Bucket: &dstBucket, + Key: getPtr("obj-1"), + CopySource: getPtr(fmt.Sprintf("%v/%v", bucket, srcObj)), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil { + return err + } + return nil + }) +} + func CopyObject_copy_to_itself(s *S3Conf) { testName := "CopyObject_copy_to_itself" actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index 3199f79..2e131f7 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -598,6 +598,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { CopySourceIfNoneMatch: ©SrcIfNoneMatch, CopySourceIfModifiedSince: &mtime, CopySourceIfUnmodifiedSince: &umtime, + ExpectedBucketOwner: &access, }) if err == nil { return SendXMLResponse(ctx, res, err, &MetaOpts{