Skip to content

Commit e0d7036

Browse files
authored
feat(storage): add support for encryption enforcement configurations (#8937)
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.
1 parent 6d2252e commit e0d7036

7 files changed

Lines changed: 262 additions & 0 deletions

File tree

Storage/src/Bucket.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,17 @@ public function delete(array $options = [])
10291029
* `projects/my-project/locations/kr-location/keyRings/my-kr/cryptoKeys/my-key`.
10301030
* Please note the KMS key ring must use the same location as the
10311031
* bucket.
1032+
* @type array $encryption.googleManagedEncryptionEnforcementConfig
1033+
* Enforcement configuration for Google-managed encryption.
1034+
* @type array $encryption.customerManagedEncryptionEnforcementConfig
1035+
* Enforcement configuration for Cloud KMS (customer-managed) encryption.
1036+
* @type array $encryption.customerSuppliedEncryptionEnforcementConfig
1037+
* Enforcement configuration for customer-supplied encryption keys (CSEK).
1038+
* @type string $encryption.*.restrictionMode The restriction state of
1039+
* the encryption policy. Acceptable values are `"NotRestricted"`
1040+
* and `"FullyRestricted"`.
1041+
* @type string $encryption.*.effectiveTime [readonly] The time from which
1042+
* the policy was effective in RFC 3339 format.
10321043
* @type bool $defaultEventBasedHold When `true`, newly created objects
10331044
* in this bucket will be retained indefinitely until an event
10341045
* occurs, signified by the hold's release.

Storage/src/Connection/ServiceDefinition/storage-v1.json

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,78 @@
195195
"defaultKmsKeyName": {
196196
"type": "string",
197197
"description": "A Cloud KMS key that will be used to encrypt objects inserted into this bucket, if no encryption method is specified."
198+
},
199+
"googleManagedEncryptionEnforcementConfig": {
200+
"type": "object",
201+
"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.",
202+
"properties": {
203+
"restrictionMode": {
204+
"type": "string",
205+
"description": "Restriction mode for Google-Managed Encryption Keys. Defaults to NotRestricted.",
206+
"enum": [
207+
"NotRestricted",
208+
"FullyRestricted"
209+
],
210+
"enumDescriptions": [
211+
"Creation of new objects with Google Managed Encryption is not restricted.",
212+
"Creation of new objects with Google Managed Encryption is fully restricted."
213+
]
214+
},
215+
"effectiveTime": {
216+
"type": "string",
217+
"description": "Server-determined value indicating when this configuration became effective. In RFC 3339 format.",
218+
"format": "date-time",
219+
"readOnly": true
220+
}
221+
}
222+
},
223+
"customerManagedEncryptionEnforcementConfig": {
224+
"type": "object",
225+
"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.",
226+
"properties": {
227+
"restrictionMode": {
228+
"type": "string",
229+
"description": "Restriction mode for Customer-Managed Encryption Keys. Defaults to NotRestricted.",
230+
"enum": [
231+
"NotRestricted",
232+
"FullyRestricted"
233+
],
234+
"enumDescriptions": [
235+
"Creation of new objects with Customer-Managed Encryption is not restricted.",
236+
"Creation of new objects with Customer-Managed Encryption is fully restricted."
237+
]
238+
},
239+
"effectiveTime": {
240+
"type": "string",
241+
"description": "Server-determined value indicating when this configuration became effective. In RFC 3339 format.",
242+
"format": "date-time",
243+
"readOnly": true
244+
}
245+
}
246+
},
247+
"customerSuppliedEncryptionEnforcementConfig": {
248+
"type": "object",
249+
"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.",
250+
"properties": {
251+
"restrictionMode": {
252+
"type": "string",
253+
"description": "Restriction mode for Customer-Supplied Encryption Keys. Defaults to NotRestricted.",
254+
"enum": [
255+
"NotRestricted",
256+
"FullyRestricted"
257+
],
258+
"enumDescriptions": [
259+
"Creation of new objects with Customer-Supplied Encryption is not restricted.",
260+
"Creation of new objects with Customer-Supplied Encryption is fully restricted."
261+
]
262+
},
263+
"effectiveTime": {
264+
"type": "string",
265+
"description": "Server-determined value indicating when this configuration became effective. In RFC 3339 format.",
266+
"format": "date-time",
267+
"readOnly": true
268+
}
269+
}
198270
}
199271
}
200272
},

