From d749e135b4e49c29e2655ee0f7cd0678348596a4 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Mon, 6 Apr 2026 11:21:27 +0000 Subject: [PATCH 1/5] Feat(storage): add Object Contexts samples and system tests - Add `setObjectContexts.js` to demonstrate CRUD and deletion of contexts. - Add `getObjectContexts.js` to show retrieval of structured metadata. - Add `listObjectsWithContextFilter.js` to demonstrate server-side filtering. - Implement comprehensive system tests in `files.test.js` covering presence, absence (-), and existence (:) filter operators. - Ensure samples use correct 'contexts' field with 'custom' map structure. --- storage/getObjectContexts.js | 71 +++++++++++++++ storage/listObjectContexts.js | 104 +++++++++++++++++++++ storage/setObjectContexts.js | 91 ++++++++++++++++++ storage/system-test/files.test.js | 147 ++++++++++++++++++++++++++++++ 4 files changed, 413 insertions(+) create mode 100644 storage/getObjectContexts.js create mode 100644 storage/listObjectContexts.js create mode 100644 storage/setObjectContexts.js create mode 100644 storage/system-test/files.test.js diff --git a/storage/getObjectContexts.js b/storage/getObjectContexts.js new file mode 100644 index 0000000000..613a22c2ab --- /dev/null +++ b/storage/getObjectContexts.js @@ -0,0 +1,71 @@ +// 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 Object Contexts +// description: Retrieves the structured Object Contexts from an object. +// usage: node getObjectContexts.js + +/** + * This application demonstrates how to retrieve the 'contexts' field from a file + * in Google Cloud Storage. + */ + +function main(bucketName = 'my-bucket', fileName = 'test.txt') { + // [START storage_get_object_contexts] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // The ID of your GCS file + // const fileName = 'your-file-name'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function getObjectContexts() { + // Gets the metadata for the file + const [metadata] = await storage + .bucket(bucketName) + .file(fileName) + .getMetadata(); + + // Contexts are stored in metadata.contexts.custom + if (metadata.contexts && metadata.contexts.custom) { + console.log(`Object Contexts for ${fileName}:`); + + // Iterate through the custom contexts to show values and timestamps + for (const [key, details] of Object.entries(metadata.contexts.custom)) { + console.log(`- Key: ${key}`); + console.log(` Value: ${details.value}`); + console.log(` Created: ${details.createTime}`); + console.log(` Updated: ${details.updateTime}`); + } + } else { + console.log(`No Object Contexts found for ${fileName}.`); + } + } + + getObjectContexts().catch(console.error); + // [END storage_get_object_contexts] +} + +main(...process.argv.slice(2)); diff --git a/storage/listObjectContexts.js b/storage/listObjectContexts.js new file mode 100644 index 0000000000..228cc721be --- /dev/null +++ b/storage/listObjectContexts.js @@ -0,0 +1,104 @@ +// 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: List Objects with Context Filter +// description: Lists objects in a bucket that match specific custom contexts. +// usage: node listObjectsWithContextFilter.js + +/** + * This application demonstrates how to list objects in a bucket while filtering + * by their custom 'contexts' metadata. + */ + +function main(bucketName = 'my-bucket') { + // [START storage_list_object_contexts] + /** + * 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 listObjectContexts() { + // Define the filter for contexts. + const bucket = storage.bucket(bucketName); + + /** + * List any object that has a context with the specified key attached. + * Syntax: contexts.""="" + */ + const filterByValue = 'contexts."priority"="high"'; + const [filesByValue] = await bucket.getFiles({ + filter: filterByValue, + }); + + console.log(`\nFiles matching filter [${filterByValue}]:`); + filesByValue.forEach(file => console.log(` - ${file.name}`)); + + /** + * List any object that has a context with the specified key attached. + * Syntax: contexts."":* + */ + const filterByExistence = 'contexts."team-owner":*'; + const [filesWithKey] = await bucket.getFiles({ + filter: filterByExistence, + }); + + console.log( + `\nFiles with the "team-owner" context key [${filterByExistence}]:` + ); + filesWithKey.forEach(file => console.log(` - ${file.name}`)); + + /** + * List any object that does not have a context with the specified key and value attached. + * Syntax: -contexts.""="" + */ + const absenceOfValuePair = '-contexts."priority"="high"'; + const [filesNoHighPriority] = await bucket.getFiles({ + filter: absenceOfValuePair, + }); + + console.log( + `\nFiles matching absence of value pair [${absenceOfValuePair}]:` + ); + filesNoHighPriority.forEach(file => console.log(` - ${file.name}`)); + + /** + * List any object that that does not have a context with the specified key attached. + * Syntax: -contexts."":* + */ + const absenceOfKey = '-contexts."team-owner":*'; + const [filesNoTeamOwner] = await bucket.getFiles({ + filter: absenceOfKey, + }); + + console.log( + `\nFiles matching absence of key regardless of value [${absenceOfKey}]:` + ); + filesNoTeamOwner.forEach(file => console.log(` - ${file.name}`)); + } + + listObjectContexts().catch(console.error); + // [END storage_list_object_contexts] +} + +main(...process.argv.slice(2)); diff --git a/storage/setObjectContexts.js b/storage/setObjectContexts.js new file mode 100644 index 0000000000..1e6470e345 --- /dev/null +++ b/storage/setObjectContexts.js @@ -0,0 +1,91 @@ +// 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 Object Contexts +// description: Sets custom metadata (contexts) on an object. +// usage: node setObjectContexts.js + +/** + * This application demonstrates how to set, update, and delete object contexts (metadata) on a file + * in Google Cloud Storage. + */ + +function main(bucketName = 'my-bucket', fileName = 'test.txt') { + // [START storage_set_object_contexts] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // The ID of your GCS file + // const fileName = 'your-file-name'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function setObjectContexts() { + const file = storage.bucket(bucketName).file(fileName); + try { + // Create/Update Object Contexts + // Object Contexts live in the 'contexts' field, not the 'metadata' field. + const [metadata] = await file.setMetadata({ + contexts: { + custom: { + 'team-owner': {value: 'storage-team'}, + priority: {value: 'high'}, + }, + }, + }); + + console.log(`Updated Object Contexts for ${fileName}:`); + console.log(JSON.stringify(metadata.contexts, null, 2)); + } catch (error) { + console.error( + 'Error executing set object contexts:', + error.message || error + ); + } + // Delete a specific key from the context: + // We send 'null' for the specific key we want to remove. + await file.setMetadata({ + contexts: { + custom: { + 'team-owner': null, + }, + }, + }); + console.log(`Deleted 'team-owner' key from contexts for ${fileName}.`); + + // Delete all keys from the context: + // We set the 'custom' property to null to wipe the entire map. + await file.setMetadata({ + contexts: { + custom: null, + }, + }); + console.log(`Cleared all custom contexts for ${fileName}.`); + } + + setObjectContexts().catch(console.error); + // [END storage_set_object_contexts] +} + +main(...process.argv.slice(2)); diff --git a/storage/system-test/files.test.js b/storage/system-test/files.test.js new file mode 100644 index 0000000000..002e520329 --- /dev/null +++ b/storage/system-test/files.test.js @@ -0,0 +1,147 @@ +// Copyright 2019 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'; + +const fs = require('fs'); +const path = require('path'); +const {Storage} = require('@google-cloud/storage'); +const {assert} = require('chai'); +const {before, after, it, describe} = require('mocha'); +const cp = require('child_process'); +const uuid = require('uuid'); +const {promisify} = require('util'); + +const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); + +const storage = new Storage({ + projectId: process.env.GOOGLE_CLOUD_PROJECT, +}); +const cwd = path.join(__dirname, '..'); +const bucketName = generateName(); +const bucket = storage.bucket(bucketName); +const fileName = 'test.txt'; +const filePath = path.join(cwd, 'resources', fileName); +const downloadFilePath = path.join(cwd, 'downloaded.txt'); + +describe('file', () => { + before(async () => { + await bucket.create(); + }); + + after(async () => { + await promisify(fs.unlink)(downloadFilePath).catch(console.error); + // Try deleting all files twice, just to make sure + await bucket.deleteFiles({force: true}).catch(console.error); + await bucket.deleteFiles({force: true}).catch(console.error); + await bucket.delete().catch(console.error); + }); + + describe('Object Contexts', () => { + const contextFile = bucket.file(fileName); + + beforeEach(async () => { + await bucket.upload(filePath, {destination: fileName}); + + await contextFile.setMetadata({ + contexts: { + custom: { + 'team-owner': {value: 'storage-team'}, + priority: {value: 'high'}, + }, + }, + }); + }); + + it('should set object contexts', () => { + const output = execSync( + `node setObjectContexts.js ${bucketName} ${fileName}` + ); + // Verify Initial Creation + assert.include(output, `Updated Object Contexts for ${fileName}:`); + assert.include(output, '"team-owner":'); + assert.include(output, '"value": "storage-team"'); + assert.include(output, '"priority":'); + assert.include(output, '"value": "high"'); + assert.include(output, '"createTime":'); + assert.include(output, '"updateTime":'); + + // Verify Specific Key Deletion + assert.include( + output, + `Deleted 'team-owner' key from contexts for ${fileName}.` + ); + + // Verify Clearing All Contexts + assert.include(output, `Cleared all custom contexts for ${fileName}.`); + }); + + it('should get object contexts', () => { + const output = execSync( + `node getObjectContexts.js ${bucketName} ${fileName}` + ); + assert.include(output, `Object Contexts for ${fileName}:`); + assert.include(output, 'Key: priority'); + assert.include(output, 'Value: high'); + assert.include(output, 'Key: team-owner'); + assert.include(output, 'Value: storage-team'); + }); + + it('should list objects with context filters', async () => { + const noContextFileName = `no-context-${fileName}`; + await bucket.upload(filePath, {destination: noContextFileName}); + // Ensure it has no contexts + await bucket + .file(noContextFileName) + .setMetadata({contexts: {custom: null}}); + + const output = execSync(`node listObjectContexts.js ${bucketName}`); + + // Testing Existence of Value Pair (contexts."key"="val") + assert.include( + output, + 'Files matching filter [contexts."priority"="high"]' + ); + assert.include(output, ` - ${fileName}`); + assert.include( + output, + 'Files matching filter [contexts."priority"="high"]' + ); + + // Testing Existence of Key (contexts."key":*) + assert.include(output, 'Files with the "team-owner" context key'); + assert.include(output, ` - ${fileName}`); + + // Testing Absence of Value Pair (-contexts."key"="val") + assert.include( + output, + 'Files matching absence of value pair [-contexts."priority"="high"]' + ); + assert.include(output, ` - ${noContextFileName}`); + + // Testing Absence of Key (-contexts."key":*) + assert.include( + output, + 'Files matching absence of key regardless of value [-contexts."team-owner":*]' + ); + assert.include(output, ` - ${noContextFileName}`); + + await bucket.file(noContextFileName).delete(); + }); + }); +}); + +function generateName() { + return `nodejs-storage-samples-${uuid.v4()}`; +} From 19f1454a5c994b5503590056af20e778207d920d Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Mon, 6 Apr 2026 11:56:18 +0000 Subject: [PATCH 2/5] code refactor --- storage/listObjectContexts.js | 2 +- storage/setObjectContexts.js | 31 +++++++++++++------------------ storage/system-test/files.test.js | 4 ---- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/storage/listObjectContexts.js b/storage/listObjectContexts.js index 228cc721be..78956008de 100644 --- a/storage/listObjectContexts.js +++ b/storage/listObjectContexts.js @@ -17,7 +17,7 @@ // sample-metadata: // title: List Objects with Context Filter // description: Lists objects in a bucket that match specific custom contexts. -// usage: node listObjectsWithContextFilter.js +// usage: node listObjectContexts.js /** * This application demonstrates how to list objects in a bucket while filtering diff --git a/storage/setObjectContexts.js b/storage/setObjectContexts.js index 1e6470e345..0557028133 100644 --- a/storage/setObjectContexts.js +++ b/storage/setObjectContexts.js @@ -43,26 +43,21 @@ function main(bucketName = 'my-bucket', fileName = 'test.txt') { async function setObjectContexts() { const file = storage.bucket(bucketName).file(fileName); - try { - // Create/Update Object Contexts - // Object Contexts live in the 'contexts' field, not the 'metadata' field. - const [metadata] = await file.setMetadata({ - contexts: { - custom: { - 'team-owner': {value: 'storage-team'}, - priority: {value: 'high'}, - }, + + // Create/Update Object Contexts + // Object Contexts live in the 'contexts' field, not the 'metadata' field. + const [metadata] = await file.setMetadata({ + contexts: { + custom: { + 'team-owner': {value: 'storage-team'}, + priority: {value: 'high'}, }, - }); + }, + }); + + console.log(`Updated Object Contexts for ${fileName}:`); + console.log(JSON.stringify(metadata.contexts, null, 2)); - console.log(`Updated Object Contexts for ${fileName}:`); - console.log(JSON.stringify(metadata.contexts, null, 2)); - } catch (error) { - console.error( - 'Error executing set object contexts:', - error.message || error - ); - } // Delete a specific key from the context: // We send 'null' for the specific key we want to remove. await file.setMetadata({ diff --git a/storage/system-test/files.test.js b/storage/system-test/files.test.js index 002e520329..17b1520100 100644 --- a/storage/system-test/files.test.js +++ b/storage/system-test/files.test.js @@ -114,10 +114,6 @@ describe('file', () => { 'Files matching filter [contexts."priority"="high"]' ); assert.include(output, ` - ${fileName}`); - assert.include( - output, - 'Files matching filter [contexts."priority"="high"]' - ); // Testing Existence of Key (contexts."key":*) assert.include(output, 'Files with the "team-owner" context key'); From 58c1c2ac0a2a0757375822716898c07c17f786e7 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Mon, 6 Apr 2026 12:27:23 +0000 Subject: [PATCH 3/5] fix: typo corrections --- storage/listObjectContexts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/storage/listObjectContexts.js b/storage/listObjectContexts.js index 78956008de..3e0c0b00bf 100644 --- a/storage/listObjectContexts.js +++ b/storage/listObjectContexts.js @@ -43,7 +43,7 @@ function main(bucketName = 'my-bucket') { const bucket = storage.bucket(bucketName); /** - * List any object that has a context with the specified key attached. + * List any object that has a context with the specified key and value. * Syntax: contexts.""="" */ const filterByValue = 'contexts."priority"="high"'; @@ -83,7 +83,7 @@ function main(bucketName = 'my-bucket') { filesNoHighPriority.forEach(file => console.log(` - ${file.name}`)); /** - * List any object that that does not have a context with the specified key attached. + * List any object that does not have a context with the specified key attached. * Syntax: -contexts."":* */ const absenceOfKey = '-contexts."team-owner":*'; From f27e22e55c2947cece92d9b04ecfb0c9034f11dc Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Mon, 11 May 2026 11:53:07 +0000 Subject: [PATCH 4/5] test(storage): refactor to state-based assertions --- storage/setObjectContexts.js | 20 ------------------ storage/system-test/files.test.js | 34 ++++++++++++++++++++----------- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/storage/setObjectContexts.js b/storage/setObjectContexts.js index 0557028133..19e308fd43 100644 --- a/storage/setObjectContexts.js +++ b/storage/setObjectContexts.js @@ -57,26 +57,6 @@ function main(bucketName = 'my-bucket', fileName = 'test.txt') { console.log(`Updated Object Contexts for ${fileName}:`); console.log(JSON.stringify(metadata.contexts, null, 2)); - - // Delete a specific key from the context: - // We send 'null' for the specific key we want to remove. - await file.setMetadata({ - contexts: { - custom: { - 'team-owner': null, - }, - }, - }); - console.log(`Deleted 'team-owner' key from contexts for ${fileName}.`); - - // Delete all keys from the context: - // We set the 'custom' property to null to wipe the entire map. - await file.setMetadata({ - contexts: { - custom: null, - }, - }); - console.log(`Cleared all custom contexts for ${fileName}.`); } setObjectContexts().catch(console.error); diff --git a/storage/system-test/files.test.js b/storage/system-test/files.test.js index 17b1520100..a0147fa8a0 100644 --- a/storage/system-test/files.test.js +++ b/storage/system-test/files.test.js @@ -25,9 +25,7 @@ const {promisify} = require('util'); const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); -const storage = new Storage({ - projectId: process.env.GOOGLE_CLOUD_PROJECT, -}); +const storage = new Storage(); const cwd = path.join(__dirname, '..'); const bucketName = generateName(); const bucket = storage.bucket(bucketName); @@ -64,7 +62,7 @@ describe('file', () => { }); }); - it('should set object contexts', () => { + it('should set object contexts', async () => { const output = execSync( `node setObjectContexts.js ${bucketName} ${fileName}` ); @@ -77,17 +75,14 @@ describe('file', () => { assert.include(output, '"createTime":'); assert.include(output, '"updateTime":'); - // Verify Specific Key Deletion - assert.include( - output, - `Deleted 'team-owner' key from contexts for ${fileName}.` - ); + const [metadata] = await contextFile.getMetadata(); + const customContexts = metadata.contexts?.custom || {}; - // Verify Clearing All Contexts - assert.include(output, `Cleared all custom contexts for ${fileName}.`); + assert.strictEqual(customContexts['priority']?.value, 'high'); + assert.strictEqual(customContexts['team-owner']?.value, 'storage-team'); }); - it('should get object contexts', () => { + it('should get object contexts', async () => { const output = execSync( `node getObjectContexts.js ${bucketName} ${fileName}` ); @@ -96,6 +91,12 @@ describe('file', () => { assert.include(output, 'Value: high'); assert.include(output, 'Key: team-owner'); assert.include(output, 'Value: storage-team'); + + const [metadata] = await contextFile.getMetadata(); + const customContexts = metadata.contexts?.custom || {}; + + assert.strictEqual(customContexts['priority'].value, 'high'); + assert.strictEqual(customContexts['team-owner'].value, 'storage-team'); }); it('should list objects with context filters', async () => { @@ -133,6 +134,15 @@ describe('file', () => { ); assert.include(output, ` - ${noContextFileName}`); + const [files] = await bucket.getFiles(); + const targetFile = files.find(f => f.name === fileName); + + assert.exists(targetFile); + const [metadata] = await targetFile.getMetadata(); + + // Verify the state that the list filter is supposed to find + assert.strictEqual(metadata.contexts?.custom?.priority?.value, 'high'); + await bucket.file(noContextFileName).delete(); }); }); From fd1aed38d75129961405c67d98e9ca2736c2f33e Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 20 May 2026 16:05:12 +0000 Subject: [PATCH 5/5] feat(storage): add samples and system tests for bucket encryption enforcement (#4272) * feat(storage): add samples and system tests for bucket encryption enforcement Adds comprehensive code samples and system tests to verify Google-managed, Customer-managed, and Customer-supplied encryption enforcement logic. - Add setBucketEncryptionEnforcementConfig.js sample - Add getBucketEncryptionEnforcementConfig.js sample - Add updateBucketEncryptionEnforcementConfig.js sample - Add system tests to verify CLI output and backend metadata state - Ensure server-side effectiveTime is correctly captured and displayed * code refactor * test(storage): refactor encryption tests to assert state over strings * test: skip bucket encryption enforcement tests if defaultKmsKeyName is missing --------- Co-authored-by: Jennifer Davis --- .../getBucketEncryptionEnforcementConfig.js | 76 ++++++++++ .../setBucketEncryptionEnforcementConfig.js | 93 ++++++++++++ storage/system-test/buckets.test.js | 143 ++++++++++++++++++ ...updateBucketEncryptionEnforcementConfig.js | 74 +++++++++ 4 files changed, 386 insertions(+) create mode 100644 storage/getBucketEncryptionEnforcementConfig.js create mode 100644 storage/setBucketEncryptionEnforcementConfig.js create mode 100644 storage/system-test/buckets.test.js create mode 100644 storage/updateBucketEncryptionEnforcementConfig.js diff --git a/storage/getBucketEncryptionEnforcementConfig.js b/storage/getBucketEncryptionEnforcementConfig.js new file mode 100644 index 0000000000..aa791ab1bb --- /dev/null +++ b/storage/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/storage/setBucketEncryptionEnforcementConfig.js b/storage/setBucketEncryptionEnforcementConfig.js new file mode 100644 index 0000000000..625ada0629 --- /dev/null +++ b/storage/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, + 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/storage/system-test/buckets.test.js b/storage/system-test/buckets.test.js new file mode 100644 index 0000000000..d5055007d8 --- /dev/null +++ b/storage/system-test/buckets.test.js @@ -0,0 +1,143 @@ +// Copyright 2019 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'; + +const {Storage} = require('@google-cloud/storage'); +const {assert} = require('chai'); +const {before, after, afterEach, it} = require('mocha'); +const cp = require('child_process'); +const uuid = require('uuid'); + +const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); + +const storage = new Storage(); +const samplesTestBucketPrefix = `nodejs-storage-samples-${uuid.v4()}`; +const bucketName = `${samplesTestBucketPrefix}-a`; +const defaultKmsKeyName = process.env.GOOGLE_CLOUD_KMS_KEY_ASIA; +const bucket = storage.bucket(bucketName); + +before(async () => { + await storage.createBucket(bucketName); +}); + +async function deleteAllBucketsAsync() { + const [buckets] = await storage.getBuckets({prefix: samplesTestBucketPrefix}); + + for (const bucket of buckets) { + await bucket.deleteFiles({force: true}); + await bucket.delete({ignoreNotFound: true}); + } +} + +after(deleteAllBucketsAsync); +afterEach(async () => { + await new Promise(res => setTimeout(res, 1000)); +}); + +it('should set bucket encryption enforcement configuration', async function () { + if (!defaultKmsKeyName) { + this.skip(); + } + 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(); + const encryption = metadata.encryption || {}; + assert.strictEqual( + encryption.googleManagedEncryptionEnforcementConfig?.restrictionMode, + 'FullyRestricted' + ); + assert.strictEqual( + encryption.customerManagedEncryptionEnforcementConfig?.restrictionMode, + 'NotRestricted' + ); + assert.strictEqual( + encryption.customerSuppliedEncryptionEnforcementConfig?.restrictionMode, + 'FullyRestricted' + ); +}); + +it('should get bucket encryption enforcement configuration', async function () { + if (!defaultKmsKeyName) { + this.skip(); + } + 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:/); + + const [metadata] = await bucket.getMetadata(); + const encryption = metadata.encryption || {}; + + assert.strictEqual(encryption.defaultKmsKeyName, defaultKmsKeyName); + assert.strictEqual( + encryption.googleManagedEncryptionEnforcementConfig?.restrictionMode, + 'FullyRestricted' + ); + assert.strictEqual( + encryption.customerManagedEncryptionEnforcementConfig?.restrictionMode, + 'NotRestricted' + ); + assert.strictEqual( + encryption.customerSuppliedEncryptionEnforcementConfig?.restrictionMode, + 'FullyRestricted' + ); +}); + +it('should update and then remove bucket encryption enforcement configuration', async () => { + const output = execSync( + `node updateBucketEncryptionEnforcementConfig.js ${bucketName}` + ); + + assert.include( + output, + `Google-managed encryption enforcement set to FullyRestricted for ${bucketName}.` + ); + assert.include( + output, + `All encryption enforcement configurations removed from bucket ${bucketName}.` + ); + + const [metadata] = await bucket.getMetadata(); + assert.ok(!metadata.encryption); +}); diff --git a/storage/updateBucketEncryptionEnforcementConfig.js b/storage/updateBucketEncryptionEnforcementConfig.js new file mode 100644 index 0000000000..1bf5feb839 --- /dev/null +++ b/storage/updateBucketEncryptionEnforcementConfig.js @@ -0,0 +1,74 @@ +// 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: Update Bucket Encryption Enforcement Config +// description: Updates and then removes encryption enforcement configurations from a bucket. +// usage: node updateBucketEncryptionEnforcementConfig.js + +function main(bucketName = 'my-bucket') { + // [START storage_update_bucket_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 updateBucketEncryptionEnforcementConfig() { + const bucket = storage.bucket(bucketName); + + // Update a specific encryption type's restriction mode + // This partial update preserves other existing encryption settings. + const updateOptions = { + encryption: { + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + }, + }; + + await bucket.setMetadata(updateOptions); + console.log( + `Google-managed encryption enforcement set to FullyRestricted for ${bucketName}.` + ); + + // Remove all encryption enforcement configurations altogether + // Setting these values to null removes the policies from the bucket metadata. + const clearOptions = { + encryption: { + defaultKmsKeyName: null, + googleManagedEncryptionEnforcementConfig: null, + customerSuppliedEncryptionEnforcementConfig: null, + customerManagedEncryptionEnforcementConfig: null, + }, + }; + + await bucket.setMetadata(clearOptions); + console.log( + `All encryption enforcement configurations removed from bucket ${bucketName}.` + ); + } + + updateBucketEncryptionEnforcementConfig().catch(console.error); + // [END storage_update_bucket_encryption_enforcement_config] +} +main(...process.argv.slice(2));