mirror of
https://github.com/nextcloud/server.git
synced 2026-04-23 07:08:34 -04:00
Merge 5b565ad586 into ae45f67a75
This commit is contained in:
commit
aee53a8ca2
5 changed files with 489 additions and 17 deletions
|
|
@ -115,7 +115,7 @@ class AmazonS3 extends Common {
|
|||
$this->objectCache[$key] = $this->getConnection()->headObject([
|
||||
'Bucket' => $this->bucket,
|
||||
'Key' => $key
|
||||
] + $this->getSSECParameters())->toArray();
|
||||
] + $this->getServerSideEncryptionParameters())->toArray();
|
||||
} catch (S3Exception $e) {
|
||||
if ($e->getStatusCode() >= 500) {
|
||||
throw $e;
|
||||
|
|
@ -209,7 +209,7 @@ class AmazonS3 extends Common {
|
|||
'Key' => $path . '/',
|
||||
'Body' => '',
|
||||
'ContentType' => FileInfo::MIMETYPE_FOLDER
|
||||
] + $this->getSSECParameters());
|
||||
] + $this->getServerSideEncryptionParameters());
|
||||
$this->testTimeout();
|
||||
} catch (S3Exception $e) {
|
||||
$this->logger->error($e->getMessage(), [
|
||||
|
|
@ -470,7 +470,7 @@ class AmazonS3 extends Common {
|
|||
'Body' => '',
|
||||
'ContentType' => $mimeType,
|
||||
'MetadataDirective' => 'REPLACE',
|
||||
] + $this->getSSECParameters());
|
||||
] + $this->getServerSideEncryptionParameters());
|
||||
$this->testTimeout();
|
||||
} catch (S3Exception $e) {
|
||||
$this->logger->error($e->getMessage(), [
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class S3 implements IObjectStore, IObjectStoreMultiPartUpload, IObjectStoreMetaD
|
|||
$upload = $this->getConnection()->createMultipartUpload([
|
||||
'Bucket' => $this->bucket,
|
||||
'Key' => $urn,
|
||||
] + $this->getSSECParameters());
|
||||
] + $this->getServerSideEncryptionParameters());
|
||||
$uploadId = $upload->get('UploadId');
|
||||
if ($uploadId === null) {
|
||||
throw new Exception('No upload id returned');
|
||||
|
|
@ -50,7 +50,7 @@ class S3 implements IObjectStore, IObjectStoreMultiPartUpload, IObjectStoreMetaD
|
|||
'ContentLength' => $size,
|
||||
'PartNumber' => $partId,
|
||||
'UploadId' => $uploadId,
|
||||
] + $this->getSSECParameters());
|
||||
] + $this->getServerSideEncryptionParameters());
|
||||
}
|
||||
|
||||
public function getMultipartUploads(string $urn, string $uploadId): array {
|
||||
|
|
@ -65,7 +65,7 @@ class S3 implements IObjectStore, IObjectStoreMultiPartUpload, IObjectStoreMetaD
|
|||
'UploadId' => $uploadId,
|
||||
'MaxParts' => 1000,
|
||||
'PartNumberMarker' => $partNumberMarker,
|
||||
] + $this->getSSECParameters());
|
||||
] + $this->getServerSideEncryptionParameters());
|
||||
$parts = array_merge($parts, $result->get('Parts') ?? []);
|
||||
$isTruncated = $result->get('IsTruncated');
|
||||
$partNumberMarker = $result->get('NextPartNumberMarker');
|
||||
|
|
@ -80,11 +80,11 @@ class S3 implements IObjectStore, IObjectStoreMultiPartUpload, IObjectStoreMetaD
|
|||
'Key' => $urn,
|
||||
'UploadId' => $uploadId,
|
||||
'MultipartUpload' => ['Parts' => $result],
|
||||
] + $this->getSSECParameters());
|
||||
] + $this->getServerSideEncryptionParameters());
|
||||
$stat = $this->getConnection()->headObject([
|
||||
'Bucket' => $this->bucket,
|
||||
'Key' => $urn,
|
||||
] + $this->getSSECParameters());
|
||||
] + $this->getServerSideEncryptionParameters());
|
||||
return (int)$stat->get('ContentLength');
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ class S3 implements IObjectStore, IObjectStoreMultiPartUpload, IObjectStoreMetaD
|
|||
$object = $this->getConnection()->headObject([
|
||||
'Bucket' => $this->bucket,
|
||||
'Key' => $urn
|
||||
] + $this->getSSECParameters())->toArray();
|
||||
] + $this->getServerSideEncryptionParameters())->toArray();
|
||||
return [
|
||||
'mtime' => $object['LastModified'],
|
||||
'etag' => trim($object['ETag'], '"'),
|
||||
|
|
@ -125,7 +125,7 @@ class S3 implements IObjectStore, IObjectStoreMultiPartUpload, IObjectStoreMetaD
|
|||
$results = $this->getConnection()->getPaginator('ListObjectsV2', [
|
||||
'Bucket' => $this->bucket,
|
||||
'Prefix' => $prefix,
|
||||
] + $this->getSSECParameters());
|
||||
] + $this->getServerSideEncryptionParameters());
|
||||
|
||||
foreach ($results as $result) {
|
||||
if (is_array($result['Contents'])) {
|
||||
|
|
|
|||
|
|
@ -295,6 +295,80 @@ trait S3ConnectionTrait {
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SSE-KMS key ID from configuration
|
||||
* @return string|null KMS key ARN/ID or null for bucket default key
|
||||
*/
|
||||
protected function getSSEKMSKeyId(): ?string {
|
||||
if (isset($this->params['sse_kms_key_id']) && !empty($this->params['sse_kms_key_id'])) {
|
||||
return $this->params['sse_kms_key_id'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SSE-KMS is enabled
|
||||
* @return bool
|
||||
*/
|
||||
protected function isSSEKMSEnabled(): bool {
|
||||
return !empty($this->params['sse_kms_enabled']) && $this->params['sse_kms_enabled'] === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SSE-KMS parameters for S3 operations
|
||||
*
|
||||
* When SSE-KMS is enabled, AWS S3 encrypts objects server-side using
|
||||
* AWS Key Management Service (KMS) keys. This provides:
|
||||
* - Centralized key management via AWS KMS
|
||||
* - Audit trail of key usage
|
||||
* - No client-side encryption overhead
|
||||
* - Automatic key rotation support
|
||||
*
|
||||
* @param bool $copy Whether this is for a copy operation (unused for KMS)
|
||||
* @return array Parameters to merge into S3 API calls
|
||||
*/
|
||||
protected function getSSEKMSParameters(bool $copy = false): array {
|
||||
if (!$this->isSSEKMSEnabled()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$params = [
|
||||
'ServerSideEncryption' => 'aws:kms',
|
||||
];
|
||||
|
||||
// Add specific KMS key if configured, otherwise use bucket default key
|
||||
$keyId = $this->getSSEKMSKeyId();
|
||||
if ($keyId !== null) {
|
||||
$params['SSEKMSKeyId'] = $keyId;
|
||||
}
|
||||
|
||||
// Note: For copy operations, S3 re-encrypts with the destination key
|
||||
// No special source parameters needed (unlike SSE-C)
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unified server-side encryption parameters
|
||||
*
|
||||
* Supports both SSE-C (customer-provided keys) and SSE-KMS (AWS-managed keys).
|
||||
* SSE-C takes precedence if both are configured (for backward compatibility
|
||||
* during migration from SSE-C to SSE-KMS).
|
||||
*
|
||||
* @param bool $copy Whether this is for a copy operation
|
||||
* @return array Encryption parameters to merge into S3 API calls
|
||||
*/
|
||||
protected function getServerSideEncryptionParameters(bool $copy = false): array {
|
||||
// SSE-C takes precedence for backward compatibility during migration
|
||||
$sseC = $this->getSSECParameters($copy);
|
||||
if (!empty($sseC)) {
|
||||
return $sseC;
|
||||
}
|
||||
|
||||
// Fall back to SSE-KMS if enabled
|
||||
return $this->getSSEKMSParameters($copy);
|
||||
}
|
||||
|
||||
public function isUsePresignedUrl(): bool {
|
||||
return $this->usePresignedUrl;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ trait S3ObjectTrait {
|
|||
|
||||
abstract protected function getCertificateBundlePath(): ?string;
|
||||
abstract protected function getSSECParameters(bool $copy = false): array;
|
||||
abstract protected function getServerSideEncryptionParameters(bool $copy = false): array;
|
||||
|
||||
/**
|
||||
* @param string $urn the unified resource name used to identify the object
|
||||
|
|
@ -46,7 +47,7 @@ trait S3ObjectTrait {
|
|||
'Bucket' => $this->bucket,
|
||||
'Key' => $urn,
|
||||
'Range' => 'bytes=' . $range,
|
||||
] + $this->getSSECParameters());
|
||||
] + $this->getServerSideEncryptionParameters());
|
||||
$request = \Aws\serialize($command);
|
||||
$headers = [];
|
||||
foreach ($request->getHeaders() as $key => $values) {
|
||||
|
|
@ -114,7 +115,7 @@ trait S3ObjectTrait {
|
|||
'ContentType' => $mimetype,
|
||||
'Metadata' => $this->buildS3Metadata($metaData),
|
||||
'StorageClass' => $this->storageClass,
|
||||
] + $this->getSSECParameters();
|
||||
] + $this->getServerSideEncryptionParameters();
|
||||
|
||||
if ($size = $stream->getSize()) {
|
||||
$args['ContentLength'] = $size;
|
||||
|
|
@ -157,7 +158,7 @@ trait S3ObjectTrait {
|
|||
'ContentType' => $mimetype,
|
||||
'Metadata' => $this->buildS3Metadata($metaData),
|
||||
'StorageClass' => $this->storageClass,
|
||||
] + $this->getSSECParameters(),
|
||||
] + $this->getServerSideEncryptionParameters(),
|
||||
'before_upload' => function (Command $command) use (&$totalWritten): void {
|
||||
$totalWritten += $command['ContentLength'];
|
||||
},
|
||||
|
|
@ -267,14 +268,14 @@ trait S3ObjectTrait {
|
|||
}
|
||||
|
||||
public function objectExists($urn) {
|
||||
return $this->getConnection()->doesObjectExist($this->bucket, $urn, $this->getSSECParameters());
|
||||
return $this->getConnection()->doesObjectExist($this->bucket, $urn, $this->getServerSideEncryptionParameters());
|
||||
}
|
||||
|
||||
public function copyObject($from, $to, array $options = []) {
|
||||
$sourceMetadata = $this->getConnection()->headObject([
|
||||
'Bucket' => $this->getBucket(),
|
||||
'Key' => $from,
|
||||
] + $this->getSSECParameters());
|
||||
] + $this->getServerSideEncryptionParameters());
|
||||
|
||||
$size = (int)($sourceMetadata->get('Size') ?? $sourceMetadata->get('ContentLength'));
|
||||
|
||||
|
|
@ -286,13 +287,13 @@ trait S3ObjectTrait {
|
|||
'bucket' => $this->getBucket(),
|
||||
'key' => $to,
|
||||
'acl' => 'private',
|
||||
'params' => $this->getSSECParameters() + $this->getSSECParameters(true),
|
||||
'params' => $this->getServerSideEncryptionParameters() + $this->getServerSideEncryptionParameters(true),
|
||||
'source_metadata' => $sourceMetadata
|
||||
], $options));
|
||||
$copy->copy();
|
||||
} else {
|
||||
$this->getConnection()->copy($this->getBucket(), $from, $this->getBucket(), $to, 'private', array_merge([
|
||||
'params' => $this->getSSECParameters() + $this->getSSECParameters(true),
|
||||
'params' => $this->getServerSideEncryptionParameters() + $this->getServerSideEncryptionParameters(true),
|
||||
'mup_threshold' => PHP_INT_MAX,
|
||||
], $options));
|
||||
}
|
||||
|
|
|
|||
397
tests/lib/Files/ObjectStore/S3SSEKMSTest.php
Normal file
397
tests/lib/Files/ObjectStore/S3SSEKMSTest.php
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace Test\Files\ObjectStore;
|
||||
|
||||
use OC\Files\ObjectStore\S3;
|
||||
use OCP\IConfig;
|
||||
use OCP\Server;
|
||||
|
||||
/**
|
||||
* Test suite for AWS SSE-KMS (Server-Side Encryption with Key Management Service).
|
||||
*
|
||||
* SSE-KMS provides:
|
||||
* - AWS-managed server-side encryption
|
||||
* - Centralized key management via AWS KMS
|
||||
* - Audit trail of key usage via CloudTrail
|
||||
* - No client-side encryption overhead
|
||||
* - Automatic key rotation support
|
||||
*
|
||||
* Configuration options:
|
||||
* - sse_kms_enabled: true - Enable SSE-KMS
|
||||
* - sse_kms_key_id: (optional) Specific KMS key ARN, or use bucket default
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\Group('PRIMARY-s3')]
|
||||
#[\PHPUnit\Framework\Attributes\Group('SSE-KMS')]
|
||||
class S3SSEKMSTest extends ObjectStoreTestCase {
|
||||
|
||||
private S3 $instance;
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
$config = Server::get(IConfig::class)->getSystemValue('objectstore');
|
||||
if (!is_array($config) || $config['class'] !== S3::class) {
|
||||
self::markTestSkipped('S3 primary storage not configured');
|
||||
}
|
||||
|
||||
$arguments = $config['arguments'] ?? [];
|
||||
if (empty($arguments['sse_kms_enabled'])) {
|
||||
self::markTestSkipped('SSE-KMS not enabled. Set sse_kms_enabled=true in objectstore config');
|
||||
}
|
||||
}
|
||||
|
||||
protected function getInstance() {
|
||||
if (!isset($this->instance)) {
|
||||
$config = Server::get(IConfig::class)->getSystemValue('objectstore');
|
||||
$this->instance = new S3($config['arguments']);
|
||||
}
|
||||
return $this->instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test basic write and read with SSE-KMS
|
||||
*/
|
||||
public function testWriteReadWithKMS(): void {
|
||||
$this->cleanupAfter('kms-test-write-read');
|
||||
|
||||
$s3 = $this->getInstance();
|
||||
$data = 'Test data for SSE-KMS encryption';
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $data);
|
||||
rewind($stream);
|
||||
|
||||
// Write with SSE-KMS
|
||||
$s3->writeObject('kms-test-write-read', $stream);
|
||||
|
||||
// Read back
|
||||
$result = $s3->readObject('kms-test-write-read');
|
||||
$readData = stream_get_contents($result);
|
||||
fclose($result);
|
||||
|
||||
$this->assertEquals($data, $readData, 'Data should be readable after SSE-KMS encryption');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test copy operation with SSE-KMS
|
||||
*/
|
||||
public function testCopyWithKMS(): void {
|
||||
$this->cleanupAfter('kms-test-copy-source');
|
||||
$this->cleanupAfter('kms-test-copy-target');
|
||||
|
||||
$s3 = $this->getInstance();
|
||||
$data = 'Test data for SSE-KMS copy operation';
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $data);
|
||||
rewind($stream);
|
||||
|
||||
// Write source file
|
||||
$s3->writeObject('kms-test-copy-source', $stream);
|
||||
|
||||
// Copy (should re-encrypt with same KMS key)
|
||||
$s3->copyObject('kms-test-copy-source', 'kms-test-copy-target');
|
||||
|
||||
// Verify copy
|
||||
$this->assertTrue($s3->objectExists('kms-test-copy-target'), 'Copied object should exist');
|
||||
|
||||
$result = $s3->readObject('kms-test-copy-target');
|
||||
$readData = stream_get_contents($result);
|
||||
fclose($result);
|
||||
|
||||
$this->assertEquals($data, $readData, 'Copied data should match original');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test multipart upload with SSE-KMS
|
||||
*/
|
||||
public function testMultipartUploadWithKMS(): void {
|
||||
$this->cleanupAfter('kms-test-multipart');
|
||||
|
||||
$s3 = $this->getInstance();
|
||||
|
||||
// Create 6MB data to trigger multipart
|
||||
$data = str_repeat('A', 6 * 1024 * 1024);
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $data);
|
||||
rewind($stream);
|
||||
|
||||
// Write with multipart (forces multipart upload)
|
||||
$s3->writeObject('kms-test-multipart', $stream, 'application/octet-stream');
|
||||
|
||||
// Verify
|
||||
$this->assertTrue($s3->objectExists('kms-test-multipart'), 'Multipart object should exist');
|
||||
|
||||
// Read back first 1000 bytes to verify
|
||||
$result = $s3->readObject('kms-test-multipart');
|
||||
$readData = fread($result, 1000);
|
||||
fclose($result);
|
||||
|
||||
$this->assertEquals(substr($data, 0, 1000), $readData, 'Multipart data should be readable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for various file sizes
|
||||
*/
|
||||
public static function dataFileSizes(): array {
|
||||
return [
|
||||
'1KB' => [1024],
|
||||
'1MB' => [1024 * 1024],
|
||||
'10MB' => [10 * 1024 * 1024],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for large file sizes to test multipart upload threshold behavior
|
||||
*/
|
||||
public static function dataLargeFileSizes(): array {
|
||||
return [
|
||||
'50MB' => [50 * 1024 * 1024],
|
||||
'100MB' => [100 * 1024 * 1024],
|
||||
'150MB' => [150 * 1024 * 1024],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test various file sizes with SSE-KMS
|
||||
*
|
||||
* @dataProvider dataFileSizes
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataFileSizes')]
|
||||
public function testFileSizesWithKMS(int $size): void {
|
||||
$urn = 'kms-test-size-' . ($size / 1024) . 'kb';
|
||||
$this->cleanupAfter($urn);
|
||||
|
||||
$s3 = $this->getInstance();
|
||||
$data = str_repeat('X', $size);
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $data);
|
||||
rewind($stream);
|
||||
|
||||
// Write
|
||||
$s3->writeObject($urn, $stream);
|
||||
|
||||
// Verify object exists
|
||||
$this->assertTrue($s3->objectExists($urn), "Object should exist for size $size");
|
||||
|
||||
// Read back and verify
|
||||
$result = $s3->readObject($urn);
|
||||
$readData = stream_get_contents($result);
|
||||
fclose($result);
|
||||
|
||||
$this->assertEquals($size, strlen($readData), "Size mismatch for $size byte file");
|
||||
$this->assertEquals($data, $readData, "Content mismatch for $size byte file");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that SSE-KMS metadata is set on objects
|
||||
*/
|
||||
public function testKMSMetadataPresent(): void {
|
||||
$this->cleanupAfter('kms-test-metadata');
|
||||
|
||||
$s3 = $this->getInstance();
|
||||
$data = 'Test KMS metadata';
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $data);
|
||||
rewind($stream);
|
||||
|
||||
// Write with SSE-KMS
|
||||
$s3->writeObject('kms-test-metadata', $stream);
|
||||
|
||||
// Check metadata via headObject
|
||||
$result = $s3->getConnection()->headObject([
|
||||
'Bucket' => $s3->getBucket(),
|
||||
'Key' => 'kms-test-metadata',
|
||||
]);
|
||||
|
||||
// Verify SSE is KMS
|
||||
$this->assertEquals('aws:kms', $result->get('ServerSideEncryption'),
|
||||
'Object should have SSE-KMS encryption');
|
||||
|
||||
// If specific key configured, verify it's used
|
||||
$config = Server::get(IConfig::class)->getSystemValue('objectstore');
|
||||
if (!empty($config['arguments']['sse_kms_key_id'])) {
|
||||
$this->assertNotNull($result->get('SSEKMSKeyId'),
|
||||
'KMS Key ID should be present when specific key configured');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test zero-byte file with SSE-KMS
|
||||
*
|
||||
* Note: Zero-byte files are a known edge case with S3.
|
||||
* While they can be written, reading them back may fail due to
|
||||
* Range header issues with empty objects.
|
||||
*/
|
||||
public function testZeroByteFileWithKMS(): void {
|
||||
$this->cleanupAfter('kms-test-zerobyte');
|
||||
|
||||
$s3 = $this->getInstance();
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
// Write nothing (zero bytes)
|
||||
rewind($stream);
|
||||
|
||||
// Write zero-byte file
|
||||
$s3->writeObject('kms-test-zerobyte', $stream);
|
||||
|
||||
// Verify exists
|
||||
$this->assertTrue($s3->objectExists('kms-test-zerobyte'), 'Zero-byte object should exist');
|
||||
|
||||
// Verify via headObject instead of read (avoids Range header issue)
|
||||
$metadata = $s3->getConnection()->headObject([
|
||||
'Bucket' => $s3->getBucket(),
|
||||
'Key' => 'kms-test-zerobyte',
|
||||
]);
|
||||
|
||||
$this->assertEquals(0, $metadata->get('ContentLength'),
|
||||
'Zero-byte file should have ContentLength of 0');
|
||||
$this->assertEquals('aws:kms', $metadata->get('ServerSideEncryption'),
|
||||
'Zero-byte file should still have SSE-KMS encryption');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test large file sizes with SSE-KMS to verify multipart threshold behavior
|
||||
*
|
||||
* @dataProvider dataLargeFileSizes
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataLargeFileSizes')]
|
||||
#[\PHPUnit\Framework\Attributes\Group('SLOWDB')]
|
||||
public function testLargeFileSizesWithKMS(int $size): void {
|
||||
$urn = 'kms-test-large-size-' . ($size / 1024 / 1024) . 'mb';
|
||||
$this->cleanupAfter($urn);
|
||||
|
||||
$s3 = $this->getInstance();
|
||||
$data = str_repeat('L', $size);
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $data);
|
||||
rewind($stream);
|
||||
|
||||
// Write (should trigger multipart for files >= 100MB)
|
||||
$s3->writeObject($urn, $stream);
|
||||
|
||||
// Verify object exists
|
||||
$this->assertTrue($s3->objectExists($urn), "Object should exist for size $size");
|
||||
|
||||
// Verify metadata via headObject
|
||||
$metadata = $s3->getConnection()->headObject([
|
||||
'Bucket' => $s3->getBucket(),
|
||||
'Key' => $urn,
|
||||
]);
|
||||
|
||||
$this->assertEquals('aws:kms', $metadata->get('ServerSideEncryption'),
|
||||
"Object should have SSE-KMS encryption for size $size");
|
||||
$this->assertEquals($size, $metadata->get('ContentLength'),
|
||||
"Size should match for $size byte file");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test multipart copy operation with large files
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\Group('SLOWDB')]
|
||||
public function testMultipartCopyWithKMS(): void {
|
||||
$this->cleanupAfter('kms-test-multipart-copy-source');
|
||||
$this->cleanupAfter('kms-test-multipart-copy-target');
|
||||
|
||||
$s3 = $this->getInstance();
|
||||
|
||||
// Create large file to trigger multipart copy (> copySizeLimit, default 5GB)
|
||||
// Use 10MB for test efficiency, but in production this would be > 5GB
|
||||
$size = 10 * 1024 * 1024;
|
||||
$data = str_repeat('C', $size);
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $data);
|
||||
rewind($stream);
|
||||
|
||||
// Write source file
|
||||
$s3->writeObject('kms-test-multipart-copy-source', $stream);
|
||||
|
||||
// Copy (should re-encrypt with same KMS key)
|
||||
$s3->copyObject('kms-test-multipart-copy-source', 'kms-test-multipart-copy-target');
|
||||
|
||||
// Verify copy exists
|
||||
$this->assertTrue($s3->objectExists('kms-test-multipart-copy-target'), 'Copied object should exist');
|
||||
|
||||
// Verify encryption on target
|
||||
$metadata = $s3->getConnection()->headObject([
|
||||
'Bucket' => $s3->getBucket(),
|
||||
'Key' => 'kms-test-multipart-copy-target',
|
||||
]);
|
||||
|
||||
$this->assertEquals('aws:kms', $metadata->get('ServerSideEncryption'),
|
||||
'Copied object should have SSE-KMS encryption');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test delete operation with KMS-encrypted objects
|
||||
*/
|
||||
public function testDeleteWithKMS(): void {
|
||||
$this->cleanupAfter('kms-test-delete');
|
||||
|
||||
$s3 = $this->getInstance();
|
||||
$data = 'Test data for delete operation';
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $data);
|
||||
rewind($stream);
|
||||
|
||||
// Write with SSE-KMS
|
||||
$s3->writeObject('kms-test-delete', $stream);
|
||||
|
||||
// Verify exists
|
||||
$this->assertTrue($s3->objectExists('kms-test-delete'), 'Object should exist before delete');
|
||||
|
||||
// Delete
|
||||
$s3->deleteObject('kms-test-delete');
|
||||
|
||||
// Verify deleted
|
||||
$this->assertFalse($s3->objectExists('kms-test-delete'), 'Object should not exist after delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test overwriting existing KMS-encrypted objects
|
||||
*/
|
||||
public function testOverwriteWithKMS(): void {
|
||||
$this->cleanupAfter('kms-test-overwrite');
|
||||
|
||||
$s3 = $this->getInstance();
|
||||
|
||||
// Write initial data
|
||||
$data1 = 'Initial data for overwrite test';
|
||||
$stream1 = fopen('php://temp', 'r+');
|
||||
fwrite($stream1, $data1);
|
||||
rewind($stream1);
|
||||
$s3->writeObject('kms-test-overwrite', $stream1);
|
||||
|
||||
// Verify initial write
|
||||
$result1 = $s3->readObject('kms-test-overwrite');
|
||||
$readData1 = stream_get_contents($result1);
|
||||
fclose($result1);
|
||||
$this->assertEquals($data1, $readData1, 'Initial data should match');
|
||||
|
||||
// Overwrite with new data
|
||||
$data2 = 'Overwritten data with different content';
|
||||
$stream2 = fopen('php://temp', 'r+');
|
||||
fwrite($stream2, $data2);
|
||||
rewind($stream2);
|
||||
$s3->writeObject('kms-test-overwrite', $stream2);
|
||||
|
||||
// Verify overwrite
|
||||
$result2 = $s3->readObject('kms-test-overwrite');
|
||||
$readData2 = stream_get_contents($result2);
|
||||
fclose($result2);
|
||||
$this->assertEquals($data2, $readData2, 'Overwritten data should match');
|
||||
|
||||
// Verify still encrypted with KMS
|
||||
$metadata = $s3->getConnection()->headObject([
|
||||
'Bucket' => $s3->getBucket(),
|
||||
'Key' => 'kms-test-overwrite',
|
||||
]);
|
||||
|
||||
$this->assertEquals('aws:kms', $metadata->get('ServerSideEncryption'),
|
||||
'Overwritten object should still have SSE-KMS encryption');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue