s3_retention_test.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  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/config"
  10. "github.com/aws/aws-sdk-go-v2/credentials"
  11. "github.com/aws/aws-sdk-go-v2/service/s3"
  12. "github.com/aws/aws-sdk-go-v2/service/s3/types"
  13. "github.com/stretchr/testify/assert"
  14. "github.com/stretchr/testify/require"
  15. )
  16. // S3TestConfig holds configuration for S3 tests
  17. type S3TestConfig struct {
  18. Endpoint string
  19. AccessKey string
  20. SecretKey string
  21. Region string
  22. BucketPrefix string
  23. UseSSL bool
  24. SkipVerifySSL bool
  25. }
  26. // Default test configuration - should match test_config.json
  27. var defaultConfig = &S3TestConfig{
  28. Endpoint: "http://localhost:8333", // Default SeaweedFS S3 port
  29. AccessKey: "some_access_key1",
  30. SecretKey: "some_secret_key1",
  31. Region: "us-east-1",
  32. BucketPrefix: "test-retention-",
  33. UseSSL: false,
  34. SkipVerifySSL: true,
  35. }
  36. // getS3Client creates an AWS S3 client for testing
  37. func getS3Client(t *testing.T) *s3.Client {
  38. cfg, err := config.LoadDefaultConfig(context.TODO(),
  39. config.WithRegion(defaultConfig.Region),
  40. config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
  41. defaultConfig.AccessKey,
  42. defaultConfig.SecretKey,
  43. "",
  44. )),
  45. config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(
  46. func(service, region string, options ...interface{}) (aws.Endpoint, error) {
  47. return aws.Endpoint{
  48. URL: defaultConfig.Endpoint,
  49. SigningRegion: defaultConfig.Region,
  50. HostnameImmutable: true,
  51. }, nil
  52. })),
  53. )
  54. require.NoError(t, err)
  55. return s3.NewFromConfig(cfg, func(o *s3.Options) {
  56. o.UsePathStyle = true // Important for SeaweedFS
  57. })
  58. }
  59. // getNewBucketName generates a unique bucket name
  60. func getNewBucketName() string {
  61. timestamp := time.Now().UnixNano()
  62. return fmt.Sprintf("%s%d", defaultConfig.BucketPrefix, timestamp)
  63. }
  64. // createBucket creates a new bucket for testing
  65. func createBucket(t *testing.T, client *s3.Client, bucketName string) {
  66. _, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
  67. Bucket: aws.String(bucketName),
  68. })
  69. require.NoError(t, err)
  70. }
  71. // deleteBucket deletes a bucket and all its contents
  72. func deleteBucket(t *testing.T, client *s3.Client, bucketName string) {
  73. // First, try to delete all objects and versions
  74. err := deleteAllObjectVersions(t, client, bucketName)
  75. if err != nil {
  76. t.Logf("Warning: failed to delete all object versions in first attempt: %v", err)
  77. // Try once more in case of transient errors
  78. time.Sleep(500 * time.Millisecond)
  79. err = deleteAllObjectVersions(t, client, bucketName)
  80. if err != nil {
  81. t.Logf("Warning: failed to delete all object versions in second attempt: %v", err)
  82. }
  83. }
  84. // Wait a bit for eventual consistency
  85. time.Sleep(100 * time.Millisecond)
  86. // Try to delete the bucket multiple times in case of eventual consistency issues
  87. for retries := 0; retries < 3; retries++ {
  88. _, err = client.DeleteBucket(context.TODO(), &s3.DeleteBucketInput{
  89. Bucket: aws.String(bucketName),
  90. })
  91. if err == nil {
  92. t.Logf("Successfully deleted bucket %s", bucketName)
  93. return
  94. }
  95. t.Logf("Warning: failed to delete bucket %s (attempt %d): %v", bucketName, retries+1, err)
  96. if retries < 2 {
  97. time.Sleep(200 * time.Millisecond)
  98. }
  99. }
  100. }
  101. // deleteAllObjectVersions deletes all object versions in a bucket
  102. func deleteAllObjectVersions(t *testing.T, client *s3.Client, bucketName string) error {
  103. // List all object versions
  104. paginator := s3.NewListObjectVersionsPaginator(client, &s3.ListObjectVersionsInput{
  105. Bucket: aws.String(bucketName),
  106. })
  107. for paginator.HasMorePages() {
  108. page, err := paginator.NextPage(context.TODO())
  109. if err != nil {
  110. return err
  111. }
  112. var objectsToDelete []types.ObjectIdentifier
  113. // Add versions - first try to remove retention/legal hold
  114. for _, version := range page.Versions {
  115. // Try to remove legal hold if present
  116. _, err := client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
  117. Bucket: aws.String(bucketName),
  118. Key: version.Key,
  119. VersionId: version.VersionId,
  120. LegalHold: &types.ObjectLockLegalHold{
  121. Status: types.ObjectLockLegalHoldStatusOff,
  122. },
  123. })
  124. if err != nil {
  125. // Legal hold might not be set, ignore error
  126. t.Logf("Note: could not remove legal hold for %s@%s: %v", *version.Key, *version.VersionId, err)
  127. }
  128. objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{
  129. Key: version.Key,
  130. VersionId: version.VersionId,
  131. })
  132. }
  133. // Add delete markers
  134. for _, deleteMarker := range page.DeleteMarkers {
  135. objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{
  136. Key: deleteMarker.Key,
  137. VersionId: deleteMarker.VersionId,
  138. })
  139. }
  140. // Delete objects in batches with bypass governance retention
  141. if len(objectsToDelete) > 0 {
  142. _, err := client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
  143. Bucket: aws.String(bucketName),
  144. BypassGovernanceRetention: aws.Bool(true),
  145. Delete: &types.Delete{
  146. Objects: objectsToDelete,
  147. Quiet: aws.Bool(true),
  148. },
  149. })
  150. if err != nil {
  151. t.Logf("Warning: batch delete failed, trying individual deletion: %v", err)
  152. // Try individual deletion for each object
  153. for _, obj := range objectsToDelete {
  154. _, delErr := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  155. Bucket: aws.String(bucketName),
  156. Key: obj.Key,
  157. VersionId: obj.VersionId,
  158. BypassGovernanceRetention: aws.Bool(true),
  159. })
  160. if delErr != nil {
  161. t.Logf("Warning: failed to delete object %s@%s: %v", *obj.Key, *obj.VersionId, delErr)
  162. }
  163. }
  164. }
  165. }
  166. }
  167. return nil
  168. }
  169. // enableVersioning enables versioning on a bucket
  170. func enableVersioning(t *testing.T, client *s3.Client, bucketName string) {
  171. _, err := client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
  172. Bucket: aws.String(bucketName),
  173. VersioningConfiguration: &types.VersioningConfiguration{
  174. Status: types.BucketVersioningStatusEnabled,
  175. },
  176. })
  177. require.NoError(t, err)
  178. }
  179. // putObject puts an object into a bucket
  180. func putObject(t *testing.T, client *s3.Client, bucketName, key, content string) *s3.PutObjectOutput {
  181. resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
  182. Bucket: aws.String(bucketName),
  183. Key: aws.String(key),
  184. Body: strings.NewReader(content),
  185. })
  186. require.NoError(t, err)
  187. return resp
  188. }
  189. // cleanupAllTestBuckets cleans up any leftover test buckets
  190. func cleanupAllTestBuckets(t *testing.T, client *s3.Client) {
  191. // List all buckets
  192. listResp, err := client.ListBuckets(context.TODO(), &s3.ListBucketsInput{})
  193. if err != nil {
  194. t.Logf("Warning: failed to list buckets for cleanup: %v", err)
  195. return
  196. }
  197. // Delete buckets that match our test prefix
  198. for _, bucket := range listResp.Buckets {
  199. if bucket.Name != nil && strings.HasPrefix(*bucket.Name, defaultConfig.BucketPrefix) {
  200. t.Logf("Cleaning up leftover test bucket: %s", *bucket.Name)
  201. deleteBucket(t, client, *bucket.Name)
  202. }
  203. }
  204. }
  205. // TestBasicRetentionWorkflow tests the basic retention functionality
  206. func TestBasicRetentionWorkflow(t *testing.T) {
  207. client := getS3Client(t)
  208. bucketName := getNewBucketName()
  209. // Create bucket
  210. createBucket(t, client, bucketName)
  211. defer deleteBucket(t, client, bucketName)
  212. // Enable versioning (required for retention)
  213. enableVersioning(t, client, bucketName)
  214. // Create object
  215. key := "test-object"
  216. content := "test content for retention"
  217. putResp := putObject(t, client, bucketName, key, content)
  218. require.NotNil(t, putResp.VersionId)
  219. // Set retention with GOVERNANCE mode
  220. retentionUntil := time.Now().Add(24 * time.Hour)
  221. _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  222. Bucket: aws.String(bucketName),
  223. Key: aws.String(key),
  224. Retention: &types.ObjectLockRetention{
  225. Mode: types.ObjectLockRetentionModeGovernance,
  226. RetainUntilDate: aws.Time(retentionUntil),
  227. },
  228. })
  229. require.NoError(t, err)
  230. // Get retention and verify it was set correctly
  231. retentionResp, err := client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
  232. Bucket: aws.String(bucketName),
  233. Key: aws.String(key),
  234. })
  235. require.NoError(t, err)
  236. assert.Equal(t, types.ObjectLockRetentionModeGovernance, retentionResp.Retention.Mode)
  237. assert.WithinDuration(t, retentionUntil, *retentionResp.Retention.RetainUntilDate, time.Second)
  238. // Try to delete object without bypass - should fail
  239. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  240. Bucket: aws.String(bucketName),
  241. Key: aws.String(key),
  242. })
  243. require.Error(t, err)
  244. // Delete object with bypass governance - should succeed
  245. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  246. Bucket: aws.String(bucketName),
  247. Key: aws.String(key),
  248. BypassGovernanceRetention: aws.Bool(true),
  249. })
  250. require.NoError(t, err)
  251. }
  252. // TestRetentionModeCompliance tests COMPLIANCE mode retention
  253. func TestRetentionModeCompliance(t *testing.T) {
  254. client := getS3Client(t)
  255. bucketName := getNewBucketName()
  256. // Create bucket and enable versioning
  257. createBucket(t, client, bucketName)
  258. defer deleteBucket(t, client, bucketName)
  259. enableVersioning(t, client, bucketName)
  260. // Create object
  261. key := "compliance-test-object"
  262. content := "compliance test content"
  263. putResp := putObject(t, client, bucketName, key, content)
  264. require.NotNil(t, putResp.VersionId)
  265. // Set retention with COMPLIANCE mode
  266. retentionUntil := time.Now().Add(1 * time.Hour)
  267. _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  268. Bucket: aws.String(bucketName),
  269. Key: aws.String(key),
  270. Retention: &types.ObjectLockRetention{
  271. Mode: types.ObjectLockRetentionModeCompliance,
  272. RetainUntilDate: aws.Time(retentionUntil),
  273. },
  274. })
  275. require.NoError(t, err)
  276. // Get retention and verify
  277. retentionResp, err := client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
  278. Bucket: aws.String(bucketName),
  279. Key: aws.String(key),
  280. })
  281. require.NoError(t, err)
  282. assert.Equal(t, types.ObjectLockRetentionModeCompliance, retentionResp.Retention.Mode)
  283. // Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
  284. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  285. Bucket: aws.String(bucketName),
  286. Key: aws.String(key),
  287. })
  288. require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
  289. // Try DELETE with version ID - should fail for COMPLIANCE mode
  290. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  291. Bucket: aws.String(bucketName),
  292. Key: aws.String(key),
  293. VersionId: putResp.VersionId,
  294. })
  295. require.Error(t, err, "DELETE with version ID should be blocked by COMPLIANCE retention")
  296. // Try DELETE with version ID and bypass - should still fail (COMPLIANCE mode ignores bypass)
  297. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  298. Bucket: aws.String(bucketName),
  299. Key: aws.String(key),
  300. VersionId: putResp.VersionId,
  301. BypassGovernanceRetention: aws.Bool(true),
  302. })
  303. require.Error(t, err, "COMPLIANCE mode should ignore governance bypass")
  304. }
  305. // TestLegalHoldWorkflow tests legal hold functionality
  306. func TestLegalHoldWorkflow(t *testing.T) {
  307. client := getS3Client(t)
  308. bucketName := getNewBucketName()
  309. // Create bucket and enable versioning
  310. createBucket(t, client, bucketName)
  311. defer deleteBucket(t, client, bucketName)
  312. enableVersioning(t, client, bucketName)
  313. // Create object
  314. key := "legal-hold-test-object"
  315. content := "legal hold test content"
  316. putResp := putObject(t, client, bucketName, key, content)
  317. require.NotNil(t, putResp.VersionId)
  318. // Set legal hold ON
  319. _, err := client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
  320. Bucket: aws.String(bucketName),
  321. Key: aws.String(key),
  322. LegalHold: &types.ObjectLockLegalHold{
  323. Status: types.ObjectLockLegalHoldStatusOn,
  324. },
  325. })
  326. require.NoError(t, err)
  327. // Get legal hold and verify
  328. legalHoldResp, err := client.GetObjectLegalHold(context.TODO(), &s3.GetObjectLegalHoldInput{
  329. Bucket: aws.String(bucketName),
  330. Key: aws.String(key),
  331. })
  332. require.NoError(t, err)
  333. assert.Equal(t, types.ObjectLockLegalHoldStatusOn, legalHoldResp.LegalHold.Status)
  334. // Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
  335. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  336. Bucket: aws.String(bucketName),
  337. Key: aws.String(key),
  338. })
  339. require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
  340. // Try DELETE with version ID - should fail due to legal hold
  341. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  342. Bucket: aws.String(bucketName),
  343. Key: aws.String(key),
  344. VersionId: putResp.VersionId,
  345. })
  346. require.Error(t, err, "DELETE with version ID should be blocked by legal hold")
  347. // Remove legal hold (must specify version ID since latest version is now delete marker)
  348. _, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
  349. Bucket: aws.String(bucketName),
  350. Key: aws.String(key),
  351. VersionId: putResp.VersionId,
  352. LegalHold: &types.ObjectLockLegalHold{
  353. Status: types.ObjectLockLegalHoldStatusOff,
  354. },
  355. })
  356. require.NoError(t, err)
  357. // Verify legal hold is off (must specify version ID)
  358. legalHoldResp, err = client.GetObjectLegalHold(context.TODO(), &s3.GetObjectLegalHoldInput{
  359. Bucket: aws.String(bucketName),
  360. Key: aws.String(key),
  361. VersionId: putResp.VersionId,
  362. })
  363. require.NoError(t, err)
  364. assert.Equal(t, types.ObjectLockLegalHoldStatusOff, legalHoldResp.LegalHold.Status)
  365. // Now DELETE with version ID should succeed after legal hold removed
  366. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  367. Bucket: aws.String(bucketName),
  368. Key: aws.String(key),
  369. VersionId: putResp.VersionId,
  370. })
  371. require.NoError(t, err, "DELETE with version ID should succeed after legal hold removed")
  372. }
  373. // TestObjectLockConfiguration tests bucket object lock configuration
  374. func TestObjectLockConfiguration(t *testing.T) {
  375. client := getS3Client(t)
  376. // Use a more unique bucket name to avoid conflicts
  377. bucketName := fmt.Sprintf("object-lock-config-%d-%d", time.Now().UnixNano(), time.Now().UnixMilli()%10000)
  378. // Create bucket and enable versioning
  379. createBucket(t, client, bucketName)
  380. defer deleteBucket(t, client, bucketName)
  381. enableVersioning(t, client, bucketName)
  382. // Set object lock configuration
  383. _, err := client.PutObjectLockConfiguration(context.TODO(), &s3.PutObjectLockConfigurationInput{
  384. Bucket: aws.String(bucketName),
  385. ObjectLockConfiguration: &types.ObjectLockConfiguration{
  386. ObjectLockEnabled: types.ObjectLockEnabledEnabled,
  387. Rule: &types.ObjectLockRule{
  388. DefaultRetention: &types.DefaultRetention{
  389. Mode: types.ObjectLockRetentionModeGovernance,
  390. Days: aws.Int32(30),
  391. },
  392. },
  393. },
  394. })
  395. if err != nil {
  396. t.Logf("PutObjectLockConfiguration failed (may not be supported): %v", err)
  397. t.Skip("Object lock configuration not supported, skipping test")
  398. return
  399. }
  400. // Get object lock configuration and verify
  401. configResp, err := client.GetObjectLockConfiguration(context.TODO(), &s3.GetObjectLockConfigurationInput{
  402. Bucket: aws.String(bucketName),
  403. })
  404. require.NoError(t, err)
  405. assert.Equal(t, types.ObjectLockEnabledEnabled, configResp.ObjectLockConfiguration.ObjectLockEnabled)
  406. require.NotNil(t, configResp.ObjectLockConfiguration.Rule.DefaultRetention, "DefaultRetention should not be nil")
  407. require.NotNil(t, configResp.ObjectLockConfiguration.Rule.DefaultRetention.Days, "Days should not be nil")
  408. assert.Equal(t, types.ObjectLockRetentionModeGovernance, configResp.ObjectLockConfiguration.Rule.DefaultRetention.Mode)
  409. assert.Equal(t, int32(30), *configResp.ObjectLockConfiguration.Rule.DefaultRetention.Days)
  410. }
  411. // TestRetentionWithVersions tests retention with specific object versions
  412. func TestRetentionWithVersions(t *testing.T) {
  413. client := getS3Client(t)
  414. bucketName := getNewBucketName()
  415. // Create bucket and enable versioning
  416. createBucket(t, client, bucketName)
  417. defer deleteBucket(t, client, bucketName)
  418. enableVersioning(t, client, bucketName)
  419. // Create multiple versions of the same object
  420. key := "versioned-retention-test"
  421. content1 := "version 1 content"
  422. content2 := "version 2 content"
  423. putResp1 := putObject(t, client, bucketName, key, content1)
  424. require.NotNil(t, putResp1.VersionId)
  425. putResp2 := putObject(t, client, bucketName, key, content2)
  426. require.NotNil(t, putResp2.VersionId)
  427. // Set retention on first version only
  428. retentionUntil := time.Now().Add(1 * time.Hour)
  429. _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  430. Bucket: aws.String(bucketName),
  431. Key: aws.String(key),
  432. VersionId: putResp1.VersionId,
  433. Retention: &types.ObjectLockRetention{
  434. Mode: types.ObjectLockRetentionModeGovernance,
  435. RetainUntilDate: aws.Time(retentionUntil),
  436. },
  437. })
  438. require.NoError(t, err)
  439. // Get retention for first version
  440. retentionResp, err := client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
  441. Bucket: aws.String(bucketName),
  442. Key: aws.String(key),
  443. VersionId: putResp1.VersionId,
  444. })
  445. require.NoError(t, err)
  446. assert.Equal(t, types.ObjectLockRetentionModeGovernance, retentionResp.Retention.Mode)
  447. // Try to get retention for second version - should fail (no retention set)
  448. _, err = client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
  449. Bucket: aws.String(bucketName),
  450. Key: aws.String(key),
  451. VersionId: putResp2.VersionId,
  452. })
  453. require.Error(t, err)
  454. // Delete second version should succeed (no retention)
  455. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  456. Bucket: aws.String(bucketName),
  457. Key: aws.String(key),
  458. VersionId: putResp2.VersionId,
  459. })
  460. require.NoError(t, err)
  461. // Delete first version should fail (has retention)
  462. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  463. Bucket: aws.String(bucketName),
  464. Key: aws.String(key),
  465. VersionId: putResp1.VersionId,
  466. })
  467. require.Error(t, err)
  468. // Delete first version with bypass should succeed
  469. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  470. Bucket: aws.String(bucketName),
  471. Key: aws.String(key),
  472. VersionId: putResp1.VersionId,
  473. BypassGovernanceRetention: aws.Bool(true),
  474. })
  475. require.NoError(t, err)
  476. }
  477. // TestRetentionAndLegalHoldCombination tests retention and legal hold together
  478. func TestRetentionAndLegalHoldCombination(t *testing.T) {
  479. client := getS3Client(t)
  480. bucketName := getNewBucketName()
  481. // Create bucket and enable versioning
  482. createBucket(t, client, bucketName)
  483. defer deleteBucket(t, client, bucketName)
  484. enableVersioning(t, client, bucketName)
  485. // Create object
  486. key := "combined-protection-test"
  487. content := "combined protection test content"
  488. putResp := putObject(t, client, bucketName, key, content)
  489. require.NotNil(t, putResp.VersionId)
  490. // Set both retention and legal hold
  491. retentionUntil := time.Now().Add(1 * time.Hour)
  492. // Set retention
  493. _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  494. Bucket: aws.String(bucketName),
  495. Key: aws.String(key),
  496. Retention: &types.ObjectLockRetention{
  497. Mode: types.ObjectLockRetentionModeGovernance,
  498. RetainUntilDate: aws.Time(retentionUntil),
  499. },
  500. })
  501. require.NoError(t, err)
  502. // Set legal hold
  503. _, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
  504. Bucket: aws.String(bucketName),
  505. Key: aws.String(key),
  506. LegalHold: &types.ObjectLockLegalHold{
  507. Status: types.ObjectLockLegalHoldStatusOn,
  508. },
  509. })
  510. require.NoError(t, err)
  511. // Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
  512. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  513. Bucket: aws.String(bucketName),
  514. Key: aws.String(key),
  515. })
  516. require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
  517. // Try DELETE with version ID and bypass - should still fail due to legal hold
  518. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  519. Bucket: aws.String(bucketName),
  520. Key: aws.String(key),
  521. VersionId: putResp.VersionId,
  522. BypassGovernanceRetention: aws.Bool(true),
  523. })
  524. require.Error(t, err, "Legal hold should prevent deletion even with governance bypass")
  525. // Remove legal hold (must specify version ID since latest version is now delete marker)
  526. _, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
  527. Bucket: aws.String(bucketName),
  528. Key: aws.String(key),
  529. VersionId: putResp.VersionId,
  530. LegalHold: &types.ObjectLockLegalHold{
  531. Status: types.ObjectLockLegalHoldStatusOff,
  532. },
  533. })
  534. require.NoError(t, err)
  535. // Now DELETE with version ID and bypass governance should succeed
  536. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  537. Bucket: aws.String(bucketName),
  538. Key: aws.String(key),
  539. VersionId: putResp.VersionId,
  540. BypassGovernanceRetention: aws.Bool(true),
  541. })
  542. require.NoError(t, err, "DELETE with version ID should succeed after legal hold removed and with governance bypass")
  543. }
  544. // TestExpiredRetention tests that objects can be deleted after retention expires
  545. func TestExpiredRetention(t *testing.T) {
  546. client := getS3Client(t)
  547. bucketName := getNewBucketName()
  548. // Create bucket and enable versioning
  549. createBucket(t, client, bucketName)
  550. defer deleteBucket(t, client, bucketName)
  551. enableVersioning(t, client, bucketName)
  552. // Create object
  553. key := "expired-retention-test"
  554. content := "expired retention test content"
  555. putResp := putObject(t, client, bucketName, key, content)
  556. require.NotNil(t, putResp.VersionId)
  557. // Set retention for a very short time (2 seconds)
  558. retentionUntil := time.Now().Add(2 * time.Second)
  559. _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  560. Bucket: aws.String(bucketName),
  561. Key: aws.String(key),
  562. Retention: &types.ObjectLockRetention{
  563. Mode: types.ObjectLockRetentionModeGovernance,
  564. RetainUntilDate: aws.Time(retentionUntil),
  565. },
  566. })
  567. require.NoError(t, err)
  568. // Try to delete immediately - should fail
  569. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  570. Bucket: aws.String(bucketName),
  571. Key: aws.String(key),
  572. })
  573. require.Error(t, err)
  574. // Wait for retention to expire
  575. time.Sleep(3 * time.Second)
  576. // Now delete should succeed
  577. _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
  578. Bucket: aws.String(bucketName),
  579. Key: aws.String(key),
  580. })
  581. require.NoError(t, err)
  582. }
  583. // TestRetentionErrorCases tests various error conditions
  584. func TestRetentionErrorCases(t *testing.T) {
  585. client := getS3Client(t)
  586. bucketName := getNewBucketName()
  587. // Create bucket and enable versioning
  588. createBucket(t, client, bucketName)
  589. defer deleteBucket(t, client, bucketName)
  590. enableVersioning(t, client, bucketName)
  591. // Test setting retention on non-existent object
  592. _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  593. Bucket: aws.String(bucketName),
  594. Key: aws.String("non-existent-key"),
  595. Retention: &types.ObjectLockRetention{
  596. Mode: types.ObjectLockRetentionModeGovernance,
  597. RetainUntilDate: aws.Time(time.Now().Add(1 * time.Hour)),
  598. },
  599. })
  600. require.Error(t, err)
  601. // Test getting retention on non-existent object
  602. _, err = client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
  603. Bucket: aws.String(bucketName),
  604. Key: aws.String("non-existent-key"),
  605. })
  606. require.Error(t, err)
  607. // Test setting legal hold on non-existent object
  608. _, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
  609. Bucket: aws.String(bucketName),
  610. Key: aws.String("non-existent-key"),
  611. LegalHold: &types.ObjectLockLegalHold{
  612. Status: types.ObjectLockLegalHoldStatusOn,
  613. },
  614. })
  615. require.Error(t, err)
  616. // Test getting legal hold on non-existent object
  617. _, err = client.GetObjectLegalHold(context.TODO(), &s3.GetObjectLegalHoldInput{
  618. Bucket: aws.String(bucketName),
  619. Key: aws.String("non-existent-key"),
  620. })
  621. require.Error(t, err)
  622. // Test setting retention with past date
  623. key := "retention-past-date-test"
  624. content := "test content"
  625. putObject(t, client, bucketName, key, content)
  626. pastDate := time.Now().Add(-1 * time.Hour)
  627. _, err = client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
  628. Bucket: aws.String(bucketName),
  629. Key: aws.String(key),
  630. Retention: &types.ObjectLockRetention{
  631. Mode: types.ObjectLockRetentionModeGovernance,
  632. RetainUntilDate: aws.Time(pastDate),
  633. },
  634. })
  635. require.Error(t, err)
  636. }