diff --git a/samples/getBucketEncryptionEnforcementConfig.js b/samples/getBucketEncryptionEnforcementConfig.js new file mode 100644 index 000000000..aa791ab1b --- /dev/null +++ b/samples/getBucketEncryptionEnforcementConfig.js @@ -0,0 +1,76 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +// sample-metadata: +// title: Get Bucket Encryption Enforcement +// description: Retrieves the current encryption enforcement configurations for a bucket. +// usage: node getBucketEncryptionEnforcementConfig.js + +function main(bucketName = 'my-bucket') { + // [START storage_get_encryption_enforcement_config] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function getBucketEncryptionEnforcementConfig() { + const [metadata] = await storage.bucket(bucketName).getMetadata(); + + console.log( + `Encryption enforcement configuration for bucket ${bucketName}.` + ); + const enc = metadata.encryption; + if (!enc) { + console.log( + 'No encryption configuration found (Default GMEK is active).' + ); + return; + } + console.log(`Default KMS Key: ${enc.defaultKmsKeyName || 'None'}`); + + const printConfig = (label, config) => { + if (config) { + console.log(`${label}:`); + console.log(` Mode: ${config.restrictionMode}`); + console.log(` Effective: ${config.effectiveTime}`); + } + }; + + printConfig( + 'Google Managed (GMEK) Enforcement', + enc.googleManagedEncryptionEnforcementConfig + ); + printConfig( + 'Customer Managed (CMEK) Enforcement', + enc.customerManagedEncryptionEnforcementConfig + ); + printConfig( + 'Customer Supplied (CSEK) Enforcement', + enc.customerSuppliedEncryptionEnforcementConfig + ); + } + + getBucketEncryptionEnforcementConfig().catch(console.error); + // [END storage_get_encryption_enforcement_config] +} +main(...process.argv.slice(2)); diff --git a/samples/removeAllBucketEncryptionEnforcementConfig.js b/samples/removeAllBucketEncryptionEnforcementConfig.js new file mode 100644 index 000000000..5cf508ae8 --- /dev/null +++ b/samples/removeAllBucketEncryptionEnforcementConfig.js @@ -0,0 +1,58 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +// sample-metadata: +// title: Remove All Bucket Encryption Enforcement +// description: Removes all encryption enforcement configurations and resets to default behavior. +// usage: node removeAllBucketEncryptionEnforcementConfig.js + +function main(bucketName = 'my-bucket') { + // [START storage_remove_all_encryption_enforcement_config] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + // Setting these to null explicitly removes the enforcement policy. + // We also include defaultKmsKeyName: null to fully reset the bucket encryption state. + async function removeAllBucketEncryptionEnforcementConfig() { + const options = { + encryption: { + defaultKmsKeyName: null, + googleManagedEncryptionEnforcementConfig: null, + customerSuppliedEncryptionEnforcementConfig: null, + customerManagedEncryptionEnforcementConfig: null, + }, + }; + + await storage.bucket(bucketName).setMetadata(options); + + console.log( + `Encryption enforcement configuration removed from bucket ${bucketName}.` + ); + } + + removeAllBucketEncryptionEnforcementConfig().catch(console.error); + // [END storage_remove_all_encryption_enforcement_config] +} +main(...process.argv.slice(2)); diff --git a/samples/setBucketEncryptionEnforcementConfig.js b/samples/setBucketEncryptionEnforcementConfig.js new file mode 100644 index 000000000..6aa9bb49f --- /dev/null +++ b/samples/setBucketEncryptionEnforcementConfig.js @@ -0,0 +1,93 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +// sample-metadata: +// title: Set Bucket Encryption Enforcement +// description: Configures a bucket to enforce specific encryption types (e.g., CMEK-only). +// usage: node setBucketEncryptionEnforcementConfig.js + +function main( + bucketName = 'my-bucket', + defaultKmsKeyName = process.env.GOOGLE_CLOUD_KMS_KEY_ASIA +) { + // [START storage_set_encryption_enforcement_config] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // The name of the KMS key to be used as the default + // const defaultKmsKeyName = 'my-key'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function setBucketEncryptionEnforcementConfig() { + const options = { + encryption: { + defaultKmsKeyName: defaultKmsKeyName, + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + customerSuppliedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + customerManagedEncryptionEnforcementConfig: { + restrictionMode: 'NotRestricted', + }, + }, + }; + + const [metadata] = await storage.bucket(bucketName).setMetadata(options); + + console.log( + `Encryption enforcement configuration updated for bucket ${bucketName}.` + ); + const enc = metadata.encryption; + if (enc) { + console.log(`Default KMS Key: ${enc.defaultKmsKeyName}`); + + const logEnforcement = (label, config) => { + if (config) { + console.log(`${label}:`); + console.log(` Mode: ${config.restrictionMode}`); + console.log(` Effective: ${config.effectiveTime}`); + } + }; + + logEnforcement( + 'Google Managed (GMEK) Enforcement', + enc.googleManagedEncryptionEnforcementConfig + ); + logEnforcement( + 'Customer Managed (CMEK) Enforcement', + enc.customerManagedEncryptionEnforcementConfig + ); + logEnforcement( + 'Customer Supplied (CSEK) Enforcement', + enc.customerSuppliedEncryptionEnforcementConfig + ); + } + } + + setBucketEncryptionEnforcementConfig().catch(console.error); + // [END storage_set_encryption_enforcement_config] +} +main(...process.argv.slice(2)); diff --git a/samples/system-test/buckets.test.js b/samples/system-test/buckets.test.js index 4e2a03ebe..9b8975e6a 100644 --- a/samples/system-test/buckets.test.js +++ b/samples/system-test/buckets.test.js @@ -129,6 +129,65 @@ it('should remove a buckets default KMS key', async () => { assert.ok(!metadata.encryption); }); +it('should set bucket encryption enforcement configuration', async () => { + const output = execSync( + `node setBucketEncryptionEnforcementConfig.js ${bucketName} ${defaultKmsKeyName}` + ); + + assert.include( + output, + `Encryption enforcement configuration updated for bucket ${bucketName}.` + ); + + assert.include(output, `Default KMS Key: ${defaultKmsKeyName}`); + + assert.include(output, 'Google Managed (GMEK) Enforcement:'); + assert.include(output, 'Mode: FullyRestricted'); + + assert.include(output, 'Customer Managed (CMEK) Enforcement:'); + assert.include(output, 'Mode: NotRestricted'); + + assert.include(output, 'Customer Supplied (CSEK) Enforcement:'); + assert.include(output, 'Mode: FullyRestricted'); + + assert.match(output, new RegExp('Effective:')); + + const [metadata] = await bucket.getMetadata(); + assert.strictEqual( + metadata.encryption.googleManagedEncryptionEnforcementConfig + .restrictionMode, + 'FullyRestricted' + ); +}); + +it('should get bucket encryption enforcement configuration', async () => { + const output = execSync( + `node getBucketEncryptionEnforcementConfig.js ${bucketName}` + ); + + assert.include( + output, + `Encryption enforcement configuration for bucket ${bucketName}.` + ); + assert.include(output, `Default KMS Key: ${defaultKmsKeyName}`); + + assert.include(output, 'Google Managed (GMEK) Enforcement:'); + assert.include(output, 'Mode: FullyRestricted'); + assert.match(output, /Effective:/); +}); + +it('should remove all bucket encryption enforcement configuration', async () => { + const output = execSync( + `node removeAllBucketEncryptionEnforcementConfig.js ${bucketName}` + ); + assert.include( + output, + `Encryption enforcement configuration removed from bucket ${bucketName}` + ); + await bucket.getMetadata(); + assert.ok(!bucket.metadata.encryption); +}); + it("should enable a bucket's uniform bucket-level access", async () => { const output = execSync( `node enableUniformBucketLevelAccess.js ${bucketName}` diff --git a/src/bucket.ts b/src/bucket.ts index d35420905..c691d8b9f 100644 --- a/src/bucket.ts +++ b/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/system-test/storage.ts b/system-test/storage.ts index 15257fb59..2b6f074a6 100644 --- a/system-test/storage.ts +++ b/system-test/storage.ts @@ -2992,6 +2992,53 @@ 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); + }); + }); }); }); diff --git a/test/bucket.ts b/test/bucket.ts index 5b49fa518..ec81a25a3 100644 --- a/test/bucket.ts +++ b/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); + }); + }); + }); });