From 1cedb14a77cad81429ec6e6fb95c1ca5765bc050 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Fri, 20 Feb 2026 08:07:06 +0000 Subject: [PATCH 1/2] feat(storage): add support for encryption enforcement configurations Adds metadata support for `customerManagedEncryptionEnforcementConfig` and `customerSuppliedEncryptionEnforcementConfig` to the Bucket resource. Includes: - Unit tests in BucketTest and StorageClientTest for metadata mapping. - System tests in KmsTest verifying FullyRestricted enforcement and 412 error handling. --- Storage/src/Bucket.php | 11 +++ .../ServiceDefinition/storage-v1.json | 72 +++++++++++++++++++ Storage/src/StorageClient.php | 11 +++ Storage/tests/System/KmsTest.php | 40 +++++++++++ Storage/tests/System/ManageBucketsTest.php | 53 ++++++++++++++ Storage/tests/Unit/BucketTest.php | 26 +++++++ Storage/tests/Unit/StorageClientTest.php | 31 ++++++++ 7 files changed, 244 insertions(+) diff --git a/Storage/src/Bucket.php b/Storage/src/Bucket.php index 4f6b30024980..f005b19bebc2 100644 --- a/Storage/src/Bucket.php +++ b/Storage/src/Bucket.php @@ -1029,6 +1029,17 @@ public function delete(array $options = []) * `projects/my-project/locations/kr-location/keyRings/my-kr/cryptoKeys/my-key`. * Please note the KMS key ring must use the same location as the * bucket. + * @type array $encryption.googleManagedEncryptionEnforcementConfig + * Enforcement configuration for Google-managed encryption. + * @type array $encryption.customerManagedEncryptionEnforcementConfig + * Enforcement configuration for Cloud KMS (customer-managed) encryption. + * @type array $encryption.customerSuppliedEncryptionEnforcementConfig + * Enforcement configuration for customer-supplied encryption keys (CSEK). + * @type string $encryption.*.restrictionMode The restriction state of + * the encryption policy. Acceptable values are `"NotRestricted"` + * and `"FullyRestricted"`. + * @type string $encryption.*.effectiveTime [readonly] The time from which + * the policy was effective in RFC 3339 format. * @type bool $defaultEventBasedHold When `true`, newly created objects * in this bucket will be retained indefinitely until an event * occurs, signified by the hold's release. diff --git a/Storage/src/Connection/ServiceDefinition/storage-v1.json b/Storage/src/Connection/ServiceDefinition/storage-v1.json index 52aa9050a729..f585d2d5d752 100644 --- a/Storage/src/Connection/ServiceDefinition/storage-v1.json +++ b/Storage/src/Connection/ServiceDefinition/storage-v1.json @@ -195,6 +195,78 @@ "defaultKmsKeyName": { "type": "string", "description": "A Cloud KMS key that will be used to encrypt objects inserted into this bucket, if no encryption method is specified." + }, + "googleManagedEncryptionEnforcementConfig": { + "type": "object", + "description": "Enforcement configuration for Google-managed encryption.", + "properties": { + "restrictionMode": { + "type": "string", + "description": "The restriction state of the encryption policy.", + "enum": [ + "NotRestricted", + "FullyRestricted" + ], + "enumDescriptions": [ + "No restrictions are applied to this encryption type.", + "Specific restrictions are enforced for this encryption type." + ] + }, + "effectiveTime": { + "type": "string", + "description": "Server-determined value indicating when this configuration became effective. In RFC 3339 format.", + "format": "date-time", + "readOnly": true + } + } + }, + "customerManagedEncryptionEnforcementConfig": { + "type": "object", + "description": "Enforcement configuration for Cloud KMS (customer-managed) encryption.", + "properties": { + "restrictionMode": { + "type": "string", + "description": "The restriction state of the encryption policy.", + "enum": [ + "NotRestricted", + "FullyRestricted" + ], + "enumDescriptions": [ + "No restrictions are applied to this encryption type.", + "Specific restrictions are enforced for this encryption type." + ] + }, + "effectiveTime": { + "type": "string", + "description": "Server-determined value indicating when this configuration became effective. In RFC 3339 format.", + "format": "date-time", + "readOnly": true + } + } + }, + "customerSuppliedEncryptionEnforcementConfig": { + "type": "object", + "description": "Enforcement configuration for customer-supplied encryption keys (CSEK).", + "properties": { + "restrictionMode": { + "type": "string", + "description": "The restriction state of the encryption policy.", + "enum": [ + "NotRestricted", + "FullyRestricted" + ], + "enumDescriptions": [ + "No restrictions are applied to this encryption type.", + "Specific restrictions are enforced for this encryption type." + ] + }, + "effectiveTime": { + "type": "string", + "description": "Server-determined value indicating when this configuration became effective. In RFC 3339 format.", + "format": "date-time", + "readOnly": true + } + } } } }, diff --git a/Storage/src/StorageClient.php b/Storage/src/StorageClient.php index 9e9e1c0a32c3..ad884029c9a2 100644 --- a/Storage/src/StorageClient.php +++ b/Storage/src/StorageClient.php @@ -454,6 +454,17 @@ public function restore(string $name, string $generation, array $options = []) * `projects/my-project/locations/kr-location/keyRings/my-kr/cryptoKeys/my-key`. * Please note the KMS key ring must use the same location as the * bucket. + * @type array $encryption.googleManagedEncryptionEnforcementConfig + * Enforcement configuration for Google-managed encryption. + * @type array $encryption.customerManagedEncryptionEnforcementConfig + * Enforcement configuration for Cloud KMS (customer-managed) encryption. + * @type array $encryption.customerSuppliedEncryptionEnforcementConfig + * Enforcement configuration for customer-supplied encryption keys (CSEK). + * @type string $encryption.*.restrictionMode The restriction state of + * the encryption policy. Acceptable values are `"NotRestricted"` + * and `"FullyRestricted"`. + * @type string $encryption.*.effectiveTime [readonly] The time from which + * the policy was effective in RFC 3339 format. * @type bool $defaultEventBasedHold When `true`, newly created objects * in this bucket will be retained indefinitely until an event * occurs, signified by the hold's release. diff --git a/Storage/tests/System/KmsTest.php b/Storage/tests/System/KmsTest.php index 8ce0bd9a70a6..648a551f6312 100644 --- a/Storage/tests/System/KmsTest.php +++ b/Storage/tests/System/KmsTest.php @@ -19,6 +19,7 @@ use Google\Cloud\Core\Testing\System\KeyManager; use Google\Cloud\Storage\StorageObject; +use Google\Cloud\Core\Exception\ServiceException; /** * @group storage @@ -155,6 +156,45 @@ public function testRotatesKmsToCustomerSuppliedEncrpytion() $this->assertEquals(self::DATA, $rewrittenObject->downloadAsString()); } + public function testUploadFailsWhenCsekViolatesCmekEnforcement() + { + self::$bucket->update([ + 'encryption' => [ + 'customerSuppliedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ] + ] + ]); + + $this->expectException(ServiceException::class); + $this->expectExceptionCode(412); + + $key = base64_encode(openssl_random_pseudo_bytes(32)); + self::$bucket->upload('data', [ + 'name' => uniqid(self::TESTING_PREFIX), + 'encryptionKey' => $key + ]); + self::$bucket->update(['encryption' => null]); + } + + public function testUploadSucceedsWhenNotRestricted() + { + self::$bucket->update([ + 'encryption' => [ + 'defaultKmsKeyName' => self::$keyName1, + 'googleManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'NotRestricted' + ] + ] + ]); + $object = self::$bucket->upload('data', ['name' => uniqid(self::TESTING_PREFIX)]); + + $this->assertTrue($object->exists()); + + $object->delete(); + self::$bucket->update(['encryption' => null]); + } + /** * @param array $options * @return StorageObject diff --git a/Storage/tests/System/ManageBucketsTest.php b/Storage/tests/System/ManageBucketsTest.php index c11486bb9d4f..68fb3e79e121 100644 --- a/Storage/tests/System/ManageBucketsTest.php +++ b/Storage/tests/System/ManageBucketsTest.php @@ -524,4 +524,57 @@ public function testSoftDeleteBucket() self::$client->restore($name, $generation); $this->assertTrue(self::$client->bucket($name)->exists()); } + + /** + * @dataProvider encryptionEnforcementConfigs + */ + public function testCreateAndUpdateBucketWithEncryptionEnforcement($config) + { + $name = uniqid(self::TESTING_PREFIX); + $options = ['encryption' => $config]; + + // Test Creation + $bucket = self::createBucket(self::$client, $name, $options); + $this->assertArrayHasKey('encryption', $bucket->info()); + + $encryption = $bucket->info()['encryption']; + foreach ($config as $key => $val) { + $this->assertEquals($val['restrictionMode'], $encryption[$key]['restrictionMode']); + $this->assertArrayHasKey('effectiveTime', $encryption[$key]); + } + + // Test Update (Changing restrictionMode) + $updatedConfig = $config; + $firstKey = array_key_first($updatedConfig); + $updatedConfig[$firstKey]['restrictionMode'] = 'NotRestricted'; + + $info = $bucket->update(['encryption' => $updatedConfig]); + $this->assertEquals( + 'NotRestricted', + $info['encryption'][$firstKey]['restrictionMode'] + ); + } + + public function encryptionEnforcementConfigs() + { + return [ + [ + [ + 'googleManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ] + ] + ], + [ + [ + 'customerManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ], + 'customerSuppliedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ] + ] + ] + ]; + } } diff --git a/Storage/tests/Unit/BucketTest.php b/Storage/tests/Unit/BucketTest.php index 5ac38e70763f..a01a46b452f9 100644 --- a/Storage/tests/Unit/BucketTest.php +++ b/Storage/tests/Unit/BucketTest.php @@ -463,6 +463,32 @@ public function testUpdatesDataWithLifecycleBuilder() ); } + + public function testUpdatesEncryptionEnforcementConfig() + { + $encryptionConfig = [ + 'googleManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ] + ]; + $this->connection->patchBucket(Argument::withEntry('encryption', $encryptionConfig)) + ->shouldBeCalled() + ->willReturn([ + 'name' => self::BUCKET_NAME, + 'encryption' => $encryptionConfig + ]); + + $bucket = $this->getBucket(['name' => self::BUCKET_NAME]); + + $bucket->update(['encryption' => $encryptionConfig]); + + $this->assertArrayHasKey('encryption', $bucket->info()); + $this->assertEquals( + 'FullyRestricted', + $bucket->info()['encryption']['googleManagedEncryptionEnforcementConfig']['restrictionMode'] + ); + } + public function testGetsInfo() { $bucketInfo = [ diff --git a/Storage/tests/Unit/StorageClientTest.php b/Storage/tests/Unit/StorageClientTest.php index 9f8b5ea117c7..9883dfe58564 100644 --- a/Storage/tests/Unit/StorageClientTest.php +++ b/Storage/tests/Unit/StorageClientTest.php @@ -235,6 +235,37 @@ public function testCreatesBucketWithLifecycleBuilder() ); } + public function testCreatesBucketWithEncryptionEnforcement() + { + $bucketName = 'encrypted-bucket'; + $encryptionConfig = [ + 'googleManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ], + 'customerManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'NotRestricted' + ] + ]; + $this->connection->projectId() + ->willReturn(self::PROJECT); + $this->connection + ->insertBucket(Argument::allOf( + Argument::withEntry('name', $bucketName), + Argument::withEntry('project', self::PROJECT), + Argument::withEntry('encryption', $encryptionConfig) + )) + ->willReturn(['name' => $bucketName]); + $this->client->___setProperty('connection', $this->connection->reveal()); + + $this->assertInstanceOf( + Bucket::class, + $this->client->createBucket( + $bucketName, + ['encryption' => $encryptionConfig] + ) + ); + } + public function testRegisteringStreamWrapper() { $this->assertTrue($this->client->registerStreamWrapper()); From 1c043e0fb995dfcb96f1f7de34315b0fc26679e0 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Fri, 20 Feb 2026 08:21:09 +0000 Subject: [PATCH 2/2] code refactor Improving the robustness of the tests --- Storage/tests/System/KmsTest.php | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/Storage/tests/System/KmsTest.php b/Storage/tests/System/KmsTest.php index 648a551f6312..9a7334310431 100644 --- a/Storage/tests/System/KmsTest.php +++ b/Storage/tests/System/KmsTest.php @@ -169,12 +169,15 @@ public function testUploadFailsWhenCsekViolatesCmekEnforcement() $this->expectException(ServiceException::class); $this->expectExceptionCode(412); - $key = base64_encode(openssl_random_pseudo_bytes(32)); - self::$bucket->upload('data', [ - 'name' => uniqid(self::TESTING_PREFIX), - 'encryptionKey' => $key - ]); - self::$bucket->update(['encryption' => null]); + try { + $key = base64_encode(openssl_random_pseudo_bytes(32)); + self::$bucket->upload('data', [ + 'name' => uniqid(self::TESTING_PREFIX), + 'encryptionKey' => $key + ]); + } finally { + self::$bucket->update(['encryption' => null]); + } } public function testUploadSucceedsWhenNotRestricted() @@ -187,12 +190,17 @@ public function testUploadSucceedsWhenNotRestricted() ] ] ]); - $object = self::$bucket->upload('data', ['name' => uniqid(self::TESTING_PREFIX)]); - - $this->assertTrue($object->exists()); - - $object->delete(); - self::$bucket->update(['encryption' => null]); + $object = null; + try { + $object = self::$bucket->upload('data', ['name' => uniqid(self::TESTING_PREFIX)]); + + $this->assertTrue($object->exists()); + } finally { + if ($object) { + $object->delete(); + } + self::$bucket->update(['encryption' => null]); + } } /**