Storage/src/StorageClient.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,27 @@ public function restore(string $name, string $generation, array $options = [])
454454
* `projects/my-project/locations/kr-location/keyRings/my-kr/cryptoKeys/my-key`.
455455
* Please note the KMS key ring must use the same location as the
456456
* bucket.
457+
* @type array $encryption.googleManagedEncryptionEnforcementConfig
458+
* Enforcement configuration for Google-managed encryption.
459+
* @type string $encryption.googleManagedEncryptionEnforcementConfig.restrictionMode
460+
* The restriction state of the encryption policy. Acceptable values are
461+
* `"NotRestricted"` and `"FullyRestricted"`.
462+
* @type string $encryption.googleManagedEncryptionEnforcementConfig.effectiveTime
463+
* [readonly] The time from which the policy was effective in RFC 3339 format.
464+
* @type array $encryption.customerManagedEncryptionEnforcementConfig
465+
* Enforcement configuration for Cloud KMS (customer-managed) encryption.
466+
* @type string $encryption.customerManagedEncryptionEnforcementConfig.restrictionMode
467+
* The restriction state of the encryption policy. Acceptable values are
468+
* `"NotRestricted"` and `"FullyRestricted"`.
469+
* @type string $encryption.customerManagedEncryptionEnforcementConfig.effectiveTime
470+
* [readonly] The time from which the policy was effective in RFC 3339 format.
471+
* @type array $encryption.customerSuppliedEncryptionEnforcementConfig
472+
* Enforcement configuration for customer-supplied encryption keys (CSEK).
473+
* @type string $encryption.customerSuppliedEncryptionEnforcementConfig.restrictionMode
474+
* The restriction state of the encryption policy. Acceptable values are
475+
* `"NotRestricted"` and `"FullyRestricted"`.
476+
* @type string $encryption.customerSuppliedEncryptionEnforcementConfig.effectiveTime
477+
* [readonly] The time from which the policy was effective in RFC 3339 format.
457478
* @type bool $defaultEventBasedHold When `true`, newly created objects
458479
* in this bucket will be retained indefinitely until an event
459480
* occurs, signified by the hold's release.

Storage/tests/System/KmsTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
use Google\Cloud\Core\Testing\System\KeyManager;
2121
use Google\Cloud\Storage\StorageObject;
22+
use Google\Cloud\Core\Exception\ServiceException;
2223

2324
/**
2425
* @group storage
@@ -155,6 +156,53 @@ public function testRotatesKmsToCustomerSuppliedEncrpytion()
155156
$this->assertEquals(self::DATA, $rewrittenObject->downloadAsString());
156157
}
157158

159+
public function testUploadFailsWhenCsekViolatesCmekEnforcement()
160+
{
161+
self::$bucket->update([
162+
'encryption' => [
163+
'customerSuppliedEncryptionEnforcementConfig' => [
164+
'restrictionMode' => 'FullyRestricted'
165+
]
166+
]
167+
]);
168+
169+
$this->expectException(ServiceException::class);
170+
$this->expectExceptionCode(412);
171+
172+
try {
173+
$key = base64_encode(openssl_random_pseudo_bytes(32));
174+
self::$bucket->upload('data', [
175+
'name' => uniqid(self::TESTING_PREFIX),
176+
'encryptionKey' => $key
177+
]);
178+
} finally {
179+
self::$bucket->update(['encryption' => null]);
180+
}
181+
}
182+
183+
public function testUploadSucceedsWhenNotRestricted()
184+
{
185+
self::$bucket->update([
186+
'encryption' => [
187+
'defaultKmsKeyName' => self::$keyName1,
188+
'googleManagedEncryptionEnforcementConfig' => [
189+
'restrictionMode' => 'NotRestricted'
190+
]
191+
]
192+
]);
193+
$object = null;
194+
try {
195+
$object = self::$bucket->upload('data', ['name' => uniqid(self::TESTING_PREFIX)]);
196+
197+
$this->assertTrue($object->exists());
198+
} finally {
199+
if ($object) {
200+
$object->delete();
201+
}
202+
self::$bucket->update(['encryption' => null]);
203+
}
204+
}
205+
158206
/**
159207
* @param array $options
160208
* @return StorageObject

Storage/tests/System/ManageBucketsTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,4 +524,57 @@ public function testSoftDeleteBucket()
524524
self::$client->restore($name, $generation);
525525
$this->assertTrue(self::$client->bucket($name)->exists());
526526
}
527+
528+
/**
529+
* @dataProvider encryptionEnforcementConfigs
530+
*/
531+
public function testCreateAndUpdateBucketWithEncryptionEnforcement($config)
532+
{
533+
$name = uniqid(self::TESTING_PREFIX);
534+
$options = ['encryption' => $config];
535+
536+
// Test Creation
537+
$bucket = self::createBucket(self::$client, $name, $options);
538+
$this->assertArrayHasKey('encryption', $bucket->info());
539+
540+
$encryption = $bucket->info()['encryption'];
541+
foreach ($config as $key => $val) {
542+
$this->assertEquals($val['restrictionMode'], $encryption[$key]['restrictionMode']);
543+
$this->assertArrayHasKey('effectiveTime', $encryption[$key]);
544+
}
545+
546+
// Test Update (Changing restrictionMode)
547+
$updatedConfig = $config;
548+
$firstKey = array_key_first($updatedConfig);
549+
$updatedConfig[$firstKey]['restrictionMode'] = 'NotRestricted';
550+
551+
$info = $bucket->update(['encryption' => $updatedConfig]);
552+
$this->assertEquals(
553+
'NotRestricted',
554+
$info['encryption'][$firstKey]['restrictionMode']
555+
);
556+
}
557+
558+
public function encryptionEnforcementConfigs()
559+
{
560+
return [
561+
[
562+
[
563+
'googleManagedEncryptionEnforcementConfig' => [
564+
'restrictionMode' => 'FullyRestricted'
565+
]
566+
]
567+
],
568+
[
569+
[
570+
'customerManagedEncryptionEnforcementConfig' => [
571+
'restrictionMode' => 'FullyRestricted'
572+
],
573+
'customerSuppliedEncryptionEnforcementConfig' => [
574+
'restrictionMode' => 'FullyRestricted'
575+
]
576+
]
577+
]
578+
];
579+
}
527580
}

