s3_worm_integration_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. package retention
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. "testing"
  7. "time"
  8. "github.com/aws/aws-sdk-go-v2/aws"
  9. "github.com/aws/aws-sdk-go-v2/service/s3"
  10. "github.com/aws/aws-sdk-go-v2/service/s3/types"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/require"
  13. )
  14. // TestWORMRetentionIntegration tests that both retention and legacy WORM work together
  15. func TestWORMRetentionIntegration(t *testing.T) {
  16. client := getS3Client(t)
  17. bucketName := getNewBucketName()
  18. // Create bucket and enable versioning
  19. createBucket(t, client, bucketName)
  20. defer deleteBucket(t, client, bucketName)
  21. enableVersioning(t, client, bucketName)
  22. // Create object
  23. key := "worm-retention-integration-test"
  24. content := "worm retention integration test content"
  25. putResp := putObject(t, client, bucketName, key, content)
  26. require.NotNil(t, putResp.VersionId)
  27. // Set retention (new system)
  28. retentionUntil := time.Now().Add(1 * time.Hour)
  29. _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  30. Bucket: aws.String(bucketName),
  31. Key: aws.String(key),
  32. Retention: &types.ObjectLockRetention{
  33. Mode: types.ObjectLockRetentionModeGovernance,
  34. RetainUntilDate: aws.Time(retentionUntil),
  35. },
  36. })
  37. require.NoError(t, err)
  38. // Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
  39. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  40. Bucket: aws.String(bucketName),
  41. Key: aws.String(key),
  42. })
  43. require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
  44. // Try DELETE with version ID - should fail due to GOVERNANCE retention
  45. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  46. Bucket: aws.String(bucketName),
  47. Key: aws.String(key),
  48. VersionId: putResp.VersionId,
  49. })
  50. require.Error(t, err, "DELETE with version ID should be blocked by GOVERNANCE retention")
  51. // Delete with version ID and bypass should succeed
  52. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  53. Bucket: aws.String(bucketName),
  54. Key: aws.String(key),
  55. VersionId: putResp.VersionId,
  56. BypassGovernanceRetention: aws.Bool(true),
  57. })
  58. require.NoError(t, err)
  59. }
  60. // TestWORMLegacyCompatibility tests that legacy WORM functionality still works
  61. func TestWORMLegacyCompatibility(t *testing.T) {
  62. client := getS3Client(t)
  63. bucketName := getNewBucketName()
  64. // Create bucket and enable versioning
  65. createBucket(t, client, bucketName)
  66. defer deleteBucket(t, client, bucketName)
  67. enableVersioning(t, client, bucketName)
  68. // Create object with legacy WORM headers (if supported)
  69. key := "legacy-worm-test"
  70. content := "legacy worm test content"
  71. // Try to create object with legacy WORM TTL header
  72. putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
  73. Bucket: aws.String(bucketName),
  74. Key: aws.String(key),
  75. Body: strings.NewReader(content),
  76. // Add legacy WORM headers if supported
  77. Metadata: map[string]string{
  78. "x-amz-meta-worm-ttl": fmt.Sprintf("%d", time.Now().Add(1*time.Hour).Unix()),
  79. },
  80. })
  81. require.NoError(t, err)
  82. require.NotNil(t, putResp.VersionId)
  83. // Object should be created successfully
  84. resp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  85. Bucket: aws.String(bucketName),
  86. Key: aws.String(key),
  87. })
  88. require.NoError(t, err)
  89. assert.NotNil(t, resp.Metadata)
  90. }
  91. // TestRetentionOverwriteProtection tests that retention prevents overwrites
  92. func TestRetentionOverwriteProtection(t *testing.T) {
  93. client := getS3Client(t)
  94. bucketName := getNewBucketName()
  95. // Create bucket and enable versioning
  96. createBucket(t, client, bucketName)
  97. defer deleteBucket(t, client, bucketName)
  98. enableVersioning(t, client, bucketName)
  99. // Create object
  100. key := "overwrite-protection-test"
  101. content := "original content"
  102. putResp := putObject(t, client, bucketName, key, content)
  103. require.NotNil(t, putResp.VersionId)
  104. // Verify object exists before setting retention
  105. _, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  106. Bucket: aws.String(bucketName),
  107. Key: aws.String(key),
  108. })
  109. require.NoError(t, err, "Object should exist before setting retention")
  110. // Set retention with specific version ID
  111. retentionUntil := time.Now().Add(1 * time.Hour)
  112. _, err = client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  113. Bucket: aws.String(bucketName),
  114. Key: aws.String(key),
  115. VersionId: putResp.VersionId,
  116. Retention: &types.ObjectLockRetention{
  117. Mode: types.ObjectLockRetentionModeGovernance,
  118. RetainUntilDate: aws.Time(retentionUntil),
  119. },
  120. })
  121. require.NoError(t, err)
  122. // Try to overwrite object - should fail in non-versioned bucket context
  123. content2 := "new content"
  124. _, err = client.PutObject(context.TODO(), &s3.PutObjectInput{
  125. Bucket: aws.String(bucketName),
  126. Key: aws.String(key),
  127. Body: strings.NewReader(content2),
  128. })
  129. // Note: In a real scenario, this might fail or create a new version
  130. // The actual behavior depends on the implementation
  131. if err != nil {
  132. t.Logf("Expected behavior: overwrite blocked due to retention: %v", err)
  133. } else {
  134. t.Logf("Overwrite allowed, likely created new version")
  135. }
  136. }
  137. // TestRetentionBulkOperations tests retention with bulk operations
  138. func TestRetentionBulkOperations(t *testing.T) {
  139. client := getS3Client(t)
  140. bucketName := getNewBucketName()
  141. // Create bucket and enable versioning
  142. createBucket(t, client, bucketName)
  143. defer deleteBucket(t, client, bucketName)
  144. enableVersioning(t, client, bucketName)
  145. // Create multiple objects with retention
  146. var objectsToDelete []types.ObjectIdentifier
  147. retentionUntil := time.Now().Add(1 * time.Hour)
  148. for i := 0; i < 3; i++ {
  149. key := fmt.Sprintf("bulk-test-object-%d", i)
  150. content := fmt.Sprintf("bulk test content %d", i)
  151. putResp := putObject(t, client, bucketName, key, content)
  152. require.NotNil(t, putResp.VersionId)
  153. // Set retention on each object with version ID
  154. _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  155. Bucket: aws.String(bucketName),
  156. Key: aws.String(key),
  157. VersionId: putResp.VersionId,
  158. Retention: &types.ObjectLockRetention{
  159. Mode: types.ObjectLockRetentionModeGovernance,
  160. RetainUntilDate: aws.Time(retentionUntil),
  161. },
  162. })
  163. require.NoError(t, err)
  164. objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{
  165. Key: aws.String(key),
  166. VersionId: putResp.VersionId,
  167. })
  168. }
  169. // Try bulk delete without bypass - should fail or have errors
  170. deleteResp, err := client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
  171. Bucket: aws.String(bucketName),
  172. Delete: &types.Delete{
  173. Objects: objectsToDelete,
  174. Quiet: aws.Bool(false),
  175. },
  176. })
  177. // Check if operation failed or returned errors for protected objects
  178. if err != nil {
  179. t.Logf("Expected: bulk delete failed due to retention: %v", err)
  180. } else if deleteResp != nil && len(deleteResp.Errors) > 0 {
  181. t.Logf("Expected: bulk delete returned %d errors due to retention", len(deleteResp.Errors))
  182. for _, delErr := range deleteResp.Errors {
  183. t.Logf("Delete error: %s - %s", *delErr.Code, *delErr.Message)
  184. }
  185. } else {
  186. t.Logf("Warning: bulk delete succeeded - retention may not be enforced for bulk operations")
  187. }
  188. // Try bulk delete with bypass - should succeed
  189. _, err = client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
  190. Bucket: aws.String(bucketName),
  191. BypassGovernanceRetention: aws.Bool(true),
  192. Delete: &types.Delete{
  193. Objects: objectsToDelete,
  194. Quiet: aws.Bool(false),
  195. },
  196. })
  197. if err != nil {
  198. t.Logf("Bulk delete with bypass failed (may not be supported): %v", err)
  199. } else {
  200. t.Logf("Bulk delete with bypass succeeded")
  201. }
  202. }
  203. // TestRetentionWithMultipartUpload tests retention with multipart uploads
  204. func TestRetentionWithMultipartUpload(t *testing.T) {
  205. client := getS3Client(t)
  206. bucketName := getNewBucketName()
  207. // Create bucket and enable versioning
  208. createBucket(t, client, bucketName)
  209. defer deleteBucket(t, client, bucketName)
  210. enableVersioning(t, client, bucketName)
  211. // Start multipart upload
  212. key := "multipart-retention-test"
  213. createResp, err := client.CreateMultipartUpload(context.TODO(), &s3.CreateMultipartUploadInput{
  214. Bucket: aws.String(bucketName),
  215. Key: aws.String(key),
  216. })
  217. require.NoError(t, err)
  218. uploadId := createResp.UploadId
  219. // Upload a part
  220. partContent := "This is a test part for multipart upload"
  221. uploadResp, err := client.UploadPart(context.TODO(), &s3.UploadPartInput{
  222. Bucket: aws.String(bucketName),
  223. Key: aws.String(key),
  224. PartNumber: aws.Int32(1),
  225. UploadId: uploadId,
  226. Body: strings.NewReader(partContent),
  227. })
  228. require.NoError(t, err)
  229. // Complete multipart upload
  230. completeResp, err := client.CompleteMultipartUpload(context.TODO(), &s3.CompleteMultipartUploadInput{
  231. Bucket: aws.String(bucketName),
  232. Key: aws.String(key),
  233. UploadId: uploadId,
  234. MultipartUpload: &types.CompletedMultipartUpload{
  235. Parts: []types.CompletedPart{
  236. {
  237. ETag: uploadResp.ETag,
  238. PartNumber: aws.Int32(1),
  239. },
  240. },
  241. },
  242. })
  243. require.NoError(t, err)
  244. // Add a small delay to ensure the object is fully created
  245. time.Sleep(500 * time.Millisecond)
  246. // Verify object exists after multipart upload - retry if needed
  247. var headErr error
  248. for retries := 0; retries < 10; retries++ {
  249. _, headErr = client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  250. Bucket: aws.String(bucketName),
  251. Key: aws.String(key),
  252. })
  253. if headErr == nil {
  254. break
  255. }
  256. t.Logf("HeadObject attempt %d failed: %v", retries+1, headErr)
  257. time.Sleep(200 * time.Millisecond)
  258. }
  259. if headErr != nil {
  260. t.Logf("Object not found after multipart upload completion, checking if multipart upload is fully supported")
  261. // Check if the object exists by trying to list it
  262. listResp, listErr := client.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
  263. Bucket: aws.String(bucketName),
  264. Prefix: aws.String(key),
  265. })
  266. if listErr != nil || len(listResp.Contents) == 0 {
  267. t.Skip("Multipart upload may not be fully supported, skipping test")
  268. return
  269. }
  270. // If object exists in listing but not accessible via HeadObject, skip test
  271. t.Skip("Object exists in listing but not accessible via HeadObject, multipart upload may not be fully supported")
  272. return
  273. }
  274. require.NoError(t, headErr, "Object should exist after multipart upload")
  275. // Set retention on the completed multipart object with version ID
  276. retentionUntil := time.Now().Add(1 * time.Hour)
  277. _, err = client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  278. Bucket: aws.String(bucketName),
  279. Key: aws.String(key),
  280. VersionId: completeResp.VersionId,
  281. Retention: &types.ObjectLockRetention{
  282. Mode: types.ObjectLockRetentionModeGovernance,
  283. RetainUntilDate: aws.Time(retentionUntil),
  284. },
  285. })
  286. require.NoError(t, err)
  287. // Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
  288. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  289. Bucket: aws.String(bucketName),
  290. Key: aws.String(key),
  291. })
  292. require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
  293. // Try DELETE with version ID - should fail due to GOVERNANCE retention
  294. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  295. Bucket: aws.String(bucketName),
  296. Key: aws.String(key),
  297. VersionId: completeResp.VersionId,
  298. })
  299. require.Error(t, err, "DELETE with version ID should be blocked by GOVERNANCE retention")
  300. }
  301. // TestRetentionExtendedAttributes tests that retention uses extended attributes correctly
  302. func TestRetentionExtendedAttributes(t *testing.T) {
  303. client := getS3Client(t)
  304. bucketName := getNewBucketName()
  305. // Create bucket and enable versioning
  306. createBucket(t, client, bucketName)
  307. defer deleteBucket(t, client, bucketName)
  308. enableVersioning(t, client, bucketName)
  309. // Create object
  310. key := "extended-attrs-test"
  311. content := "extended attributes test content"
  312. putResp := putObject(t, client, bucketName, key, content)
  313. require.NotNil(t, putResp.VersionId)
  314. // Set retention
  315. retentionUntil := time.Now().Add(1 * time.Hour)
  316. _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  317. Bucket: aws.String(bucketName),
  318. Key: aws.String(key),
  319. VersionId: putResp.VersionId,
  320. Retention: &types.ObjectLockRetention{
  321. Mode: types.ObjectLockRetentionModeGovernance,
  322. RetainUntilDate: aws.Time(retentionUntil),
  323. },
  324. })
  325. require.NoError(t, err)
  326. // Set legal hold
  327. _, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
  328. Bucket: aws.String(bucketName),
  329. Key: aws.String(key),
  330. VersionId: putResp.VersionId,
  331. LegalHold: &types.ObjectLockLegalHold{
  332. Status: types.ObjectLockLegalHoldStatusOn,
  333. },
  334. })
  335. require.NoError(t, err)
  336. // Get object metadata to verify extended attributes are set
  337. resp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
  338. Bucket: aws.String(bucketName),
  339. Key: aws.String(key),
  340. })
  341. require.NoError(t, err)
  342. // Check that the object has metadata (may be empty in some implementations)
  343. // Note: The actual metadata keys depend on the implementation
  344. if resp.Metadata != nil && len(resp.Metadata) > 0 {
  345. t.Logf("Object metadata: %+v", resp.Metadata)
  346. } else {
  347. t.Logf("Object metadata: empty (extended attributes may be stored internally)")
  348. }
  349. // Verify retention can be retrieved
  350. retentionResp, err := client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
  351. Bucket: aws.String(bucketName),
  352. Key: aws.String(key),
  353. })
  354. require.NoError(t, err)
  355. assert.Equal(t, types.ObjectLockRetentionModeGovernance, retentionResp.Retention.Mode)
  356. // Verify legal hold can be retrieved
  357. legalHoldResp, err := client.GetObjectLegalHold(context.TODO(), &s3.GetObjectLegalHoldInput{
  358. Bucket: aws.String(bucketName),
  359. Key: aws.String(key),
  360. })
  361. require.NoError(t, err)
  362. assert.Equal(t, types.ObjectLockLegalHoldStatusOn, legalHoldResp.LegalHold.Status)
  363. }
  364. // TestRetentionBucketDefaults tests object lock configuration defaults
  365. func TestRetentionBucketDefaults(t *testing.T) {
  366. client := getS3Client(t)
  367. // Use a very unique bucket name to avoid conflicts
  368. bucketName := fmt.Sprintf("bucket-defaults-%d-%d", time.Now().UnixNano(), time.Now().UnixMilli()%10000)
  369. // Create bucket and enable versioning
  370. createBucket(t, client, bucketName)
  371. defer deleteBucket(t, client, bucketName)
  372. enableVersioning(t, client, bucketName)
  373. // Set bucket object lock configuration with default retention
  374. _, err := client.PutObjectLockConfiguration(context.TODO(), &s3.PutObjectLockConfigurationInput{
  375. Bucket: aws.String(bucketName),
  376. ObjectLockConfiguration: &types.ObjectLockConfiguration{
  377. ObjectLockEnabled: types.ObjectLockEnabledEnabled,
  378. Rule: &types.ObjectLockRule{
  379. DefaultRetention: &types.DefaultRetention{
  380. Mode: types.ObjectLockRetentionModeGovernance,
  381. Days: aws.Int32(1), // 1 day default
  382. },
  383. },
  384. },
  385. })
  386. if err != nil {
  387. t.Logf("PutObjectLockConfiguration failed (may not be supported): %v", err)
  388. t.Skip("Object lock configuration not supported, skipping test")
  389. return
  390. }
  391. // Create object (should inherit default retention)
  392. key := "bucket-defaults-test"
  393. content := "bucket defaults test content"
  394. putResp := putObject(t, client, bucketName, key, content)
  395. require.NotNil(t, putResp.VersionId)
  396. // Check if object has default retention applied
  397. // Note: This depends on the implementation - some S3 services apply
  398. // default retention automatically, others require explicit setting
  399. retentionResp, err := client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
  400. Bucket: aws.String(bucketName),
  401. Key: aws.String(key),
  402. })
  403. if err != nil {
  404. t.Logf("No automatic default retention applied: %v", err)
  405. } else {
  406. t.Logf("Default retention applied: %+v", retentionResp.Retention)
  407. assert.Equal(t, types.ObjectLockRetentionModeGovernance, retentionResp.Retention.Mode)
  408. }
  409. }
  410. // TestRetentionConcurrentOperations tests concurrent retention operations
  411. func TestRetentionConcurrentOperations(t *testing.T) {
  412. client := getS3Client(t)
  413. bucketName := getNewBucketName()
  414. // Create bucket and enable versioning
  415. createBucket(t, client, bucketName)
  416. defer deleteBucket(t, client, bucketName)
  417. enableVersioning(t, client, bucketName)
  418. // Create object
  419. key := "concurrent-ops-test"
  420. content := "concurrent operations test content"
  421. putResp := putObject(t, client, bucketName, key, content)
  422. require.NotNil(t, putResp.VersionId)
  423. // Test concurrent retention and legal hold operations
  424. retentionUntil := time.Now().Add(1 * time.Hour)
  425. // Set retention and legal hold concurrently
  426. errChan := make(chan error, 2)
  427. go func() {
  428. _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  429. Bucket: aws.String(bucketName),
  430. Key: aws.String(key),
  431. Retention: &types.ObjectLockRetention{
  432. Mode: types.ObjectLockRetentionModeGovernance,
  433. RetainUntilDate: aws.Time(retentionUntil),
  434. },
  435. })
  436. errChan <- err
  437. }()
  438. go func() {
  439. _, err := client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
  440. Bucket: aws.String(bucketName),
  441. Key: aws.String(key),
  442. LegalHold: &types.ObjectLockLegalHold{
  443. Status: types.ObjectLockLegalHoldStatusOn,
  444. },
  445. })
  446. errChan <- err
  447. }()
  448. // Wait for both operations to complete
  449. for i := 0; i < 2; i++ {
  450. err := <-errChan
  451. if err != nil {
  452. t.Logf("Concurrent operation failed: %v", err)
  453. }
  454. }
  455. // Verify both settings are applied
  456. retentionResp, err := client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
  457. Bucket: aws.String(bucketName),
  458. Key: aws.String(key),
  459. })
  460. if err == nil {
  461. assert.Equal(t, types.ObjectLockRetentionModeGovernance, retentionResp.Retention.Mode)
  462. }
  463. legalHoldResp, err := client.GetObjectLegalHold(context.TODO(), &s3.GetObjectLegalHoldInput{
  464. Bucket: aws.String(bucketName),
  465. Key: aws.String(key),
  466. })
  467. if err == nil {
  468. assert.Equal(t, types.ObjectLockLegalHoldStatusOn, legalHoldResp.LegalHold.Status)
  469. }
  470. }