You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
307 lines
10 KiB
307 lines
10 KiB
package retention
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestPutObjectWithLockHeaders tests that object lock headers in PUT requests
|
|
// are properly stored and returned in HEAD responses
|
|
func TestPutObjectWithLockHeaders(t *testing.T) {
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
// Create bucket with object lock enabled and versioning
|
|
createBucketWithObjectLock(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
key := "test-object-lock-headers"
|
|
content := "test content with object lock headers"
|
|
retainUntilDate := time.Now().Add(24 * time.Hour)
|
|
|
|
// Test 1: PUT with COMPLIANCE mode and retention date
|
|
t.Run("PUT with COMPLIANCE mode", func(t *testing.T) {
|
|
testKey := key + "-compliance"
|
|
|
|
// PUT object with lock headers
|
|
putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content,
|
|
"COMPLIANCE", retainUntilDate, "")
|
|
require.NotNil(t, putResp.VersionId)
|
|
|
|
// HEAD object and verify lock headers are returned
|
|
headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(testKey),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify object lock metadata is present in response
|
|
assert.Equal(t, types.ObjectLockModeCompliance, headResp.ObjectLockMode)
|
|
assert.NotNil(t, headResp.ObjectLockRetainUntilDate)
|
|
assert.WithinDuration(t, retainUntilDate, *headResp.ObjectLockRetainUntilDate, 5*time.Second)
|
|
})
|
|
|
|
// Test 2: PUT with GOVERNANCE mode and retention date
|
|
t.Run("PUT with GOVERNANCE mode", func(t *testing.T) {
|
|
testKey := key + "-governance"
|
|
|
|
putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content,
|
|
"GOVERNANCE", retainUntilDate, "")
|
|
require.NotNil(t, putResp.VersionId)
|
|
|
|
headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(testKey),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, types.ObjectLockModeGovernance, headResp.ObjectLockMode)
|
|
assert.NotNil(t, headResp.ObjectLockRetainUntilDate)
|
|
assert.WithinDuration(t, retainUntilDate, *headResp.ObjectLockRetainUntilDate, 5*time.Second)
|
|
})
|
|
|
|
// Test 3: PUT with legal hold
|
|
t.Run("PUT with legal hold", func(t *testing.T) {
|
|
testKey := key + "-legal-hold"
|
|
|
|
putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content,
|
|
"", time.Time{}, "ON")
|
|
require.NotNil(t, putResp.VersionId)
|
|
|
|
headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(testKey),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, types.ObjectLockLegalHoldStatusOn, headResp.ObjectLockLegalHoldStatus)
|
|
})
|
|
|
|
// Test 4: PUT with both retention and legal hold
|
|
t.Run("PUT with both retention and legal hold", func(t *testing.T) {
|
|
testKey := key + "-both"
|
|
|
|
putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content,
|
|
"GOVERNANCE", retainUntilDate, "ON")
|
|
require.NotNil(t, putResp.VersionId)
|
|
|
|
headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(testKey),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, types.ObjectLockModeGovernance, headResp.ObjectLockMode)
|
|
assert.NotNil(t, headResp.ObjectLockRetainUntilDate)
|
|
assert.Equal(t, types.ObjectLockLegalHoldStatusOn, headResp.ObjectLockLegalHoldStatus)
|
|
})
|
|
}
|
|
|
|
// TestGetObjectWithLockHeaders verifies that GET requests also return object lock metadata
|
|
func TestGetObjectWithLockHeaders(t *testing.T) {
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
createBucketWithObjectLock(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
key := "test-get-object-lock"
|
|
content := "test content for GET with lock headers"
|
|
retainUntilDate := time.Now().Add(24 * time.Hour)
|
|
|
|
// PUT object with lock headers
|
|
putResp := putObjectWithLockHeaders(t, client, bucketName, key, content,
|
|
"COMPLIANCE", retainUntilDate, "ON")
|
|
require.NotNil(t, putResp.VersionId)
|
|
|
|
// GET object and verify lock headers are returned
|
|
getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(key),
|
|
})
|
|
require.NoError(t, err)
|
|
defer getResp.Body.Close()
|
|
|
|
// Verify object lock metadata is present in GET response
|
|
assert.Equal(t, types.ObjectLockModeCompliance, getResp.ObjectLockMode)
|
|
assert.NotNil(t, getResp.ObjectLockRetainUntilDate)
|
|
assert.WithinDuration(t, retainUntilDate, *getResp.ObjectLockRetainUntilDate, 5*time.Second)
|
|
assert.Equal(t, types.ObjectLockLegalHoldStatusOn, getResp.ObjectLockLegalHoldStatus)
|
|
}
|
|
|
|
// TestVersionedObjectLockHeaders tests object lock headers work with versioned objects
|
|
func TestVersionedObjectLockHeaders(t *testing.T) {
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
createBucketWithObjectLock(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
key := "test-versioned-lock"
|
|
content1 := "version 1 content"
|
|
content2 := "version 2 content"
|
|
retainUntilDate1 := time.Now().Add(12 * time.Hour)
|
|
retainUntilDate2 := time.Now().Add(24 * time.Hour)
|
|
|
|
// PUT first version with GOVERNANCE mode
|
|
putResp1 := putObjectWithLockHeaders(t, client, bucketName, key, content1,
|
|
"GOVERNANCE", retainUntilDate1, "")
|
|
require.NotNil(t, putResp1.VersionId)
|
|
|
|
// PUT second version with COMPLIANCE mode
|
|
putResp2 := putObjectWithLockHeaders(t, client, bucketName, key, content2,
|
|
"COMPLIANCE", retainUntilDate2, "ON")
|
|
require.NotNil(t, putResp2.VersionId)
|
|
require.NotEqual(t, *putResp1.VersionId, *putResp2.VersionId)
|
|
|
|
// HEAD latest version (version 2)
|
|
headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(key),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, types.ObjectLockModeCompliance, headResp.ObjectLockMode)
|
|
assert.Equal(t, types.ObjectLockLegalHoldStatusOn, headResp.ObjectLockLegalHoldStatus)
|
|
|
|
// HEAD specific version 1
|
|
headResp1, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(key),
|
|
VersionId: putResp1.VersionId,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, types.ObjectLockModeGovernance, headResp1.ObjectLockMode)
|
|
assert.NotEqual(t, types.ObjectLockLegalHoldStatusOn, headResp1.ObjectLockLegalHoldStatus)
|
|
}
|
|
|
|
// TestObjectLockHeadersErrorCases tests various error scenarios
|
|
func TestObjectLockHeadersErrorCases(t *testing.T) {
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
createBucketWithObjectLock(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
key := "test-error-cases"
|
|
content := "test content for error cases"
|
|
|
|
// Test 1: Invalid retention mode should be rejected
|
|
t.Run("Invalid retention mode", func(t *testing.T) {
|
|
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(key + "-invalid-mode"),
|
|
Body: strings.NewReader(content),
|
|
ObjectLockMode: "INVALID_MODE", // Invalid mode
|
|
ObjectLockRetainUntilDate: aws.Time(time.Now().Add(24 * time.Hour)),
|
|
})
|
|
require.Error(t, err)
|
|
})
|
|
|
|
// Test 2: Retention date in the past should be rejected
|
|
t.Run("Past retention date", func(t *testing.T) {
|
|
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(key + "-past-date"),
|
|
Body: strings.NewReader(content),
|
|
ObjectLockMode: "GOVERNANCE",
|
|
ObjectLockRetainUntilDate: aws.Time(time.Now().Add(-24 * time.Hour)), // Past date
|
|
})
|
|
require.Error(t, err)
|
|
})
|
|
|
|
// Test 3: Mode without date should be rejected
|
|
t.Run("Mode without retention date", func(t *testing.T) {
|
|
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(key + "-no-date"),
|
|
Body: strings.NewReader(content),
|
|
ObjectLockMode: "GOVERNANCE",
|
|
// Missing ObjectLockRetainUntilDate
|
|
})
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
// TestObjectLockHeadersNonVersionedBucket tests that object lock fails on non-versioned buckets
|
|
func TestObjectLockHeadersNonVersionedBucket(t *testing.T) {
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
// Create regular bucket without object lock/versioning
|
|
createBucket(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
key := "test-non-versioned"
|
|
content := "test content"
|
|
retainUntilDate := time.Now().Add(24 * time.Hour)
|
|
|
|
// Attempting to PUT with object lock headers should fail
|
|
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(key),
|
|
Body: strings.NewReader(content),
|
|
ObjectLockMode: "GOVERNANCE",
|
|
ObjectLockRetainUntilDate: aws.Time(retainUntilDate),
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// Helper Functions
|
|
|
|
// putObjectWithLockHeaders puts an object with object lock headers
|
|
func putObjectWithLockHeaders(t *testing.T, client *s3.Client, bucketName, key, content string,
|
|
mode string, retainUntilDate time.Time, legalHold string) *s3.PutObjectOutput {
|
|
|
|
input := &s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(key),
|
|
Body: strings.NewReader(content),
|
|
}
|
|
|
|
// Add retention mode and date if specified
|
|
if mode != "" {
|
|
switch mode {
|
|
case "COMPLIANCE":
|
|
input.ObjectLockMode = types.ObjectLockModeCompliance
|
|
case "GOVERNANCE":
|
|
input.ObjectLockMode = types.ObjectLockModeGovernance
|
|
}
|
|
if !retainUntilDate.IsZero() {
|
|
input.ObjectLockRetainUntilDate = aws.Time(retainUntilDate)
|
|
}
|
|
}
|
|
|
|
// Add legal hold if specified
|
|
if legalHold != "" {
|
|
switch legalHold {
|
|
case "ON":
|
|
input.ObjectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOn
|
|
case "OFF":
|
|
input.ObjectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOff
|
|
}
|
|
}
|
|
|
|
resp, err := client.PutObject(context.TODO(), input)
|
|
require.NoError(t, err)
|
|
return resp
|
|
}
|
|
|
|
// createBucketWithObjectLock creates a bucket with object lock enabled
|
|
func createBucketWithObjectLock(t *testing.T, client *s3.Client, bucketName string) {
|
|
_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
|
|
Bucket: aws.String(bucketName),
|
|
ObjectLockEnabledForBucket: aws.Bool(true),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Enable versioning (required for object lock)
|
|
enableVersioning(t, client, bucketName)
|
|
}
|