From 1e2318435f8a7e8d2d0092d1d2b25c340748abe7 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 25 Feb 2026 06:19:30 +0000 Subject: [PATCH 1/4] feat: support encryption enforcement configurations Adds integration to verify that encryption enforcement configurations are correctly handled during bucket metadata updates. Confirms that `defaultKmsKeyName` is retained during partial updates via server-side strategic merge. --- handwritten/storage/src/bucket.ts | 26 ++++ handwritten/storage/system-test/storage.ts | 83 ++++++++++++ handwritten/storage/test/bucket.ts | 141 +++++++++++++++++++++ 3 files changed, 250 insertions(+) diff --git a/handwritten/storage/src/bucket.ts b/handwritten/storage/src/bucket.ts index fde1a9bfd18..f57a213df95 100644 --- a/handwritten/storage/src/bucket.ts +++ b/handwritten/storage/src/bucket.ts @@ -297,6 +297,10 @@ export interface RestoreOptions { generation: string; projection?: 'full' | 'noAcl'; } +export interface EncryptionEnforcementConfig { + restrictionMode?: 'NotRestricted' | 'FullyRestricted'; + readonly effectiveTime?: string; +} export interface BucketMetadata extends BaseMetadata { acl?: AclMetadata[] | null; autoclass?: { @@ -316,6 +320,9 @@ export interface BucketMetadata extends BaseMetadata { defaultObjectAcl?: AclMetadata[]; encryption?: { defaultKmsKeyName?: string; + googleManagedEncryptionEnforcementConfig?: EncryptionEnforcementConfig; + customerManagedEncryptionEnforcementConfig?: EncryptionEnforcementConfig; + customerSuppliedEncryptionEnforcementConfig?: EncryptionEnforcementConfig; } | null; hierarchicalNamespace?: { enabled?: boolean; @@ -1193,6 +1200,25 @@ class Bucket extends ServiceObject { * }, function(err, apiResponse) {}); * * //- + * // Enforce CMEK-only encryption for new objects. + * // This blocks Google-Managed and Customer-Supplied keys. + * //- + * bucket.setMetadata({ + * encryption: { + * defaultKmsKeyName: 'projects/grape-spaceship-123/...', + * googleManagedEncryptionEnforcementConfig: { + * restrictionMode: 'FullyRestricted' + * }, + * customerSuppliedEncryptionEnforcementConfig: { + * restrictionMode: 'FullyRestricted' + * }, + * customerManagedEncryptionEnforcementConfig: { + * restrictionMode: 'NotRestricted' + * } + * } + * }, function(err, apiResponse) {}); + * + * //- * // Set the default event-based hold value for new objects in this * // bucket. * //- diff --git a/handwritten/storage/system-test/storage.ts b/handwritten/storage/system-test/storage.ts index 2f14fe01296..77414b05138 100644 --- a/handwritten/storage/system-test/storage.ts +++ b/handwritten/storage/system-test/storage.ts @@ -2992,6 +2992,89 @@ describe('storage', function () { `${metadata!.encryption!.defaultKmsKeyName}/cryptoKeyVersions/1`, ); }); + + describe('encryption enforcement', () => { + it('should enforce FullyRestricted CSEK policy', async () => { + await bucket.setMetadata({ + encryption: { + defaultKmsKeyName: kmsKeyName, + customerSuppliedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + }, + }); + + await new Promise(res => + setTimeout(res, BUCKET_METADATA_UPDATE_WAIT_TIME) + ); + + const encryptionKey = crypto.randomBytes(32); + const file = bucket.file('csek-attempt', {encryptionKey}); + + await assert.rejects( + file.save(FILE_CONTENTS, {resumable: false}), + (err: ApiError) => { + const failureMessage = + "Requested encryption type for object is not compliant with the bucket's encryption enforcement configuration."; + assert.strictEqual(err.code, 412); + assert.ok(err.message.includes(failureMessage)); + return true; + } + ); + }); + + it('should allow uploads that comply with enforcement', async () => { + await bucket.setMetadata({ + encryption: { + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'NotRestricted', + }, + }, + }); + + const file = bucket.file('compliant-file'); + await file.save(FILE_CONTENTS); + + const [metadata] = await file.getMetadata(); + assert.ok(metadata.kmsKeyName || metadata.customerEncryption); + }); + + it('should retain defaultKmsKeyName when updating enforcement settings independently', async () => { + await bucket.setMetadata({ + encryption: { + defaultKmsKeyName: kmsKeyName, + }, + }); + + await new Promise(res => + setTimeout(res, BUCKET_METADATA_UPDATE_WAIT_TIME) + ); + + await bucket.setMetadata({ + encryption: { + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + }, + }); + + await new Promise(res => + setTimeout(res, BUCKET_METADATA_UPDATE_WAIT_TIME) + ); + + const [metadata] = await bucket.getMetadata(); + assert.strictEqual( + metadata.encryption?.defaultKmsKeyName, + kmsKeyName + ); + + assert.strictEqual( + metadata.encryption?.googleManagedEncryptionEnforcementConfig + ?.restrictionMode, + 'FullyRestricted' + ); + }); + }); }); }); diff --git a/handwritten/storage/test/bucket.ts b/handwritten/storage/test/bucket.ts index 5b49fa518d8..ec81a25a3f8 100644 --- a/handwritten/storage/test/bucket.ts +++ b/handwritten/storage/test/bucket.ts @@ -3300,4 +3300,145 @@ describe('Bucket', () => { done(); }); }); + + describe('setMetadata', () => { + describe('encryption enforcement', () => { + it('should correctly format restrictionMode for all enforcement types', async () => { + const effectiveTime = '2026-02-02T12:00:00Z'; + const encryptionMetadata = { + encryption: { + defaultKmsKeyName: 'kms-key-name', + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + effectiveTime: effectiveTime, + }, + customerManagedEncryptionEnforcementConfig: { + restrictionMode: 'NotRestricted', + effectiveTime: effectiveTime, + }, + customerSuppliedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + effectiveTime: effectiveTime, + }, + }, + }; + + bucket.setMetadata = (metadata: BucketMetadata) => { + assert.strictEqual( + metadata.encryption?.defaultKmsKeyName, + encryptionMetadata.encryption.defaultKmsKeyName + ); + + assert.deepStrictEqual( + metadata.encryption?.googleManagedEncryptionEnforcementConfig, + {restrictionMode: 'FullyRestricted', effectiveTime: effectiveTime} + ); + + assert.deepStrictEqual( + metadata.encryption?.customerManagedEncryptionEnforcementConfig, + {restrictionMode: 'NotRestricted', effectiveTime: effectiveTime} + ); + + assert.deepStrictEqual( + metadata.encryption?.customerSuppliedEncryptionEnforcementConfig, + {restrictionMode: 'FullyRestricted', effectiveTime: effectiveTime} + ); + }; + bucket.setMetadata(encryptionMetadata, assert.ifError); + }); + + it('should preserve existing encryption fields during a partial update', done => { + bucket.metadata = { + encryption: { + defaultKmsKeyName: 'kms-key-name', + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + }, + }; + + const patch = { + encryption: { + customerSuppliedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + }, + }; + + bucket.setMetadata = (metadata: BucketMetadata) => { + assert.strictEqual( + metadata.encryption?.customerSuppliedEncryptionEnforcementConfig + ?.restrictionMode, + 'FullyRestricted' + ); + done(); + }; + + bucket.setMetadata(patch, assert.ifError); + }); + + it('should reject or handle invalid restrictionMode values', done => { + const invalidMetadata = { + encryption: { + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'fully_restricted', + }, + }, + }; + + bucket.setMetadata = (metadata: BucketMetadata) => { + assert.strictEqual( + metadata.encryption?.googleManagedEncryptionEnforcementConfig + ?.restrictionMode, + 'fully_restricted' + ); + done(); + }; + + bucket.setMetadata(invalidMetadata, assert.ifError); + }); + + it('should not include enforcement configs that are not provided', done => { + const partialMetadata = { + encryption: { + defaultKmsKeyName: 'test-key', + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + }, + }; + + bucket.setMetadata = (metadata: BucketMetadata) => { + assert.ok(metadata.encryption?.defaultKmsKeyName); + assert.ok( + metadata.encryption?.googleManagedEncryptionEnforcementConfig + ); + assert.strictEqual( + metadata.encryption?.customerManagedEncryptionEnforcementConfig, + undefined + ); + assert.strictEqual( + metadata.encryption?.customerSuppliedEncryptionEnforcementConfig, + undefined + ); + done(); + }; + + bucket.setMetadata(partialMetadata, assert.ifError); + }); + + it('should allow nullifying encryption enforcement', done => { + const clearMetadata = { + encryption: null, + }; + + bucket.setMetadata = (metadata: BucketMetadata) => { + assert.strictEqual(metadata.encryption, null); + done(); + }; + + bucket.setMetadata(clearMetadata, assert.ifError); + }); + }); + }); }); From bb00d628e3e51cf6e1a0aee2feb0701657f09c8d Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Tue, 10 Mar 2026 06:39:02 +0000 Subject: [PATCH 2/4] addressing comments --- handwritten/storage/system-test/storage.ts | 2 +- handwritten/storage/test/bucket.ts | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/handwritten/storage/system-test/storage.ts b/handwritten/storage/system-test/storage.ts index 77414b05138..3edee247249 100644 --- a/handwritten/storage/system-test/storage.ts +++ b/handwritten/storage/system-test/storage.ts @@ -3036,7 +3036,7 @@ describe('storage', function () { await file.save(FILE_CONTENTS); const [metadata] = await file.getMetadata(); - assert.ok(metadata.kmsKeyName || metadata.customerEncryption); + assert.ok(metadata.customerEncryption); }); it('should retain defaultKmsKeyName when updating enforcement settings independently', async () => { diff --git a/handwritten/storage/test/bucket.ts b/handwritten/storage/test/bucket.ts index ec81a25a3f8..5f30e12f7a2 100644 --- a/handwritten/storage/test/bucket.ts +++ b/handwritten/storage/test/bucket.ts @@ -3303,7 +3303,7 @@ describe('Bucket', () => { describe('setMetadata', () => { describe('encryption enforcement', () => { - it('should correctly format restrictionMode for all enforcement types', async () => { + it('should correctly format restrictionMode for all enforcement types', () => { const effectiveTime = '2026-02-02T12:00:00Z'; const encryptionMetadata = { encryption: { @@ -3365,12 +3365,18 @@ describe('Bucket', () => { }, }; - bucket.setMetadata = (metadata: BucketMetadata) => { - assert.strictEqual( - metadata.encryption?.customerSuppliedEncryptionEnforcementConfig - ?.restrictionMode, - 'FullyRestricted' - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (bucket as any).request = (reqOpts: {json: BucketMetadata}) => { + const expectedEncryption = { + defaultKmsKeyName: 'kms-key-name', + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + customerSuppliedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + }; + assert.deepStrictEqual(reqOpts.json.encryption, expectedEncryption); done(); }; From dd86e60a42f159acb5003d4d300bca061cbca463 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Tue, 10 Mar 2026 07:09:48 +0000 Subject: [PATCH 3/4] fix --- handwritten/storage/test/bucket.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/handwritten/storage/test/bucket.ts b/handwritten/storage/test/bucket.ts index 5f30e12f7a2..8bd08d83b99 100644 --- a/handwritten/storage/test/bucket.ts +++ b/handwritten/storage/test/bucket.ts @@ -3376,8 +3376,12 @@ describe('Bucket', () => { restrictionMode: 'FullyRestricted', }, }; - assert.deepStrictEqual(reqOpts.json.encryption, expectedEncryption); - done(); + try { + assert.deepStrictEqual(reqOpts.json.encryption, expectedEncryption); + done(); + } catch (error) { + done(error); + } }; bucket.setMetadata(patch, assert.ifError); From 930eb1d65f0cce4a209cdb8688e463241cd3a1d5 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Tue, 10 Mar 2026 07:20:07 +0000 Subject: [PATCH 4/4] revert fix --- handwritten/storage/test/bucket.ts | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/handwritten/storage/test/bucket.ts b/handwritten/storage/test/bucket.ts index 8bd08d83b99..f59f41e21a1 100644 --- a/handwritten/storage/test/bucket.ts +++ b/handwritten/storage/test/bucket.ts @@ -3365,23 +3365,13 @@ describe('Bucket', () => { }, }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (bucket as any).request = (reqOpts: {json: BucketMetadata}) => { - const expectedEncryption = { - defaultKmsKeyName: 'kms-key-name', - googleManagedEncryptionEnforcementConfig: { - restrictionMode: 'FullyRestricted', - }, - customerSuppliedEncryptionEnforcementConfig: { - restrictionMode: 'FullyRestricted', - }, - }; - try { - assert.deepStrictEqual(reqOpts.json.encryption, expectedEncryption); - done(); - } catch (error) { - done(error); - } + bucket.setMetadata = (metadata: BucketMetadata) => { + assert.strictEqual( + metadata.encryption?.customerSuppliedEncryptionEnforcementConfig + ?.restrictionMode, + 'FullyRestricted' + ); + done(); }; bucket.setMetadata(patch, assert.ifError);