diff --git a/Storage/src/Bucket.php b/Storage/src/Bucket.php index 4f6b30024980..b406b9ce0c72 100644 --- a/Storage/src/Bucket.php +++ b/Storage/src/Bucket.php @@ -1029,6 +1029,18 @@ 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 Google + * managed encryption enforcement configuration. + * @type string $encryption.googleManagedEncryptionEnforcementConfig.restrictionMode + * Restriction mode for Google managed encryption. + * @type array $encryption.customerManagedEncryptionEnforcementConfig Customer + * managed encryption enforcement configuration. + * @type string $encryption.customerManagedEncryptionEnforcementConfig.restrictionMode + * Restriction mode for customer managed encryption. + * @type array $encryption.customerSuppliedEncryptionEnforcementConfig Customer + * supplied encryption enforcement configuration. + * @type string $encryption.customerSuppliedEncryptionEnforcementConfig.restrictionMode + * Restriction mode for customer supplied encryption. * @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..52729ffecf3f 100644 --- a/Storage/src/Connection/ServiceDefinition/storage-v1.json +++ b/Storage/src/Connection/ServiceDefinition/storage-v1.json @@ -195,6 +195,75 @@ "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": "If set, the new objects created in this bucket must comply with this enforcement config. Changing this has no effect on existing objects; it applies to new objects only. If omitted, the new objects are allowed to be encrypted with Google Managed Encryption type by default.", + "properties": { + "restrictionMode": { + "type": "string", + "description": "Restriction mode for Google-Managed Encryption Keys. Defaults to NotRestricted.", + "enum": [ + "NotRestricted", + "FullyRestricted" + ], + "enumDescriptions": [ + "Creation of new objects with Google Managed Encryption is not restricted.", + "Creation of new objects with Google Managed Encryption is fully restricted." + ] + }, + "effectiveTime": { + "type": "string", + "description": "Server-determined value that indicates the time from which configuration was enforced and effective. This value is in RFC 3339 format.", + "format": "date-time" + } + } + }, + "customerManagedEncryptionEnforcementConfig": { + "type": "object", + "description": "If set, the new objects created in this bucket must comply with this enforcement config. Changing this has no effect on existing objects; it applies to new objects only. If omitted, the new objects are allowed to be encrypted with Customer Managed Encryption type by default.", + "properties": { + "restrictionMode": { + "type": "string", + "description": "Restriction mode for Customer-Managed Encryption Keys. Defaults to NotRestricted.", + "enum": [ + "NotRestricted", + "FullyRestricted" + ], + "enumDescriptions": [ + "Creation of new objects with Customer-Managed Encryption is not restricted.", + "Creation of new objects with Customer-Managed Encryption is fully restricted." + ] + }, + "effectiveTime": { + "type": "string", + "description": "Server-determined value that indicates the time from which configuration was enforced and effective. This value is in RFC 3339 format.", + "format": "date-time" + } + } + }, + "customerSuppliedEncryptionEnforcementConfig": { + "type": "object", + "description": "If set, the new objects created in this bucket must comply with this enforcement config. Changing this has no effect on existing objects; it applies to new objects only. If omitted, the new objects are allowed to be encrypted with Customer Supplied Encryption type by default.", + "properties": { + "restrictionMode": { + "type": "string", + "description": "Restriction mode for Customer-Supplied Encryption Keys. Defaults to NotRestricted.", + "enum": [ + "NotRestricted", + "FullyRestricted" + ], + "enumDescriptions": [ + "Creation of new objects with Customer-Supplied Encryption is not restricted.", + "Creation of new objects with Customer-Supplied Encryption is fully restricted." + ] + }, + "effectiveTime": { + "type": "string", + "description": "Server-determined value that indicates the time from which configuration was enforced and effective. This value is in RFC 3339 format.", + "format": "date-time" + } + } } } }, diff --git a/Storage/src/StorageClient.php b/Storage/src/StorageClient.php index 9e9e1c0a32c3..9954c2bfd366 100644 --- a/Storage/src/StorageClient.php +++ b/Storage/src/StorageClient.php @@ -454,6 +454,18 @@ 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 Google + * managed encryption enforcement configuration. + * @type string $encryption.googleManagedEncryptionEnforcementConfig.restrictionMode + * Restriction mode for Google managed encryption. + * @type array $encryption.customerManagedEncryptionEnforcementConfig Customer + * managed encryption enforcement configuration. + * @type string $encryption.customerManagedEncryptionEnforcementConfig.restrictionMode + * Restriction mode for customer managed encryption. + * @type array $encryption.customerSuppliedEncryptionEnforcementConfig Customer + * supplied encryption enforcement configuration. + * @type string $encryption.customerSuppliedEncryptionEnforcementConfig.restrictionMode + * Restriction mode for customer supplied encryption. * @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..af22c5975ffb 100644 --- a/Storage/tests/System/KmsTest.php +++ b/Storage/tests/System/KmsTest.php @@ -155,6 +155,50 @@ public function testRotatesKmsToCustomerSuppliedEncrpytion() $this->assertEquals(self::DATA, $rewrittenObject->downloadAsString()); } + public function testEncryptionEnforcementConfig() + { + self::$bucket->update([ + 'encryption' => [ + 'googleManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'NotRestricted' + ], + 'customerManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'NotRestricted' + ], + 'customerSuppliedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'NotRestricted' + ], + ] + ]); + + $info = self::$bucket->info(); + $this->assertArrayHasKey('encryption', $info); + $this->assertArrayHasKey('googleManagedEncryptionEnforcementConfig', $info['encryption']); + $this->assertArrayHasKey('customerManagedEncryptionEnforcementConfig', $info['encryption']); + $this->assertArrayHasKey('customerSuppliedEncryptionEnforcementConfig', $info['encryption']); + $this->assertEquals( + 'NotRestricted', + $info['encryption']['googleManagedEncryptionEnforcementConfig']['restrictionMode'] + ); + $this->assertEquals( + 'NotRestricted', + $info['encryption']['customerManagedEncryptionEnforcementConfig']['restrictionMode'] + ); + $this->assertEquals( + 'NotRestricted', + $info['encryption']['customerSuppliedEncryptionEnforcementConfig']['restrictionMode'] + ); + + // Reset + self::$bucket->update([ + 'encryption' => [ + 'googleManagedEncryptionEnforcementConfig' => null, + 'customerManagedEncryptionEnforcementConfig' => null, + 'customerSuppliedEncryptionEnforcementConfig' => null, + ] + ]); + } + /** * @param array $options * @return StorageObject diff --git a/Storage/tests/Unit/BucketTest.php b/Storage/tests/Unit/BucketTest.php index 5ac38e70763f..d24181608459 100644 --- a/Storage/tests/Unit/BucketTest.php +++ b/Storage/tests/Unit/BucketTest.php @@ -438,6 +438,143 @@ public function testUpdateAutoclassConfig($terminalStorageClass) $this->assertArrayHasKey('terminalStorageClassUpdateTime', $autoclassInfo); } + public function testUpdateEncryptionEnforcementConfig() + { + $encryptionConfig = [ + 'encryption' => [ + 'defaultKmsKeyName' => 'key', + 'googleManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ], + 'customerManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ], + 'customerSuppliedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ], + ], + ]; + $this->connection->patchBucket(Argument::any())->willReturn( + ['name' => 'bucket'] + + $encryptionConfig + ); + $bucket = $this->getBucket([ + 'name' => 'bucket', + ]); + + $bucket->update($encryptionConfig); + + $this->assertArrayHasKey('encryption', $bucket->info()); + $encryptionInfo = $bucket->info()['encryption']; + $this->assertEquals('key', $encryptionInfo['defaultKmsKeyName']); + $this->assertEquals( + 'FullyRestricted', + $encryptionInfo['googleManagedEncryptionEnforcementConfig']['restrictionMode'] + ); + $this->assertEquals( + 'FullyRestricted', + $encryptionInfo['customerManagedEncryptionEnforcementConfig']['restrictionMode'] + ); + $this->assertEquals( + 'FullyRestricted', + $encryptionInfo['customerSuppliedEncryptionEnforcementConfig']['restrictionMode'] + ); + } + + public function testUpdatePartialEncryptionEnforcementConfig() + { + $encryptionConfig = [ + 'encryption' => [ + 'googleManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ], + ], + ]; + $this->connection->patchBucket(Argument::any())->willReturn( + ['name' => 'bucket'] + + $encryptionConfig + ); + $bucket = $this->getBucket([ + 'name' => 'bucket', + ]); + + $bucket->update($encryptionConfig); + + $this->assertArrayHasKey('encryption', $bucket->info()); + $encryptionInfo = $bucket->info()['encryption']; + $this->assertEquals( + 'FullyRestricted', + $encryptionInfo['googleManagedEncryptionEnforcementConfig']['restrictionMode'] + ); + $this->assertArrayNotHasKey('customerManagedEncryptionEnforcementConfig', $encryptionInfo); + $this->assertArrayNotHasKey('customerSuppliedEncryptionEnforcementConfig', $encryptionInfo); + } + + public function testReloadWithEncryptionEnforcementConfig() + { + $encryptionConfig = [ + 'encryption' => [ + 'defaultKmsKeyName' => 'key', + 'googleManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted', + 'effectiveTime' => '2025-12-18T18:13:15Z' + ], + 'customerManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'NotRestricted', + 'effectiveTime' => '2025-12-18T18:13:15Z' + ], + 'customerSuppliedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'NotRestricted', + 'effectiveTime' => '2025-12-18T18:13:15Z' + ], + ], + ]; + $this->connection->getBucket(Argument::any())->willReturn( + ['name' => self::BUCKET_NAME] + + $encryptionConfig + ); + $bucket = $this->getBucket(); + + $info = $bucket->reload(); + + $this->assertArrayHasKey('encryption', $info); + $encryptionInfo = $info['encryption']; + $this->assertEquals('key', $encryptionInfo['defaultKmsKeyName']); + $this->assertEquals( + 'FullyRestricted', + $encryptionInfo['googleManagedEncryptionEnforcementConfig']['restrictionMode'] + ); + $this->assertEquals( + '2025-12-18T18:13:15Z', + $encryptionInfo['googleManagedEncryptionEnforcementConfig']['effectiveTime'] + ); + } + + public function testReloadWithUnknownEncryptionEnforcementRestrictionMode() + { + $encryptionConfig = [ + 'encryption' => [ + 'googleManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'NOT_YET_DEFINED' + ], + ], + ]; + $this->connection->getBucket(Argument::any())->willReturn( + ['name' => self::BUCKET_NAME] + + $encryptionConfig + ); + $bucket = $this->getBucket(); + + $info = $bucket->reload(); + + $this->assertArrayHasKey('encryption', $info); + $encryptionInfo = $info['encryption']; + $this->assertEquals( + 'NOT_YET_DEFINED', + $encryptionInfo['googleManagedEncryptionEnforcementConfig']['restrictionMode'] + ); + } + public function testUpdatesDataWithLifecycleBuilder() { $lifecycleArr = ['test' => 'test']; diff --git a/Storage/tests/Unit/StorageClientTest.php b/Storage/tests/Unit/StorageClientTest.php index 9f8b5ea117c7..d2d574871859 100644 --- a/Storage/tests/Unit/StorageClientTest.php +++ b/Storage/tests/Unit/StorageClientTest.php @@ -208,6 +208,39 @@ public function testCreatesDualRegionBucket() $this->assertInstanceOf(Bucket::class, $createdBucket); } + public function testCreateBucketWithEncryptionEnforcementConfig() + { + $bucket = 'bucket'; + $encryptionConfig = [ + 'encryption' => [ + 'defaultKmsKeyName' => 'key', + 'googleManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ], + 'customerManagedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ], + 'customerSuppliedEncryptionEnforcementConfig' => [ + 'restrictionMode' => 'FullyRestricted' + ], + ], + ]; + $this->connection->projectId() + ->willReturn(self::PROJECT); + $this->connection + ->insertBucket([ + 'project' => self::PROJECT, + 'encryption' => $encryptionConfig['encryption'], + 'name' => $bucket + ]) + ->willReturn(['name' => $bucket] + $encryptionConfig); + $this->client->___setProperty('connection', $this->connection->reveal()); + + $createdBucket = $this->client->createBucket($bucket, $encryptionConfig); + $this->assertInstanceOf(Bucket::class, $createdBucket); + $this->assertEquals($encryptionConfig['encryption'], $createdBucket->info()['encryption']); + } + public function testCreatesBucketWithLifecycleBuilder() { $bucket = 'bucket';