s3_versioning_object_lock_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. package s3api
  2. import (
  3. "context"
  4. "strings"
  5. "testing"
  6. "time"
  7. "github.com/aws/aws-sdk-go-v2/aws"
  8. "github.com/aws/aws-sdk-go-v2/service/s3"
  9. "github.com/aws/aws-sdk-go-v2/service/s3/types"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. )
  13. // TestVersioningWithObjectLockHeaders ensures that versioned objects properly
  14. // handle object lock headers in PUT requests and return them in HEAD/GET responses.
  15. // This test would have caught the bug where object lock metadata was not returned
  16. // in HEAD/GET responses.
  17. func TestVersioningWithObjectLockHeaders(t *testing.T) {
  18. client := getS3Client(t)
  19. bucketName := getNewBucketName()
  20. // Create bucket with object lock and versioning enabled
  21. createBucketWithObjectLock(t, client, bucketName)
  22. defer deleteBucket(t, client, bucketName)
  23. key := "versioned-object-with-lock"
  24. content1 := "version 1 content"
  25. content2 := "version 2 content"
  26. // PUT first version with object lock headers
  27. retainUntilDate1 := time.Now().Add(12 * time.Hour)
  28. putResp1, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
  29. Bucket: aws.String(bucketName),
  30. Key: aws.String(key),
  31. Body: strings.NewReader(content1),
  32. ObjectLockMode: types.ObjectLockModeGovernance,
  33. ObjectLockRetainUntilDate: aws.Time(retainUntilDate1),
  34. })
  35. require.NoError(t, err)
  36. require.NotNil(t, putResp1.VersionId)
  37. // PUT second version with different object lock settings
  38. retainUntilDate2 := time.Now().Add(24 * time.Hour)
  39. putResp2, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
  40. Bucket: aws.String(bucketName),
  41. Key: aws.String(key),
  42. Body: strings.NewReader(content2),
  43. ObjectLockMode: types.ObjectLockModeCompliance,
  44. ObjectLockRetainUntilDate: aws.Time(retainUntilDate2),
  45. ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
  46. })
  47. require.NoError(t, err)
  48. require.NotNil(t, putResp2.VersionId)
  49. require.NotEqual(t, *putResp1.VersionId, *putResp2.VersionId)
  50. // Test HEAD latest version returns correct object lock metadata
  51. t.Run("HEAD latest version", func(t *testing.T) {
  52. headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  53. Bucket: aws.String(bucketName),
  54. Key: aws.String(key),
  55. })
  56. require.NoError(t, err)
  57. // Should return metadata for version 2 (latest)
  58. assert.Equal(t, types.ObjectLockModeCompliance, headResp.ObjectLockMode)
  59. assert.NotNil(t, headResp.ObjectLockRetainUntilDate)
  60. assert.WithinDuration(t, retainUntilDate2, *headResp.ObjectLockRetainUntilDate, 5*time.Second)
  61. assert.Equal(t, types.ObjectLockLegalHoldStatusOn, headResp.ObjectLockLegalHoldStatus)
  62. })
  63. // Test HEAD specific version returns correct object lock metadata
  64. t.Run("HEAD specific version", func(t *testing.T) {
  65. headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  66. Bucket: aws.String(bucketName),
  67. Key: aws.String(key),
  68. VersionId: putResp1.VersionId,
  69. })
  70. require.NoError(t, err)
  71. // Should return metadata for version 1
  72. assert.Equal(t, types.ObjectLockModeGovernance, headResp.ObjectLockMode)
  73. assert.NotNil(t, headResp.ObjectLockRetainUntilDate)
  74. assert.WithinDuration(t, retainUntilDate1, *headResp.ObjectLockRetainUntilDate, 5*time.Second)
  75. // Version 1 was created without legal hold, so AWS S3 defaults it to "OFF"
  76. assert.Equal(t, types.ObjectLockLegalHoldStatusOff, headResp.ObjectLockLegalHoldStatus)
  77. })
  78. // Test GET latest version returns correct object lock metadata
  79. t.Run("GET latest version", func(t *testing.T) {
  80. getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
  81. Bucket: aws.String(bucketName),
  82. Key: aws.String(key),
  83. })
  84. require.NoError(t, err)
  85. defer getResp.Body.Close()
  86. // Should return metadata for version 2 (latest)
  87. assert.Equal(t, types.ObjectLockModeCompliance, getResp.ObjectLockMode)
  88. assert.NotNil(t, getResp.ObjectLockRetainUntilDate)
  89. assert.WithinDuration(t, retainUntilDate2, *getResp.ObjectLockRetainUntilDate, 5*time.Second)
  90. assert.Equal(t, types.ObjectLockLegalHoldStatusOn, getResp.ObjectLockLegalHoldStatus)
  91. })
  92. // Test GET specific version returns correct object lock metadata
  93. t.Run("GET specific version", func(t *testing.T) {
  94. getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
  95. Bucket: aws.String(bucketName),
  96. Key: aws.String(key),
  97. VersionId: putResp1.VersionId,
  98. })
  99. require.NoError(t, err)
  100. defer getResp.Body.Close()
  101. // Should return metadata for version 1
  102. assert.Equal(t, types.ObjectLockModeGovernance, getResp.ObjectLockMode)
  103. assert.NotNil(t, getResp.ObjectLockRetainUntilDate)
  104. assert.WithinDuration(t, retainUntilDate1, *getResp.ObjectLockRetainUntilDate, 5*time.Second)
  105. // Version 1 was created without legal hold, so AWS S3 defaults it to "OFF"
  106. assert.Equal(t, types.ObjectLockLegalHoldStatusOff, getResp.ObjectLockLegalHoldStatus)
  107. })
  108. }
  109. // waitForVersioningToBeEnabled polls the bucket versioning status until it's enabled
  110. // This helps avoid race conditions where object lock is configured but versioning
  111. // isn't immediately available
  112. func waitForVersioningToBeEnabled(t *testing.T, client *s3.Client, bucketName string) {
  113. timeout := time.Now().Add(10 * time.Second)
  114. for time.Now().Before(timeout) {
  115. resp, err := client.GetBucketVersioning(context.TODO(), &s3.GetBucketVersioningInput{
  116. Bucket: aws.String(bucketName),
  117. })
  118. if err == nil && resp.Status == types.BucketVersioningStatusEnabled {
  119. return // Versioning is enabled
  120. }
  121. time.Sleep(100 * time.Millisecond)
  122. }
  123. t.Fatalf("Timeout waiting for versioning to be enabled on bucket %s", bucketName)
  124. }
  125. // Helper function for creating buckets with object lock enabled
  126. func createBucketWithObjectLock(t *testing.T, client *s3.Client, bucketName string) {
  127. _, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
  128. Bucket: aws.String(bucketName),
  129. ObjectLockEnabledForBucket: aws.Bool(true),
  130. })
  131. require.NoError(t, err)
  132. // Wait for versioning to be automatically enabled by object lock
  133. waitForVersioningToBeEnabled(t, client, bucketName)
  134. // Verify that object lock was actually enabled
  135. t.Logf("Verifying object lock configuration for bucket %s", bucketName)
  136. _, err = client.GetObjectLockConfiguration(context.TODO(), &s3.GetObjectLockConfigurationInput{
  137. Bucket: aws.String(bucketName),
  138. })
  139. require.NoError(t, err, "Object lock should be configured for bucket %s", bucketName)
  140. }