Storage/tests/Unit/BucketTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,32 @@ public function testUpdatesDataWithLifecycleBuilder()
463463
);
464464
}
465465

466+
467+
public function testUpdatesEncryptionEnforcementConfig()
468+
{
469+
$encryptionConfig = [
470+
'googleManagedEncryptionEnforcementConfig' => [
471+
'restrictionMode' => 'FullyRestricted'
472+
]
473+
];
474+
$this->connection->patchBucket(Argument::withEntry('encryption', $encryptionConfig))
475+
->shouldBeCalled()
476+
->willReturn([
477+
'name' => self::BUCKET_NAME,
478+
'encryption' => $encryptionConfig
479+
]);
480+
481+
$bucket = $this->getBucket(['name' => self::BUCKET_NAME]);
482+
483+
$bucket->update(['encryption' => $encryptionConfig]);
484+
485+
$this->assertArrayHasKey('encryption', $bucket->info());
486+
$this->assertEquals(
487+
'FullyRestricted',
488+
$bucket->info()['encryption']['googleManagedEncryptionEnforcementConfig']['restrictionMode']
489+
);
490+
}
491+
466492
public function testGetsInfo()
467493
{
468494
$bucketInfo = [

Storage/tests/Unit/StorageClientTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,37 @@ public function testCreatesBucketWithLifecycleBuilder()
235235
);
236236
}
237237

238+
public function testCreatesBucketWithEncryptionEnforcement()
239+
{
240+
$bucketName = 'encrypted-bucket';
241+
$encryptionConfig = [
242+
'googleManagedEncryptionEnforcementConfig' => [
243+
'restrictionMode' => 'FullyRestricted'
244+
],
245+
'customerManagedEncryptionEnforcementConfig' => [
246+
'restrictionMode' => 'NotRestricted'
247+
]
248+
];
249+
$this->connection->projectId()
250+
->willReturn(self::PROJECT);
251+
$this->connection
252+
->insertBucket(Argument::allOf(
253+
Argument::withEntry('name', $bucketName),
254+
Argument::withEntry('project', self::PROJECT),
255+
Argument::withEntry('encryption', $encryptionConfig)
256+
))
257+
->willReturn(['name' => $bucketName]);
258+
$this->client->___setProperty('connection', $this->connection->reveal());
259+
260+
$this->assertInstanceOf(
261+
Bucket::class,
262+
$this->client->createBucket(
263+
$bucketName,
264+
['encryption' => $encryptionConfig]
265+
)
266+
);
267+
}
268+
238269
public function testRegisteringStreamWrapper()
239270
{
240271
$this->assertTrue($this->client->registerStreamWrapper());

0 commit comments

Comments
 (0)