diff options
| author | Chris Lu <chrislusf@users.noreply.github.com> | 2025-11-28 13:28:17 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-28 13:28:17 -0800 |
| commit | bd419fda5119ae7f7225cfca0fcb98bce04f4980 (patch) | |
| tree | 16e1f6d020daf38663c44c423f935d3a0546f7f8 /test | |
| parent | 7e4bab80326866b1ade17bf3ebddb96999507a25 (diff) | |
| download | seaweedfs-bd419fda5119ae7f7225cfca0fcb98bce04f4980.tar.xz seaweedfs-bd419fda5119ae7f7225cfca0fcb98bce04f4980.zip | |
fix: copy to bucket with default SSE-S3 encryption fails (#7562) (#7568)
* filer use context without cancellation
* pass along context
* fix: copy to bucket with default SSE-S3 encryption fails (#7562)
When copying an object from an encrypted bucket to a temporary unencrypted
bucket, then to another bucket with default SSE-S3 encryption, the operation
fails with 'invalid SSE-S3 source key type' error.
Root cause:
When objects are copied from an SSE-S3 encrypted bucket to an unencrypted
bucket, the 'X-Amz-Server-Side-Encryption: AES256' header is preserved but
the actual encryption key (SeaweedFSSSES3Key) is stripped. This creates an
'orphaned' SSE-S3 header that causes IsSSES3EncryptedInternal() to return
true, triggering decryption logic with a nil key.
Fix:
1. Modified IsSSES3EncryptedInternal() to require BOTH the AES256 header
AND the SeaweedFSSSES3Key to be present before returning true
2. Added isOrphanedSSES3Header() to detect orphaned SSE-S3 headers
3. Updated copy handler to strip orphaned headers during copy operations
Fixes #7562
* fmt
* refactor: simplify isOrphanedSSES3Header function logic
Remove redundant existence check since the caller iterates through
metadata map, making the check unnecessary. Improves readability
while maintaining the same functionality.
Diffstat (limited to 'test')
| -rw-r--r-- | test/s3/sse/s3_sse_integration_test.go | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/test/s3/sse/s3_sse_integration_test.go b/test/s3/sse/s3_sse_integration_test.go index 0b3ff8f04..7b939ea76 100644 --- a/test/s3/sse/s3_sse_integration_test.go +++ b/test/s3/sse/s3_sse_integration_test.go @@ -1856,6 +1856,234 @@ func TestCrossSSECopy(t *testing.T) { }) } +// TestCopyToBucketDefaultEncryptedRegression tests copying objects to buckets with default +// encryption enabled. This is a regression test for GitHub issue #7562 where copying from +// an unencrypted bucket to a bucket with SSE-S3 default encryption fails with error +// "invalid SSE-S3 source key type". +// +// The scenario is: +// 1. Create source bucket with SSE-S3 encryption +// 2. Upload encrypted object +// 3. Copy to temp bucket (unencrypted) - data is decrypted +// 4. Copy from temp bucket to dest bucket with SSE-S3 default encryption - this should re-encrypt +// +// The bug occurs because the source detection incorrectly identifies the temp object as +// SSE-S3 encrypted (based on leftover metadata) when it's actually unencrypted. +func TestCopyToBucketDefaultEncryptedRegression(t *testing.T) { + ctx := context.Background() + client, err := createS3Client(ctx, defaultConfig) + require.NoError(t, err, "Failed to create S3 client") + + // Create three buckets for the test scenario + srcBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"copy-src-") + require.NoError(t, err, "Failed to create source bucket") + defer cleanupTestBucket(ctx, client, srcBucket) + + tempBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"copy-temp-") + require.NoError(t, err, "Failed to create temp bucket") + defer cleanupTestBucket(ctx, client, tempBucket) + + dstBucket, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"copy-dst-") + require.NoError(t, err, "Failed to create destination bucket") + defer cleanupTestBucket(ctx, client, dstBucket) + + // Enable SSE-S3 default encryption on source bucket + _, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{ + Bucket: aws.String(srcBucket), + ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{ + Rules: []types.ServerSideEncryptionRule{ + { + ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{ + SSEAlgorithm: types.ServerSideEncryptionAes256, + }, + }, + }, + }, + }) + require.NoError(t, err, "Failed to set source bucket encryption") + + // Enable SSE-S3 default encryption on destination bucket + _, err = client.PutBucketEncryption(ctx, &s3.PutBucketEncryptionInput{ + Bucket: aws.String(dstBucket), + ServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{ + Rules: []types.ServerSideEncryptionRule{ + { + ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{ + SSEAlgorithm: types.ServerSideEncryptionAes256, + }, + }, + }, + }, + }) + require.NoError(t, err, "Failed to set destination bucket encryption") + + // Test data + testData := []byte("Test data for copy-to-default-encrypted bucket regression test - GitHub issue #7562") + objectKey := "test-object.txt" + + t.Run("CopyEncrypted_ToTemp_ToEncrypted", func(t *testing.T) { + // Step 1: Upload object to source bucket (will be automatically encrypted) + _, err = client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(srcBucket), + Key: aws.String(objectKey), + Body: bytes.NewReader(testData), + // No encryption header - bucket default applies + }) + require.NoError(t, err, "Failed to upload to source bucket") + + // Verify source object is encrypted + srcHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: aws.String(srcBucket), + Key: aws.String(objectKey), + }) + require.NoError(t, err, "Failed to HEAD source object") + assert.Equal(t, types.ServerSideEncryptionAes256, srcHead.ServerSideEncryption, + "Source object should be SSE-S3 encrypted") + + // Step 2: Copy to temp bucket (unencrypted) - this should decrypt + _, err = client.CopyObject(ctx, &s3.CopyObjectInput{ + Bucket: aws.String(tempBucket), + Key: aws.String(objectKey), + CopySource: aws.String(fmt.Sprintf("%s/%s", srcBucket, objectKey)), + // No encryption - data should be stored unencrypted + }) + require.NoError(t, err, "Failed to copy to temp bucket") + + // Verify temp object is unencrypted + tempHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: aws.String(tempBucket), + Key: aws.String(objectKey), + }) + require.NoError(t, err, "Failed to HEAD temp object") + assert.Empty(t, tempHead.ServerSideEncryption, + "Temp object should be unencrypted") + + // Verify temp object content is correct + tempGet, err := client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(tempBucket), + Key: aws.String(objectKey), + }) + require.NoError(t, err, "Failed to GET temp object") + tempData, err := io.ReadAll(tempGet.Body) + tempGet.Body.Close() + require.NoError(t, err, "Failed to read temp object") + assertDataEqual(t, testData, tempData, "Temp object data mismatch") + + // Step 3: Copy from temp bucket to dest bucket (with default encryption) + // THIS IS THE BUG: This copy fails with "invalid SSE-S3 source key type" + _, err = client.CopyObject(ctx, &s3.CopyObjectInput{ + Bucket: aws.String(dstBucket), + Key: aws.String(objectKey), + CopySource: aws.String(fmt.Sprintf("%s/%s", tempBucket, objectKey)), + // No encryption header - bucket default should apply + }) + require.NoError(t, err, "Failed to copy to destination bucket - GitHub issue #7562") + + // Verify destination object is encrypted + dstHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: aws.String(dstBucket), + Key: aws.String(objectKey), + }) + require.NoError(t, err, "Failed to HEAD destination object") + assert.Equal(t, types.ServerSideEncryptionAes256, dstHead.ServerSideEncryption, + "Destination object should be SSE-S3 encrypted via bucket default") + + // Verify destination object content is correct + dstGet, err := client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(dstBucket), + Key: aws.String(objectKey), + }) + require.NoError(t, err, "Failed to GET destination object") + dstData, err := io.ReadAll(dstGet.Body) + dstGet.Body.Close() + require.NoError(t, err, "Failed to read destination object") + assertDataEqual(t, testData, dstData, "Destination object data mismatch after re-encryption") + }) + + t.Run("DirectCopyUnencrypted_ToEncrypted", func(t *testing.T) { + // Simpler test case: copy from unencrypted bucket directly to encrypted bucket + objectKey := "direct-copy-test.txt" + + // Upload to temp bucket (no default encryption) + _, err = client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(tempBucket), + Key: aws.String(objectKey), + Body: bytes.NewReader(testData), + }) + require.NoError(t, err, "Failed to upload to temp bucket") + + // Copy to destination bucket with default encryption + _, err = client.CopyObject(ctx, &s3.CopyObjectInput{ + Bucket: aws.String(dstBucket), + Key: aws.String(objectKey), + CopySource: aws.String(fmt.Sprintf("%s/%s", tempBucket, objectKey)), + }) + require.NoError(t, err, "Failed direct copy unencrypted to default-encrypted bucket") + + // Verify destination is encrypted + dstHead, err := client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: aws.String(dstBucket), + Key: aws.String(objectKey), + }) + require.NoError(t, err, "Failed to HEAD destination object") + assert.Equal(t, types.ServerSideEncryptionAes256, dstHead.ServerSideEncryption, + "Object should be encrypted via bucket default") + + // Verify content + dstGet, err := client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(dstBucket), + Key: aws.String(objectKey), + }) + require.NoError(t, err, "Failed to GET destination object") + dstData, err := io.ReadAll(dstGet.Body) + dstGet.Body.Close() + require.NoError(t, err, "Failed to read destination object") + assertDataEqual(t, testData, dstData, "Data mismatch after encryption") + }) + + t.Run("CopyWithExplicitSSES3Header", func(t *testing.T) { + // Test explicit SSE-S3 header during copy (should work even without bucket default) + objectKey := "explicit-sse-copy-test.txt" + + // Upload to temp bucket (unencrypted) + _, err = client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(tempBucket), + Key: aws.String(objectKey), + Body: bytes.NewReader(testData), + }) + require.NoError(t, err, "Failed to upload to temp bucket") + + // Copy with explicit SSE-S3 header + _, err = client.CopyObject(ctx, &s3.CopyObjectInput{ + Bucket: aws.String(tempBucket), // Same bucket, but with encryption + Key: aws.String(objectKey + "-encrypted"), + CopySource: aws.String(fmt.Sprintf("%s/%s", tempBucket, objectKey)), + ServerSideEncryption: types.ServerSideEncryptionAes256, + }) + require.NoError(t, err, "Failed copy with explicit SSE-S3 header") + + // Verify encrypted + head, err := client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: aws.String(tempBucket), + Key: aws.String(objectKey + "-encrypted"), + }) + require.NoError(t, err, "Failed to HEAD object") + assert.Equal(t, types.ServerSideEncryptionAes256, head.ServerSideEncryption, + "Object should be SSE-S3 encrypted") + + // Verify content + getResp, err := client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(tempBucket), + Key: aws.String(objectKey + "-encrypted"), + }) + require.NoError(t, err, "Failed to GET object") + data, err := io.ReadAll(getResp.Body) + getResp.Body.Close() + require.NoError(t, err, "Failed to read object") + assertDataEqual(t, testData, data, "Data mismatch") + }) +} + // REGRESSION TESTS FOR CRITICAL BUGS FIXED // These tests specifically target the IV storage bugs that were fixed |
