s3_object_lock_headers_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. package retention
  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. // TestPutObjectWithLockHeaders tests that object lock headers in PUT requests
  14. // are properly stored and returned in HEAD responses
  15. func TestPutObjectWithLockHeaders(t *testing.T) {
  16. client := getS3Client(t)
  17. bucketName := getNewBucketName()
  18. // Create bucket with object lock enabled and versioning
  19. createBucketWithObjectLock(t, client, bucketName)
  20. defer deleteBucket(t, client, bucketName)
  21. key := "test-object-lock-headers"
  22. content := "test content with object lock headers"
  23. retainUntilDate := time.Now().Add(24 * time.Hour)
  24. // Test 1: PUT with COMPLIANCE mode and retention date
  25. t.Run("PUT with COMPLIANCE mode", func(t *testing.T) {
  26. testKey := key + "-compliance"
  27. // PUT object with lock headers
  28. putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content,
  29. "COMPLIANCE", retainUntilDate, "")
  30. require.NotNil(t, putResp.VersionId)
  31. // HEAD object and verify lock headers are returned
  32. headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  33. Bucket: aws.String(bucketName),
  34. Key: aws.String(testKey),
  35. })
  36. require.NoError(t, err)
  37. // Verify object lock metadata is present in response
  38. assert.Equal(t, types.ObjectLockModeCompliance, headResp.ObjectLockMode)
  39. assert.NotNil(t, headResp.ObjectLockRetainUntilDate)
  40. assert.WithinDuration(t, retainUntilDate, *headResp.ObjectLockRetainUntilDate, 5*time.Second)
  41. })
  42. // Test 2: PUT with GOVERNANCE mode and retention date
  43. t.Run("PUT with GOVERNANCE mode", func(t *testing.T) {
  44. testKey := key + "-governance"
  45. putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content,
  46. "GOVERNANCE", retainUntilDate, "")
  47. require.NotNil(t, putResp.VersionId)
  48. headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  49. Bucket: aws.String(bucketName),
  50. Key: aws.String(testKey),
  51. })
  52. require.NoError(t, err)
  53. assert.Equal(t, types.ObjectLockModeGovernance, headResp.ObjectLockMode)
  54. assert.NotNil(t, headResp.ObjectLockRetainUntilDate)
  55. assert.WithinDuration(t, retainUntilDate, *headResp.ObjectLockRetainUntilDate, 5*time.Second)
  56. })
  57. // Test 3: PUT with legal hold
  58. t.Run("PUT with legal hold", func(t *testing.T) {
  59. testKey := key + "-legal-hold"
  60. putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content,
  61. "", time.Time{}, "ON")
  62. require.NotNil(t, putResp.VersionId)
  63. headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  64. Bucket: aws.String(bucketName),
  65. Key: aws.String(testKey),
  66. })
  67. require.NoError(t, err)
  68. assert.Equal(t, types.ObjectLockLegalHoldStatusOn, headResp.ObjectLockLegalHoldStatus)
  69. })
  70. // Test 4: PUT with both retention and legal hold
  71. t.Run("PUT with both retention and legal hold", func(t *testing.T) {
  72. testKey := key + "-both"
  73. putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content,
  74. "GOVERNANCE", retainUntilDate, "ON")
  75. require.NotNil(t, putResp.VersionId)
  76. headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  77. Bucket: aws.String(bucketName),
  78. Key: aws.String(testKey),
  79. })
  80. require.NoError(t, err)
  81. assert.Equal(t, types.ObjectLockModeGovernance, headResp.ObjectLockMode)
  82. assert.NotNil(t, headResp.ObjectLockRetainUntilDate)
  83. assert.Equal(t, types.ObjectLockLegalHoldStatusOn, headResp.ObjectLockLegalHoldStatus)
  84. })
  85. }
  86. // TestGetObjectWithLockHeaders verifies that GET requests also return object lock metadata
  87. func TestGetObjectWithLockHeaders(t *testing.T) {
  88. client := getS3Client(t)
  89. bucketName := getNewBucketName()
  90. createBucketWithObjectLock(t, client, bucketName)
  91. defer deleteBucket(t, client, bucketName)
  92. key := "test-get-object-lock"
  93. content := "test content for GET with lock headers"
  94. retainUntilDate := time.Now().Add(24 * time.Hour)
  95. // PUT object with lock headers
  96. putResp := putObjectWithLockHeaders(t, client, bucketName, key, content,
  97. "COMPLIANCE", retainUntilDate, "ON")
  98. require.NotNil(t, putResp.VersionId)
  99. // GET object and verify lock headers are returned
  100. getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
  101. Bucket: aws.String(bucketName),
  102. Key: aws.String(key),
  103. })
  104. require.NoError(t, err)
  105. defer getResp.Body.Close()
  106. // Verify object lock metadata is present in GET response
  107. assert.Equal(t, types.ObjectLockModeCompliance, getResp.ObjectLockMode)
  108. assert.NotNil(t, getResp.ObjectLockRetainUntilDate)
  109. assert.WithinDuration(t, retainUntilDate, *getResp.ObjectLockRetainUntilDate, 5*time.Second)
  110. assert.Equal(t, types.ObjectLockLegalHoldStatusOn, getResp.ObjectLockLegalHoldStatus)
  111. }
  112. // TestVersionedObjectLockHeaders tests object lock headers work with versioned objects
  113. func TestVersionedObjectLockHeaders(t *testing.T) {
  114. client := getS3Client(t)
  115. bucketName := getNewBucketName()
  116. createBucketWithObjectLock(t, client, bucketName)
  117. defer deleteBucket(t, client, bucketName)
  118. key := "test-versioned-lock"
  119. content1 := "version 1 content"
  120. content2 := "version 2 content"
  121. retainUntilDate1 := time.Now().Add(12 * time.Hour)
  122. retainUntilDate2 := time.Now().Add(24 * time.Hour)
  123. // PUT first version with GOVERNANCE mode
  124. putResp1 := putObjectWithLockHeaders(t, client, bucketName, key, content1,
  125. "GOVERNANCE", retainUntilDate1, "")
  126. require.NotNil(t, putResp1.VersionId)
  127. // PUT second version with COMPLIANCE mode
  128. putResp2 := putObjectWithLockHeaders(t, client, bucketName, key, content2,
  129. "COMPLIANCE", retainUntilDate2, "ON")
  130. require.NotNil(t, putResp2.VersionId)
  131. require.NotEqual(t, *putResp1.VersionId, *putResp2.VersionId)
  132. // HEAD latest version (version 2)
  133. headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  134. Bucket: aws.String(bucketName),
  135. Key: aws.String(key),
  136. })
  137. require.NoError(t, err)
  138. assert.Equal(t, types.ObjectLockModeCompliance, headResp.ObjectLockMode)
  139. assert.Equal(t, types.ObjectLockLegalHoldStatusOn, headResp.ObjectLockLegalHoldStatus)
  140. // HEAD specific version 1
  141. headResp1, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  142. Bucket: aws.String(bucketName),
  143. Key: aws.String(key),
  144. VersionId: putResp1.VersionId,
  145. })
  146. require.NoError(t, err)
  147. assert.Equal(t, types.ObjectLockModeGovernance, headResp1.ObjectLockMode)
  148. assert.NotEqual(t, types.ObjectLockLegalHoldStatusOn, headResp1.ObjectLockLegalHoldStatus)
  149. }
  150. // TestObjectLockHeadersErrorCases tests various error scenarios
  151. func TestObjectLockHeadersErrorCases(t *testing.T) {
  152. client := getS3Client(t)
  153. bucketName := getNewBucketName()
  154. createBucketWithObjectLock(t, client, bucketName)
  155. defer deleteBucket(t, client, bucketName)
  156. key := "test-error-cases"
  157. content := "test content for error cases"
  158. // Test 1: Invalid retention mode should be rejected
  159. t.Run("Invalid retention mode", func(t *testing.T) {
  160. _, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
  161. Bucket: aws.String(bucketName),
  162. Key: aws.String(key + "-invalid-mode"),
  163. Body: strings.NewReader(content),
  164. ObjectLockMode: "INVALID_MODE", // Invalid mode
  165. ObjectLockRetainUntilDate: aws.Time(time.Now().Add(24 * time.Hour)),
  166. })
  167. require.Error(t, err)
  168. })
  169. // Test 2: Retention date in the past should be rejected
  170. t.Run("Past retention date", func(t *testing.T) {
  171. _, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
  172. Bucket: aws.String(bucketName),
  173. Key: aws.String(key + "-past-date"),
  174. Body: strings.NewReader(content),
  175. ObjectLockMode: "GOVERNANCE",
  176. ObjectLockRetainUntilDate: aws.Time(time.Now().Add(-24 * time.Hour)), // Past date
  177. })
  178. require.Error(t, err)
  179. })
  180. // Test 3: Mode without date should be rejected
  181. t.Run("Mode without retention date", func(t *testing.T) {
  182. _, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
  183. Bucket: aws.String(bucketName),
  184. Key: aws.String(key + "-no-date"),
  185. Body: strings.NewReader(content),
  186. ObjectLockMode: "GOVERNANCE",
  187. // Missing ObjectLockRetainUntilDate
  188. })
  189. require.Error(t, err)
  190. })
  191. }
  192. // TestObjectLockHeadersNonVersionedBucket tests that object lock fails on non-versioned buckets
  193. func TestObjectLockHeadersNonVersionedBucket(t *testing.T) {
  194. client := getS3Client(t)
  195. bucketName := getNewBucketName()
  196. // Create regular bucket without object lock/versioning
  197. createBucket(t, client, bucketName)
  198. defer deleteBucket(t, client, bucketName)
  199. key := "test-non-versioned"
  200. content := "test content"
  201. retainUntilDate := time.Now().Add(24 * time.Hour)
  202. // Attempting to PUT with object lock headers should fail
  203. _, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
  204. Bucket: aws.String(bucketName),
  205. Key: aws.String(key),
  206. Body: strings.NewReader(content),
  207. ObjectLockMode: "GOVERNANCE",
  208. ObjectLockRetainUntilDate: aws.Time(retainUntilDate),
  209. })
  210. require.Error(t, err)
  211. }
  212. // Helper Functions
  213. // putObjectWithLockHeaders puts an object with object lock headers
  214. func putObjectWithLockHeaders(t *testing.T, client *s3.Client, bucketName, key, content string,
  215. mode string, retainUntilDate time.Time, legalHold string) *s3.PutObjectOutput {
  216. input := &s3.PutObjectInput{
  217. Bucket: aws.String(bucketName),
  218. Key: aws.String(key),
  219. Body: strings.NewReader(content),
  220. }
  221. // Add retention mode and date if specified
  222. if mode != "" {
  223. switch mode {
  224. case "COMPLIANCE":
  225. input.ObjectLockMode = types.ObjectLockModeCompliance
  226. case "GOVERNANCE":
  227. input.ObjectLockMode = types.ObjectLockModeGovernance
  228. }
  229. if !retainUntilDate.IsZero() {
  230. input.ObjectLockRetainUntilDate = aws.Time(retainUntilDate)
  231. }
  232. }
  233. // Add legal hold if specified
  234. if legalHold != "" {
  235. switch legalHold {
  236. case "ON":
  237. input.ObjectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOn
  238. case "OFF":
  239. input.ObjectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOff
  240. }
  241. }
  242. resp, err := client.PutObject(context.TODO(), input)
  243. require.NoError(t, err)
  244. return resp
  245. }
  246. // createBucketWithObjectLock creates a bucket with object lock enabled
  247. func createBucketWithObjectLock(t *testing.T, client *s3.Client, bucketName string) {
  248. _, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
  249. Bucket: aws.String(bucketName),
  250. ObjectLockEnabledForBucket: aws.Bool(true),
  251. })
  252. require.NoError(t, err)
  253. // Enable versioning (required for object lock)
  254. enableVersioning(t, client, bucketName)
  255. }