diff --git a/backend/azure/azure.go b/backend/azure/azure.go index aa77bfd..e5de128 100644 --- a/backend/azure/azure.go +++ b/backend/azure/azure.go @@ -951,6 +951,20 @@ func (az *Azure) PutObjectLockConfiguration(ctx context.Context, bucket string, return azureErrToS3Err(err) } + cfg, exists := props.Metadata[string(keyBucketLock)] + if !exists { + return s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotAllowed) + } + + var bucketLockCfg auth.BucketLockConfig + if err := json.Unmarshal([]byte(*cfg), &bucketLockCfg); err != nil { + return fmt.Errorf("unmarshal object lock config: %w", err) + } + + if !bucketLockCfg.Enabled { + return s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotAllowed) + } + props.Metadata[string(keyBucketLock)] = backend.GetStringPtr(string(config)) _, err = client.SetMetadata(ctx, &container.SetMetadataOptions{ diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 40540a2..c95f336 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -2207,6 +2207,23 @@ func (p *Posix) PutObjectLockConfiguration(_ context.Context, bucket string, con return fmt.Errorf("stat bucket: %w", err) } + cfg, err := p.meta.RetrieveAttribute(bucket, "", bucketLockKey) + if errors.Is(err, meta.ErrNoSuchKey) { + return s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotAllowed) + } + if err != nil { + return fmt.Errorf("get object lock config: %w", err) + } + + var bucketLockCfg auth.BucketLockConfig + if err := json.Unmarshal(cfg, &bucketLockCfg); err != nil { + return fmt.Errorf("unmarshal object lock config: %w", err) + } + + if !bucketLockCfg.Enabled { + return s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotAllowed) + } + if err := p.meta.StoreAttribute(bucket, "", bucketLockKey, config); err != nil { return fmt.Errorf("set object lock config: %w", err) } diff --git a/s3err/s3err.go b/s3err/s3err.go index 2a14e89..ad493f7 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -115,6 +115,7 @@ const ( ErrObjectLockConfigurationNotFound ErrNoSuchObjectLockConfiguration ErrInvalidBucketObjectLockConfiguration + ErrObjectLockConfigurationNotAllowed ErrObjectLocked ErrPastObjectLockRetainDate ErrNoSuchBucketPolicy @@ -430,6 +431,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Bucket is missing ObjectLockConfiguration", HTTPStatusCode: http.StatusBadRequest, }, + ErrObjectLockConfigurationNotAllowed: { + Code: "InvalidBucketState", + Description: "Object Lock configuration cannot be enabled on existing buckets", + HTTPStatusCode: http.StatusConflict, + }, ErrObjectLocked: { Code: "InvalidRequest", Description: "Object is WORM protected and cannot be overwritten", diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 646afe9..13dea4b 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -310,6 +310,7 @@ func TestDeleteBucketPolicy(s *S3Conf) { func TestPutObjectLockConfiguration(s *S3Conf) { PutObjectLockConfiguration_non_existing_bucket(s) PutObjectLockConfiguration_empty_config(s) + PutObjectLockConfiguration_not_enabled_on_bucket_creation(s) PutObjectLockConfiguration_both_years_and_days(s) PutObjectLockConfiguration_success(s) } @@ -628,6 +629,7 @@ func GetIntTests() IntTests { "DeleteBucketPolicy_success": DeleteBucketPolicy_success, "PutObjectLockConfiguration_non_existing_bucket": PutObjectLockConfiguration_non_existing_bucket, "PutObjectLockConfiguration_empty_config": PutObjectLockConfiguration_empty_config, + "PutObjectLockConfiguration_not_enabled_on_bucket_creation": PutObjectLockConfiguration_not_enabled_on_bucket_creation, "PutObjectLockConfiguration_both_years_and_days": PutObjectLockConfiguration_both_years_and_days, "PutObjectLockConfiguration_success": PutObjectLockConfiguration_success, "GetObjectLockConfiguration_non_existing_bucket": GetObjectLockConfiguration_non_existing_bucket, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index 84ff58b..b195d17 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -6343,6 +6343,30 @@ func PutObjectLockConfiguration_empty_config(s *S3Conf) error { }) } +func PutObjectLockConfiguration_not_enabled_on_bucket_creation(s *S3Conf) error { + testName := "PutObjectLockConfiguration_not_enabled_on_bucket_creation" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + var days int32 = 12 + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: &bucket, + ObjectLockConfiguration: &types.ObjectLockConfiguration{ + ObjectLockEnabled: types.ObjectLockEnabledEnabled, + Rule: &types.ObjectLockRule{ + DefaultRetention: &types.DefaultRetention{ + Days: &days, + }, + }, + }, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotAllowed)); err != nil { + return err + } + return nil + }) +} + func PutObjectLockConfiguration_both_years_and_days(s *S3Conf) error { testName := "PutObjectLockConfiguration_both_years_and_days" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { @@ -6383,7 +6407,7 @@ func PutObjectLockConfiguration_success(s *S3Conf) error { return err } return nil - }) + }, withLock()) } func GetObjectLockConfiguration_non_existing_bucket(s *S3Conf) error { @@ -6467,7 +6491,7 @@ func GetObjectLockConfiguration_success(s *S3Conf) error { } return nil - }) + }, withLock()) } func PutObjectRetention_non_existing_bucket(s *S3Conf) error { @@ -6510,7 +6534,7 @@ func PutObjectRetention_non_existing_object(s *S3Conf) error { } return nil - }) + }, withLock()) } func PutObjectRetention_unset_bucket_object_lock_config(s *S3Conf) error { @@ -6576,7 +6600,7 @@ func PutObjectRetention_disabled_bucket_object_lock_config(s *S3Conf) error { } return nil - }) + }, withLock()) } func PutObjectRetention_expired_retain_until_date(s *S3Conf) error { @@ -6603,7 +6627,7 @@ func PutObjectRetention_expired_retain_until_date(s *S3Conf) error { } return nil - }) + }, withLock()) } func PutObjectRetention_success(s *S3Conf) error { @@ -6639,7 +6663,7 @@ func PutObjectRetention_success(s *S3Conf) error { } return nil - }) + }, withLock()) } func GetObjectRetention_non_existing_bucket(s *S3Conf) error { @@ -6755,7 +6779,7 @@ func GetObjectRetention_success(s *S3Conf) error { } return nil - }) + }, withLock()) } func PutObjectLegalHold_non_existing_bucket(s *S3Conf) error { @@ -6796,7 +6820,7 @@ func PutObjectLegalHold_non_existing_object(s *S3Conf) error { } return nil - }) + }, withLock()) } func PutObjectLegalHold_invalid_body(s *S3Conf) error { @@ -6875,7 +6899,7 @@ func PutObjectLegalHold_disabled_bucket_object_lock_config(s *S3Conf) error { } return nil - }) + }, withLock()) } func PutObjectLegalHold_success(s *S3Conf) error { @@ -6909,7 +6933,7 @@ func PutObjectLegalHold_success(s *S3Conf) error { } return nil - }) + }, withLock()) } func GetObjectLegalHold_non_existing_bucket(s *S3Conf) error { @@ -7011,7 +7035,7 @@ func GetObjectLegalHold_success(s *S3Conf) error { } return nil - }) + }, withLock()) } func WORMProtection_bucket_object_lock_configuration_compliance_mode(s *S3Conf) error { @@ -7049,7 +7073,7 @@ func WORMProtection_bucket_object_lock_configuration_compliance_mode(s *S3Conf) } return nil - }) + }, withLock()) } func WORMProtection_bucket_object_lock_governance_root_overwrite(s *S3Conf) error { @@ -7090,7 +7114,7 @@ func WORMProtection_bucket_object_lock_governance_root_overwrite(s *S3Conf) erro } return nil - }) + }, withLock()) } func WORMProtection_object_lock_retention_compliance_root_access_denied(s *S3Conf) error { @@ -7129,7 +7153,7 @@ func WORMProtection_object_lock_retention_compliance_root_access_denied(s *S3Con } return nil - }) + }, withLock()) } func WORMProtection_object_lock_retention_governance_root_overwrite(s *S3Conf) error { @@ -7169,7 +7193,7 @@ func WORMProtection_object_lock_retention_governance_root_overwrite(s *S3Conf) e } return nil - }) + }, withLock()) } func WORMProtection_object_lock_retention_governance_user_access_denied(s *S3Conf) error { @@ -7226,7 +7250,7 @@ func WORMProtection_object_lock_retention_governance_user_access_denied(s *S3Con } return nil - }) + }, withLock()) } func WORMProtection_object_lock_legal_hold_user_access_denied(s *S3Conf) error { @@ -7281,7 +7305,7 @@ func WORMProtection_object_lock_legal_hold_user_access_denied(s *S3Conf) error { } return nil - }) + }, withLock()) } func WORMProtection_object_lock_legal_hold_root_overwrite(s *S3Conf) error { @@ -7319,7 +7343,7 @@ func WORMProtection_object_lock_legal_hold_root_overwrite(s *S3Conf) error { } return nil - }) + }, withLock()) } // Access control tests (with bucket ACLs and Policies) diff --git a/tests/integration/utils.go b/tests/integration/utils.go index 6c9c587..6dbc1e0 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -54,12 +54,18 @@ func getBucketName() string { return fmt.Sprintf("test-bucket-%v", bcktCount) } -func setup(s *S3Conf, bucket string) error { +func setup(s *S3Conf, bucket string, opts ...setupOpt) error { s3client := s3.NewFromConfig(s.Config()) + cfg := new(setupCfg) + for _, opt := range opts { + opt(cfg) + } + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) _, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{ - Bucket: &bucket, + Bucket: &bucket, + ObjectLockEnabledForBucket: &cfg.LockEnabled, }) cancel() return err @@ -113,10 +119,20 @@ func teardown(s *S3Conf, bucket string) error { return err } -func actionHandler(s *S3Conf, testName string, handler func(s3client *s3.Client, bucket string) error) error { +type setupCfg struct { + LockEnabled bool +} + +type setupOpt func(*setupCfg) + +func withLock() setupOpt { + return func(s *setupCfg) { s.LockEnabled = true } +} + +func actionHandler(s *S3Conf, testName string, handler func(s3client *s3.Client, bucket string) error, opts ...setupOpt) error { runF(testName) bucketName := getBucketName() - err := setup(s, bucketName) + err := setup(s, bucketName, opts...) if err != nil { failF("%v: failed to create a bucket: %v", testName, err) return fmt.Errorf("%v: failed to create a bucket: %w", testName, err)