From 7d368be82ede1c53e768bfbde7e77f091188381d Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Mon, 30 Sep 2024 17:26:49 -0400 Subject: [PATCH] feat: Implemented object locking for object versions --- auth/object_lock.go | 17 +- backend/posix/posix.go | 92 ++++++++- backend/scoutfs/scoutfs.go | 4 +- s3api/controllers/base.go | 6 +- tests/integration/group-tests.go | 21 ++ tests/integration/tests.go | 333 +++++++++++++++++++++++++++++++ 6 files changed, 461 insertions(+), 12 deletions(-) diff --git a/auth/object_lock.go b/auth/object_lock.go index 76286a0..41bb533 100644 --- a/auth/object_lock.go +++ b/auth/object_lock.go @@ -135,7 +135,7 @@ func ParseObjectLegalHoldOutput(status *bool) *types.ObjectLockLegalHold { } } -func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects []string, bypass bool, be backend.Backend) error { +func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects []types.ObjectIdentifier, bypass bool, be backend.Backend) error { data, err := be.GetObjectLockConfiguration(ctx, bucket) if err != nil { if errors.Is(err, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)) { @@ -171,8 +171,15 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ } for _, obj := range objects { + var key, versionId string + if obj.Key != nil { + key = *obj.Key + } + if obj.VersionId != nil { + versionId = *obj.VersionId + } checkRetention := true - retentionData, err := be.GetObjectRetention(ctx, bucket, obj, "") + retentionData, err := be.GetObjectRetention(ctx, bucket, key, versionId) if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchKey)) { continue } @@ -203,7 +210,7 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ if err != nil { return err } - err = VerifyBucketPolicy(policy, userAccess, bucket, obj, BypassGovernanceRetentionAction) + err = VerifyBucketPolicy(policy, userAccess, bucket, key, BypassGovernanceRetentionAction) if err != nil { return s3err.GetAPIError(s3err.ErrObjectLocked) } @@ -217,7 +224,7 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ checkLegalHold := true - status, err := be.GetObjectLegalHold(ctx, bucket, obj, "") + status, err := be.GetObjectLegalHold(ctx, bucket, key, versionId) if err != nil { if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)) { checkLegalHold = false @@ -243,7 +250,7 @@ func CheckObjectAccess(ctx context.Context, bucket, userAccess string, objects [ if err != nil { return err } - err = VerifyBucketPolicy(policy, userAccess, bucket, obj, BypassGovernanceRetentionAction) + err = VerifyBucketPolicy(policy, userAccess, bucket, key, BypassGovernanceRetentionAction) if err != nil { return s3err.GetAPIError(s3err.ErrObjectLocked) } diff --git a/backend/posix/posix.go b/backend/posix/posix.go index ba55b35..397398c 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -2792,7 +2792,7 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3. size := fi.Size() var objectLockLegalHoldStatus types.ObjectLockLegalHoldStatus - status, err := p.GetObjectLegalHold(ctx, bucket, object, "") + status, err := p.GetObjectLegalHold(ctx, bucket, object, *input.VersionId) if err == nil { if *status { objectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOn @@ -2803,7 +2803,7 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3. var objectLockMode types.ObjectLockMode var objectLockRetainUntilDate *time.Time - retention, err := p.GetObjectRetention(ctx, bucket, object, "") + retention, err := p.GetObjectRetention(ctx, bucket, object, *input.VersionId) if err == nil { var config types.ObjectLockRetention if err := json.Unmarshal(retention, &config); err == nil { @@ -3495,8 +3495,30 @@ func (p *Posix) PutObjectLegalHold(_ context.Context, bucket, object, versionId statusData = []byte{0} } + if versionId != "" { + if !p.versioningEnabled() { + //TODO: Maybe we need to return our custom error here? + return s3err.GetAPIError(s3err.ErrInvalidVersionId) + } + vId, err := p.meta.RetrieveAttribute(bucket, object, versionIdKey) + if errors.Is(err, fs.ErrNotExist) { + return s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if err != nil && !errors.Is(err, meta.ErrNoSuchKey) { + return fmt.Errorf("get obj versionId: %w", err) + } + + if string(vId) != versionId { + bucket = filepath.Join(p.versioningDir, bucket) + object = filepath.Join(genObjVersionKey(object), versionId) + } + } + err = p.meta.StoreAttribute(bucket, object, objectLegalHoldKey, statusData) if errors.Is(err, fs.ErrNotExist) { + if versionId != "" { + return s3err.GetAPIError(s3err.ErrInvalidVersionId) + } return s3err.GetAPIError(s3err.ErrNoSuchKey) } if err != nil { @@ -3515,8 +3537,30 @@ func (p *Posix) GetObjectLegalHold(_ context.Context, bucket, object, versionId return nil, fmt.Errorf("stat bucket: %w", err) } + if versionId != "" { + if !p.versioningEnabled() { + //TODO: Maybe we need to return our custom error here? + return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + } + vId, err := p.meta.RetrieveAttribute(bucket, object, versionIdKey) + if errors.Is(err, fs.ErrNotExist) { + return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if err != nil && !errors.Is(err, meta.ErrNoSuchKey) { + return nil, fmt.Errorf("get obj versionId: %w", err) + } + + if string(vId) != versionId { + bucket = filepath.Join(p.versioningDir, bucket) + object = filepath.Join(genObjVersionKey(object), versionId) + } + } + data, err := p.meta.RetrieveAttribute(bucket, object, objectLegalHoldKey) if errors.Is(err, fs.ErrNotExist) { + if versionId != "" { + return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + } return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, meta.ErrNoSuchKey) { @@ -3557,8 +3601,30 @@ func (p *Posix) PutObjectRetention(_ context.Context, bucket, object, versionId return s3err.GetAPIError(s3err.ErrInvalidBucketObjectLockConfiguration) } + if versionId != "" { + if !p.versioningEnabled() { + //TODO: Maybe we need to return our custom error here? + return s3err.GetAPIError(s3err.ErrInvalidVersionId) + } + vId, err := p.meta.RetrieveAttribute(bucket, object, versionIdKey) + if errors.Is(err, fs.ErrNotExist) { + return s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if err != nil && !errors.Is(err, meta.ErrNoSuchKey) { + return fmt.Errorf("get obj versionId: %w", err) + } + + if string(vId) != versionId { + bucket = filepath.Join(p.versioningDir, bucket) + object = filepath.Join(genObjVersionKey(object), versionId) + } + } + objectLockCfg, err := p.meta.RetrieveAttribute(bucket, object, objectRetentionKey) if errors.Is(err, fs.ErrNotExist) { + if versionId != "" { + return s3err.GetAPIError(s3err.ErrInvalidVersionId) + } return s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, meta.ErrNoSuchKey) { @@ -3604,8 +3670,30 @@ func (p *Posix) GetObjectRetention(_ context.Context, bucket, object, versionId return nil, fmt.Errorf("stat bucket: %w", err) } + if versionId != "" { + if !p.versioningEnabled() { + //TODO: Maybe we need to return our custom error here? + return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + } + vId, err := p.meta.RetrieveAttribute(bucket, object, versionIdKey) + if errors.Is(err, fs.ErrNotExist) { + return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) + } + if err != nil && !errors.Is(err, meta.ErrNoSuchKey) { + return nil, fmt.Errorf("get obj versionId: %w", err) + } + + if string(vId) != versionId { + bucket = filepath.Join(p.versioningDir, bucket) + object = filepath.Join(genObjVersionKey(object), versionId) + } + } + data, err := p.meta.RetrieveAttribute(bucket, object, objectRetentionKey) if errors.Is(err, fs.ErrNotExist) { + if versionId != "" { + return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + } return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, meta.ErrNoSuchKey) { diff --git a/backend/scoutfs/scoutfs.go b/backend/scoutfs/scoutfs.go index b9c608d..59c4444 100644 --- a/backend/scoutfs/scoutfs.go +++ b/backend/scoutfs/scoutfs.go @@ -554,7 +554,7 @@ func (s *ScoutFS) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s contentLength := fi.Size() var objectLockLegalHoldStatus types.ObjectLockLegalHoldStatus - status, err := s.Posix.GetObjectLegalHold(ctx, bucket, object, "") + status, err := s.Posix.GetObjectLegalHold(ctx, bucket, object, *input.VersionId) if err == nil { if *status { objectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOn @@ -565,7 +565,7 @@ func (s *ScoutFS) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s var objectLockMode types.ObjectLockMode var objectLockRetainUntilDate *time.Time - retention, err := s.Posix.GetObjectRetention(ctx, bucket, object, "") + retention, err := s.Posix.GetObjectRetention(ctx, bucket, object, *input.VersionId) if err == nil { var config types.ObjectLockRetention if err := json.Unmarshal(retention, &config); err == nil { diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index 03ee2cc..b2de22d 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -2243,7 +2243,7 @@ func (c S3ApiController) PutActions(ctx *fiber.Ctx) error { }) } - err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []string{keyStart}, true, c.be) + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []types.ObjectIdentifier{{Key: &keyStart}}, true, c.be) if err != nil { return SendResponse(ctx, err, &MetaOpts{ @@ -2527,7 +2527,7 @@ func (c S3ApiController) DeleteObjects(ctx *fiber.Ctx) error { // The AWS CLI sends 'True', while Go SDK sends 'true' bypass := strings.EqualFold(bypassHdr, "true") - err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, utils.ParseDeleteObjects(dObj.Objects), bypass, c.be) + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, dObj.Objects, bypass, c.be) if err != nil { return SendResponse(ctx, err, &MetaOpts{ @@ -2680,7 +2680,7 @@ func (c S3ApiController) DeleteActions(ctx *fiber.Ctx) error { // The AWS CLI sends 'True', while Go SDK sends 'true' bypass := strings.EqualFold(bypassHdr, "true") - err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []string{key}, bypass, c.be) + err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []types.ObjectIdentifier{{Key: &key, VersionId: &versionId}}, bypass, c.be) if err != nil { return SendResponse(ctx, err, &MetaOpts{ diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 0ceaa1c..c207081 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -553,6 +553,18 @@ func TestVersioning(s *S3Conf) { Versioning_Multipart_Upload_overwrite_an_object(s) Versioning_UploadPartCopy_non_existing_versionId(s) Versioning_UploadPartCopy_from_an_object_version(s) + // Object-Lock Retention + Versionsin_PutObjectRetention_invalid_versionId(s) + Versioning_GetObjectRetention_invalid_versionId(s) + Versioning_Put_GetObjectRetention_success(s) + // Object-Lock Legal hold + Versionsin_PutObjectLegalHold_invalid_versionId(s) + Versioning_GetObjectLegalHold_invalid_versionId(s) + Versioning_Put_GetObjectLegalHold_success(s) + // WORM protection + Versioning_WORM_obj_version_locked_with_legal_hold(s) + Versioning_WORM_obj_version_locked_with_governance_retention(s) + Versioning_WORM_obj_version_locked_with_compliance_retention(s) } type IntTests map[string]func(s *S3Conf) error @@ -900,5 +912,14 @@ func GetIntTests() IntTests { "Versioning_Multipart_Upload_overwrite_an_object": Versioning_Multipart_Upload_overwrite_an_object, "Versioning_UploadPartCopy_non_existing_versionId": Versioning_UploadPartCopy_non_existing_versionId, "Versioning_UploadPartCopy_from_an_object_version": Versioning_UploadPartCopy_from_an_object_version, + "Versionsin_PutObjectRetention_invalid_versionId": Versionsin_PutObjectRetention_invalid_versionId, + "Versioning_GetObjectRetention_invalid_versionId": Versioning_GetObjectRetention_invalid_versionId, + "Versioning_Put_GetObjectRetention_success": Versioning_Put_GetObjectRetention_success, + "Versionsin_PutObjectLegalHold_invalid_versionId": Versionsin_PutObjectLegalHold_invalid_versionId, + "Versioning_GetObjectLegalHold_invalid_versionId": Versioning_GetObjectLegalHold_invalid_versionId, + "Versioning_Put_GetObjectLegalHold_success": Versioning_Put_GetObjectLegalHold_success, + "Versioning_WORM_obj_version_locked_with_legal_hold": Versioning_WORM_obj_version_locked_with_legal_hold, + "Versioning_WORM_obj_version_locked_with_governance_retention": Versioning_WORM_obj_version_locked_with_governance_retention, + "Versioning_WORM_obj_version_locked_with_compliance_retention": Versioning_WORM_obj_version_locked_with_compliance_retention, } } diff --git a/tests/integration/tests.go b/tests/integration/tests.go index 9b6253f..e585582 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -11745,3 +11745,336 @@ func Versioning_UploadPartCopy_from_an_object_version(s *S3Conf) error { return nil }, withVersioning()) } + +func Versionsin_PutObjectRetention_invalid_versionId(s *S3Conf) error { + testName := "Versionsin_PutObjectRetention_invalid_versionId" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + _, err := createObjVersions(s3client, bucket, obj, 3) + if err != nil { + return err + } + + rDate := time.Now().Add(time.Hour * 48) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &obj, + VersionId: getPtr("invalid_versionId"), + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeGovernance, + RetainUntilDate: &rDate, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)); err != nil { + return err + } + + return nil + }, withLock(), withVersioning()) +} + +func Versioning_GetObjectRetention_invalid_versionId(s *S3Conf) error { + testName := "Versioning_GetObjectRetention_invalid_versionId" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + _, err := createObjVersions(s3client, bucket, obj, 3) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{ + Bucket: &bucket, + Key: &obj, + VersionId: getPtr("invalid_versionId"), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)); err != nil { + return err + } + + return nil + }, withLock(), withVersioning()) +} + +func Versioning_Put_GetObjectRetention_success(s *S3Conf) error { + testName := "Versioning_Put_GetObjectRetention_success" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + objVersions, err := createObjVersions(s3client, bucket, obj, 3) + if err != nil { + return err + } + objVersion := objVersions[1] + + rDate := time.Now().Add(time.Hour * 48) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &obj, + VersionId: objVersion.VersionId, + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeGovernance, + RetainUntilDate: &rDate, + }, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + res, err := s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{ + Bucket: &bucket, + Key: &obj, + VersionId: objVersion.VersionId, + }) + cancel() + if err != nil { + return err + } + + if res.Retention.Mode != types.ObjectLockRetentionModeGovernance { + return fmt.Errorf("expected the object retention mode to be %v, instead got %v", types.ObjectLockRetentionModeGovernance, res.Retention.Mode) + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }, withLock(), withVersioning()) +} + +func Versionsin_PutObjectLegalHold_invalid_versionId(s *S3Conf) error { + testName := "Versionsin_PutObjectLegalHold_invalid_versionId" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + _, err := createObjVersions(s3client, bucket, obj, 3) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: &obj, + VersionId: getPtr("invalid_versionId"), + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)); err != nil { + return err + } + + return nil + }, withLock(), withVersioning()) +} + +func Versioning_GetObjectLegalHold_invalid_versionId(s *S3Conf) error { + testName := "Versioning_GetObjectLegalHold_invalid_versionId" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + _, err := createObjVersions(s3client, bucket, obj, 3) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{ + Bucket: &bucket, + Key: &obj, + VersionId: getPtr("invalid_versionId"), + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)); err != nil { + return err + } + + return nil + }, withLock(), withVersioning()) +} + +func Versioning_Put_GetObjectLegalHold_success(s *S3Conf) error { + testName := "Versioning_Put_GetObjectLegalHold_success" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + objVersions, err := createObjVersions(s3client, bucket, obj, 3) + if err != nil { + return err + } + objVersion := objVersions[1] + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: &obj, + VersionId: objVersion.VersionId, + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + res, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{ + Bucket: &bucket, + Key: &obj, + VersionId: objVersion.VersionId, + }) + cancel() + if err != nil { + return err + } + + if res.LegalHold.Status != types.ObjectLockLegalHoldStatusOn { + return fmt.Errorf("expected the object version legal hold status to be %v, instead got %v", types.ObjectLockLegalHoldStatusOn, res.LegalHold.Status) + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }, withLock(), withVersioning()) +} + +func Versioning_WORM_obj_version_locked_with_legal_hold(s *S3Conf) error { + testName := "Versioning_WORM_obj_version_locked_with_legal_hold" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + objVersions, err := createObjVersions(s3client, bucket, obj, 2) + if err != nil { + return err + } + version := objVersions[1] + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{ + Bucket: &bucket, + Key: &obj, + VersionId: version.VersionId, + LegalHold: &types.ObjectLockLegalHold{ + Status: types.ObjectLockLegalHoldStatusOn, + }, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: &obj, + VersionId: version.VersionId, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil { + return err + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }, withLock(), withVersioning()) +} + +func Versioning_WORM_obj_version_locked_with_governance_retention(s *S3Conf) error { + testName := "Versioning_WORM_obj_version_locked_with_governance_retention" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + objVersions, err := createObjVersions(s3client, bucket, obj, 2) + if err != nil { + return err + } + version := objVersions[0] + + rDate := time.Now().Add(time.Hour * 48) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &obj, + VersionId: version.VersionId, + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeGovernance, + RetainUntilDate: &rDate, + }, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: &obj, + VersionId: version.VersionId, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil { + return err + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }, withLock(), withVersioning()) +} + +func Versioning_WORM_obj_version_locked_with_compliance_retention(s *S3Conf) error { + testName := "Versioning_WORM_obj_version_locked_with_compliance_retention" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + obj := "my-obj" + objVersions, err := createObjVersions(s3client, bucket, obj, 2) + if err != nil { + return err + } + version := objVersions[0] + + rDate := time.Now().Add(time.Hour * 48) + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{ + Bucket: &bucket, + Key: &obj, + VersionId: version.VersionId, + Retention: &types.ObjectLockRetention{ + Mode: types.ObjectLockRetentionModeCompliance, + RetainUntilDate: &rDate, + }, + }) + cancel() + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + _, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &bucket, + Key: &obj, + VersionId: version.VersionId, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil { + return err + } + + if err := changeBucketObjectLockStatus(s3client, bucket, false); err != nil { + return err + } + + return nil + }, withLock(), withVersioning()) +}