From 5925b0faa80f7644802f9c7057873c8b879e98b4 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 13 Feb 2025 18:51:04 +0000 Subject: [PATCH 01/29] feat(bigquery): initial project setup --- bigquery/cloud-client/app.js | 0 bigquery/cloud-client/package.json | 27 +++++++++++++++++++ .../src/viewDatasetAccessPolicy.js | 0 .../src/viewTableOrViewAccessPolicy.js | 0 .../test/viewDatasetAccessPolicy.test.js | 0 .../test/viewTableOrViewAccessPolicy.test.js | 0 6 files changed, 27 insertions(+) create mode 100644 bigquery/cloud-client/app.js create mode 100644 bigquery/cloud-client/package.json create mode 100644 bigquery/cloud-client/src/viewDatasetAccessPolicy.js create mode 100644 bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js create mode 100644 bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js create mode 100644 bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js diff --git a/bigquery/cloud-client/app.js b/bigquery/cloud-client/app.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bigquery/cloud-client/package.json b/bigquery/cloud-client/package.json new file mode 100644 index 0000000000..9f9ef2b1de --- /dev/null +++ b/bigquery/cloud-client/package.json @@ -0,0 +1,27 @@ +{ + "name": "bigquery-cloud-client", + "description": "Big Query Cloud Client Node.js for Google App", + "version": "0.0.1", + "private": true, + "license": "Apache Version 2.0", + "author": "Google Inc.", + "engines": { + "node": "20.x" + }, + "scripts": { + "deploy": "gcloud app deploy", + "start": "node app.js", + "unit-test": "c8 mocha -p -j 2 test/ --timeout=10000 --exit", + "test": "npm run unit-test" + }, + "dependencies": { + "@google-cloud/bigquery": "7.9.2" + }, + "devDependencies": { + "c8": "^10.0.0", + "chai": "^4.5.0", + "mocha": "^10.0.0", + "proxysquire": "^2.1.0", + "sinon": "^18.0.0" + } +} \ No newline at end of file diff --git a/bigquery/cloud-client/src/viewDatasetAccessPolicy.js b/bigquery/cloud-client/src/viewDatasetAccessPolicy.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js b/bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js b/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js new file mode 100644 index 0000000000..e69de29bb2 From e24c2e897fcc345c98b877ece5c9160d8a5a7db5 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 13 Feb 2025 19:18:36 +0000 Subject: [PATCH 02/29] feat(bigquery): basic structure - Configure main app.js entry point - Configure viewDatasetAccessPolicy.js file --- bigquery/cloud-client/app.js | 34 +++++++++++ .../src/viewDatasetAccessPolicy.js | 58 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/bigquery/cloud-client/app.js b/bigquery/cloud-client/app.js index e69de29bb2..f4bee8b387 100644 --- a/bigquery/cloud-client/app.js +++ b/bigquery/cloud-client/app.js @@ -0,0 +1,34 @@ +// Copyright 2025 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 { viewDatasetAccessPolicy } = require("./src/viewDatasetAccessPolicy") + +async function main() { + try { + // Example usage of dataset access policy viewer + await viewDatasetAccessPolicy(); + } catch (error) { + console.error("Error:", error); + processors.exitCode = 1; + } +} + +// Run the samples if this file is run directly +if (require.main === module) { + main(); +} + +module.export = { viewDatasetAccessPolicy } \ No newline at end of file diff --git a/bigquery/cloud-client/src/viewDatasetAccessPolicy.js b/bigquery/cloud-client/src/viewDatasetAccessPolicy.js index e69de29bb2..86ca72f30c 100644 --- a/bigquery/cloud-client/src/viewDatasetAccessPolicy.js +++ b/bigquery/cloud-client/src/viewDatasetAccessPolicy.js @@ -0,0 +1,58 @@ +// Copyright 2025 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 { BigQuery } = require("@google-cloud/bigquery"); + +// [START bigquery_view_dataset_access_policy] +/** + * View access policies for a BigQuery dataset + * + * @param {object} [overrideValues] Optional parameters to override defaults + * @param {string} [overrideValues.datasetId] Dataset ID to view access policies + */ + +async function viewDatasetAccessPolicy(overrideValues = {}) { + // Instantiate BigQuery client + const bigquery = new BigQuery(); + + // Dataset from which to get the access policy + const datasetId = overrideValues.datasetId || "my_new_dataset"; + + try { + // Prepares a reference to the dataset + const dataset = bigquery.dataset(datasetId); + const [metadata] = await dataset.getMetadata(); + + // Shows the Access policy as a list of access entries + console.log("Access entries:", metadata.access); + + // Get properties for an AccessEntry + if (metadata.access && metadata.access.length > 0) { + console.log(`Details for Access entry 0 in dataset '${datasetId}':`); + console.log(`Role: '${metadata.access[0].role || "N/A"}'`); + console.log(`SpecialGroup: '${metadata.access[0].specialGroup || "N/A"}'`); + console.log(`UserByEmail: '${metadata.access[0].userByEmail || "N/A"}'`); + } + } catch (error) { + console.lerror(`Error viewing dataset access policy: ${error.message}`); + } +} + +// [END bigquery_view_dataset_access_policy] + +module.exports = { + viewDatasetAccessPolicy, +}; \ No newline at end of file From 84ec0f6b464802a00d3012690eb728cc42a5cdb3 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 13 Feb 2025 19:36:24 +0000 Subject: [PATCH 03/29] feat(bigquery): Add table and view access policy viewer - Add viewTableOrViewAccessPolicy to the app.js entry point - Configure viewTableOrViewAccessPolicy.js file --- bigquery/cloud-client/app.js | 11 ++- .../src/viewTableOrViewAccessPolicy.js | 71 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/bigquery/cloud-client/app.js b/bigquery/cloud-client/app.js index f4bee8b387..3a23852a0c 100644 --- a/bigquery/cloud-client/app.js +++ b/bigquery/cloud-client/app.js @@ -15,11 +15,20 @@ "use strict"; const { viewDatasetAccessPolicy } = require("./src/viewDatasetAccessPolicy") +const { viewTableOrViewAccessPolicy } = require("./src/viewTableOrViewAccessPolicy") async function main() { try { // Example usage of dataset access policy viewer await viewDatasetAccessPolicy(); + + // Example usage of table/view access policy viewer + const projectId = process.env.GOOGLE_CLOUD_PROJECT; + await viewTableOrViewAccessPolicy({ + projectId, + datasetId: "my_new_dataset", + resourceName: "my_table", + }); } catch (error) { console.error("Error:", error); processors.exitCode = 1; @@ -31,4 +40,4 @@ if (require.main === module) { main(); } -module.export = { viewDatasetAccessPolicy } \ No newline at end of file +module.export = { viewDatasetAccessPolicy, viewTableOrViewAccessPolicy } \ No newline at end of file diff --git a/bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js b/bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js index e69de29bb2..94956da41d 100644 --- a/bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js +++ b/bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js @@ -0,0 +1,71 @@ +// Copyright 2025 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 { BigQuery } = require("@google-cloud/bigquery"); + +// [START bigquery_view_table_or_view_access_policy] +/** + * View access policies for a BigQuery table or view + * + * @param {object} [overrideValues] Optional parameters to override defaults + * @param {string} [overrideValues.projectId] Google Cloud project ID + * @param {string} [overrideValues.datasetId] Dataset ID where the table or view is located + * @param {string} [overrideValues.resourceName] Table or view name to get the access policy + * @throws {Error} If required parameters are missing or if there's an API error + */ + +async function viewTableOrViewAccessPolicy(overrideValues = {}) { + // Initialize default values + const projectId = overrideValues.projectId || processors.env.GOOGLE_CLOUD_PROJECT; + const datasetId = overrideValues.datasetId || "my_new_dataset"; + const resourceName = overrideValues.resourceName || "my_table"; + + if (!projectId) { + throw new Error("Project ID is required. Set it in overrideValues or GOOGLE_CLOUD_PROJECT environment variable.") + } + + try { + // // Instantiate BigQuery client + const bigquery = new BigQuery(); + + // Get the IAM access policy from the table or view + const [policy] = await bigquery + .dataset(datasetId) + .table(resourceName) + .getIamPolicy(); + + // Show policy details + console.log(`Access Policy details for table or view '${resourceName}':`); + console.log(`Bindings: ${JSON.stringify(policy.bindings, null, 2)}`); + console.log(`etag: ${policy.etag}`); + console.log(`version: ${policy.version}`); + + return policy; + } catch (error) { + console.error(`Error viewing table/view access policy: ${error.message}`); + throw error; + } +} +// [END bigquery_view_table_or_view_access_policy] + +// If this file is run directly, execute the function with default values +if (require.main === module) { + viewTableOrViewAccessPolicy().catch(console.error); +} + +module.exports = { + viewTableOrViewAccessPolicy, +}; \ No newline at end of file From 21f786e48a532ef155aa56332a60cd1e3ddbf1b0 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 13 Feb 2025 19:58:59 +0000 Subject: [PATCH 04/29] feat(bigquery): Add viewDatasetAccessPolicy tests --- .../test/viewDatasetAccessPolicy.test.js | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js b/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js index e69de29bb2..b781f451b5 100644 --- a/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js +++ b/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js @@ -0,0 +1,99 @@ +// Copyright 2025 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 { assert } = require("chai"); +const sinon = require("sinon"); +const { BigQuery } = require("@google-cloud/bigquery") +const { viewDatasetAccessPolicy } = require("../src/viewDatasetAccessPolicy") + +describe("viewDatasetAccessPolicy", () => { + let bigQueryStub; + let consoleLogSpy; + + const sampleAccessEntry = { + role: "READER", + specialGroup: "projectReaders", + userByEmail: "test@example.com", + }; + + beforeEach(() => { + // Stub BigQuery client + bigQueryStub = { + dataset: sinon.stub().returns({ + getMetadata: sinon.stub().resolves([ + { + access: [sampleAccessEntry], + }, + ]), + }), + }; + + // Stub BigQuery constructor + sinon.stub(BigQuery.prototype, "dataset").callsFake(bigQueryStub.dataset); + + // Spy on console.log + consoleLogSpy = sinon.spy(console, "log"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should display access policy details for a dataset", async () => { + const datasetId = "test_dataset"; + + await viewDatasetAccessPolicy({ datasetId }); + + // Verify BigQuery client was called correctly + assert.ok(consoleLogSpy.calledWith("Access entries:", [sampleAccessEntry])); + assert.ok(consoleLogSpy.calledWith(`Details for Access entry 0 in dataset '${datasetId}':`)); + assert.ok(consoleLogSpy.calledWith("Role: READER")); + assert.ok(consoleLogSpy.calledWith("SpecialGroup: projectReaders")); + assert.ok(consoleLogSpy.calledWith("UserByEmail: test@example.com")); + }); + + it("should handle datasets with no access entries", async () => { + // Override stub to return empty access array + bigQueryStub.dataset.returns({ + getMetadata: sinon.stub().resolves([ + { + access: [], + }, + ]), + }); + + await viewDatasetAccessPolicy({ datasetId: "empty_dataset" }); + + // Verify only the access entries message was logged + assert.ok(consoleLogSpy.calledWith("Access entries:", [])); + }); + + it("should handle errors gracefully", async () => { + const errorMessage = "Dataset not found"; + + // Override stub to throw error + bigQueryStub.dataset.returns({ + getMetadata: sinon.stub().rejects(new Error(errorMessage)), + }); + + try { + await viewDatasetAccessPolicy({ datasetId: "non_existent_dataset" }); + assert.fail("Should have thrown an error"); + } catch (error) { + assert.include(error.message, errorMessage); + } + }); +}) \ No newline at end of file From 5d408c76e308f04d2e55f417788cb37fd47793ff Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Fri, 14 Feb 2025 14:56:48 +0000 Subject: [PATCH 05/29] feat(bigquery): Add viewTableOrViewAccessPolicy tests --- .../test/viewTableOrViewAccessPolicy.test.js | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js index e69de29bb2..090082b5f7 100644 --- a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js +++ b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js @@ -0,0 +1,126 @@ +// Copyright 2025 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 { assert } = require("chai"); +const sinon = require("sinon"); +const proxysquire = require("proxysquire"); +const { BigQuery } = require("@google-cloud/bigquery") + +describe("viewTableOrViewAccessPolicy", () => { + let bigQueryStub; + let consoleLogSpy; + let consoleErrorSpy; + let viewTableOrViewAccessPolicy; + + const samplePolicy = { + bindings: [ + { + role: "roles/bigquery.dataViewer", + members: ["user:test@example.com"], + }, + ], + etag: "CAE=", + version: 1, + }; + + beforeEach(() => { + // Set required environment variables + processors.env.GOOGLE_CLOUD_PROJECT = "test-project"; + + // Stub BigQuery client + BigQueryStub = { + dataset: sinon.stub().returns({ + table: sinon.stub().returns({ + getIamPolicy: sinon.stub().resolves([samplePolicy]), + }), + }), + }; + + // Stub BigQuery constructor + sinon.stub(BigQuery.prototype, "dataset").callsFake(bigQueryStub.dataset); + + // Spy on console methods + consoleLogSpy = sinon.spy(console, "log"); + consoleErrorSpy = sinon.spy(console, "error"); + + // Reset module for each test + viewTableOrViewAccessPolicy = require("../src/viewTableOrViewAccessPolicy").viewTableOrViewAccessPolicy; + }); + + afterEach(() => { + delete processors.env.GOOGLE_CLOUD_PROJECT; + sinon.restore(); + }); + + it("should display access policy details for a table", async () => { + const params = { + projectId: "test-project", + datasetId: "test_dataset", + resourceName: "test_table", + }; + + const policy = await viewTableOrViewAccessPolicy(params); + + assert.ok(bigQueryStub.dataset.calledWith(params.datasetId)); + assert.ok(bigQueryStub.dataset().table.calledWith(params.resourceName)); + + assert.ok( + consoleLogSpy.calledWith( + `Access Policy details for table or view '${params.resourceName}':` + ) + ); + assert.ok( + consoleLogSpy.calledWith( + `Bindings: ${JSON.stringify(samplePolicy.bindings, null, 2)}` + ) + ); + assert.ok(consoleLogSpy.calledWith(`etag: ${samplePolicy.etag}`)); + assert.ok(consoleLogSpy.calledWith(`version: ${samplePolicy.version}`)); + + assert.deepEqual(policy, samplePolicy); + }); + + it("should throw error when project ID is missing", async () => { + delete processors.env.GOOGLE_CLOUD_PROJECT; + + try { + await viewTableOrViewAccessPolicy({}); + assert.fail("Should have thrown an error"); + } catch (error) { + assert.include(error.message, "Project ID is required"); + } + }); + + it("should handle errors gracefully", async () => { + const errorMessage = "Table not found"; + + bigQueryStub.dataset.returns({ + table: sinon.stub().returns({ + getIamPolicy: sinon.stub().rejects(new Error(errorMessage)), + }), + }); + + try { + await viewTableOrViewAccessPolicy({ + projectId: "test-project", + datasetId: "test_dataset", + resourceName: "non_existent_table", + }) + } catch (error) { + assert.include(error.message, errorMessage); + } + }); +}); \ No newline at end of file From 62e343a3d5701eb760a7208093f43cc73f823e04 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Fri, 14 Feb 2025 19:43:07 +0000 Subject: [PATCH 06/29] fix(bigquery): Fix linting errors --- bigquery/cloud-client/app.js | 42 ++-- bigquery/cloud-client/package.json | 1 - .../src/viewDatasetAccessPolicy.js | 61 +++--- .../src/viewTableOrViewAccessPolicy.js | 68 +++--- .../test/viewDatasetAccessPolicy.test.js | 161 +++++++------- .../test/viewTableOrViewAccessPolicy.test.js | 207 +++++++++--------- 6 files changed, 274 insertions(+), 266 deletions(-) diff --git a/bigquery/cloud-client/app.js b/bigquery/cloud-client/app.js index 3a23852a0c..36bd1f7e33 100644 --- a/bigquery/cloud-client/app.js +++ b/bigquery/cloud-client/app.js @@ -4,7 +4,7 @@ // 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 +// 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, @@ -12,32 +12,34 @@ // See the License for the specific language governing permissions and // limitations under the License. -"use strict"; +'use strict'; -const { viewDatasetAccessPolicy } = require("./src/viewDatasetAccessPolicy") -const { viewTableOrViewAccessPolicy } = require("./src/viewTableOrViewAccessPolicy") +const {viewDatasetAccessPolicy} = require('./src/viewDatasetAccessPolicy'); +const { + viewTableOrViewAccessPolicy, +} = require('./src/viewTableOrViewAccessPolicy'); async function main() { - try { - // Example usage of dataset access policy viewer - await viewDatasetAccessPolicy(); + try { + // Example usage of dataset access policy viewer + await viewDatasetAccessPolicy(); - // Example usage of table/view access policy viewer - const projectId = process.env.GOOGLE_CLOUD_PROJECT; - await viewTableOrViewAccessPolicy({ - projectId, - datasetId: "my_new_dataset", - resourceName: "my_table", - }); - } catch (error) { - console.error("Error:", error); - processors.exitCode = 1; - } + // Example usage of table/view access policy viewer + const projectId = process.env.GOOGLE_CLOUD_PROJECT; + await viewTableOrViewAccessPolicy({ + projectId, + datasetId: 'my_new_dataset', + resourceName: 'my_table', + }); + } catch (error) { + console.error('Error:', error); + process.exitCode = 1; + } } // Run the samples if this file is run directly if (require.main === module) { - main(); + main(); } -module.export = { viewDatasetAccessPolicy, viewTableOrViewAccessPolicy } \ No newline at end of file +module.export = {viewDatasetAccessPolicy, viewTableOrViewAccessPolicy}; diff --git a/bigquery/cloud-client/package.json b/bigquery/cloud-client/package.json index 9f9ef2b1de..49d935ea21 100644 --- a/bigquery/cloud-client/package.json +++ b/bigquery/cloud-client/package.json @@ -21,7 +21,6 @@ "c8": "^10.0.0", "chai": "^4.5.0", "mocha": "^10.0.0", - "proxysquire": "^2.1.0", "sinon": "^18.0.0" } } \ No newline at end of file diff --git a/bigquery/cloud-client/src/viewDatasetAccessPolicy.js b/bigquery/cloud-client/src/viewDatasetAccessPolicy.js index 86ca72f30c..7c2a936845 100644 --- a/bigquery/cloud-client/src/viewDatasetAccessPolicy.js +++ b/bigquery/cloud-client/src/viewDatasetAccessPolicy.js @@ -4,7 +4,7 @@ // 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 +// 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, @@ -12,47 +12,46 @@ // See the License for the specific language governing permissions and // limitations under the License. -"use strict"; +'use strict'; -const { BigQuery } = require("@google-cloud/bigquery"); +const {BigQuery} = require('@google-cloud/bigquery'); // [START bigquery_view_dataset_access_policy] /** * View access policies for a BigQuery dataset - * + * * @param {object} [overrideValues] Optional parameters to override defaults * @param {string} [overrideValues.datasetId] Dataset ID to view access policies */ - async function viewDatasetAccessPolicy(overrideValues = {}) { - // Instantiate BigQuery client - const bigquery = new BigQuery(); - - // Dataset from which to get the access policy - const datasetId = overrideValues.datasetId || "my_new_dataset"; - - try { - // Prepares a reference to the dataset - const dataset = bigquery.dataset(datasetId); - const [metadata] = await dataset.getMetadata(); - - // Shows the Access policy as a list of access entries - console.log("Access entries:", metadata.access); - - // Get properties for an AccessEntry - if (metadata.access && metadata.access.length > 0) { - console.log(`Details for Access entry 0 in dataset '${datasetId}':`); - console.log(`Role: '${metadata.access[0].role || "N/A"}'`); - console.log(`SpecialGroup: '${metadata.access[0].specialGroup || "N/A"}'`); - console.log(`UserByEmail: '${metadata.access[0].userByEmail || "N/A"}'`); - } - } catch (error) { - console.lerror(`Error viewing dataset access policy: ${error.message}`); + // Instantiate BigQuery client + const bigquery = new BigQuery(); + + // Dataset from which to get the access policy + const datasetId = overrideValues.datasetId || 'my_new_dataset'; + + try { + // Prepares a reference to the dataset + const dataset = bigquery.dataset(datasetId); + const [metadata] = await dataset.getMetadata(); + + // Shows the Access policy as a list of access entries + console.log('Access entries:', metadata.access); + + // Get properties for an AccessEntry + if (metadata.access && metadata.access.length > 0) { + console.log(`Details for Access entry 0 in dataset '${datasetId}':`); + console.log(`Role: ${metadata.access[0].role || 'N/A'}`); + console.log(`SpecialGroup: ${metadata.access[0].specialGroup || 'N/A'}`); + console.log(`UserByEmail: ${metadata.access[0].userByEmail || 'N/A'}`); } + } catch (error) { + console.error(`Error viewing dataset access policy: ${error.message}`); + throw error; + } } - // [END bigquery_view_dataset_access_policy] module.exports = { - viewDatasetAccessPolicy, -}; \ No newline at end of file + viewDatasetAccessPolicy, +}; diff --git a/bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js b/bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js index 94956da41d..67f97caf1c 100644 --- a/bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js +++ b/bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js @@ -4,7 +4,7 @@ // 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 +// 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, @@ -12,60 +12,62 @@ // See the License for the specific language governing permissions and // limitations under the License. -"use strict"; +'use strict'; -const { BigQuery } = require("@google-cloud/bigquery"); +const {BigQuery} = require('@google-cloud/bigquery'); // [START bigquery_view_table_or_view_access_policy] /** * View access policies for a BigQuery table or view - * + * * @param {object} [overrideValues] Optional parameters to override defaults * @param {string} [overrideValues.projectId] Google Cloud project ID * @param {string} [overrideValues.datasetId] Dataset ID where the table or view is located * @param {string} [overrideValues.resourceName] Table or view name to get the access policy * @throws {Error} If required parameters are missing or if there's an API error */ - async function viewTableOrViewAccessPolicy(overrideValues = {}) { - // Initialize default values - const projectId = overrideValues.projectId || processors.env.GOOGLE_CLOUD_PROJECT; - const datasetId = overrideValues.datasetId || "my_new_dataset"; - const resourceName = overrideValues.resourceName || "my_table"; + // Initialize default values + const projectId = + overrideValues.projectId || process.env.GOOGLE_CLOUD_PROJECT; + const datasetId = overrideValues.datasetId || 'my_new_dataset'; + const resourceName = overrideValues.resourceName || 'my_table'; - if (!projectId) { - throw new Error("Project ID is required. Set it in overrideValues or GOOGLE_CLOUD_PROJECT environment variable.") - } + if (!projectId) { + throw new Error( + 'Project ID is required. Set it in overrideValues or GOOGLE_CLOUD_PROJECT environment variable.' + ); + } - try { - // // Instantiate BigQuery client - const bigquery = new BigQuery(); + try { + // Instantiate BigQuery client + const bigquery = new BigQuery({projectId}); - // Get the IAM access policy from the table or view - const [policy] = await bigquery - .dataset(datasetId) - .table(resourceName) - .getIamPolicy(); + // Get the IAM access policy for the table or view + const [policy] = await bigquery + .dataset(datasetId) + .table(resourceName) + .getIamPolicy(); - // Show policy details - console.log(`Access Policy details for table or view '${resourceName}':`); - console.log(`Bindings: ${JSON.stringify(policy.bindings, null, 2)}`); - console.log(`etag: ${policy.etag}`); - console.log(`version: ${policy.version}`); + // Show policy details + console.log(`Access Policy details for table or view '${resourceName}':`); + console.log(`Bindings: ${JSON.stringify(policy.bindings, null, 2)}`); + console.log(`etag: ${policy.etag}`); + console.log(`version: ${policy.version}`); - return policy; - } catch (error) { - console.error(`Error viewing table/view access policy: ${error.message}`); - throw error; - } + return policy; + } catch (error) { + console.error(`Error viewing table/view access policy: ${error.message}`); + throw error; + } } // [END bigquery_view_table_or_view_access_policy] // If this file is run directly, execute the function with default values if (require.main === module) { - viewTableOrViewAccessPolicy().catch(console.error); + viewTableOrViewAccessPolicy().catch(console.error); } module.exports = { - viewTableOrViewAccessPolicy, -}; \ No newline at end of file + viewTableOrViewAccessPolicy, +}; diff --git a/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js b/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js index b781f451b5..2e4c9a18cd 100644 --- a/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js +++ b/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js @@ -4,7 +4,7 @@ // 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 +// 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, @@ -12,88 +12,95 @@ // See the License for the specific language governing permissions and // limitations under the License. -"use strict"; - -const { assert } = require("chai"); -const sinon = require("sinon"); -const { BigQuery } = require("@google-cloud/bigquery") -const { viewDatasetAccessPolicy } = require("../src/viewDatasetAccessPolicy") - -describe("viewDatasetAccessPolicy", () => { - let bigQueryStub; - let consoleLogSpy; - - const sampleAccessEntry = { - role: "READER", - specialGroup: "projectReaders", - userByEmail: "test@example.com", +'use strict'; + +const {assert} = require('chai'); +const sinon = require('sinon'); +const {BigQuery} = require('@google-cloud/bigquery'); +const {viewDatasetAccessPolicy} = require('../src/viewDatasetAccessPolicy'); + +describe('viewDatasetAccessPolicy', () => { + let bigQueryStub; + let consoleLogSpy; + + const sampleAccessEntry = { + role: 'READER', + specialGroup: 'projectReaders', + userByEmail: 'test@example.com', + }; + + beforeEach(() => { + // Stub BigQuery client + bigQueryStub = { + dataset: sinon.stub().returns({ + getMetadata: sinon.stub().resolves([ + { + access: [sampleAccessEntry], + }, + ]), + }), }; - beforeEach(() => { - // Stub BigQuery client - bigQueryStub = { - dataset: sinon.stub().returns({ - getMetadata: sinon.stub().resolves([ - { - access: [sampleAccessEntry], - }, - ]), - }), - }; - - // Stub BigQuery constructor - sinon.stub(BigQuery.prototype, "dataset").callsFake(bigQueryStub.dataset); - - // Spy on console.log - consoleLogSpy = sinon.spy(console, "log"); + // Stub BigQuery constructor + sinon.stub(BigQuery.prototype, 'dataset').callsFake(bigQueryStub.dataset); + + // Spy on console.log + consoleLogSpy = sinon.spy(console, 'log'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should display access policy details for a dataset', async () => { + const datasetId = 'test_dataset'; + + await viewDatasetAccessPolicy({datasetId}); + + // Verify BigQuery client was called correctly + assert.ok(bigQueryStub.dataset.calledWith(datasetId)); + + // Verify console output + assert.ok(consoleLogSpy.calledWith('Access entries:', [sampleAccessEntry])); + assert.ok( + consoleLogSpy.calledWith( + `Details for Access entry 0 in dataset '${datasetId}':` + ) + ); + assert.ok(consoleLogSpy.calledWith('Role: READER')); + assert.ok(consoleLogSpy.calledWith('SpecialGroup: projectReaders')); + assert.ok(consoleLogSpy.calledWith('UserByEmail: test@example.com')); + }); + + it('should handle datasets with no access entries', async () => { + // Override stub to return empty access array + bigQueryStub.dataset.returns({ + getMetadata: sinon.stub().resolves([ + { + access: [], + }, + ]), }); - afterEach(() => { - sinon.restore(); - }); + await viewDatasetAccessPolicy({datasetId: 'empty_dataset'}); - it("should display access policy details for a dataset", async () => { - const datasetId = "test_dataset"; + // Verify only the access entries message was logged + assert.ok(consoleLogSpy.calledWith('Access entries:', [])); + }); - await viewDatasetAccessPolicy({ datasetId }); + it('should handle errors gracefully', async () => { + const errorMessage = 'Dataset not found'; - // Verify BigQuery client was called correctly - assert.ok(consoleLogSpy.calledWith("Access entries:", [sampleAccessEntry])); - assert.ok(consoleLogSpy.calledWith(`Details for Access entry 0 in dataset '${datasetId}':`)); - assert.ok(consoleLogSpy.calledWith("Role: READER")); - assert.ok(consoleLogSpy.calledWith("SpecialGroup: projectReaders")); - assert.ok(consoleLogSpy.calledWith("UserByEmail: test@example.com")); + // Override stub to throw error + bigQueryStub.dataset.returns({ + getMetadata: sinon.stub().rejects(new Error(errorMessage)), }); - it("should handle datasets with no access entries", async () => { - // Override stub to return empty access array - bigQueryStub.dataset.returns({ - getMetadata: sinon.stub().resolves([ - { - access: [], - }, - ]), - }); - - await viewDatasetAccessPolicy({ datasetId: "empty_dataset" }); - - // Verify only the access entries message was logged - assert.ok(consoleLogSpy.calledWith("Access entries:", [])); - }); - - it("should handle errors gracefully", async () => { - const errorMessage = "Dataset not found"; - - // Override stub to throw error - bigQueryStub.dataset.returns({ - getMetadata: sinon.stub().rejects(new Error(errorMessage)), - }); - - try { - await viewDatasetAccessPolicy({ datasetId: "non_existent_dataset" }); - assert.fail("Should have thrown an error"); - } catch (error) { - assert.include(error.message, errorMessage); - } - }); -}) \ No newline at end of file + try { + await viewDatasetAccessPolicy({datasetId: 'non_existent_dataset'}); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.include(error.message, errorMessage); + } + }); +}); diff --git a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js index 090082b5f7..7b79d04569 100644 --- a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js +++ b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js @@ -4,7 +4,7 @@ // 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 +// 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, @@ -12,115 +12,114 @@ // See the License for the specific language governing permissions and // limitations under the License. -"use strict"; - -const { assert } = require("chai"); -const sinon = require("sinon"); -const proxysquire = require("proxysquire"); -const { BigQuery } = require("@google-cloud/bigquery") - -describe("viewTableOrViewAccessPolicy", () => { - let bigQueryStub; - let consoleLogSpy; - let consoleErrorSpy; - let viewTableOrViewAccessPolicy; - - const samplePolicy = { - bindings: [ - { - role: "roles/bigquery.dataViewer", - members: ["user:test@example.com"], - }, - ], - etag: "CAE=", - version: 1, +'use strict'; + +const {assert} = require('chai'); +const sinon = require('sinon'); +const {BigQuery} = require('@google-cloud/bigquery'); + +describe('viewTableOrViewAccessPolicy', () => { + let bigQueryStub; + let consoleLogSpy; + let viewTableOrViewAccessPolicy; + + const samplePolicy = { + bindings: [ + { + role: 'roles/bigquery.dataViewer', + members: ['user:test@example.com'], + }, + ], + etag: 'CAE=', + version: 1, + }; + + beforeEach(() => { + // Set required environment variable + process.env.GOOGLE_CLOUD_PROJECT = 'test-project'; + + // Stub BigQuery client + bigQueryStub = { + dataset: sinon.stub().returns({ + table: sinon.stub().returns({ + getIamPolicy: sinon.stub().resolves([samplePolicy]), + }), + }), }; - beforeEach(() => { - // Set required environment variables - processors.env.GOOGLE_CLOUD_PROJECT = "test-project"; + // Stub BigQuery constructor + sinon.stub(BigQuery.prototype, 'dataset').callsFake(bigQueryStub.dataset); - // Stub BigQuery client - BigQueryStub = { - dataset: sinon.stub().returns({ - table: sinon.stub().returns({ - getIamPolicy: sinon.stub().resolves([samplePolicy]), - }), - }), - }; + // Spy on console log methods + consoleLogSpy = sinon.spy(console, 'log'); - // Stub BigQuery constructor - sinon.stub(BigQuery.prototype, "dataset").callsFake(bigQueryStub.dataset); + // Reset module for each test + viewTableOrViewAccessPolicy = + require('../src/viewTableOrViewAccessPolicy').viewTableOrViewAccessPolicy; + }); - // Spy on console methods - consoleLogSpy = sinon.spy(console, "log"); - consoleErrorSpy = sinon.spy(console, "error"); + afterEach(() => { + delete process.env.GOOGLE_CLOUD_PROJECT; + sinon.restore(); + }); - // Reset module for each test - viewTableOrViewAccessPolicy = require("../src/viewTableOrViewAccessPolicy").viewTableOrViewAccessPolicy; - }); - - afterEach(() => { - delete processors.env.GOOGLE_CLOUD_PROJECT; - sinon.restore(); - }); - - it("should display access policy details for a table", async () => { - const params = { - projectId: "test-project", - datasetId: "test_dataset", - resourceName: "test_table", - }; - - const policy = await viewTableOrViewAccessPolicy(params); - - assert.ok(bigQueryStub.dataset.calledWith(params.datasetId)); - assert.ok(bigQueryStub.dataset().table.calledWith(params.resourceName)); - - assert.ok( - consoleLogSpy.calledWith( - `Access Policy details for table or view '${params.resourceName}':` - ) - ); - assert.ok( - consoleLogSpy.calledWith( - `Bindings: ${JSON.stringify(samplePolicy.bindings, null, 2)}` - ) - ); - assert.ok(consoleLogSpy.calledWith(`etag: ${samplePolicy.etag}`)); - assert.ok(consoleLogSpy.calledWith(`version: ${samplePolicy.version}`)); - - assert.deepEqual(policy, samplePolicy); - }); - - it("should throw error when project ID is missing", async () => { - delete processors.env.GOOGLE_CLOUD_PROJECT; + it('should display access policy details for a table', async () => { + const params = { + projectId: 'test-project', + datasetId: 'test_dataset', + resourceName: 'test_table', + }; - try { - await viewTableOrViewAccessPolicy({}); - assert.fail("Should have thrown an error"); - } catch (error) { - assert.include(error.message, "Project ID is required"); - } + const policy = await viewTableOrViewAccessPolicy(params); + + assert.ok(bigQueryStub.dataset.calledWith(params.datasetId)); + assert.ok(bigQueryStub.dataset().table.calledWith(params.resourceName)); + + assert.ok( + consoleLogSpy.calledWith( + `Access Policy details for table or view '${params.resourceName}':` + ) + ); + assert.ok( + consoleLogSpy.calledWith( + `Bindings: ${JSON.stringify(samplePolicy.bindings, null, 2)}` + ) + ); + assert.ok(consoleLogSpy.calledWith(`etag: ${samplePolicy.etag}`)); + assert.ok(consoleLogSpy.calledWith(`version: ${samplePolicy.version}`)); + + assert.deepEqual(policy, samplePolicy); + }); + + it('should throw error when project ID is missing', async () => { + delete process.env.GOOGLE_CLOUD_PROJECT; + + try { + await viewTableOrViewAccessPolicy({}); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.include(error.message, 'Project ID is required'); + } + }); + + it('should handle errors gracefully', async () => { + const errorMessage = 'Table not found'; + + bigQueryStub.dataset.returns({ + table: sinon.stub().returns({ + getIamPolicy: sinon.stub().rejects(new Error(errorMessage)), + }), }); - it("should handle errors gracefully", async () => { - const errorMessage = "Table not found"; - - bigQueryStub.dataset.returns({ - table: sinon.stub().returns({ - getIamPolicy: sinon.stub().rejects(new Error(errorMessage)), - }), - }); - - try { - await viewTableOrViewAccessPolicy({ - projectId: "test-project", - datasetId: "test_dataset", - resourceName: "non_existent_table", - }) - } catch (error) { - assert.include(error.message, errorMessage); - } - }); -}); \ No newline at end of file + try { + await viewTableOrViewAccessPolicy({ + projectId: 'test-project', + datasetId: 'test_dataset', + resourceName: 'non_existent_table', + }); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.include(error.message, errorMessage); + } + }); +}); From 58ee72a44324191f68cddef054419d4500791696 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Mon, 17 Feb 2025 17:34:36 +0000 Subject: [PATCH 07/29] feat(bigquery): Add revokeTableOrViewAccess feawture and tests --- .../src/revokeTableOrViewAccess.js | 93 ++++++++++++ .../test/revokeTableOrViewAccess.test.js | 139 ++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 bigquery/cloud-client/src/revokeTableOrViewAccess.js create mode 100644 bigquery/cloud-client/test/revokeTableOrViewAccess.test.js diff --git a/bigquery/cloud-client/src/revokeTableOrViewAccess.js b/bigquery/cloud-client/src/revokeTableOrViewAccess.js new file mode 100644 index 0000000000..caf69807ee --- /dev/null +++ b/bigquery/cloud-client/src/revokeTableOrViewAccess.js @@ -0,0 +1,93 @@ +// Copyright 2025 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 +// +// https://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. + +const { BigQuery } = require("@google-cloud/bigquery"); + +/** + * Revokes access to a BigQuery table or view + * @param {Object} params - The parameters object + * @param {string} params.projectId - The ID of the Google Cloud project + * @param {string} params.datasetId - The ID of the dataset containing the table/view + * @param {string} params.resourceId - The ID of the table or view + * @param {string} [params.memberToRevoke] - Optional. Specific member to revoke access from (e.g., 'group:example@google.com') + * @param {string} [params.roleToRevoke='roles/bigquery.dataViewer'] - Optional. Specific role to revoke + * @returns {Promise} + */ +async function revokeTableOrViewAccess({ + projectId, + datasetId, + resourceId, + memberToRevoke, + roleToRevoke = "roles/bigquery.dataViewer", +}) { + try { + // Create BigQuery client + const bigquery = new BigQuery({ + projectId: projectId, + }); + + // Get reference to the table or view + const dataset = bigquery.dataset(datasetId); + const table = dataset.table(resourceId); + + // Get current IAM policy + const [policy] = await table.iam.getPolicy(); + console.log( + "Current IAM Policy:", + JSON.stringify(policy.bindings, null, 2) + ); + + // Filter bindings based on parameters + let newBindings = policy.bindings; + + if (memberToRevoke) { + // Remove specific member from specific role + newBindings = policy.bindings + .map((binding) => ({ + ...binding, + members: + binding.role === roleToRevoke + ? binding.members.filter((member) => member !== memberToRevoke) + : binding.members, + })) + .filter((binding) => binding.members.length > 0); + } else { + // Remove all bindings for the specified role + newBindings = policy.bindings.filter( + (binding) => binding.role !== roleToRevoke + ); + } + + // Create new policy with updated bindings + const newPolicy = { + bindings: newBindings, + }; + + // Set the new IAM policy + await table.iam.setPolicy(newPolicy); + console.log(`Access revoked successfully for ${resourceId}`); + + // Verify the changes + const [updatedPolicy] = await table.iam.getPolicy(); + console.log( + "Updated IAM Policy:", + JSON.stringify(updatedPolicy.bindings, null, 2) + ); + } catch (error) { + console.error("Error revoking access:", error); + throw error; + } +} + +module.exports = { revokeTableOrViewAccess }; \ No newline at end of file diff --git a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js new file mode 100644 index 0000000000..0fcef4afde --- /dev/null +++ b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js @@ -0,0 +1,139 @@ +const { expect } = require("chai"); +const sinon = require("sinon"); +const { BigQuery } = require("@google-cloud/bigquery"); +const { revokeTableOrViewAccess } = require("../src/revokeTableOrViewAccess"); + +describe("revokeTableOrViewAccess", () => { + let bigQueryStub; + let tableStub; + let constructorStub; + + beforeEach(() => { + // Create stubs for BigQuery client and its methods + tableStub = { + iam: { + getPolicy: sinon.stub(), + setPolicy: sinon.stub(), + }, + }; + + const datasetStub = { + table: sinon.stub().returns(tableStub), + }; + + bigQueryStub = { + dataset: sinon.stub().returns(datasetStub), + }; + + // Stub the BigQuery constructor correctly + constructorStub = sinon.stub(BigQuery.prototype, "constructor"); + constructorStub.returns(bigQueryStub); + + // Stub the BigQuery class itself + sinon.stub(BigQuery.prototype, "dataset").returns(datasetStub); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should revoke access for a specific member and role", async () => { + // Setup test data + const testPolicy = { + bindings: [ + { + role: "roles/bigquery.dataViewer", + members: ["group:test@google.com", "group:other@google.com"], + }, + ], + }; + + const expectedNewPolicy = { + bindings: [ + { + role: "roles/bigquery.dataViewer", + members: ["group:other@google.com"], + }, + ], + }; + + // Configure stubs + tableStub.iam.getPolicy.resolves([testPolicy]); + tableStub.iam.setPolicy.resolves([]); + + // Test the function + await revokeTableOrViewAccess({ + projectId: "test-project", + datasetId: "test-dataset", + resourceId: "test-table", + memberToRevoke: "group:test@google.com", + roleToRevoke: "roles/bigquery.dataViewer", + }); + + // Verify the new policy was set correctly + sinon.assert.calledWith( + tableStub.iam.setPolicy, + sinon.match(expectedNewPolicy) + ); + }); + + it("should revoke all access for a specific role", async () => { + // Setup test data + const testPolicy = { + bindings: [ + { + role: "roles/bigquery.dataViewer", + members: ["group:test@google.com"], + }, + { + role: "roles/bigquery.dataEditor", + members: ["group:editor@google.com"], + }, + ], + }; + + const expectedNewPolicy = { + bindings: [ + { + role: "roles/bigquery.dataEditor", + members: ["group:editor@google.com"], + }, + ], + }; + + // Configure stubs + tableStub.iam.getPolicy.resolves([testPolicy]); + tableStub.iam.setPolicy.resolves([]); + + // Test the function + await revokeTableOrViewAccess({ + projectId: "test-project", + datasetId: "test-dataset", + resourceId: "test-table", + roleToRevoke: "roles/bigquery.dataViewer", + }); + + // Verify the new policy was set correctly + sinon.assert.calledWith( + tableStub.iam.setPolicy, + sinon.match(expectedNewPolicy) + ); + }); + + it("should handle errors appropriately", async () => { + // Configure stub to throw an error + tableStub.iam.getPolicy.rejects(new Error("Test error")); + + // Test the function + try { + await revokeTableOrViewAccess({ + projectId: "test-project", + datasetId: "test-dataset", + resourceId: "test-table", + }); + expect.fail("Should have thrown an error"); + } catch (error) { + expect(error.message).to.equal("Test error"); + } + }); +}); \ No newline at end of file From 62ebc2c3f0d3e26ed070f831c55713a42b52bdff Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Mon, 17 Feb 2025 17:47:20 +0000 Subject: [PATCH 08/29] feat(bigquery): Update app.js file to add revokeTableOrViewAccess & fix linting errors --- bigquery/cloud-client/app.js | 12 +- .../src/revokeTableOrViewAccess.js | 114 ++++---- .../test/revokeTableOrViewAccess.test.js | 270 +++++++++--------- 3 files changed, 203 insertions(+), 193 deletions(-) diff --git a/bigquery/cloud-client/app.js b/bigquery/cloud-client/app.js index 36bd1f7e33..b5a273d14c 100644 --- a/bigquery/cloud-client/app.js +++ b/bigquery/cloud-client/app.js @@ -18,19 +18,29 @@ const {viewDatasetAccessPolicy} = require('./src/viewDatasetAccessPolicy'); const { viewTableOrViewAccessPolicy, } = require('./src/viewTableOrViewAccessPolicy'); +const {revokeTableOrViewAccess} = require('./src/revokeTableOrViewAccess'); async function main() { try { + const projectId = process.env.GOOGLE_CLOUD_PROJECT; + // Example usage of dataset access policy viewer await viewDatasetAccessPolicy(); // Example usage of table/view access policy viewer - const projectId = process.env.GOOGLE_CLOUD_PROJECT; await viewTableOrViewAccessPolicy({ projectId, datasetId: 'my_new_dataset', resourceName: 'my_table', }); + + await revokeTableOrViewAccess({ + projectId, + datasetId: 'my_new_dataset', + resourceName: 'my_table', + memberToRevoke: 'group:example-analyst-group@google.com', + roleToRevoke: 'roles/bigquery.dataViewer', + }); } catch (error) { console.error('Error:', error); process.exitCode = 1; diff --git a/bigquery/cloud-client/src/revokeTableOrViewAccess.js b/bigquery/cloud-client/src/revokeTableOrViewAccess.js index caf69807ee..28da6a1f30 100644 --- a/bigquery/cloud-client/src/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/src/revokeTableOrViewAccess.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -const { BigQuery } = require("@google-cloud/bigquery"); +const {BigQuery} = require('@google-cloud/bigquery'); /** * Revokes access to a BigQuery table or view @@ -25,69 +25,69 @@ const { BigQuery } = require("@google-cloud/bigquery"); * @returns {Promise} */ async function revokeTableOrViewAccess({ - projectId, - datasetId, - resourceId, - memberToRevoke, - roleToRevoke = "roles/bigquery.dataViewer", + projectId, + datasetId, + resourceId, + memberToRevoke, + roleToRevoke = 'roles/bigquery.dataViewer', }) { - try { - // Create BigQuery client - const bigquery = new BigQuery({ - projectId: projectId, - }); + try { + // Create BigQuery client + const bigquery = new BigQuery({ + projectId: projectId, + }); - // Get reference to the table or view - const dataset = bigquery.dataset(datasetId); - const table = dataset.table(resourceId); + // Get reference to the table or view + const dataset = bigquery.dataset(datasetId); + const table = dataset.table(resourceId); - // Get current IAM policy - const [policy] = await table.iam.getPolicy(); - console.log( - "Current IAM Policy:", - JSON.stringify(policy.bindings, null, 2) - ); + // Get current IAM policy + const [policy] = await table.iam.getPolicy(); + console.log( + 'Current IAM Policy:', + JSON.stringify(policy.bindings, null, 2) + ); - // Filter bindings based on parameters - let newBindings = policy.bindings; + // Filter bindings based on parameters + let newBindings = policy.bindings; - if (memberToRevoke) { - // Remove specific member from specific role - newBindings = policy.bindings - .map((binding) => ({ - ...binding, - members: - binding.role === roleToRevoke - ? binding.members.filter((member) => member !== memberToRevoke) - : binding.members, - })) - .filter((binding) => binding.members.length > 0); - } else { - // Remove all bindings for the specified role - newBindings = policy.bindings.filter( - (binding) => binding.role !== roleToRevoke - ); - } + if (memberToRevoke) { + // Remove specific member from specific role + newBindings = policy.bindings + .map(binding => ({ + ...binding, + members: + binding.role === roleToRevoke + ? binding.members.filter(member => member !== memberToRevoke) + : binding.members, + })) + .filter(binding => binding.members.length > 0); + } else { + // Remove all bindings for the specified role + newBindings = policy.bindings.filter( + binding => binding.role !== roleToRevoke + ); + } - // Create new policy with updated bindings - const newPolicy = { - bindings: newBindings, - }; + // Create new policy with updated bindings + const newPolicy = { + bindings: newBindings, + }; - // Set the new IAM policy - await table.iam.setPolicy(newPolicy); - console.log(`Access revoked successfully for ${resourceId}`); + // Set the new IAM policy + await table.iam.setPolicy(newPolicy); + console.log(`Access revoked successfully for ${resourceId}`); - // Verify the changes - const [updatedPolicy] = await table.iam.getPolicy(); - console.log( - "Updated IAM Policy:", - JSON.stringify(updatedPolicy.bindings, null, 2) - ); - } catch (error) { - console.error("Error revoking access:", error); - throw error; - } + // Verify the changes + const [updatedPolicy] = await table.iam.getPolicy(); + console.log( + 'Updated IAM Policy:', + JSON.stringify(updatedPolicy.bindings, null, 2) + ); + } catch (error) { + console.error('Error revoking access:', error); + throw error; + } } -module.exports = { revokeTableOrViewAccess }; \ No newline at end of file +module.exports = {revokeTableOrViewAccess}; diff --git a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js index 0fcef4afde..256a050858 100644 --- a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js +++ b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js @@ -1,139 +1,139 @@ -const { expect } = require("chai"); -const sinon = require("sinon"); -const { BigQuery } = require("@google-cloud/bigquery"); -const { revokeTableOrViewAccess } = require("../src/revokeTableOrViewAccess"); - -describe("revokeTableOrViewAccess", () => { - let bigQueryStub; - let tableStub; - let constructorStub; - - beforeEach(() => { - // Create stubs for BigQuery client and its methods - tableStub = { - iam: { - getPolicy: sinon.stub(), - setPolicy: sinon.stub(), - }, - }; - - const datasetStub = { - table: sinon.stub().returns(tableStub), - }; - - bigQueryStub = { - dataset: sinon.stub().returns(datasetStub), - }; - - // Stub the BigQuery constructor correctly - constructorStub = sinon.stub(BigQuery.prototype, "constructor"); - constructorStub.returns(bigQueryStub); - - // Stub the BigQuery class itself - sinon.stub(BigQuery.prototype, "dataset").returns(datasetStub); +const {expect} = require('chai'); +const sinon = require('sinon'); +const {BigQuery} = require('@google-cloud/bigquery'); +const {revokeTableOrViewAccess} = require('../src/revokeTableOrViewAccess'); + +describe('revokeTableOrViewAccess', () => { + let bigQueryStub; + let tableStub; + let constructorStub; + + beforeEach(() => { + // Create stubs for BigQuery client and its methods + tableStub = { + iam: { + getPolicy: sinon.stub(), + setPolicy: sinon.stub(), + }, + }; + + const datasetStub = { + table: sinon.stub().returns(tableStub), + }; + + bigQueryStub = { + dataset: sinon.stub().returns(datasetStub), + }; + + // Stub the BigQuery constructor correctly + constructorStub = sinon.stub(BigQuery.prototype, 'constructor'); + constructorStub.returns(bigQueryStub); + + // Stub the BigQuery class itself + sinon.stub(BigQuery.prototype, 'dataset').returns(datasetStub); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should revoke access for a specific member and role', async () => { + // Setup test data + const testPolicy = { + bindings: [ + { + role: 'roles/bigquery.dataViewer', + members: ['group:test@google.com', 'group:other@google.com'], + }, + ], + }; + + const expectedNewPolicy = { + bindings: [ + { + role: 'roles/bigquery.dataViewer', + members: ['group:other@google.com'], + }, + ], + }; + + // Configure stubs + tableStub.iam.getPolicy.resolves([testPolicy]); + tableStub.iam.setPolicy.resolves([]); + + // Test the function + await revokeTableOrViewAccess({ + projectId: 'test-project', + datasetId: 'test-dataset', + resourceId: 'test-table', + memberToRevoke: 'group:test@google.com', + roleToRevoke: 'roles/bigquery.dataViewer', }); - afterEach(() => { - sinon.restore(); + // Verify the new policy was set correctly + sinon.assert.calledWith( + tableStub.iam.setPolicy, + sinon.match(expectedNewPolicy) + ); + }); + + it('should revoke all access for a specific role', async () => { + // Setup test data + const testPolicy = { + bindings: [ + { + role: 'roles/bigquery.dataViewer', + members: ['group:test@google.com'], + }, + { + role: 'roles/bigquery.dataEditor', + members: ['group:editor@google.com'], + }, + ], + }; + + const expectedNewPolicy = { + bindings: [ + { + role: 'roles/bigquery.dataEditor', + members: ['group:editor@google.com'], + }, + ], + }; + + // Configure stubs + tableStub.iam.getPolicy.resolves([testPolicy]); + tableStub.iam.setPolicy.resolves([]); + + // Test the function + await revokeTableOrViewAccess({ + projectId: 'test-project', + datasetId: 'test-dataset', + resourceId: 'test-table', + roleToRevoke: 'roles/bigquery.dataViewer', }); - it("should revoke access for a specific member and role", async () => { - // Setup test data - const testPolicy = { - bindings: [ - { - role: "roles/bigquery.dataViewer", - members: ["group:test@google.com", "group:other@google.com"], - }, - ], - }; - - const expectedNewPolicy = { - bindings: [ - { - role: "roles/bigquery.dataViewer", - members: ["group:other@google.com"], - }, - ], - }; - - // Configure stubs - tableStub.iam.getPolicy.resolves([testPolicy]); - tableStub.iam.setPolicy.resolves([]); - - // Test the function - await revokeTableOrViewAccess({ - projectId: "test-project", - datasetId: "test-dataset", - resourceId: "test-table", - memberToRevoke: "group:test@google.com", - roleToRevoke: "roles/bigquery.dataViewer", - }); - - // Verify the new policy was set correctly - sinon.assert.calledWith( - tableStub.iam.setPolicy, - sinon.match(expectedNewPolicy) - ); - }); - - it("should revoke all access for a specific role", async () => { - // Setup test data - const testPolicy = { - bindings: [ - { - role: "roles/bigquery.dataViewer", - members: ["group:test@google.com"], - }, - { - role: "roles/bigquery.dataEditor", - members: ["group:editor@google.com"], - }, - ], - }; - - const expectedNewPolicy = { - bindings: [ - { - role: "roles/bigquery.dataEditor", - members: ["group:editor@google.com"], - }, - ], - }; - - // Configure stubs - tableStub.iam.getPolicy.resolves([testPolicy]); - tableStub.iam.setPolicy.resolves([]); - - // Test the function - await revokeTableOrViewAccess({ - projectId: "test-project", - datasetId: "test-dataset", - resourceId: "test-table", - roleToRevoke: "roles/bigquery.dataViewer", - }); - - // Verify the new policy was set correctly - sinon.assert.calledWith( - tableStub.iam.setPolicy, - sinon.match(expectedNewPolicy) - ); - }); - - it("should handle errors appropriately", async () => { - // Configure stub to throw an error - tableStub.iam.getPolicy.rejects(new Error("Test error")); - - // Test the function - try { - await revokeTableOrViewAccess({ - projectId: "test-project", - datasetId: "test-dataset", - resourceId: "test-table", - }); - expect.fail("Should have thrown an error"); - } catch (error) { - expect(error.message).to.equal("Test error"); - } - }); -}); \ No newline at end of file + // Verify the new policy was set correctly + sinon.assert.calledWith( + tableStub.iam.setPolicy, + sinon.match(expectedNewPolicy) + ); + }); + + it('should handle errors appropriately', async () => { + // Configure stub to throw an error + tableStub.iam.getPolicy.rejects(new Error('Test error')); + + // Test the function + try { + await revokeTableOrViewAccess({ + projectId: 'test-project', + datasetId: 'test-dataset', + resourceId: 'test-table', + }); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error.message).to.equal('Test error'); + } + }); +}); From 9d0ce8820fce869f1458f2ef0d69b5c3d3741d0f Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Mon, 17 Feb 2025 17:50:39 +0000 Subject: [PATCH 09/29] fix(bigquery):Fix headers for revokeTableOrViewAccess.js & revokeTableOrViewAccess.test.js --- .../cloud-client/src/revokeTableOrViewAccess.js | 2 +- .../test/revokeTableOrViewAccess.test.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/bigquery/cloud-client/src/revokeTableOrViewAccess.js b/bigquery/cloud-client/src/revokeTableOrViewAccess.js index 28da6a1f30..eb39a19885 100644 --- a/bigquery/cloud-client/src/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/src/revokeTableOrViewAccess.js @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.apache.org/licenses/LICENSE-2.0 +// 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, diff --git a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js index 256a050858..c6431568c9 100644 --- a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js +++ b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js @@ -1,3 +1,19 @@ +// Copyright 2025 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 {expect} = require('chai'); const sinon = require('sinon'); const {BigQuery} = require('@google-cloud/bigquery'); From 627536a62be8bf514e8ea05b903abf0eb72da095 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Mon, 17 Feb 2025 17:55:06 +0000 Subject: [PATCH 10/29] feat(bigquery): Add bigquery_revoke_access_to_table_or_view tag --- bigquery/cloud-client/src/revokeTableOrViewAccess.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bigquery/cloud-client/src/revokeTableOrViewAccess.js b/bigquery/cloud-client/src/revokeTableOrViewAccess.js index eb39a19885..27211e681f 100644 --- a/bigquery/cloud-client/src/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/src/revokeTableOrViewAccess.js @@ -14,6 +14,7 @@ const {BigQuery} = require('@google-cloud/bigquery'); +// [START bigquery_revoke_access_to_table_or_view] /** * Revokes access to a BigQuery table or view * @param {Object} params - The parameters object @@ -90,4 +91,6 @@ async function revokeTableOrViewAccess({ } } +// [END bigquery_revoke_access_to_table_or_view] + module.exports = {revokeTableOrViewAccess}; From 70cfb83da995b43154c37b776996dc1c404c4f62 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Tue, 18 Feb 2025 17:46:37 +0000 Subject: [PATCH 11/29] fix(bigquery): Update if/else to if/else if --- .../cloud-client/src/revokeTableOrViewAccess.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/bigquery/cloud-client/src/revokeTableOrViewAccess.js b/bigquery/cloud-client/src/revokeTableOrViewAccess.js index 27211e681f..7b427ec13e 100644 --- a/bigquery/cloud-client/src/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/src/revokeTableOrViewAccess.js @@ -32,6 +32,12 @@ async function revokeTableOrViewAccess({ memberToRevoke, roleToRevoke = 'roles/bigquery.dataViewer', }) { + // Validate required parameters + if (!projectId || !datasetId || !resourceId) { + throw new Error( + 'projectId, datasetId and resourceID are required parameters' + ); + } try { // Create BigQuery client const bigquery = new BigQuery({ @@ -52,7 +58,7 @@ async function revokeTableOrViewAccess({ // Filter bindings based on parameters let newBindings = policy.bindings; - if (memberToRevoke) { + if (memberToRevoke && roleToRevoke) { // Remove specific member from specific role newBindings = policy.bindings .map(binding => ({ @@ -63,11 +69,14 @@ async function revokeTableOrViewAccess({ : binding.members, })) .filter(binding => binding.members.length > 0); - } else { + } else if (!memberToRevoke && roleToRevoke) { // Remove all bindings for the specified role newBindings = policy.bindings.filter( binding => binding.role !== roleToRevoke ); + } else { + // Keep the current binding as is + newBindings = policy.bindings; } // Create new policy with updated bindings From ff35988e7790faf1cc36559f369d5d67c68eb251 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 20 Feb 2025 14:03:17 +0000 Subject: [PATCH 12/29] feat(bigquery): Add grantAccessToDataset sample and tests --- .../cloud-client/src/grantAccessToDataset.js | 75 +++++++++ .../test/grantAccessToDataset.test.js | 144 ++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 bigquery/cloud-client/src/grantAccessToDataset.js create mode 100644 bigquery/cloud-client/test/grantAccessToDataset.test.js diff --git a/bigquery/cloud-client/src/grantAccessToDataset.js b/bigquery/cloud-client/src/grantAccessToDataset.js new file mode 100644 index 0000000000..3ff9d533fd --- /dev/null +++ b/bigquery/cloud-client/src/grantAccessToDataset.js @@ -0,0 +1,75 @@ +// Copyright 2025 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 {BigQuery} = require('@google-cloud/bigquery'); + +/** + * Grants access to a BigQuery dataset for a specified entity + * + * @param {object} options The configuration object + * @param {string} options.datasetId ID of the dataset to grant access to (e.g. "my_project_id.my_dataset") + * @param {string} options.entityId ID of the user or group to grant access to (e.g. "user-or-group-to-add@example.com") + * @param {string} options.role One of the basic roles for datasets (e.g. "READER") + * @returns {Promise} Array of access entries + */ +// [START bigquery_grant_access_to_dataset] +async function grantAccessToDataset(options) { + // Create a BigQuery client + const bigquery = new BigQuery(); + + const {datasetId, entityId, role} = options; + + try { + // Get a reference to the dataset + const dataset = bigquery.dataset(datasetId); + const [metadata] = await dataset.getMetadata(); + + // The access entries list is immutable. Create a copy for modifications + const entries = [...(metadata.access || [])]; + + // Add the new access entry + entries.push({ + role: role, + groupByEmail: entityId, // For group access. Use userByEmail for user access + }); + + // Update the dataset's access entries + const [updatedMetadata] = await dataset.setMetadata({ + ...metadata, + access: entries, + }); + + console.log( + `Role '${role}' granted for entity '${entityId}' in dataset '${datasetId}'.` + ); + + return updatedMetadata.access; + } catch (error) { + if (error.code === 412) { + // 412 Precondition Failed - Dataset was modified between get and update + console.error( + `Dataset '${datasetId}' was modified remotely before this update. ` + + 'Fetch the latest version and retry.' + ); + } + throw error; + } +} +// [END bigquery_grant_access_to_dataset] + +module.exports = { + grantAccessToDataset, +}; diff --git a/bigquery/cloud-client/test/grantAccessToDataset.test.js b/bigquery/cloud-client/test/grantAccessToDataset.test.js new file mode 100644 index 0000000000..96250a6301 --- /dev/null +++ b/bigquery/cloud-client/test/grantAccessToDataset.test.js @@ -0,0 +1,144 @@ +// Copyright 2025 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 {assert} = require('chai'); +const sinon = require('sinon'); +const {BigQuery} = require('@google-cloud/bigquery'); +const {grantAccessToDataset} = require('../src/grantAccessToDataset'); + +describe('grantAccessToDataset', () => { + const datasetId = 'test_dataset'; + const entityId = 'test-group@example.com'; + const role = 'READER'; + + let bigqueryStub; + let datasetStub; + let getMetadataStub; + let setMetadataStub; + + beforeEach(() => { + // Initial empty metadata + const initialMetadata = { + access: [], + }; + + // Updated metadata with new access + const updatedMetadata = { + access: [ + { + role: role, + groupByEmail: entityId, + }, + ], + }; + + // Create stubs for dataset methods + getMetadataStub = sinon.stub().resolves([initialMetadata]); + setMetadataStub = sinon.stub().resolves([updatedMetadata]); + + // Create dataset stub + datasetStub = { + getMetadata: getMetadataStub, + setMetadata: setMetadataStub, + }; + + // Create BigQuery stub + bigqueryStub = { + dataset: sinon.stub().returns(datasetStub), + }; + + // Replace BigQuery constructor + sinon.stub(BigQuery.prototype, 'dataset').callsFake(() => datasetStub); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should grant access to a dataset', async () => { + const result = await grantAccessToDataset({ + datasetId, + entityId, + role, + }); + + // Verify getMetadata was called + assert.strictEqual( + getMetadataStub.callCount, + 1, + 'getMetadata should be called once' + ); + + // Verify setMetadata was called with correct parameters + const setMetadataCall = setMetadataStub.getCall(0); + assert.deepStrictEqual( + setMetadataCall.args[0].access, + [ + { + role: role, + groupByEmail: entityId, + }, + ], + 'setMetadata should be called with correct access entries' + ); + + // Verify the result contains the expected access entry + assert.isArray(result, 'Result should be an array'); + assert.deepStrictEqual( + result[0], + { + role: role, + groupByEmail: entityId, + }, + 'Result should contain the new access entry' + ); + }); + + it('should handle concurrent modification errors', async () => { + // Simulate a 412 Precondition Failed error + const error = new Error('Precondition Failed'); + error.code = 412; + setMetadataStub.rejects(error); + + try { + await grantAccessToDataset({ + datasetId, + entityId, + role, + }); + assert.fail('Should have thrown an error'); + } catch (err) { + assert.strictEqual(err.code, 412, 'Should throw 412 error'); + } + }); + + it('should propagate other errors', async () => { + const errorMessage = 'Unknown error'; + const error = new Error(errorMessage); + setMetadataStub.rejects(error); + + try { + await grantAccessToDataset({ + datasetId, + entityId, + role, + }); + assert.fail('Should have thrown an error'); + } catch (err) { + assert.strictEqual(err.message, errorMessage); + } + }); +}); From 7b38e5ad742d931928136f7c652d09d8582da9a2 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 20 Feb 2025 14:17:55 +0000 Subject: [PATCH 13/29] fix(bigquery): Update lint error --- bigquery/cloud-client/test/grantAccessToDataset.test.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bigquery/cloud-client/test/grantAccessToDataset.test.js b/bigquery/cloud-client/test/grantAccessToDataset.test.js index 96250a6301..485d9bd53c 100644 --- a/bigquery/cloud-client/test/grantAccessToDataset.test.js +++ b/bigquery/cloud-client/test/grantAccessToDataset.test.js @@ -24,7 +24,6 @@ describe('grantAccessToDataset', () => { const entityId = 'test-group@example.com'; const role = 'READER'; - let bigqueryStub; let datasetStub; let getMetadataStub; let setMetadataStub; @@ -55,11 +54,6 @@ describe('grantAccessToDataset', () => { setMetadata: setMetadataStub, }; - // Create BigQuery stub - bigqueryStub = { - dataset: sinon.stub().returns(datasetStub), - }; - // Replace BigQuery constructor sinon.stub(BigQuery.prototype, 'dataset').callsFake(() => datasetStub); }); From 5cfa8dc7b037d7c031f8defb722fc2fb61a237f3 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 20 Feb 2025 17:37:59 +0000 Subject: [PATCH 14/29] feat(bigquery): Add grantAccessToTableOrView sample --- .../src/grantAccessToTableOrView.js | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 bigquery/cloud-client/src/grantAccessToTableOrView.js diff --git a/bigquery/cloud-client/src/grantAccessToTableOrView.js b/bigquery/cloud-client/src/grantAccessToTableOrView.js new file mode 100644 index 0000000000..4252a83f65 --- /dev/null +++ b/bigquery/cloud-client/src/grantAccessToTableOrView.js @@ -0,0 +1,81 @@ +// Copyright 2025 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 {BigQuery} = require('@google-cloud/bigquery'); + +/** + * Grants access to a BigQuery table or view for a specified principal. + * + * @param {string} projectId - Google Cloud Platform project ID + * @param {string} datasetId - Dataset where the table or view is + * @param {string} resourceName - Table or view name to get the access policy + * @param {string} principalId - The principal requesting access to the table or view + * @param {string} role - Role to assign to the member + * @returns {Promise} The updated policy bindings + */ +async function grantAccessToTableOrView({ + projectId, + datasetId, + resourceName, + principalId, + role, +}) { + // [START bigquery_grant_access_to_table_or_view] + // Uncomment and update these variables: + // const projectId = 'my_project_id'; + // const datasetId = 'my_dataset'; + // const resourceName = 'my_table'; + // const principalId = 'user:bob@example.com'; + // const role = 'roles/bigquery.dataViewer'; + + // Create a BigQuery client + const bigquery = new BigQuery(); + + // Get the dataset and table references + const dataset = bigquery.dataset(datasetId); + const table = dataset.table(resourceName); + + try { + // Get the IAM access policy for the table or view + const [policy] = await table.iam.getPolicy(); + + // Create a new binding for the principal and role + const binding = { + role: role, + members: [principalId], + }; + + // Add the new binding to the policy + policy.bindings.push(binding); + + // Set the updated IAM access policy + const [updatedPolicy] = await table.iam.setPolicy(policy); + + console.log( + `Role '${role}' granted for principal '${principalId}' on resource '${projectId}.${datasetId}.${resourceName}'.` + ); + + return updatedPolicy.bindings; + } catch (error) { + console.error('Error granting access:', error); + throw error; + } + // [END bigquery_grant_access_to_table_or_view] +} + +module.exports = { + grantAccessToTableOrView, +}; From ccf30d6b3d1e7a4dd03c0a8c5c99360446f56f45 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 20 Feb 2025 17:43:38 +0000 Subject: [PATCH 15/29] feat(bigquery): Add grantAccessToTableOrView test --- .../test/grantAccessToTableOrView.test.js | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 bigquery/cloud-client/test/grantAccessToTableOrView.test.js diff --git a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js new file mode 100644 index 0000000000..4e7985362b --- /dev/null +++ b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js @@ -0,0 +1,129 @@ +// Copyright 2025 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 +// +// https://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 {assert} = require('chai'); +const sinon = require('sinon'); +const {BigQuery} = require('@google-cloud/bigquery'); +const {grantAccessToTableOrView} = require('../src/grantAccessToTableOrView'); + +describe('grantAccessToTableOrView', () => { + const PROJECT_ID = 'test-project'; + const DATASET_ID = 'test_dataset'; + const TABLE_ID = 'test_table'; + const ENTITY_ID = 'test-group@example.com'; + const ROLE = 'roles/bigquery.dataViewer'; + const PRINCIPAL_ID = `group:${ENTITY_ID}`; + + let sandbox; + let bigQueryStub; + let tableMock; + let datasetMock; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + // Create empty initial policy + const emptyPolicy = { + bindings: [], + }; + + // Create policy with the new binding + const updatedPolicy = { + bindings: [ + { + role: ROLE, + members: [PRINCIPAL_ID], + }, + ], + }; + + // Create the complete mock structure + tableMock = { + iam: { + getPolicy: sandbox.stub().resolves([emptyPolicy]), + setPolicy: sandbox.stub().resolves([updatedPolicy]), + }, + }; + + datasetMock = { + table: sandbox.stub().returns(tableMock), + }; + + bigQueryStub = sandbox.stub(BigQuery.prototype); + bigQueryStub.dataset = sandbox.stub().returns(datasetMock); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should grant access to table or view', async () => { + const updatedBindings = await grantAccessToTableOrView({ + projectId: PROJECT_ID, + datasetId: DATASET_ID, + resourceName: TABLE_ID, + principalId: PRINCIPAL_ID, + role: ROLE, + }); + + // Verify the dataset method was called + assert.ok( + bigQueryStub.dataset.calledWith(DATASET_ID), + 'dataset method should be called with correct dataset ID' + ); + + // Verify the table method was called with correct table ID + assert.ok( + datasetMock.table.calledWith(TABLE_ID), + 'table method should be called with correct table ID' + ); + + // Verify getPolicy was called + assert.ok(tableMock.iam.getPolicy.called, 'getPolicy should be called'); + + // Verify setPolicy was called + assert.ok(tableMock.iam.setPolicy.called, 'setPolicy should be called'); + + // Verify the updated bindings contain the new role + const roleBinding = updatedBindings.find(binding => binding.role === ROLE); + assert.exists(roleBinding, `Binding with role ${ROLE} should exist`); + + // Verify the principal was added to the members + assert.include( + roleBinding.members, + PRINCIPAL_ID, + `Principal ${PRINCIPAL_ID} should be in members list` + ); + }); + + it('should throw error when BigQuery API fails', async () => { + // Make getPolicy throw an error + tableMock.iam.getPolicy.rejects(new Error('API Error')); + + try { + await grantAccessToTableOrView({ + projectId: PROJECT_ID, + datasetId: DATASET_ID, + resourceName: TABLE_ID, + principalId: PRINCIPAL_ID, + role: ROLE, + }); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.equal(error.message, 'API Error'); + } + }); +}); From eb32aa81d1d3f62e2630407db91b2c24c0375fbb Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 20 Feb 2025 17:56:42 +0000 Subject: [PATCH 16/29] feat(bigquery): Update app.js file with new samples --- bigquery/cloud-client/app.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/bigquery/cloud-client/app.js b/bigquery/cloud-client/app.js index b5a273d14c..e2387824a7 100644 --- a/bigquery/cloud-client/app.js +++ b/bigquery/cloud-client/app.js @@ -19,6 +19,8 @@ const { viewTableOrViewAccessPolicy, } = require('./src/viewTableOrViewAccessPolicy'); const {revokeTableOrViewAccess} = require('./src/revokeTableOrViewAccess'); +const {grantAccessToDataset} = require('./src/grantAccessToDataset'); +const {grantAccessToTableOrView} = require('./src/grantAccessToTableOrView'); async function main() { try { @@ -34,6 +36,7 @@ async function main() { resourceName: 'my_table', }); + // Example usage of revoking table/view access await revokeTableOrViewAccess({ projectId, datasetId: 'my_new_dataset', @@ -41,6 +44,22 @@ async function main() { memberToRevoke: 'group:example-analyst-group@google.com', roleToRevoke: 'roles/bigquery.dataViewer', }); + + // Example usage of granting access to a dataset + await grantAccessToDataset({ + datasetId: 'my_dataset', + entityId: 'group-to-add@example.com', + role: 'READER', + }); + + // Example usage of granting table/view access + await grantAccessToTableOrView({ + projectId, + datasetId: 'my_dataset', + resourceName: 'my_table', + principalId: 'user:example@google.com', + role: 'roles/bigquery.dataViewer', + }); } catch (error) { console.error('Error:', error); process.exitCode = 1; @@ -52,4 +71,10 @@ if (require.main === module) { main(); } -module.export = {viewDatasetAccessPolicy, viewTableOrViewAccessPolicy}; +module.export = { + viewDatasetAccessPolicy, + viewTableOrViewAccessPolicy, + revokeTableOrViewAccess, + grantAccessToDataset, + grantAccessToTableOrView, +}; From 0a5f5ddd7cd33b83438a88af2aa9276727ff7de2 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 20 Feb 2025 18:28:33 +0000 Subject: [PATCH 17/29] feat(bigquery): Add revokeDatasetAccess sample --- .../cloud-client/src/revokeDatasetAccess.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 bigquery/cloud-client/src/revokeDatasetAccess.js diff --git a/bigquery/cloud-client/src/revokeDatasetAccess.js b/bigquery/cloud-client/src/revokeDatasetAccess.js new file mode 100644 index 0000000000..d424cc7743 --- /dev/null +++ b/bigquery/cloud-client/src/revokeDatasetAccess.js @@ -0,0 +1,64 @@ +// Copyright 2024 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 +// +// https://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 {BigQuery} = require('@google-cloud/bigquery'); + +// [START bigquery_revoke_dataset_access] +/** + * Revokes access to a BigQuery dataset for a specified entity. + * + * @param {Object} params The parameters for revoking dataset access + * @param {string} params.datasetId The ID of the dataset to revoke access from + * @param {string} params.entityId The ID of the user or group to revoke access from + * @returns {Promise} A promise that resolves to the updated access entries + */ +async function revokeDatasetAccess({datasetId, entityId}) { + // Instantiate a client + const bigquery = new BigQuery(); + + try { + // Get a reference to the dataset + const [dataset] = await bigquery.dataset(datasetId).get(); + + // Filter out the access entry for the specified entity + dataset.metadata.access = dataset.metadata.access.filter( + entry => entry.userByEmail !== entityId && entry.groupByEmail !== entityId + ); + + // Update the dataset with the new access entries + const [updatedDataset] = await dataset.setMetadata(dataset.metadata); + + console.log( + `Revoked dataset access for '${entityId}' to dataset '${dataset.id}'.` + ); + + return updatedDataset.metadata.access; + } catch (error) { + if (error.code === 412) { + // Handle precondition failed error (dataset modified externally) + console.error( + `Dataset '${datasetId}' was modified remotely before this update. ` + + 'Fetch the latest version and retry.' + ); + } + throw error; + } +} +// [END bigquery_revoke_dataset_access] + +module.exports = { + revokeDatasetAccess, +}; From 6c409401798e4f12c0c0e6cef2c089a4fbf20bab Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 20 Feb 2025 18:36:28 +0000 Subject: [PATCH 18/29] feat(bigquery): Add revokeDatasetAccess tests --- .../test/revokeDatasetAccess.test.js | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 bigquery/cloud-client/test/revokeDatasetAccess.test.js diff --git a/bigquery/cloud-client/test/revokeDatasetAccess.test.js b/bigquery/cloud-client/test/revokeDatasetAccess.test.js new file mode 100644 index 0000000000..ac48661481 --- /dev/null +++ b/bigquery/cloud-client/test/revokeDatasetAccess.test.js @@ -0,0 +1,134 @@ +// Copyright 2024 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 +// +// https://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 {assert} = require('chai'); +const sinon = require('sinon'); +const {BigQuery} = require('@google-cloud/bigquery'); +const {revokeDatasetAccess} = require('../src/revokeDatasetAccess'); + +describe('revokeDatasetAccess', () => { + const datasetId = 'test_dataset'; + const entityId = 'user@example.com'; + let sandbox; + let mockBigQuery; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + // Mock the BigQuery constructor + mockBigQuery = sandbox.stub(BigQuery.prototype); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should successfully revoke access from a dataset', async () => { + const initialAccess = [ + { + role: 'READER', + userByEmail: entityId, + }, + ]; + + const dataset = { + id: datasetId, + metadata: { + access: initialAccess, + }, + setMetadata: sandbox.stub().resolves([ + { + metadata: { + access: [], + }, + }, + ]), + }; + + // Mock the dataset method to return our mock dataset + mockBigQuery.dataset = sandbox.stub().returns({ + get: sandbox.stub().resolves([dataset]), + }); + + // Execute revoke access + const updatedAccess = await revokeDatasetAccess({ + datasetId, + entityId, + }); + + // Verify the access was revoked + assert.isArray(updatedAccess); + assert.isEmpty(updatedAccess); + }); + + it('should handle precondition failed error', async () => { + const preconditionError = new Error('Precondition Failed'); + preconditionError.code = 412; + + const dataset = { + id: datasetId, + metadata: { + access: [], + }, + setMetadata: sandbox.stub().rejects(preconditionError), + }; + + // Mock the dataset method to return our mock dataset + mockBigQuery.dataset = sandbox.stub().returns({ + get: sandbox.stub().resolves([dataset]), + }); + + try { + await revokeDatasetAccess({ + datasetId, + entityId, + }); + assert.fail('Should have thrown an error'); + } catch (error) { + assert.equal(error.code, 412); + assert.equal(error.message, 'Precondition Failed'); + } + }); + + it('should handle missing entity gracefully', async () => { + const dataset = { + id: datasetId, + metadata: { + access: [], + }, + setMetadata: sandbox.stub().resolves([ + { + metadata: { + access: [], + }, + }, + ]), + }; + + // Mock the dataset method to return our mock dataset + mockBigQuery.dataset = sandbox.stub().returns({ + get: sandbox.stub().resolves([dataset]), + }); + + const updatedAccess = await revokeDatasetAccess({ + datasetId, + entityId, + }); + + assert.isArray(updatedAccess); + assert.isEmpty(updatedAccess); + }); +}); From b600729747ffe0c34d2ad8dc561a28c5c3877520 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 20 Feb 2025 18:48:48 +0000 Subject: [PATCH 19/29] feat(bigquery): Update app.js file with new sample --- bigquery/cloud-client/app.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bigquery/cloud-client/app.js b/bigquery/cloud-client/app.js index e2387824a7..ba7c7339b0 100644 --- a/bigquery/cloud-client/app.js +++ b/bigquery/cloud-client/app.js @@ -21,6 +21,7 @@ const { const {revokeTableOrViewAccess} = require('./src/revokeTableOrViewAccess'); const {grantAccessToDataset} = require('./src/grantAccessToDataset'); const {grantAccessToTableOrView} = require('./src/grantAccessToTableOrView'); +const {revokeDatasetAccess} = require('./src/revokeDatasetAccess'); async function main() { try { @@ -60,6 +61,12 @@ async function main() { principalId: 'user:example@google.com', role: 'roles/bigquery.dataViewer', }); + + // Example usage of revoking dataset access + await revokeDatasetAccess({ + datasetId: 'my_dataset', + entityId: 'user-to-remove@example.com', + }); } catch (error) { console.error('Error:', error); process.exitCode = 1; @@ -77,4 +84,5 @@ module.export = { revokeTableOrViewAccess, grantAccessToDataset, grantAccessToTableOrView, + revokeDatasetAccess, }; From db956a7873f0d361198beacb7b781b8ebff57799 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 20 Feb 2025 19:09:37 +0000 Subject: [PATCH 20/29] chore(bigquery): Update project structure --- bigquery/cloud-client/app.js | 88 ------------------- .../{src => }/grantAccessToDataset.js | 0 .../{src => }/grantAccessToTableOrView.js | 0 .../{src => }/revokeDatasetAccess.js | 0 .../{src => }/revokeTableOrViewAccess.js | 0 .../test/grantAccessToDataset.test.js | 2 +- .../test/grantAccessToTableOrView.test.js | 2 +- .../test/revokeDatasetAccess.test.js | 2 +- .../test/revokeTableOrViewAccess.test.js | 2 +- .../test/viewDatasetAccessPolicy.test.js | 2 +- .../test/viewTableOrViewAccessPolicy.test.js | 2 +- .../{src => }/viewDatasetAccessPolicy.js | 0 .../{src => }/viewTableOrViewAccessPolicy.js | 0 13 files changed, 6 insertions(+), 94 deletions(-) delete mode 100644 bigquery/cloud-client/app.js rename bigquery/cloud-client/{src => }/grantAccessToDataset.js (100%) rename bigquery/cloud-client/{src => }/grantAccessToTableOrView.js (100%) rename bigquery/cloud-client/{src => }/revokeDatasetAccess.js (100%) rename bigquery/cloud-client/{src => }/revokeTableOrViewAccess.js (100%) rename bigquery/cloud-client/{src => }/viewDatasetAccessPolicy.js (100%) rename bigquery/cloud-client/{src => }/viewTableOrViewAccessPolicy.js (100%) diff --git a/bigquery/cloud-client/app.js b/bigquery/cloud-client/app.js deleted file mode 100644 index ba7c7339b0..0000000000 --- a/bigquery/cloud-client/app.js +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2025 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 {viewDatasetAccessPolicy} = require('./src/viewDatasetAccessPolicy'); -const { - viewTableOrViewAccessPolicy, -} = require('./src/viewTableOrViewAccessPolicy'); -const {revokeTableOrViewAccess} = require('./src/revokeTableOrViewAccess'); -const {grantAccessToDataset} = require('./src/grantAccessToDataset'); -const {grantAccessToTableOrView} = require('./src/grantAccessToTableOrView'); -const {revokeDatasetAccess} = require('./src/revokeDatasetAccess'); - -async function main() { - try { - const projectId = process.env.GOOGLE_CLOUD_PROJECT; - - // Example usage of dataset access policy viewer - await viewDatasetAccessPolicy(); - - // Example usage of table/view access policy viewer - await viewTableOrViewAccessPolicy({ - projectId, - datasetId: 'my_new_dataset', - resourceName: 'my_table', - }); - - // Example usage of revoking table/view access - await revokeTableOrViewAccess({ - projectId, - datasetId: 'my_new_dataset', - resourceName: 'my_table', - memberToRevoke: 'group:example-analyst-group@google.com', - roleToRevoke: 'roles/bigquery.dataViewer', - }); - - // Example usage of granting access to a dataset - await grantAccessToDataset({ - datasetId: 'my_dataset', - entityId: 'group-to-add@example.com', - role: 'READER', - }); - - // Example usage of granting table/view access - await grantAccessToTableOrView({ - projectId, - datasetId: 'my_dataset', - resourceName: 'my_table', - principalId: 'user:example@google.com', - role: 'roles/bigquery.dataViewer', - }); - - // Example usage of revoking dataset access - await revokeDatasetAccess({ - datasetId: 'my_dataset', - entityId: 'user-to-remove@example.com', - }); - } catch (error) { - console.error('Error:', error); - process.exitCode = 1; - } -} - -// Run the samples if this file is run directly -if (require.main === module) { - main(); -} - -module.export = { - viewDatasetAccessPolicy, - viewTableOrViewAccessPolicy, - revokeTableOrViewAccess, - grantAccessToDataset, - grantAccessToTableOrView, - revokeDatasetAccess, -}; diff --git a/bigquery/cloud-client/src/grantAccessToDataset.js b/bigquery/cloud-client/grantAccessToDataset.js similarity index 100% rename from bigquery/cloud-client/src/grantAccessToDataset.js rename to bigquery/cloud-client/grantAccessToDataset.js diff --git a/bigquery/cloud-client/src/grantAccessToTableOrView.js b/bigquery/cloud-client/grantAccessToTableOrView.js similarity index 100% rename from bigquery/cloud-client/src/grantAccessToTableOrView.js rename to bigquery/cloud-client/grantAccessToTableOrView.js diff --git a/bigquery/cloud-client/src/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js similarity index 100% rename from bigquery/cloud-client/src/revokeDatasetAccess.js rename to bigquery/cloud-client/revokeDatasetAccess.js diff --git a/bigquery/cloud-client/src/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js similarity index 100% rename from bigquery/cloud-client/src/revokeTableOrViewAccess.js rename to bigquery/cloud-client/revokeTableOrViewAccess.js diff --git a/bigquery/cloud-client/test/grantAccessToDataset.test.js b/bigquery/cloud-client/test/grantAccessToDataset.test.js index 485d9bd53c..85b86b2e82 100644 --- a/bigquery/cloud-client/test/grantAccessToDataset.test.js +++ b/bigquery/cloud-client/test/grantAccessToDataset.test.js @@ -17,7 +17,7 @@ const {assert} = require('chai'); const sinon = require('sinon'); const {BigQuery} = require('@google-cloud/bigquery'); -const {grantAccessToDataset} = require('../src/grantAccessToDataset'); +const {grantAccessToDataset} = require('../grantAccessToDataset'); describe('grantAccessToDataset', () => { const datasetId = 'test_dataset'; diff --git a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js index 4e7985362b..323ce911e3 100644 --- a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js +++ b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js @@ -17,7 +17,7 @@ const {assert} = require('chai'); const sinon = require('sinon'); const {BigQuery} = require('@google-cloud/bigquery'); -const {grantAccessToTableOrView} = require('../src/grantAccessToTableOrView'); +const {grantAccessToTableOrView} = require('../grantAccessToTableOrView'); describe('grantAccessToTableOrView', () => { const PROJECT_ID = 'test-project'; diff --git a/bigquery/cloud-client/test/revokeDatasetAccess.test.js b/bigquery/cloud-client/test/revokeDatasetAccess.test.js index ac48661481..88bdf1c01f 100644 --- a/bigquery/cloud-client/test/revokeDatasetAccess.test.js +++ b/bigquery/cloud-client/test/revokeDatasetAccess.test.js @@ -17,7 +17,7 @@ const {assert} = require('chai'); const sinon = require('sinon'); const {BigQuery} = require('@google-cloud/bigquery'); -const {revokeDatasetAccess} = require('../src/revokeDatasetAccess'); +const {revokeDatasetAccess} = require('../revokeDatasetAccess'); describe('revokeDatasetAccess', () => { const datasetId = 'test_dataset'; diff --git a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js index c6431568c9..4112bf5697 100644 --- a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js +++ b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js @@ -17,7 +17,7 @@ const {expect} = require('chai'); const sinon = require('sinon'); const {BigQuery} = require('@google-cloud/bigquery'); -const {revokeTableOrViewAccess} = require('../src/revokeTableOrViewAccess'); +const {revokeTableOrViewAccess} = require('../revokeTableOrViewAccess'); describe('revokeTableOrViewAccess', () => { let bigQueryStub; diff --git a/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js b/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js index 2e4c9a18cd..7537c35239 100644 --- a/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js +++ b/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js @@ -17,7 +17,7 @@ const {assert} = require('chai'); const sinon = require('sinon'); const {BigQuery} = require('@google-cloud/bigquery'); -const {viewDatasetAccessPolicy} = require('../src/viewDatasetAccessPolicy'); +const {viewDatasetAccessPolicy} = require('../viewDatasetAccessPolicy'); describe('viewDatasetAccessPolicy', () => { let bigQueryStub; diff --git a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js index 7b79d04569..092a61c2e1 100644 --- a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js +++ b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js @@ -55,7 +55,7 @@ describe('viewTableOrViewAccessPolicy', () => { // Reset module for each test viewTableOrViewAccessPolicy = - require('../src/viewTableOrViewAccessPolicy').viewTableOrViewAccessPolicy; + require('../viewTableOrViewAccessPolicy').viewTableOrViewAccessPolicy; }); afterEach(() => { diff --git a/bigquery/cloud-client/src/viewDatasetAccessPolicy.js b/bigquery/cloud-client/viewDatasetAccessPolicy.js similarity index 100% rename from bigquery/cloud-client/src/viewDatasetAccessPolicy.js rename to bigquery/cloud-client/viewDatasetAccessPolicy.js diff --git a/bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js similarity index 100% rename from bigquery/cloud-client/src/viewTableOrViewAccessPolicy.js rename to bigquery/cloud-client/viewTableOrViewAccessPolicy.js From b013b3c57bff2f277306ba64e55ecf64bc2bed00 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 27 Feb 2025 18:41:48 +0000 Subject: [PATCH 21/29] fix(bigquery): Update samples and tests related to Dataset --- bigquery/cloud-client/grantAccessToDataset.js | 92 +++++-- bigquery/cloud-client/revokeDatasetAccess.js | 86 ++++-- bigquery/cloud-client/test/config.js | 257 ++++++++++++++++++ .../test/grantAccessToDataset.test.js | 150 +++------- .../test/revokeDatasetAccess.test.js | 167 +++++------- .../test/viewDatasetAccessPolicy.test.js | 92 +------ .../cloud-client/viewDatasetAccessPolicy.js | 59 ++-- 7 files changed, 531 insertions(+), 372 deletions(-) create mode 100644 bigquery/cloud-client/test/config.js diff --git a/bigquery/cloud-client/grantAccessToDataset.js b/bigquery/cloud-client/grantAccessToDataset.js index 3ff9d533fd..b855fbb4f4 100644 --- a/bigquery/cloud-client/grantAccessToDataset.js +++ b/bigquery/cloud-client/grantAccessToDataset.js @@ -14,61 +14,99 @@ 'use strict'; -const {BigQuery} = require('@google-cloud/bigquery'); - /** * Grants access to a BigQuery dataset for a specified entity * - * @param {object} options The configuration object - * @param {string} options.datasetId ID of the dataset to grant access to (e.g. "my_project_id.my_dataset") - * @param {string} options.entityId ID of the user or group to grant access to (e.g. "user-or-group-to-add@example.com") - * @param {string} options.role One of the basic roles for datasets (e.g. "READER") + * @param {string} datasetId ID of the dataset to grant access to + * @param {string} entityId ID of the entity to grant access to + * @param {string} role Role to grant * @returns {Promise} Array of access entries */ -// [START bigquery_grant_access_to_dataset] -async function grantAccessToDataset(options) { - // Create a BigQuery client - const bigquery = new BigQuery(); +async function grantAccessToDataset(datasetId, entityId, role) { + // [START bigquery_grant_access_to_dataset] + const {BigQuery} = require('@google-cloud/bigquery'); + + // TODO(developer): Update and un-comment below lines + + // ID of the dataset to revoke access to. + // datasetId = "my_project_id.my_dataset"; + + // ID of the user or group from whom you are adding access. + // Alternatively, the JSON REST API representation of the entity, + // such as a view's table reference. + // entityId = "user-or-group-to-add@example.com"; + + // One of the "Basic roles for datasets" described here: + // https://cloud.google.com/bigquery/docs/access-control-basic-roles#dataset-basic-roles + // role = "READER"; - const {datasetId, entityId, role} = options; + // Type of entity you are granting access to. + // Find allowed allowed entity type names here: + // https://cloud.google.com/python/docs/reference/bigquery/latest/enums#class-googlecloudbigqueryenumsentitytypesvalue + // In this case, we're using the equivalent of GROUP_BY_EMAIL + const entityType = 'groupByEmail'; + + // Instantiate a client. + const client = new BigQuery(); try { - // Get a reference to the dataset - const dataset = bigquery.dataset(datasetId); - const [metadata] = await dataset.getMetadata(); + // Get a reference to the dataset. + const [dataset] = await client.dataset(datasetId).get(); - // The access entries list is immutable. Create a copy for modifications - const entries = [...(metadata.access || [])]; + // The 'access entries' list is immutable. Create a copy for modifications. + const entries = Array.isArray(dataset.metadata.access) + ? [...dataset.metadata.access] + : []; - // Add the new access entry + // Append an AccessEntry to grant the role to a dataset. + // Find more details about the AccessEntry object in the BigQuery documentation entries.push({ role: role, - groupByEmail: entityId, // For group access. Use userByEmail for user access + [entityType]: entityId, }); - // Update the dataset's access entries - const [updatedMetadata] = await dataset.setMetadata({ - ...metadata, + // Assign the list of AccessEntries back to the dataset. + const metadata = { access: entries, - }); + }; + + // Update will only succeed if the dataset + // has not been modified externally since retrieval. + // + // See the BigQuery client library documentation for more details on metadata updates + + // Update just the 'access entries' property of the dataset. + const [updatedDataset] = await client + .dataset(datasetId) + .setMetadata(metadata); + + // Show a success message. + const fullDatasetId = + updatedDataset && + updatedDataset.metadata && + updatedDataset.metadata.datasetReference + ? `${updatedDataset.metadata.datasetReference.projectId}.${updatedDataset.metadata.datasetReference.datasetId}` + : datasetId; console.log( - `Role '${role}' granted for entity '${entityId}' in dataset '${datasetId}'.` + `Role '${role}' granted for entity '${entityId}'` + + ` in dataset '${fullDatasetId}'.` ); - return updatedMetadata.access; + return updatedDataset.access; } catch (error) { if (error.code === 412) { - // 412 Precondition Failed - Dataset was modified between get and update + // A read-modify-write error (PreconditionFailed equivalent) console.error( `Dataset '${datasetId}' was modified remotely before this update. ` + 'Fetch the latest version and retry.' ); + } else { + throw error; } - throw error; } + // [END bigquery_grant_access_to_dataset] } -// [END bigquery_grant_access_to_dataset] module.exports = { grantAccessToDataset, diff --git a/bigquery/cloud-client/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js index d424cc7743..7c6e08b834 100644 --- a/bigquery/cloud-client/revokeDatasetAccess.js +++ b/bigquery/cloud-client/revokeDatasetAccess.js @@ -18,43 +18,89 @@ const {BigQuery} = require('@google-cloud/bigquery'); // [START bigquery_revoke_dataset_access] /** - * Revokes access to a BigQuery dataset for a specified entity. + * Revokes access to a dataset for a specified entity. * - * @param {Object} params The parameters for revoking dataset access - * @param {string} params.datasetId The ID of the dataset to revoke access from - * @param {string} params.entityId The ID of the user or group to revoke access from + * @param {string} datasetId - ID of the dataset to revoke access to. + * @param {string} entityId - ID of the user or group from whom you are revoking access. + * Alternatively, the JSON REST API representation of the entity, + * such as a view's table reference. * @returns {Promise} A promise that resolves to the updated access entries */ -async function revokeDatasetAccess({datasetId, entityId}) { - // Instantiate a client +async function revokeDatasetAccess(datasetId, entityId) { + // Import the Google Cloud client library. const bigquery = new BigQuery(); - try { - // Get a reference to the dataset - const [dataset] = await bigquery.dataset(datasetId).get(); + // TODO (developer): Update and un-comment below lines - // Filter out the access entry for the specified entity - dataset.metadata.access = dataset.metadata.access.filter( - entry => entry.userByEmail !== entityId && entry.groupByEmail !== entityId - ); + // ID of the dataset to revoke access to. + // datasetId = "your-project.your_dataset" + + // ID of the user or group from whom you are revoking access. + // Alternatively, the JSON REST API representation of the entity, + // such as a view's table reference. + // entityId = "user-or-group-to-remove@example.com" + + // Get a reference to the dataset. + const [dataset] = await bigquery.dataset(datasetId).get(); + + // To revoke access to a dataset, remove elements from the access list. + // + // See the BigQuery client library documentation for more details on access entries - // Update the dataset with the new access entries + // Filter access entries to exclude entries matching the specified entity_id + // and assign a new list back to the access list. + dataset.metadata.access = dataset.metadata.access.filter(entry => { + // Check for entity_id (specific match) + if (entry.entity_id === entityId) { + console.log( + `Found matching entity_id: ${entry.entity_id}, removing entry` + ); + return false; + } + + // Check for userByEmail field + if (entry.userByEmail === entityId) { + console.log( + `Found matching userByEmail: ${entry.userByEmail}, removing entry` + ); + return false; + } + + // Check for groupByEmail field + if (entry.groupByEmail === entityId) { + console.log( + `Found matching groupByEmail: ${entry.groupByEmail}, removing entry` + ); + return false; + } + + // Keep all other entries + return true; + }); + + // Update will only succeed if the dataset + // has not been modified externally since retrieval. + + try { + // Update just the access entries property of the dataset. const [updatedDataset] = await dataset.setMetadata(dataset.metadata); + const fullDatasetId = `${dataset.parent.projectId}.${dataset.id}`; console.log( - `Revoked dataset access for '${entityId}' to dataset '${dataset.id}'.` + `Revoked dataset access for '${entityId}' to dataset '${fullDatasetId}'.` ); - return updatedDataset.metadata.access; + return updatedDataset.access; } catch (error) { + // Check if it's a precondition failed error (a read-modify-write error) if (error.code === 412) { - // Handle precondition failed error (dataset modified externally) - console.error( - `Dataset '${datasetId}' was modified remotely before this update. ` + + console.log( + `Dataset '${dataset.id}' was modified remotely before this update. ` + 'Fetch the latest version and retry.' ); + } else { + throw error; } - throw error; } } // [END bigquery_revoke_dataset_access] diff --git a/bigquery/cloud-client/test/config.js b/bigquery/cloud-client/test/config.js new file mode 100644 index 0000000000..89ff4fa0fa --- /dev/null +++ b/bigquery/cloud-client/test/config.js @@ -0,0 +1,257 @@ +// Copyright 2025 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. + +const {BigQuery} = require('@google-cloud/bigquery'); +const crypto = require('crypto'); + +// Generate a unique prefix using a random UUID to ensure uniqueness across test runs +function createRandomPrefix() { + return `nodejs_test_${crypto.randomUUID().replace(/-/g, '').substring(0, 8)}`; +} + +const PREFIX = createRandomPrefix(); +console.log(`Generated test prefix: ${PREFIX}`); + +const ENTITY_ID = 'example-analyst-group@google.com'; // Group account +const DATASET_ID = `${PREFIX}_cloud_client`; +const TABLE_NAME = `${PREFIX}_table`; +const VIEW_NAME = `${PREFIX}_view`; + +// Shared client for all tests +let sharedClient = null; +let sharedProjectId = null; +let sharedDataset = null; +let sharedTable = null; +let sharedView = null; +let resourcesCreated = false; + +// Helper functions to get shared resources +const getClient = async () => { + if (!sharedClient) { + sharedClient = new BigQuery(); + } + return sharedClient; +}; + +const getProjectId = async () => { + if (!sharedProjectId) { + const client = await getClient(); + sharedProjectId = client.projectId; + } + return sharedProjectId; +}; + +const getEntityId = () => ENTITY_ID; + +const getDataset = async () => { + try { + if (!sharedDataset) { + const client = await getClient(); + + try { + // First try to get the dataset if it exists + console.log(`Checking for dataset ${DATASET_ID}`); + [sharedDataset] = await client.dataset(DATASET_ID).get(); + console.log(`Using existing dataset: ${DATASET_ID}`); + } catch (err) { + if (err.code === 404) { + // If dataset doesn't exist, create it + console.log(`Creating dataset: ${DATASET_ID}`); + [sharedDataset] = await client.createDataset(DATASET_ID); + resourcesCreated = true; + console.log(`Created dataset: ${DATASET_ID}`); + } else { + console.error(`Error getting dataset: ${err.message}`); + throw err; + } + } + } + return sharedDataset; + } catch (err) { + console.error(`Error in getDataset: ${err.message}`); + throw err; + } +}; + +const getTable = async () => { + try { + if (!sharedTable) { + const client = await getClient(); + + const sample_schema = [{name: 'id', type: 'INTEGER', mode: 'REQUIRED'}]; + const tableOptions = { + schema: sample_schema, + }; + + try { + // Try to get table if it exists + console.log(`Checking for table ${TABLE_NAME}`); + [sharedTable] = await client + .dataset(DATASET_ID) + .table(TABLE_NAME) + .get(); + console.log(`Using existing table: ${TABLE_NAME}`); + } catch (err) { + if (err.code === 404) { + // If table doesn't exist, create it + console.log(`Creating table: ${TABLE_NAME}`); + [sharedTable] = await client + .dataset(DATASET_ID) + .createTable(TABLE_NAME, tableOptions); + resourcesCreated = true; + console.log(`Created table: ${TABLE_NAME}`); + } else { + console.error(`Error getting table: ${err.message}`); + throw err; + } + } + } + return sharedTable; + } catch (err) { + console.error(`Error in getTable: ${err.message}`); + throw err; + } +}; +const getView = async () => { + try { + if (!sharedView) { + const client = await getClient(); + const projectId = await getProjectId(); + + const viewOptions = { + view: { + query: `SELECT * FROM \`${projectId}.${DATASET_ID}.${TABLE_NAME}\``, + useLegacySql: false, + }, + }; + + try { + // Try to get view if it exists + console.log(`Checking for view ${VIEW_NAME}`); + [sharedView] = await client.dataset(DATASET_ID).table(VIEW_NAME).get(); + console.log(`Using existing view: ${VIEW_NAME}`); + } catch (err) { + if (err.code === 404) { + // If view doesn't exist, create it + console.log(`Creating view: ${VIEW_NAME}`); + [sharedView] = await client + .dataset(DATASET_ID) + .createTable(VIEW_NAME, viewOptions); + resourcesCreated = true; + console.log(`Created view: ${VIEW_NAME}`); + } else { + console.error(`Error getting view: ${err.message}`); + throw err; + } + } + } + return sharedView; + } catch (err) { + console.error(`Error in getView: ${err.message}`); + throw err; + } +}; + +// Setup and teardown functions for test suites +const setupBeforeAll = async () => { + console.log('=== Setting up test resources ==='); + try { + await getClient(); + await getProjectId(); + // Initialize dataset, table, and view + await getDataset(); + await getTable(); + await getView(); + console.log('=== Test setup complete ==='); + } catch (err) { + console.error(`Setup failed: ${err.message}`); + throw err; + } +}; + +const cleanupResources = async () => { + console.log('=== Cleaning up test resources ==='); + + if (sharedClient) { + try { + // Check if resources exist before attempting to delete + if (sharedDataset) { + try { + console.log( + `Deleting dataset: ${DATASET_ID} and all contained tables/views` + ); + await sharedClient.dataset(DATASET_ID).delete({force: true}); + console.log(`Successfully deleted dataset: ${DATASET_ID}`); + } catch (err) { + if (err.code !== 404) { + console.error(`Error deleting dataset: ${err.message}`); + } else { + console.log(`Dataset ${DATASET_ID} already deleted or not found`); + } + } + } + } catch (err) { + console.error(`Cleanup error: ${err.message}`); + } + } + + // Reset all shared resources + sharedClient = null; + sharedProjectId = null; + sharedDataset = null; + sharedTable = null; + sharedView = null; + resourcesCreated = false; + + console.log('=== Cleanup complete ==='); +}; + +const teardownAfterAll = async () => { + // Always clean up resources after tests + await cleanupResources(); +}; + +// Cleanup on process exit or termination +process.on('exit', () => { + if (resourcesCreated) { + console.log('Process exiting, cleaning up BigQuery resources...'); + } +}); + +process.on('SIGINT', async () => { + console.log('Received SIGINT, cleaning up before exit...'); + await cleanupResources(); +}); + +process.on('uncaughtException', async err => { + console.error('Uncaught exception:', err); + await cleanupResources(); +}); + +module.exports = { + PREFIX, + ENTITY_ID, + DATASET_ID, + TABLE_NAME, + VIEW_NAME, + getClient, + getProjectId, + getEntityId, + getDataset, + getTable, + getView, + setupBeforeAll, + teardownAfterAll, + cleanupResources, +}; diff --git a/bigquery/cloud-client/test/grantAccessToDataset.test.js b/bigquery/cloud-client/test/grantAccessToDataset.test.js index 85b86b2e82..9da3d3281d 100644 --- a/bigquery/cloud-client/test/grantAccessToDataset.test.js +++ b/bigquery/cloud-client/test/grantAccessToDataset.test.js @@ -14,125 +14,57 @@ 'use strict'; -const {assert} = require('chai'); -const sinon = require('sinon'); -const {BigQuery} = require('@google-cloud/bigquery'); +const {expect} = require('chai'); +const { + getDataset, + getEntityId, + setupBeforeAll, + teardownAfterAll, +} = require('./config'); const {grantAccessToDataset} = require('../grantAccessToDataset'); describe('grantAccessToDataset', () => { - const datasetId = 'test_dataset'; - const entityId = 'test-group@example.com'; - const role = 'READER'; - - let datasetStub; - let getMetadataStub; - let setMetadataStub; - - beforeEach(() => { - // Initial empty metadata - const initialMetadata = { - access: [], - }; - - // Updated metadata with new access - const updatedMetadata = { - access: [ - { - role: role, - groupByEmail: entityId, - }, - ], - }; - - // Create stubs for dataset methods - getMetadataStub = sinon.stub().resolves([initialMetadata]); - setMetadataStub = sinon.stub().resolves([updatedMetadata]); - - // Create dataset stub - datasetStub = { - getMetadata: getMetadataStub, - setMetadata: setMetadataStub, - }; - - // Replace BigQuery constructor - sinon.stub(BigQuery.prototype, 'dataset').callsFake(() => datasetStub); + // Set up fixtures before all tests (similar to pytest's module scope) + before(async () => { + await setupBeforeAll(); }); - afterEach(() => { - sinon.restore(); + // Clean up after all tests + after(async () => { + await teardownAfterAll(); }); - it('should grant access to a dataset', async () => { - const result = await grantAccessToDataset({ - datasetId, - entityId, - role, - }); - - // Verify getMetadata was called - assert.strictEqual( - getMetadataStub.callCount, - 1, - 'getMetadata should be called once' - ); + it('should add entity to access entries', async () => { + const dataset = await getDataset(); + const entityId = getEntityId(); - // Verify setMetadata was called with correct parameters - const setMetadataCall = setMetadataStub.getCall(0); - assert.deepStrictEqual( - setMetadataCall.args[0].access, - [ - { - role: role, - groupByEmail: entityId, - }, - ], - 'setMetadata should be called with correct access entries' - ); + console.log({dataset}); + console.log({entityId}); - // Verify the result contains the expected access entry - assert.isArray(result, 'Result should be an array'); - assert.deepStrictEqual( - result[0], - { - role: role, - groupByEmail: entityId, - }, - 'Result should contain the new access entry' + // Act: Grant access to the dataset + const accessEntries = await grantAccessToDataset( + dataset.id, + entityId, + 'READER' ); - }); - - it('should handle concurrent modification errors', async () => { - // Simulate a 412 Precondition Failed error - const error = new Error('Precondition Failed'); - error.code = 412; - setMetadataStub.rejects(error); - - try { - await grantAccessToDataset({ - datasetId, - entityId, - role, - }); - assert.fail('Should have thrown an error'); - } catch (err) { - assert.strictEqual(err.code, 412, 'Should throw 412 error'); - } - }); - - it('should propagate other errors', async () => { - const errorMessage = 'Unknown error'; - const error = new Error(errorMessage); - setMetadataStub.rejects(error); - try { - await grantAccessToDataset({ - datasetId, - entityId, - role, - }); - assert.fail('Should have thrown an error'); - } catch (err) { - assert.strictEqual(err.message, errorMessage); - } + // Assert: Check if entity was added to access entries + const updatedEntityIds = accessEntries + .filter(entry => entry !== null) + .map(entry => { + // Handle different entity types + if (entry.groupByEmail) { + return entry.groupByEmail; + } else if (entry.userByEmail) { + return entry.userByEmail; + } else if (entry.specialGroup) { + return entry.specialGroup; + } + return null; + }) + .filter(id => id !== null); + + // Check if our entity ID is in the updated access entries (similar to Python assertion) + expect(updatedEntityIds).to.include(entityId); }); }); diff --git a/bigquery/cloud-client/test/revokeDatasetAccess.test.js b/bigquery/cloud-client/test/revokeDatasetAccess.test.js index 88bdf1c01f..1a6abb5126 100644 --- a/bigquery/cloud-client/test/revokeDatasetAccess.test.js +++ b/bigquery/cloud-client/test/revokeDatasetAccess.test.js @@ -1,10 +1,10 @@ -// Copyright 2024 Google LLC +// Copyright 2025 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 // -// https://www.apache.org/licenses/LICENSE-2.0 +// 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, @@ -12,123 +12,80 @@ // See the License for the specific language governing permissions and // limitations under the License. -'use strict'; - -const {assert} = require('chai'); -const sinon = require('sinon'); -const {BigQuery} = require('@google-cloud/bigquery'); +const {describe, it, before, after} = require('mocha'); +const assert = require('assert'); +const {grantAccessToDataset} = require('../grantAccessToDataset'); const {revokeDatasetAccess} = require('../revokeDatasetAccess'); +const { + getDataset, + getEntityId, + setupBeforeAll, + teardownAfterAll, +} = require('./config'); describe('revokeDatasetAccess', () => { - const datasetId = 'test_dataset'; - const entityId = 'user@example.com'; - let sandbox; - let mockBigQuery; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - - // Mock the BigQuery constructor - mockBigQuery = sandbox.stub(BigQuery.prototype); + // Setup resources before all tests + before(async () => { + await setupBeforeAll(); }); - afterEach(() => { - sandbox.restore(); + // Clean up resources after all tests + after(async () => { + await teardownAfterAll(); }); - it('should successfully revoke access from a dataset', async () => { - const initialAccess = [ - { - role: 'READER', - userByEmail: entityId, - }, - ]; + it('should revoke access to a dataset', async () => { + // Get test resources + const dataset = await getDataset(); + const entityId = getEntityId(); - const dataset = { - id: datasetId, - metadata: { - access: initialAccess, - }, - setMetadata: sandbox.stub().resolves([ - { - metadata: { - access: [], - }, - }, - ]), - }; - - // Mock the dataset method to return our mock dataset - mockBigQuery.dataset = sandbox.stub().returns({ - get: sandbox.stub().resolves([dataset]), - }); + // Directly use the dataset ID + const datasetId = dataset.id; + console.log(`Testing with dataset: ${datasetId} and entity: ${entityId}`); - // Execute revoke access - const updatedAccess = await revokeDatasetAccess({ + // First grant access to the dataset + const datasetAccessEntries = await grantAccessToDataset( datasetId, entityId, + 'READER' + ); + + // Create a set of all entity IDs and email addresses to check + const datasetEntityIds = new Set(); + datasetAccessEntries.forEach(entry => { + if (entry.entity_id) datasetEntityIds.add(entry.entity_id); + if (entry.userByEmail) datasetEntityIds.add(entry.userByEmail); + if (entry.groupByEmail) datasetEntityIds.add(entry.groupByEmail); }); - // Verify the access was revoked - assert.isArray(updatedAccess); - assert.isEmpty(updatedAccess); - }); - - it('should handle precondition failed error', async () => { - const preconditionError = new Error('Precondition Failed'); - preconditionError.code = 412; - - const dataset = { - id: datasetId, - metadata: { - access: [], - }, - setMetadata: sandbox.stub().rejects(preconditionError), - }; - - // Mock the dataset method to return our mock dataset - mockBigQuery.dataset = sandbox.stub().returns({ - get: sandbox.stub().resolves([dataset]), - }); - - try { - await revokeDatasetAccess({ - datasetId, - entityId, - }); - assert.fail('Should have thrown an error'); - } catch (error) { - assert.equal(error.code, 412); - assert.equal(error.message, 'Precondition Failed'); - } - }); - - it('should handle missing entity gracefully', async () => { - const dataset = { - id: datasetId, - metadata: { - access: [], - }, - setMetadata: sandbox.stub().resolves([ - { - metadata: { - access: [], - }, - }, - ]), - }; - - // Mock the dataset method to return our mock dataset - mockBigQuery.dataset = sandbox.stub().returns({ - get: sandbox.stub().resolves([dataset]), - }); - - const updatedAccess = await revokeDatasetAccess({ - datasetId, - entityId, + // Check if our entity ID is in the set + const hasAccess = datasetEntityIds.has(entityId); + console.log(`Entity ${entityId} has access after granting: ${hasAccess}`); + assert.strictEqual( + hasAccess, + true, + 'Entity should have access after granting' + ); + + // Now revoke access + const newAccessEntries = await revokeDatasetAccess(datasetId, entityId); + + // Check that the entity no longer has access + const updatedEntityIds = new Set(); + newAccessEntries.forEach(entry => { + if (entry.entity_id) updatedEntityIds.add(entry.entity_id); + if (entry.userByEmail) updatedEntityIds.add(entry.userByEmail); + if (entry.groupByEmail) updatedEntityIds.add(entry.groupByEmail); }); - assert.isArray(updatedAccess); - assert.isEmpty(updatedAccess); + const stillHasAccess = updatedEntityIds.has(entityId); + console.log( + `Entity ${entityId} has access after revoking: ${stillHasAccess}` + ); + assert.strictEqual( + stillHasAccess, + false, + 'Entity should not have access after revoking' + ); }); }); diff --git a/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js b/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js index 7537c35239..b1c67042be 100644 --- a/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js +++ b/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js @@ -12,95 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -'use strict'; - -const {assert} = require('chai'); -const sinon = require('sinon'); -const {BigQuery} = require('@google-cloud/bigquery'); +const assert = require('assert'); +const {getDataset, setupBeforeAll, teardownAfterAll} = require('./config'); const {viewDatasetAccessPolicy} = require('../viewDatasetAccessPolicy'); describe('viewDatasetAccessPolicy', () => { - let bigQueryStub; - let consoleLogSpy; - - const sampleAccessEntry = { - role: 'READER', - specialGroup: 'projectReaders', - userByEmail: 'test@example.com', - }; - - beforeEach(() => { - // Stub BigQuery client - bigQueryStub = { - dataset: sinon.stub().returns({ - getMetadata: sinon.stub().resolves([ - { - access: [sampleAccessEntry], - }, - ]), - }), - }; - - // Stub BigQuery constructor - sinon.stub(BigQuery.prototype, 'dataset').callsFake(bigQueryStub.dataset); - - // Spy on console.log - consoleLogSpy = sinon.spy(console, 'log'); + before(async () => { + await setupBeforeAll(); }); - afterEach(() => { - sinon.restore(); + after(async () => { + await teardownAfterAll(); }); - it('should display access policy details for a dataset', async () => { - const datasetId = 'test_dataset'; - - await viewDatasetAccessPolicy({datasetId}); - - // Verify BigQuery client was called correctly - assert.ok(bigQueryStub.dataset.calledWith(datasetId)); - - // Verify console output - assert.ok(consoleLogSpy.calledWith('Access entries:', [sampleAccessEntry])); - assert.ok( - consoleLogSpy.calledWith( - `Details for Access entry 0 in dataset '${datasetId}':` - ) - ); - assert.ok(consoleLogSpy.calledWith('Role: READER')); - assert.ok(consoleLogSpy.calledWith('SpecialGroup: projectReaders')); - assert.ok(consoleLogSpy.calledWith('UserByEmail: test@example.com')); - }); - - it('should handle datasets with no access entries', async () => { - // Override stub to return empty access array - bigQueryStub.dataset.returns({ - getMetadata: sinon.stub().resolves([ - { - access: [], - }, - ]), - }); - - await viewDatasetAccessPolicy({datasetId: 'empty_dataset'}); - - // Verify only the access entries message was logged - assert.ok(consoleLogSpy.calledWith('Access entries:', [])); - }); - - it('should handle errors gracefully', async () => { - const errorMessage = 'Dataset not found'; - - // Override stub to throw error - bigQueryStub.dataset.returns({ - getMetadata: sinon.stub().rejects(new Error(errorMessage)), - }); + it('should view dataset access policies', async () => { + const dataset = await getDataset(); + const accessPolicy = await viewDatasetAccessPolicy(dataset.id); - try { - await viewDatasetAccessPolicy({datasetId: 'non_existent_dataset'}); - assert.fail('Should have thrown an error'); - } catch (error) { - assert.include(error.message, errorMessage); - } + assert(accessPolicy); }); }); diff --git a/bigquery/cloud-client/viewDatasetAccessPolicy.js b/bigquery/cloud-client/viewDatasetAccessPolicy.js index 7c2a936845..bb8b7c6de3 100644 --- a/bigquery/cloud-client/viewDatasetAccessPolicy.js +++ b/bigquery/cloud-client/viewDatasetAccessPolicy.js @@ -14,43 +14,44 @@ 'use strict'; -const {BigQuery} = require('@google-cloud/bigquery'); - -// [START bigquery_view_dataset_access_policy] /** * View access policies for a BigQuery dataset - * - * @param {object} [overrideValues] Optional parameters to override defaults - * @param {string} [overrideValues.datasetId] Dataset ID to view access policies + * @param {string} datasetId - Dataset ID to view access policies for + * @returns {Array} List of access entries */ -async function viewDatasetAccessPolicy(overrideValues = {}) { - // Instantiate BigQuery client +function viewDatasetAccessPolicy(datasetId) { + // [START bigquery_view_dataset_access_policy] + // Import the Google Cloud client library. + const {BigQuery} = require('@google-cloud/bigquery'); + + // Instantiate a client. const bigquery = new BigQuery(); + // TODO (developer): Update and un-comment below lines // Dataset from which to get the access policy - const datasetId = overrideValues.datasetId || 'my_new_dataset'; - - try { - // Prepares a reference to the dataset - const dataset = bigquery.dataset(datasetId); - const [metadata] = await dataset.getMetadata(); - - // Shows the Access policy as a list of access entries - console.log('Access entries:', metadata.access); - - // Get properties for an AccessEntry - if (metadata.access && metadata.access.length > 0) { - console.log(`Details for Access entry 0 in dataset '${datasetId}':`); - console.log(`Role: ${metadata.access[0].role || 'N/A'}`); - console.log(`SpecialGroup: ${metadata.access[0].specialGroup || 'N/A'}`); - console.log(`UserByEmail: ${metadata.access[0].userByEmail || 'N/A'}`); + // datasetId = "my_dataset"; + + // Get a reference to the dataset. + const dataset = bigquery.dataset(datasetId); + + return dataset.getMetadata().then(([metadata]) => { + const accessEntries = metadata.access || []; + + // Show the list of AccessEntry objects. + // More details about the AccessEntry object in the BigQuery documentation. + console.log( + `${accessEntries.length} Access entries in dataset '${datasetId}':` + ); + for (const accessEntry of accessEntries) { + console.log(`Role: ${accessEntry.role || 'null'}`); + console.log(`Special group: ${accessEntry.specialGroup || 'null'}`); + console.log(`User by Email: ${accessEntry.userByEmail || 'null'}`); } - } catch (error) { - console.error(`Error viewing dataset access policy: ${error.message}`); - throw error; - } + + return accessEntries; + }); + // [END bigquery_view_dataset_access_policy] } -// [END bigquery_view_dataset_access_policy] module.exports = { viewDatasetAccessPolicy, From 12bc84b07b6154b88353bbe3091447251142aabe Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 27 Feb 2025 19:10:58 +0000 Subject: [PATCH 22/29] fix(bigquery): Update format issues --- bigquery/cloud-client/revokeDatasetAccess.js | 2 +- bigquery/cloud-client/revokeTableOrViewAccess.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bigquery/cloud-client/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js index 7c6e08b834..e151130ef1 100644 --- a/bigquery/cloud-client/revokeDatasetAccess.js +++ b/bigquery/cloud-client/revokeDatasetAccess.js @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/bigquery/cloud-client/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js index 7b427ec13e..49fb427951 100644 --- a/bigquery/cloud-client/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/revokeTableOrViewAccess.js @@ -75,7 +75,7 @@ async function revokeTableOrViewAccess({ binding => binding.role !== roleToRevoke ); } else { - // Keep the current binding as is + // Keep the current binding as it is newBindings = policy.bindings; } From b9e4bb114c22d8974df30497e7a78768ec75f886 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Mon, 3 Mar 2025 18:37:24 +0000 Subject: [PATCH 23/29] fix(bigquery): Update samples and tests related to Table or View --- .../cloud-client/grantAccessToTableOrView.js | 92 ++++--- .../cloud-client/revokeTableOrViewAccess.js | 162 +++++++------ .../test/grantAccessToTableOrView.test.js | 160 +++++------- .../test/revokeTableOrViewAccess.test.js | 228 ++++++++---------- .../test/viewTableOrViewAccessPolicy.test.js | 151 +++++------- .../viewTableOrViewAccessPolicy.js | 78 +++--- 6 files changed, 395 insertions(+), 476 deletions(-) diff --git a/bigquery/cloud-client/grantAccessToTableOrView.js b/bigquery/cloud-client/grantAccessToTableOrView.js index 4252a83f65..549e1b9702 100644 --- a/bigquery/cloud-client/grantAccessToTableOrView.js +++ b/bigquery/cloud-client/grantAccessToTableOrView.js @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -'use strict'; - const {BigQuery} = require('@google-cloud/bigquery'); /** @@ -26,56 +24,70 @@ const {BigQuery} = require('@google-cloud/bigquery'); * @param {string} role - Role to assign to the member * @returns {Promise} The updated policy bindings */ -async function grantAccessToTableOrView({ +async function grantAccessToTableOrView( projectId, datasetId, resourceName, principalId, - role, -}) { + role +) { // [START bigquery_grant_access_to_table_or_view] - // Uncomment and update these variables: - // const projectId = 'my_project_id'; - // const datasetId = 'my_dataset'; - // const resourceName = 'my_table'; - // const principalId = 'user:bob@example.com'; - // const role = 'roles/bigquery.dataViewer'; - - // Create a BigQuery client - const bigquery = new BigQuery(); - - // Get the dataset and table references - const dataset = bigquery.dataset(datasetId); - const table = dataset.table(resourceName); + // TODO(developer): Update and un-comment below lines + + // Google Cloud Platform project. + // projectId = "my_project_id" + + // Dataset where the table or view is. + // datasetId = "my_dataset" + + // Table or view name to get the access policy. + // resourceName = "my_table" - try { - // Get the IAM access policy for the table or view - const [policy] = await table.iam.getPolicy(); + // The principal requesting access to the table or view. + // Find more details about principal identifiers here: + // https://cloud.google.com/iam/docs/principal-identifiers + // principalId = "user:bob@example.com" - // Create a new binding for the principal and role - const binding = { - role: role, - members: [principalId], - }; + // Role to assign to the member. + // role = "roles/bigquery.dataViewer" - // Add the new binding to the policy - policy.bindings.push(binding); + // Instantiate a client. + const client = new BigQuery(); - // Set the updated IAM access policy - const [updatedPolicy] = await table.iam.setPolicy(policy); + // Get the table reference. + const dataset = client.dataset(datasetId); + const table = dataset.table(resourceName); - console.log( - `Role '${role}' granted for principal '${principalId}' on resource '${projectId}.${datasetId}.${resourceName}'.` - ); + // Get the IAM access policy for the table or view. + const [policy] = await table.getIamPolicy(); - return updatedPolicy.bindings; - } catch (error) { - console.error('Error granting access:', error); - throw error; + // Initialize bindings if they do not exist + if (!policy.bindings) { + policy.bindings = []; } + + // To grant access to a table or view. + // add bindings to the Table or View policy. + // + // Find more details about Policy and Binding objects here: + // https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Policy + // https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Binding + const binding = { + role: role, + members: [principalId], + }; + policy.bindings.push(binding); + + // Set the IAM access policy with updated bindings + const [updatedPolicy] = await table.setIamPolicy(policy); + + // Show a success message. + console.log( + `Role '${role}' granted for principal '${principalId}' on resource '${datasetId}.${resourceName}'.` + ); // [END bigquery_grant_access_to_table_or_view] + + return updatedPolicy.bindings; } -module.exports = { - grantAccessToTableOrView, -}; +module.exports = {grantAccessToTableOrView}; diff --git a/bigquery/cloud-client/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js index 49fb427951..fb9d49171e 100644 --- a/bigquery/cloud-client/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/revokeTableOrViewAccess.js @@ -12,94 +12,106 @@ // See the License for the specific language governing permissions and // limitations under the License. -const {BigQuery} = require('@google-cloud/bigquery'); - -// [START bigquery_revoke_access_to_table_or_view] /** * Revokes access to a BigQuery table or view - * @param {Object} params - The parameters object - * @param {string} params.projectId - The ID of the Google Cloud project - * @param {string} params.datasetId - The ID of the dataset containing the table/view - * @param {string} params.resourceId - The ID of the table or view - * @param {string} [params.memberToRevoke] - Optional. Specific member to revoke access from (e.g., 'group:example@google.com') - * @param {string} [params.roleToRevoke='roles/bigquery.dataViewer'] - Optional. Specific role to revoke - * @returns {Promise} + * @param {string} projectId - The ID of the Google Cloud project + * @param {string} datasetId - The ID of the dataset containing the table/view + * @param {string} resourceName - The ID of the table or view + * @param {string} [roleToRemove=null] - Optional. Specific role to revoke + * @param {string} [principalToRemove=null] - Optional. Specific principal to revoke access from + * @returns {Promise} The updated IAM policy */ -async function revokeTableOrViewAccess({ +async function revokeAccessToTableOrView( projectId, datasetId, - resourceId, - memberToRevoke, - roleToRevoke = 'roles/bigquery.dataViewer', -}) { - // Validate required parameters - if (!projectId || !datasetId || !resourceId) { - throw new Error( - 'projectId, datasetId and resourceID are required parameters' - ); + resourceName, + roleToRemove = null, + principalToRemove = null +) { + // [START bigquery_revoke_access_to_table_or_view] + // Imports the Google Cloud client library + const {BigQuery} = require('@google-cloud/bigquery'); + + // TODO (developer): Update and un-comment below lines + // Google Cloud Platform project. + // projectId = "my_project_id" + + // Dataset where the table or view is. + // datasetId = "my_dataset" + + // Table or view name to get the access policy. + // resourceName = "my_table" + + // (Optional) Role to remove from the table or view. + // roleToRemove = "roles/bigquery.dataViewer" + + // (Optional) Principal to remove from the table or view. + // principalToRemove = "user:alice@example.com" + + // Find more information about roles and principals (refered as members) here: + // https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Binding + + // Instantiate a client. + const client = new BigQuery(); + + // Get the table reference. + const dataset = client.dataset(datasetId); + const table = dataset.table(resourceName); + + // Get the IAM access policy for the table or view. + const [policy] = await table.getIamPolicy(); + + // Initialize bindings of they do not exist + if (!policy.bindings) { + policy.bindings = []; } - try { - // Create BigQuery client - const bigquery = new BigQuery({ - projectId: projectId, - }); - - // Get reference to the table or view - const dataset = bigquery.dataset(datasetId); - const table = dataset.table(resourceId); - - // Get current IAM policy - const [policy] = await table.iam.getPolicy(); - console.log( - 'Current IAM Policy:', - JSON.stringify(policy.bindings, null, 2) - ); - // Filter bindings based on parameters - let newBindings = policy.bindings; - - if (memberToRevoke && roleToRevoke) { - // Remove specific member from specific role - newBindings = policy.bindings - .map(binding => ({ - ...binding, - members: - binding.role === roleToRevoke - ? binding.members.filter(member => member !== memberToRevoke) - : binding.members, - })) - .filter(binding => binding.members.length > 0); - } else if (!memberToRevoke && roleToRevoke) { - // Remove all bindings for the specified role - newBindings = policy.bindings.filter( - binding => binding.role !== roleToRevoke - ); - } else { - // Keep the current binding as it is - newBindings = policy.bindings; - } + // To revoke access to a table or view, + // remove bindings from the Table or View policy. + // + // Find more details about Policy objects here: + // https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Policy - // Create new policy with updated bindings - const newPolicy = { - bindings: newBindings, - }; + if (roleToRemove) { + // Filter out all bindings with the `roleToRemove` + // and assign a new list back to the policy bindings. + policy.bindings = policy.bindings.filter(b => b.role !== roleToRemove); + } - // Set the new IAM policy - await table.iam.setPolicy(newPolicy); - console.log(`Access revoked successfully for ${resourceId}`); + if (principalToRemove) { + // Create a copy to match original code structure. + const bindings = [...policy.bindings]; - // Verify the changes - const [updatedPolicy] = await table.iam.getPolicy(); - console.log( - 'Updated IAM Policy:', - JSON.stringify(updatedPolicy.bindings, null, 2) + // Filter out the principal from each binding. + for (const binding of bindings) { + if (binding.members) { + binding.members = binding.members.filter(m => m !== principalToRemove); + } + } + + // Filter out bindings with empty members + policy.bindings = bindings.filter( + binding => binding.members && binding.members.length > 0 ); + } + + try { + // Set the IAM access policy with updated bindings + await table.setIamPolicy(policy); + + // Get the policy again to confirm it's set correctly + const [verifiedPolicy] = await table.getIamPolicy(); + + if (verifiedPolicy && verifiedPolicy.bindings) { + return verifiedPolicy.bindings; + } else { + return []; + } } catch (error) { - console.error('Error revoking access:', error); + console.error('Error settings IAM policy:', error); throw error; } + // [END bigquery_revoke_access_to_table_or_view] } -// [END bigquery_revoke_access_to_table_or_view] - -module.exports = {revokeTableOrViewAccess}; +module.exports = {revokeAccessToTableOrView}; diff --git a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js index 323ce911e3..cf02af95db 100644 --- a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js +++ b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.apache.org/licenses/LICENSE-2.0 +// 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, @@ -12,118 +12,82 @@ // See the License for the specific language governing permissions and // limitations under the License. -'use strict'; - -const {assert} = require('chai'); -const sinon = require('sinon'); -const {BigQuery} = require('@google-cloud/bigquery'); +const {describe, it, before, after} = require('mocha'); +const assert = require('assert'); const {grantAccessToTableOrView} = require('../grantAccessToTableOrView'); +const { + getProjectId, + getEntityId, + getDataset, + getTable, + setupBeforeAll, + teardownAfterAll, +} = require('./config'); describe('grantAccessToTableOrView', () => { - const PROJECT_ID = 'test-project'; - const DATASET_ID = 'test_dataset'; - const TABLE_ID = 'test_table'; - const ENTITY_ID = 'test-group@example.com'; - const ROLE = 'roles/bigquery.dataViewer'; - const PRINCIPAL_ID = `group:${ENTITY_ID}`; - - let sandbox; - let bigQueryStub; - let tableMock; - let datasetMock; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - - // Create empty initial policy - const emptyPolicy = { - bindings: [], - }; - - // Create policy with the new binding - const updatedPolicy = { - bindings: [ - { - role: ROLE, - members: [PRINCIPAL_ID], - }, - ], - }; - - // Create the complete mock structure - tableMock = { - iam: { - getPolicy: sandbox.stub().resolves([emptyPolicy]), - setPolicy: sandbox.stub().resolves([updatedPolicy]), - }, - }; - - datasetMock = { - table: sandbox.stub().returns(tableMock), - }; - - bigQueryStub = sandbox.stub(BigQuery.prototype); - bigQueryStub.dataset = sandbox.stub().returns(datasetMock); + // Setup shared resources before all tests + before(async () => { + await setupBeforeAll(); }); - afterEach(() => { - sandbox.restore(); + // Clean up resources after all tests + after(async () => { + await teardownAfterAll(); }); - it('should grant access to table or view', async () => { - const updatedBindings = await grantAccessToTableOrView({ - projectId: PROJECT_ID, - datasetId: DATASET_ID, - resourceName: TABLE_ID, - principalId: PRINCIPAL_ID, - role: ROLE, - }); - - // Verify the dataset method was called - assert.ok( - bigQueryStub.dataset.calledWith(DATASET_ID), - 'dataset method should be called with correct dataset ID' - ); + it('should grant access to a table', async () => { + // Get required test resources + const projectId = await getProjectId(); + const dataset = await getDataset(); + const table = await getTable(); + const entityId = getEntityId(); - // Verify the table method was called with correct table ID - assert.ok( - datasetMock.table.calledWith(TABLE_ID), - 'table method should be called with correct table ID' - ); + const ROLE = 'roles/bigquery.dataViewer'; + const PRINCIPAL_ID = `group:${entityId}`; - // Verify getPolicy was called - assert.ok(tableMock.iam.getPolicy.called, 'getPolicy should be called'); + // Get the initial empty policy + const [emptyPolicy] = await table.getIamPolicy(); - // Verify setPolicy was called - assert.ok(tableMock.iam.setPolicy.called, 'setPolicy should be called'); + // Initialize bindings if they do not exist + if (!emptyPolicy.bindings) { + emptyPolicy.bindings = []; + } - // Verify the updated bindings contain the new role - const roleBinding = updatedBindings.find(binding => binding.role === ROLE); - assert.exists(roleBinding, `Binding with role ${ROLE} should exist`); + // In an empty policy the role and principal should not be present + assert.strictEqual( + emptyPolicy.bindings.some(p => p.role === ROLE), + false, + 'Role should not exist in empty policy' + ); + assert.strictEqual( + emptyPolicy.bindings.some( + p => p.members && p.members.includes(PRINCIPAL_ID) + ), + false, + 'Principal should not exist in empty policy' + ); - // Verify the principal was added to the members - assert.include( - roleBinding.members, + // Grant access to the table + const updatedPolicy = await grantAccessToTableOrView( + projectId, + dataset.id, + table.id, PRINCIPAL_ID, - `Principal ${PRINCIPAL_ID} should be in members list` + ROLE ); - }); - it('should throw error when BigQuery API fails', async () => { - // Make getPolicy throw an error - tableMock.iam.getPolicy.rejects(new Error('API Error')); + // A binding with that role should exist + assert.strictEqual( + updatedPolicy.some(p => p.role === ROLE), + true, + 'Role should exist after granting access' + ); - try { - await grantAccessToTableOrView({ - projectId: PROJECT_ID, - datasetId: DATASET_ID, - resourceName: TABLE_ID, - principalId: PRINCIPAL_ID, - role: ROLE, - }); - assert.fail('Should have thrown an error'); - } catch (error) { - assert.equal(error.message, 'API Error'); - } + // A binding for that principal should exist + assert.strictEqual( + updatedPolicy.some(p => p.members && p.members.includes(PRINCIPAL_ID)), + true, + 'Principal should exist after granting access' + ); }); }); diff --git a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js index 4112bf5697..3cf9d2a339 100644 --- a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js +++ b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js @@ -4,7 +4,7 @@ // 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 +// https://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, @@ -12,144 +12,118 @@ // See the License for the specific language governing permissions and // limitations under the License. -'use strict'; +const {assert} = require('chai'); +const {describe, before, after, it} = require('mocha'); -const {expect} = require('chai'); -const sinon = require('sinon'); -const {BigQuery} = require('@google-cloud/bigquery'); -const {revokeTableOrViewAccess} = require('../revokeTableOrViewAccess'); +const {revokeAccessToTableOrView} = require('../revokeTableOrViewAccess'); +const {grantAccessToTableOrView} = require('../grantAccessToTableOrView'); +const { + getProjectId, + getEntityId, + getDataset, + getTable, + setupBeforeAll, + teardownAfterAll, +} = require('./config'); describe('revokeTableOrViewAccess', () => { - let bigQueryStub; - let tableStub; - let constructorStub; - - beforeEach(() => { - // Create stubs for BigQuery client and its methods - tableStub = { - iam: { - getPolicy: sinon.stub(), - setPolicy: sinon.stub(), - }, - }; - - const datasetStub = { - table: sinon.stub().returns(tableStub), - }; - - bigQueryStub = { - dataset: sinon.stub().returns(datasetStub), - }; - - // Stub the BigQuery constructor correctly - constructorStub = sinon.stub(BigQuery.prototype, 'constructor'); - constructorStub.returns(bigQueryStub); - - // Stub the BigQuery class itself - sinon.stub(BigQuery.prototype, 'dataset').returns(datasetStub); + before(async () => { + await setupBeforeAll(); }); - afterEach(() => { - sinon.restore(); + after(async () => { + await teardownAfterAll(); }); - it('should revoke access for a specific member and role', async () => { - // Setup test data - const testPolicy = { - bindings: [ - { - role: 'roles/bigquery.dataViewer', - members: ['group:test@google.com', 'group:other@google.com'], - }, - ], - }; - - const expectedNewPolicy = { - bindings: [ - { - role: 'roles/bigquery.dataViewer', - members: ['group:other@google.com'], - }, - ], - }; - - // Configure stubs - tableStub.iam.getPolicy.resolves([testPolicy]); - tableStub.iam.setPolicy.resolves([]); - - // Test the function - await revokeTableOrViewAccess({ - projectId: 'test-project', - datasetId: 'test-dataset', - resourceId: 'test-table', - memberToRevoke: 'group:test@google.com', - roleToRevoke: 'roles/bigquery.dataViewer', - }); - - // Verify the new policy was set correctly - sinon.assert.calledWith( - tableStub.iam.setPolicy, - sinon.match(expectedNewPolicy) + it('should revoke access to a table for a specific role', async () => { + const dataset = await getDataset(); + const projectId = await getProjectId(); + const table = await getTable(); + const entityId = getEntityId(); + + const ROLE = 'roles/bigquery.dataViewer'; + const PRINCIPAL_ID = `group:${entityId}`; + + // Get the initial empty policy + const [emptyPolicy] = await table.getIamPolicy(); + + // Initialize bindings if they do not exist + if (!emptyPolicy.bindings) { + emptyPolicy.bindings = []; + } + + // Grant access + const policyWithRole = await grantAccessToTableOrView( + projectId, + dataset.id, + table.id, + PRINCIPAL_ID, + ROLE ); - }); - it('should revoke all access for a specific role', async () => { - // Setup test data - const testPolicy = { - bindings: [ - { - role: 'roles/bigquery.dataViewer', - members: ['group:test@google.com'], - }, - { - role: 'roles/bigquery.dataEditor', - members: ['group:editor@google.com'], - }, - ], - }; - - const expectedNewPolicy = { - bindings: [ - { - role: 'roles/bigquery.dataEditor', - members: ['group:editor@google.com'], - }, - ], - }; - - // Configure stubs - tableStub.iam.getPolicy.resolves([testPolicy]); - tableStub.iam.setPolicy.resolves([]); - - // Test the function - await revokeTableOrViewAccess({ - projectId: 'test-project', - datasetId: 'test-dataset', - resourceId: 'test-table', - roleToRevoke: 'roles/bigquery.dataViewer', - }); - - // Verify the new policy was set correctly - sinon.assert.calledWith( - tableStub.iam.setPolicy, - sinon.match(expectedNewPolicy) + // Check that there is a binding with that role + const hasRole = policyWithRole.some(b => b.role === ROLE); + assert.isTrue(hasRole); + + // Revoke access for the role + const policyWithRevokedRole = await revokeAccessToTableOrView( + projectId, + dataset.id, + table.id, + ROLE, + null ); + + // Check that this role is not present in the policy anymore + const roleExists = policyWithRevokedRole.some(b => b.role === ROLE); + assert.isFalse(roleExists); }); - it('should handle errors appropriately', async () => { - // Configure stub to throw an error - tableStub.iam.getPolicy.rejects(new Error('Test error')); - - // Test the function - try { - await revokeTableOrViewAccess({ - projectId: 'test-project', - datasetId: 'test-dataset', - resourceId: 'test-table', - }); - expect.fail('Should have thrown an error'); - } catch (error) { - expect(error.message).to.equal('Test error'); + it('should revoke access to a table for a specific principal', async () => { + const dataset = await getDataset(); + const projectId = await getProjectId(); + const table = await getTable(); + const entityId = getEntityId(); + + const ROLE = 'roles/bigquery.dataViewer'; + const PRINCIPAL_ID = `group:${entityId}`; + + // Get the initial empty policy + const [emptyPolicy] = await table.getIamPolicy(); + + // Initialize bindings if they do not exist + if (!emptyPolicy.bindings) { + emptyPolicy.bindings = []; } + + // Grant access + const updatedPolicy = await grantAccessToTableOrView( + projectId, + dataset.id, + table.id, + PRINCIPAL_ID, + ROLE + ); + + // There is a binding for that principal + const hasPrincipal = updatedPolicy.some( + b => b.members && b.members.includes(PRINCIPAL_ID) + ); + assert.isTrue(hasPrincipal); + + // Revoke access for the principal + const policyWithRemovedPrincipal = await revokeAccessToTableOrView( + projectId, + dataset.id, + table.id, + null, + PRINCIPAL_ID + ); + + // This principal is not present in the policy anymore + const hasPrincipalAfterRevoke = policyWithRemovedPrincipal.some( + b => b.members && b.members.includes(PRINCIPAL_ID) + ); + assert.isFalse(hasPrincipalAfterRevoke); }); }); diff --git a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js index 092a61c2e1..b8ffca6054 100644 --- a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js +++ b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js @@ -4,7 +4,7 @@ // 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 +// https://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, @@ -12,114 +12,79 @@ // See the License for the specific language governing permissions and // limitations under the License. -'use strict'; - -const {assert} = require('chai'); -const sinon = require('sinon'); -const {BigQuery} = require('@google-cloud/bigquery'); +const assert = require('assert'); +const { + getProjectId, + getDataset, + getTable, + getView, + setupBeforeAll, + teardownAfterAll, +} = require('./config.js'); +const viewTableOrViewAccessPolicy = require('../viewTableOrViewAccessPolicy.js'); describe('viewTableOrViewAccessPolicy', () => { - let bigQueryStub; - let consoleLogSpy; - let viewTableOrViewAccessPolicy; - - const samplePolicy = { - bindings: [ - { - role: 'roles/bigquery.dataViewer', - members: ['user:test@example.com'], - }, - ], - etag: 'CAE=', - version: 1, - }; - - beforeEach(() => { - // Set required environment variable - process.env.GOOGLE_CLOUD_PROJECT = 'test-project'; - - // Stub BigQuery client - bigQueryStub = { - dataset: sinon.stub().returns({ - table: sinon.stub().returns({ - getIamPolicy: sinon.stub().resolves([samplePolicy]), - }), - }), - }; - - // Stub BigQuery constructor - sinon.stub(BigQuery.prototype, 'dataset').callsFake(bigQueryStub.dataset); - - // Spy on console log methods - consoleLogSpy = sinon.spy(console, 'log'); - - // Reset module for each test - viewTableOrViewAccessPolicy = - require('../viewTableOrViewAccessPolicy').viewTableOrViewAccessPolicy; + before(async () => { + await setupBeforeAll(); }); - afterEach(() => { - delete process.env.GOOGLE_CLOUD_PROJECT; - sinon.restore(); + after(async () => { + await teardownAfterAll(); }); - it('should display access policy details for a table', async () => { - const params = { - projectId: 'test-project', - datasetId: 'test_dataset', - resourceName: 'test_table', - }; + it('should view table access policies', async () => { + const projectId = await getProjectId(); + const dataset = await getDataset(); + const table = await getTable(); - const policy = await viewTableOrViewAccessPolicy(params); + const policy = await viewTableOrViewAccessPolicy( + projectId, + dataset.id, + table.id + ); - assert.ok(bigQueryStub.dataset.calledWith(params.datasetId)); - assert.ok(bigQueryStub.dataset().table.calledWith(params.resourceName)); + // Verificar que la política existe + assert.ok(policy, 'Policy should be defined'); - assert.ok( - consoleLogSpy.calledWith( - `Access Policy details for table or view '${params.resourceName}':` - ) - ); - assert.ok( - consoleLogSpy.calledWith( - `Bindings: ${JSON.stringify(samplePolicy.bindings, null, 2)}` - ) + // Verificar que bindings existe y es un array + assert.ok(Array.isArray(policy.bindings), 'Bindings should be an array'); + + // En una política nueva, bindings debería estar vacío + assert.strictEqual( + policy.bindings.length, + 0, + 'Bindings list should be empty' ); - assert.ok(consoleLogSpy.calledWith(`etag: ${samplePolicy.etag}`)); - assert.ok(consoleLogSpy.calledWith(`version: ${samplePolicy.version}`)); - assert.deepEqual(policy, samplePolicy); + // Verificar que etag existe, pero no validar su valor exacto + assert.ok(policy.etag, 'Etag should be defined'); }); - it('should throw error when project ID is missing', async () => { - delete process.env.GOOGLE_CLOUD_PROJECT; + it('should view view access policies', async () => { + const projectId = await getProjectId(); + const dataset = await getDataset(); + const view = await getView(); - try { - await viewTableOrViewAccessPolicy({}); - assert.fail('Should have thrown an error'); - } catch (error) { - assert.include(error.message, 'Project ID is required'); - } - }); + const policy = await viewTableOrViewAccessPolicy( + projectId, + dataset.id, + view.id + ); + + // Verificar que la política existe + assert.ok(policy, 'Policy should be defined'); - it('should handle errors gracefully', async () => { - const errorMessage = 'Table not found'; + // Verificar que bindings existe y es un array + assert.ok(Array.isArray(policy.bindings), 'Bindings should be an array'); - bigQueryStub.dataset.returns({ - table: sinon.stub().returns({ - getIamPolicy: sinon.stub().rejects(new Error(errorMessage)), - }), - }); + // En una política nueva, bindings debería estar vacío + assert.strictEqual( + policy.bindings.length, + 0, + 'Bindings list should be empty' + ); - try { - await viewTableOrViewAccessPolicy({ - projectId: 'test-project', - datasetId: 'test_dataset', - resourceName: 'non_existent_table', - }); - assert.fail('Should have thrown an error'); - } catch (error) { - assert.include(error.message, errorMessage); - } + // Verificar que etag existe, pero no validar su valor exacto + assert.ok(policy.etag, 'Etag should be defined'); }); }); diff --git a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js index 67f97caf1c..6bf6918254 100644 --- a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js +++ b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js @@ -14,60 +14,52 @@ 'use strict'; +// Imports the Google Cloud client library const {BigQuery} = require('@google-cloud/bigquery'); // [START bigquery_view_table_or_view_access_policy] /** - * View access policies for a BigQuery table or view + * View access policy for a BigQuery table or view * - * @param {object} [overrideValues] Optional parameters to override defaults - * @param {string} [overrideValues.projectId] Google Cloud project ID - * @param {string} [overrideValues.datasetId] Dataset ID where the table or view is located - * @param {string} [overrideValues.resourceName] Table or view name to get the access policy - * @throws {Error} If required parameters are missing or if there's an API error + * @param {string} projectId - Google Cloud Platform project. + * @param {string} datasetId - Dataset where the table or view is. + * @param {string} resourceName - Table or view name to get the access policy. + * @returns {Promise} - The IAM policy object */ -async function viewTableOrViewAccessPolicy(overrideValues = {}) { - // Initialize default values - const projectId = - overrideValues.projectId || process.env.GOOGLE_CLOUD_PROJECT; - const datasetId = overrideValues.datasetId || 'my_new_dataset'; - const resourceName = overrideValues.resourceName || 'my_table'; +async function viewTableOrViewAccessPolicy(projectId, datasetId, resourceName) { + // TODO(developer): Update and un-comment below lines + // Google Cloud Platform project. + // projectId = "my_project_id"; + // Dataset where the table or view is. + // datasetId = "my_dataset_id"; + // Table or view name to get the access policy. + // resourceName = "my_table_name"; - if (!projectId) { - throw new Error( - 'Project ID is required. Set it in overrideValues or GOOGLE_CLOUD_PROJECT environment variable.' - ); - } - - try { - // Instantiate BigQuery client - const bigquery = new BigQuery({projectId}); + // Instantiate a client. + const client = new BigQuery(); - // Get the IAM access policy for the table or view - const [policy] = await bigquery - .dataset(datasetId) - .table(resourceName) - .getIamPolicy(); + // Get the table reference. + const dataset = client.dataset(datasetId); + const table = dataset.table(resourceName); - // Show policy details - console.log(`Access Policy details for table or view '${resourceName}':`); - console.log(`Bindings: ${JSON.stringify(policy.bindings, null, 2)}`); - console.log(`etag: ${policy.etag}`); - console.log(`version: ${policy.version}`); + // Get the IAM access policy for the table or view. + const [policy] = await table.getIamPolicy(); - return policy; - } catch (error) { - console.error(`Error viewing table/view access policy: ${error.message}`); - throw error; + // Initialize bindings if they don't exist + if (!policy.bindings) { + policy.bindings = []; } -} -// [END bigquery_view_table_or_view_access_policy] -// If this file is run directly, execute the function with default values -if (require.main === module) { - viewTableOrViewAccessPolicy().catch(console.error); + // Show policy details. + // Find more details for the Policy object here: + // https://cloud.google.com/bigquery/docs/reference/rest/v2/Policy + console.log(`Access Policy details for table or view '${resourceName}'.`); + console.log(`Bindings: ${JSON.stringify(policy.bindings, null, 2)}`); + console.log(`etag: ${policy.etag}`); + console.log(`Version: ${policy.version}`); + + return policy; } +// [END bigquery_view_table_or_view_access_policy] -module.exports = { - viewTableOrViewAccessPolicy, -}; +module.exports = viewTableOrViewAccessPolicy; From ec3c9900f7c7e1b80dd74ba70c608add26e2aa60 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Mon, 3 Mar 2025 21:09:47 +0000 Subject: [PATCH 24/29] fix(bigquery): Update samples and tests according to PR comments --- bigquery/cloud-client/grantAccessToDataset.js | 50 +++++++++---------- .../cloud-client/grantAccessToTableOrView.js | 34 ++++++------- bigquery/cloud-client/revokeDatasetAccess.js | 39 ++++++++------- .../cloud-client/revokeTableOrViewAccess.js | 32 ++++++------ bigquery/cloud-client/test/config.js | 21 +++++--- .../test/revokeDatasetAccess.test.js | 24 ++++++--- .../cloud-client/viewDatasetAccessPolicy.js | 6 +-- .../viewTableOrViewAccessPolicy.js | 22 ++++---- 8 files changed, 123 insertions(+), 105 deletions(-) diff --git a/bigquery/cloud-client/grantAccessToDataset.js b/bigquery/cloud-client/grantAccessToDataset.js index b855fbb4f4..81112a8810 100644 --- a/bigquery/cloud-client/grantAccessToDataset.js +++ b/bigquery/cloud-client/grantAccessToDataset.js @@ -26,76 +26,76 @@ async function grantAccessToDataset(datasetId, entityId, role) { // [START bigquery_grant_access_to_dataset] const {BigQuery} = require('@google-cloud/bigquery'); + // Define enum for HTTP codes + const HTTP_STATUS = { + PRECONDITION_FAILED: 412, + }; + // TODO(developer): Update and un-comment below lines - // ID of the dataset to revoke access to. - // datasetId = "my_project_id.my_dataset"; + // ID of the dataset to revoke access to + // datasetId = "my_project_id.my_dataset_name"; - // ID of the user or group from whom you are adding access. + // ID of the user or group from whom you are adding access // Alternatively, the JSON REST API representation of the entity, - // such as a view's table reference. + // such as a view's table reference // entityId = "user-or-group-to-add@example.com"; // One of the "Basic roles for datasets" described here: // https://cloud.google.com/bigquery/docs/access-control-basic-roles#dataset-basic-roles // role = "READER"; - // Type of entity you are granting access to. + // Type of entity you are granting access to // Find allowed allowed entity type names here: - // https://cloud.google.com/python/docs/reference/bigquery/latest/enums#class-googlecloudbigqueryenumsentitytypesvalue - // In this case, we're using the equivalent of GROUP_BY_EMAIL + // https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets#resource:-dataset + // In this case, we're using groupByEmail const entityType = 'groupByEmail'; - // Instantiate a client. + // Instantiate a client const client = new BigQuery(); try { - // Get a reference to the dataset. + // Get a reference to the dataset const [dataset] = await client.dataset(datasetId).get(); - // The 'access entries' list is immutable. Create a copy for modifications. + // The 'access entries' list is immutable. Create a copy for modifications const entries = Array.isArray(dataset.metadata.access) ? [...dataset.metadata.access] : []; - // Append an AccessEntry to grant the role to a dataset. - // Find more details about the AccessEntry object in the BigQuery documentation + // Append an AccessEntry to grant the role to a dataset + // Find more details about the AccessEntry object in the BigQuery documentation: + // https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.dataset.AccessEntry entries.push({ role: role, [entityType]: entityId, }); - // Assign the list of AccessEntries back to the dataset. + // Assign the list of AccessEntries back to the dataset const metadata = { access: entries, }; // Update will only succeed if the dataset - // has not been modified externally since retrieval. + // has not been modified externally since retrieval // // See the BigQuery client library documentation for more details on metadata updates + // https://cloud.google.com/bigquery/docs/updating-model-metadata - // Update just the 'access entries' property of the dataset. + // Update just the 'access entries' property of the dataset const [updatedDataset] = await client .dataset(datasetId) .setMetadata(metadata); - // Show a success message. - const fullDatasetId = - updatedDataset && - updatedDataset.metadata && - updatedDataset.metadata.datasetReference - ? `${updatedDataset.metadata.datasetReference.projectId}.${updatedDataset.metadata.datasetReference.datasetId}` - : datasetId; - + // Show a success message console.log( `Role '${role}' granted for entity '${entityId}'` + - ` in dataset '${fullDatasetId}'.` + ` in dataset '${datasetId}'.` ); return updatedDataset.access; } catch (error) { - if (error.code === 412) { + if (error.code === HTTP_STATUS.PRECONDITION_FAILED) { // A read-modify-write error (PreconditionFailed equivalent) console.error( `Dataset '${datasetId}' was modified remotely before this update. ` + diff --git a/bigquery/cloud-client/grantAccessToTableOrView.js b/bigquery/cloud-client/grantAccessToTableOrView.js index 549e1b9702..5d1e05e0b1 100644 --- a/bigquery/cloud-client/grantAccessToTableOrView.js +++ b/bigquery/cloud-client/grantAccessToTableOrView.js @@ -15,13 +15,13 @@ const {BigQuery} = require('@google-cloud/bigquery'); /** - * Grants access to a BigQuery table or view for a specified principal. + * Grants access to a BigQuery table or view for a specified principal * - * @param {string} projectId - Google Cloud Platform project ID - * @param {string} datasetId - Dataset where the table or view is - * @param {string} resourceName - Table or view name to get the access policy - * @param {string} principalId - The principal requesting access to the table or view - * @param {string} role - Role to assign to the member + * @param {string} projectId Google Cloud Platform project ID + * @param {string} datasetId Dataset where the table or view is + * @param {string} resourceName Table or view name to get the access policy + * @param {string} principalId The principal requesting access to the table or view + * @param {string} role Role to assign to the member * @returns {Promise} The updated policy bindings */ async function grantAccessToTableOrView( @@ -34,31 +34,31 @@ async function grantAccessToTableOrView( // [START bigquery_grant_access_to_table_or_view] // TODO(developer): Update and un-comment below lines - // Google Cloud Platform project. + // Google Cloud Platform project // projectId = "my_project_id" - // Dataset where the table or view is. + // Dataset where the table or view is // datasetId = "my_dataset" - // Table or view name to get the access policy. + // Table or view name to get the access policy // resourceName = "my_table" - // The principal requesting access to the table or view. + // The principal requesting access to the table or view // Find more details about principal identifiers here: // https://cloud.google.com/iam/docs/principal-identifiers // principalId = "user:bob@example.com" - // Role to assign to the member. + // Role to assign to the member // role = "roles/bigquery.dataViewer" - // Instantiate a client. + // Instantiate a client const client = new BigQuery(); - // Get the table reference. + // Get the table reference const dataset = client.dataset(datasetId); const table = dataset.table(resourceName); - // Get the IAM access policy for the table or view. + // Get the IAM access policy for the table or view const [policy] = await table.getIamPolicy(); // Initialize bindings if they do not exist @@ -66,8 +66,8 @@ async function grantAccessToTableOrView( policy.bindings = []; } - // To grant access to a table or view. - // add bindings to the Table or View policy. + // To grant access to a table or view + // add bindings to the Table or View policy // // Find more details about Policy and Binding objects here: // https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Policy @@ -81,7 +81,7 @@ async function grantAccessToTableOrView( // Set the IAM access policy with updated bindings const [updatedPolicy] = await table.setIamPolicy(policy); - // Show a success message. + // Show a success message console.log( `Role '${role}' granted for principal '${principalId}' on resource '${datasetId}.${resourceName}'.` ); diff --git a/bigquery/cloud-client/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js index e151130ef1..9b380a1e57 100644 --- a/bigquery/cloud-client/revokeDatasetAccess.js +++ b/bigquery/cloud-client/revokeDatasetAccess.js @@ -14,41 +14,47 @@ 'use strict'; +// [START bigquery_revoke_dataset_access] + const {BigQuery} = require('@google-cloud/bigquery'); -// [START bigquery_revoke_dataset_access] +// Define enum for HTTP codes +const HTTP_STATUS = { + PRECONDITION_FAILED: 412, +}; + /** - * Revokes access to a dataset for a specified entity. + * Revokes access to a dataset for a specified entity * - * @param {string} datasetId - ID of the dataset to revoke access to. - * @param {string} entityId - ID of the user or group from whom you are revoking access. + * @param {string} datasetId ID of the dataset to revoke access to + * @param {string} entityId ID of the user or group from whom you are revoking access * Alternatively, the JSON REST API representation of the entity, - * such as a view's table reference. + * such as a view's table reference * @returns {Promise} A promise that resolves to the updated access entries */ async function revokeDatasetAccess(datasetId, entityId) { - // Import the Google Cloud client library. + // Create a client const bigquery = new BigQuery(); // TODO (developer): Update and un-comment below lines - // ID of the dataset to revoke access to. + // ID of the dataset to revoke access to // datasetId = "your-project.your_dataset" - // ID of the user or group from whom you are revoking access. + // ID of the user or group from whom you are revoking access // Alternatively, the JSON REST API representation of the entity, - // such as a view's table reference. + // such as a view's table reference // entityId = "user-or-group-to-remove@example.com" // Get a reference to the dataset. const [dataset] = await bigquery.dataset(datasetId).get(); - // To revoke access to a dataset, remove elements from the access list. + // To revoke access to a dataset, remove elements from the access list // // See the BigQuery client library documentation for more details on access entries // Filter access entries to exclude entries matching the specified entity_id - // and assign a new list back to the access list. + // and assign a new list back to the access list dataset.metadata.access = dataset.metadata.access.filter(entry => { // Check for entity_id (specific match) if (entry.entity_id === entityId) { @@ -79,21 +85,16 @@ async function revokeDatasetAccess(datasetId, entityId) { }); // Update will only succeed if the dataset - // has not been modified externally since retrieval. + // has not been modified externally since retrieval try { - // Update just the access entries property of the dataset. + // Update just the access entries property of the dataset const [updatedDataset] = await dataset.setMetadata(dataset.metadata); - const fullDatasetId = `${dataset.parent.projectId}.${dataset.id}`; - console.log( - `Revoked dataset access for '${entityId}' to dataset '${fullDatasetId}'.` - ); - return updatedDataset.access; } catch (error) { // Check if it's a precondition failed error (a read-modify-write error) - if (error.code === 412) { + if (error.code === HTTP_STATUS.PRECONDITION_FAILED) { console.log( `Dataset '${dataset.id}' was modified remotely before this update. ` + 'Fetch the latest version and retry.' diff --git a/bigquery/cloud-client/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js index fb9d49171e..fe8994c780 100644 --- a/bigquery/cloud-client/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/revokeTableOrViewAccess.js @@ -14,11 +14,11 @@ /** * Revokes access to a BigQuery table or view - * @param {string} projectId - The ID of the Google Cloud project - * @param {string} datasetId - The ID of the dataset containing the table/view - * @param {string} resourceName - The ID of the table or view - * @param {string} [roleToRemove=null] - Optional. Specific role to revoke - * @param {string} [principalToRemove=null] - Optional. Specific principal to revoke access from + * @param {string} projectId The ID of the Google Cloud project + * @param {string} datasetId The ID of the dataset containing the table/view + * @param {string} resourceName The ID of the table or view + * @param {string} [roleToRemove=null] Optional. Specific role to revoke + * @param {string} [principalToRemove=null] Optional. Specific principal to revoke access from * @returns {Promise} The updated IAM policy */ async function revokeAccessToTableOrView( @@ -36,29 +36,29 @@ async function revokeAccessToTableOrView( // Google Cloud Platform project. // projectId = "my_project_id" - // Dataset where the table or view is. + // Dataset where the table or view is // datasetId = "my_dataset" - // Table or view name to get the access policy. + // Table or view name to get the access policy // resourceName = "my_table" - // (Optional) Role to remove from the table or view. + // (Optional) Role to remove from the table or view // roleToRemove = "roles/bigquery.dataViewer" - // (Optional) Principal to remove from the table or view. + // (Optional) Principal to remove from the table or view // principalToRemove = "user:alice@example.com" // Find more information about roles and principals (refered as members) here: // https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Binding - // Instantiate a client. + // Instantiate a client const client = new BigQuery(); - // Get the table reference. + // Get the table reference const dataset = client.dataset(datasetId); const table = dataset.table(resourceName); - // Get the IAM access policy for the table or view. + // Get the IAM access policy for the table or view const [policy] = await table.getIamPolicy(); // Initialize bindings of they do not exist @@ -67,22 +67,22 @@ async function revokeAccessToTableOrView( } // To revoke access to a table or view, - // remove bindings from the Table or View policy. + // remove bindings from the Table or View policy // // Find more details about Policy objects here: // https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Policy if (roleToRemove) { // Filter out all bindings with the `roleToRemove` - // and assign a new list back to the policy bindings. + // and assign a new list back to the policy bindings policy.bindings = policy.bindings.filter(b => b.role !== roleToRemove); } if (principalToRemove) { - // Create a copy to match original code structure. + // Create a copy to match original code structure const bindings = [...policy.bindings]; - // Filter out the principal from each binding. + // Filter out the principal from each binding for (const binding of bindings) { if (binding.members) { binding.members = binding.members.filter(m => m !== principalToRemove); diff --git a/bigquery/cloud-client/test/config.js b/bigquery/cloud-client/test/config.js index 89ff4fa0fa..4df0feedc2 100644 --- a/bigquery/cloud-client/test/config.js +++ b/bigquery/cloud-client/test/config.js @@ -15,6 +15,11 @@ const {BigQuery} = require('@google-cloud/bigquery'); const crypto = require('crypto'); +// Define enum for HTTP codes +const HTTP_STATUS = { + NOT_FOUND: 404, +}; + // Generate a unique prefix using a random UUID to ensure uniqueness across test runs function createRandomPrefix() { return `nodejs_test_${crypto.randomUUID().replace(/-/g, '').substring(0, 8)}`; @@ -23,7 +28,7 @@ function createRandomPrefix() { const PREFIX = createRandomPrefix(); console.log(`Generated test prefix: ${PREFIX}`); -const ENTITY_ID = 'example-analyst-group@google.com'; // Group account +const ENTITY_ID = 'cloud-developer-relations@google.com'; // Group account const DATASET_ID = `${PREFIX}_cloud_client`; const TABLE_NAME = `${PREFIX}_table`; const VIEW_NAME = `${PREFIX}_view`; @@ -65,9 +70,9 @@ const getDataset = async () => { [sharedDataset] = await client.dataset(DATASET_ID).get(); console.log(`Using existing dataset: ${DATASET_ID}`); } catch (err) { - if (err.code === 404) { + if (err.code === HTTP_STATUS.NOT_FOUND) { // If dataset doesn't exist, create it - console.log(`Creating dataset: ${DATASET_ID}`); + console.log(`Creating dataset: ${DATASET_ID}...`); [sharedDataset] = await client.createDataset(DATASET_ID); resourcesCreated = true; console.log(`Created dataset: ${DATASET_ID}`); @@ -103,9 +108,9 @@ const getTable = async () => { .get(); console.log(`Using existing table: ${TABLE_NAME}`); } catch (err) { - if (err.code === 404) { + if (err.code === HTTP_STATUS.NOT_FOUND) { // If table doesn't exist, create it - console.log(`Creating table: ${TABLE_NAME}`); + console.log(`Creating table: ${TABLE_NAME}...`); [sharedTable] = await client .dataset(DATASET_ID) .createTable(TABLE_NAME, tableOptions); @@ -142,9 +147,9 @@ const getView = async () => { [sharedView] = await client.dataset(DATASET_ID).table(VIEW_NAME).get(); console.log(`Using existing view: ${VIEW_NAME}`); } catch (err) { - if (err.code === 404) { + if (err.code === HTTP_STATUS.NOT_FOUND) { // If view doesn't exist, create it - console.log(`Creating view: ${VIEW_NAME}`); + console.log(`Creating view: ${VIEW_NAME}...`); [sharedView] = await client .dataset(DATASET_ID) .createTable(VIEW_NAME, viewOptions); @@ -194,7 +199,7 @@ const cleanupResources = async () => { await sharedClient.dataset(DATASET_ID).delete({force: true}); console.log(`Successfully deleted dataset: ${DATASET_ID}`); } catch (err) { - if (err.code !== 404) { + if (err.code !== HTTP_STATUS.NOT_FOUND) { console.error(`Error deleting dataset: ${err.message}`); } else { console.log(`Dataset ${DATASET_ID} already deleted or not found`); diff --git a/bigquery/cloud-client/test/revokeDatasetAccess.test.js b/bigquery/cloud-client/test/revokeDatasetAccess.test.js index 1a6abb5126..e9b1169857 100644 --- a/bigquery/cloud-client/test/revokeDatasetAccess.test.js +++ b/bigquery/cloud-client/test/revokeDatasetAccess.test.js @@ -53,9 +53,15 @@ describe('revokeDatasetAccess', () => { // Create a set of all entity IDs and email addresses to check const datasetEntityIds = new Set(); datasetAccessEntries.forEach(entry => { - if (entry.entity_id) datasetEntityIds.add(entry.entity_id); - if (entry.userByEmail) datasetEntityIds.add(entry.userByEmail); - if (entry.groupByEmail) datasetEntityIds.add(entry.groupByEmail); + if (entry.entity_id) { + datasetEntityIds.add(entry.entity_id); + } + if (entry.userByEmail) { + datasetEntityIds.add(entry.userByEmail); + } + if (entry.groupByEmail) { + datasetEntityIds.add(entry.groupByEmail); + } }); // Check if our entity ID is in the set @@ -73,9 +79,15 @@ describe('revokeDatasetAccess', () => { // Check that the entity no longer has access const updatedEntityIds = new Set(); newAccessEntries.forEach(entry => { - if (entry.entity_id) updatedEntityIds.add(entry.entity_id); - if (entry.userByEmail) updatedEntityIds.add(entry.userByEmail); - if (entry.groupByEmail) updatedEntityIds.add(entry.groupByEmail); + if (entry.entity_id) { + updatedEntityIds.add(entry.entity_id); + } + if (entry.userByEmail) { + updatedEntityIds.add(entry.userByEmail); + } + if (entry.groupByEmail) { + updatedEntityIds.add(entry.groupByEmail); + } }); const stillHasAccess = updatedEntityIds.has(entityId); diff --git a/bigquery/cloud-client/viewDatasetAccessPolicy.js b/bigquery/cloud-client/viewDatasetAccessPolicy.js index bb8b7c6de3..acc0ec99a4 100644 --- a/bigquery/cloud-client/viewDatasetAccessPolicy.js +++ b/bigquery/cloud-client/viewDatasetAccessPolicy.js @@ -16,7 +16,7 @@ /** * View access policies for a BigQuery dataset - * @param {string} datasetId - Dataset ID to view access policies for + * @param {string} datasetId Dataset ID to view access policies for * @returns {Array} List of access entries */ function viewDatasetAccessPolicy(datasetId) { @@ -24,7 +24,7 @@ function viewDatasetAccessPolicy(datasetId) { // Import the Google Cloud client library. const {BigQuery} = require('@google-cloud/bigquery'); - // Instantiate a client. + // Create a client const bigquery = new BigQuery(); // TODO (developer): Update and un-comment below lines @@ -38,7 +38,7 @@ function viewDatasetAccessPolicy(datasetId) { const accessEntries = metadata.access || []; // Show the list of AccessEntry objects. - // More details about the AccessEntry object in the BigQuery documentation. + // More details about the AccessEntry object in the BigQuery documentation console.log( `${accessEntries.length} Access entries in dataset '${datasetId}':` ); diff --git a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js index 6bf6918254..978f0b4132 100644 --- a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js +++ b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js @@ -21,28 +21,28 @@ const {BigQuery} = require('@google-cloud/bigquery'); /** * View access policy for a BigQuery table or view * - * @param {string} projectId - Google Cloud Platform project. - * @param {string} datasetId - Dataset where the table or view is. - * @param {string} resourceName - Table or view name to get the access policy. - * @returns {Promise} - The IAM policy object + * @param {string} projectId Google Cloud Platform project + * @param {string} datasetId Dataset where the table or view is + * @param {string} resourceName Table or view name to get the access policy + * @returns {Promise} The IAM policy object */ async function viewTableOrViewAccessPolicy(projectId, datasetId, resourceName) { // TODO(developer): Update and un-comment below lines - // Google Cloud Platform project. + // Google Cloud Platform project // projectId = "my_project_id"; - // Dataset where the table or view is. + // Dataset where the table or view is // datasetId = "my_dataset_id"; - // Table or view name to get the access policy. + // Table or view name to get the access policy // resourceName = "my_table_name"; - // Instantiate a client. + // Instantiate a client const client = new BigQuery(); - // Get the table reference. + // Get the table reference const dataset = client.dataset(datasetId); const table = dataset.table(resourceName); - // Get the IAM access policy for the table or view. + // Get the IAM access policy for the table or view const [policy] = await table.getIamPolicy(); // Initialize bindings if they don't exist @@ -50,7 +50,7 @@ async function viewTableOrViewAccessPolicy(projectId, datasetId, resourceName) { policy.bindings = []; } - // Show policy details. + // Show policy details // Find more details for the Policy object here: // https://cloud.google.com/bigquery/docs/reference/rest/v2/Policy console.log(`Access Policy details for table or view '${resourceName}'.`); From 7c452287894e5223d7d56e0c36a641787675ebb5 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Wed, 5 Mar 2025 15:44:35 +0000 Subject: [PATCH 25/29] fix(bigquery): Update samples and tests format issues --- bigquery/cloud-client/grantAccessToDataset.js | 29 +++++++-------- .../cloud-client/grantAccessToTableOrView.js | 28 +++++++------- bigquery/cloud-client/revokeDatasetAccess.js | 37 ++++++++++--------- .../cloud-client/revokeTableOrViewAccess.js | 34 +++++++++-------- .../cloud-client/viewDatasetAccessPolicy.js | 5 ++- .../viewTableOrViewAccessPolicy.js | 22 +++++------ 6 files changed, 80 insertions(+), 75 deletions(-) diff --git a/bigquery/cloud-client/grantAccessToDataset.js b/bigquery/cloud-client/grantAccessToDataset.js index 81112a8810..1c73b92060 100644 --- a/bigquery/cloud-client/grantAccessToDataset.js +++ b/bigquery/cloud-client/grantAccessToDataset.js @@ -31,39 +31,39 @@ async function grantAccessToDataset(datasetId, entityId, role) { PRECONDITION_FAILED: 412, }; - // TODO(developer): Update and un-comment below lines + // TODO(developer): Update and un-comment below lines. - // ID of the dataset to revoke access to + // ID of the dataset to revoke access to. // datasetId = "my_project_id.my_dataset_name"; - // ID of the user or group from whom you are adding access + // ID of the user or group from whom you are adding access. // Alternatively, the JSON REST API representation of the entity, - // such as a view's table reference + // such as a view's table reference. // entityId = "user-or-group-to-add@example.com"; // One of the "Basic roles for datasets" described here: // https://cloud.google.com/bigquery/docs/access-control-basic-roles#dataset-basic-roles // role = "READER"; - // Type of entity you are granting access to + // Type of entity you are granting access to. // Find allowed allowed entity type names here: // https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets#resource:-dataset // In this case, we're using groupByEmail const entityType = 'groupByEmail'; - // Instantiate a client + // Instantiate a client. const client = new BigQuery(); try { - // Get a reference to the dataset + // Get a reference to the dataset. const [dataset] = await client.dataset(datasetId).get(); - // The 'access entries' list is immutable. Create a copy for modifications + // The 'access entries' list is immutable. Create a copy for modifications. const entries = Array.isArray(dataset.metadata.access) ? [...dataset.metadata.access] : []; - // Append an AccessEntry to grant the role to a dataset + // Append an AccessEntry to grant the role to a dataset. // Find more details about the AccessEntry object in the BigQuery documentation: // https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.dataset.AccessEntry entries.push({ @@ -71,23 +71,23 @@ async function grantAccessToDataset(datasetId, entityId, role) { [entityType]: entityId, }); - // Assign the list of AccessEntries back to the dataset + // Assign the list of AccessEntries back to the dataset. const metadata = { access: entries, }; // Update will only succeed if the dataset - // has not been modified externally since retrieval + // has not been modified externally since retrieval. // // See the BigQuery client library documentation for more details on metadata updates - // https://cloud.google.com/bigquery/docs/updating-model-metadata + // https://cloud.google.com/nodejs/docs/reference/bigquery/latest - // Update just the 'access entries' property of the dataset + // Update just the 'access entries' property of the dataset. const [updatedDataset] = await client .dataset(datasetId) .setMetadata(metadata); - // Show a success message + // Show a success message. console.log( `Role '${role}' granted for entity '${entityId}'` + ` in dataset '${datasetId}'.` @@ -96,7 +96,6 @@ async function grantAccessToDataset(datasetId, entityId, role) { return updatedDataset.access; } catch (error) { if (error.code === HTTP_STATUS.PRECONDITION_FAILED) { - // A read-modify-write error (PreconditionFailed equivalent) console.error( `Dataset '${datasetId}' was modified remotely before this update. ` + 'Fetch the latest version and retry.' diff --git a/bigquery/cloud-client/grantAccessToTableOrView.js b/bigquery/cloud-client/grantAccessToTableOrView.js index 5d1e05e0b1..562f161387 100644 --- a/bigquery/cloud-client/grantAccessToTableOrView.js +++ b/bigquery/cloud-client/grantAccessToTableOrView.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -const {BigQuery} = require('@google-cloud/bigquery'); +'use strict'; /** * Grants access to a BigQuery table or view for a specified principal @@ -32,42 +32,44 @@ async function grantAccessToTableOrView( role ) { // [START bigquery_grant_access_to_table_or_view] + const {BigQuery} = require('@google-cloud/bigquery'); + // TODO(developer): Update and un-comment below lines - // Google Cloud Platform project + // Google Cloud Platform project. // projectId = "my_project_id" - // Dataset where the table or view is + // Dataset where the table or view is. // datasetId = "my_dataset" - // Table or view name to get the access policy + // Table or view name to get the access policy. // resourceName = "my_table" - // The principal requesting access to the table or view + // The principal requesting access to the table or view. // Find more details about principal identifiers here: // https://cloud.google.com/iam/docs/principal-identifiers // principalId = "user:bob@example.com" - // Role to assign to the member + // Role to assign to the member. // role = "roles/bigquery.dataViewer" - // Instantiate a client + // Instantiate a client. const client = new BigQuery(); - // Get the table reference + // Get the table reference. const dataset = client.dataset(datasetId); const table = dataset.table(resourceName); - // Get the IAM access policy for the table or view + // Get the IAM access policy for the table or view. const [policy] = await table.getIamPolicy(); - // Initialize bindings if they do not exist + // Initialize bindings if they do not exist. if (!policy.bindings) { policy.bindings = []; } // To grant access to a table or view - // add bindings to the Table or View policy + // add bindings to the Table or View policy. // // Find more details about Policy and Binding objects here: // https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Policy @@ -78,10 +80,10 @@ async function grantAccessToTableOrView( }; policy.bindings.push(binding); - // Set the IAM access policy with updated bindings + // Set the IAM access policy with updated bindings. const [updatedPolicy] = await table.setIamPolicy(policy); - // Show a success message + // Show a success message. console.log( `Role '${role}' granted for principal '${principalId}' on resource '${datasetId}.${resourceName}'.` ); diff --git a/bigquery/cloud-client/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js index 9b380a1e57..b2aa9c3b1f 100644 --- a/bigquery/cloud-client/revokeDatasetAccess.js +++ b/bigquery/cloud-client/revokeDatasetAccess.js @@ -14,10 +14,6 @@ 'use strict'; -// [START bigquery_revoke_dataset_access] - -const {BigQuery} = require('@google-cloud/bigquery'); - // Define enum for HTTP codes const HTTP_STATUS = { PRECONDITION_FAILED: 412, @@ -33,30 +29,35 @@ const HTTP_STATUS = { * @returns {Promise} A promise that resolves to the updated access entries */ async function revokeDatasetAccess(datasetId, entityId) { - // Create a client - const bigquery = new BigQuery(); + // [START bigquery_revoke_dataset_access] + // Imports the Google Cloud client library. + const {BigQuery} = require('@google-cloud/bigquery'); - // TODO (developer): Update and un-comment below lines + // TODO (developer): Update and un-comment below lines. - // ID of the dataset to revoke access to + // ID of the dataset to revoke access to. // datasetId = "your-project.your_dataset" - // ID of the user or group from whom you are revoking access + // ID of the user or group from whom you are revoking access. // Alternatively, the JSON REST API representation of the entity, - // such as a view's table reference + // such as a view's table reference. // entityId = "user-or-group-to-remove@example.com" + // Instantiate a client. + const bigquery = new BigQuery(); + // Get a reference to the dataset. const [dataset] = await bigquery.dataset(datasetId).get(); // To revoke access to a dataset, remove elements from the access list // // See the BigQuery client library documentation for more details on access entries + // https://cloud.google.com/nodejs/docs/reference/secret-manager/4.1.4 // Filter access entries to exclude entries matching the specified entity_id - // and assign a new list back to the access list + // and assign a new list back to the access list. dataset.metadata.access = dataset.metadata.access.filter(entry => { - // Check for entity_id (specific match) + // Check for entity_id (specific match). if (entry.entity_id === entityId) { console.log( `Found matching entity_id: ${entry.entity_id}, removing entry` @@ -64,7 +65,7 @@ async function revokeDatasetAccess(datasetId, entityId) { return false; } - // Check for userByEmail field + // Check for userByEmail field. if (entry.userByEmail === entityId) { console.log( `Found matching userByEmail: ${entry.userByEmail}, removing entry` @@ -72,7 +73,7 @@ async function revokeDatasetAccess(datasetId, entityId) { return false; } - // Check for groupByEmail field + // Check for groupByEmail field. if (entry.groupByEmail === entityId) { console.log( `Found matching groupByEmail: ${entry.groupByEmail}, removing entry` @@ -80,20 +81,20 @@ async function revokeDatasetAccess(datasetId, entityId) { return false; } - // Keep all other entries + // Keep all other entries. return true; }); // Update will only succeed if the dataset - // has not been modified externally since retrieval + // has not been modified externally since retrieval. try { - // Update just the access entries property of the dataset + // Update just the access entries property of the dataset. const [updatedDataset] = await dataset.setMetadata(dataset.metadata); return updatedDataset.access; } catch (error) { - // Check if it's a precondition failed error (a read-modify-write error) + // Check if it's a precondition failed error (a read-modify-write error). if (error.code === HTTP_STATUS.PRECONDITION_FAILED) { console.log( `Dataset '${dataset.id}' was modified remotely before this update. ` + diff --git a/bigquery/cloud-client/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js index fe8994c780..6f9165e073 100644 --- a/bigquery/cloud-client/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/revokeTableOrViewAccess.js @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +'use strict'; + /** * Revokes access to a BigQuery table or view * @param {string} projectId The ID of the Google Cloud project @@ -29,77 +31,77 @@ async function revokeAccessToTableOrView( principalToRemove = null ) { // [START bigquery_revoke_access_to_table_or_view] - // Imports the Google Cloud client library + // Imports the Google Cloud client library. const {BigQuery} = require('@google-cloud/bigquery'); // TODO (developer): Update and un-comment below lines // Google Cloud Platform project. // projectId = "my_project_id" - // Dataset where the table or view is + // Dataset where the table or view is. // datasetId = "my_dataset" - // Table or view name to get the access policy + // Table or view name to get the access policy. // resourceName = "my_table" - // (Optional) Role to remove from the table or view + // (Optional) Role to remove from the table or view. // roleToRemove = "roles/bigquery.dataViewer" - // (Optional) Principal to remove from the table or view + // (Optional) Principal to remove from the table or view. // principalToRemove = "user:alice@example.com" // Find more information about roles and principals (refered as members) here: // https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Binding - // Instantiate a client + // Instantiate a client. const client = new BigQuery(); - // Get the table reference + // Get the table reference. const dataset = client.dataset(datasetId); const table = dataset.table(resourceName); - // Get the IAM access policy for the table or view + // Get the IAM access policy for the table or view. const [policy] = await table.getIamPolicy(); - // Initialize bindings of they do not exist + // Initialize bindings of they do not exist. if (!policy.bindings) { policy.bindings = []; } // To revoke access to a table or view, - // remove bindings from the Table or View policy + // remove bindings from the Table or View policy. // // Find more details about Policy objects here: // https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Policy if (roleToRemove) { // Filter out all bindings with the `roleToRemove` - // and assign a new list back to the policy bindings + // and assign a new list back to the policy bindings. policy.bindings = policy.bindings.filter(b => b.role !== roleToRemove); } if (principalToRemove) { - // Create a copy to match original code structure + // The `bindings` list is immutable. Create a copy for modifications. const bindings = [...policy.bindings]; - // Filter out the principal from each binding + // Filter out the principal from each binding. for (const binding of bindings) { if (binding.members) { binding.members = binding.members.filter(m => m !== principalToRemove); } } - // Filter out bindings with empty members + // Filter out bindings with empty members. policy.bindings = bindings.filter( binding => binding.members && binding.members.length > 0 ); } try { - // Set the IAM access policy with updated bindings + // Set the IAM access policy with updated bindings. await table.setIamPolicy(policy); - // Get the policy again to confirm it's set correctly + // Get the policy again to confirm it's set correctly. const [verifiedPolicy] = await table.getIamPolicy(); if (verifiedPolicy && verifiedPolicy.bindings) { diff --git a/bigquery/cloud-client/viewDatasetAccessPolicy.js b/bigquery/cloud-client/viewDatasetAccessPolicy.js index acc0ec99a4..aa8dd82d4a 100644 --- a/bigquery/cloud-client/viewDatasetAccessPolicy.js +++ b/bigquery/cloud-client/viewDatasetAccessPolicy.js @@ -24,10 +24,10 @@ function viewDatasetAccessPolicy(datasetId) { // Import the Google Cloud client library. const {BigQuery} = require('@google-cloud/bigquery'); - // Create a client + // Instantiate a client. const bigquery = new BigQuery(); - // TODO (developer): Update and un-comment below lines + // TODO (developer): Update and un-comment below lines. // Dataset from which to get the access policy // datasetId = "my_dataset"; @@ -39,6 +39,7 @@ function viewDatasetAccessPolicy(datasetId) { // Show the list of AccessEntry objects. // More details about the AccessEntry object in the BigQuery documentation + // https://cloud.google.com/nodejs/docs/reference/bigquery/latest console.log( `${accessEntries.length} Access entries in dataset '${datasetId}':` ); diff --git a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js index 978f0b4132..2ecf087e9c 100644 --- a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js +++ b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js @@ -14,10 +14,6 @@ 'use strict'; -// Imports the Google Cloud client library -const {BigQuery} = require('@google-cloud/bigquery'); - -// [START bigquery_view_table_or_view_access_policy] /** * View access policy for a BigQuery table or view * @@ -27,25 +23,29 @@ const {BigQuery} = require('@google-cloud/bigquery'); * @returns {Promise} The IAM policy object */ async function viewTableOrViewAccessPolicy(projectId, datasetId, resourceName) { + // [START bigquery_view_table_or_view_access_policy] + // Imports the Google Cloud client library + const {BigQuery} = require('@google-cloud/bigquery'); + // TODO(developer): Update and un-comment below lines - // Google Cloud Platform project + // Google Cloud Platform project. // projectId = "my_project_id"; - // Dataset where the table or view is + // Dataset where the table or view is. // datasetId = "my_dataset_id"; - // Table or view name to get the access policy + // Table or view name to get the access policy. // resourceName = "my_table_name"; - // Instantiate a client + // Instantiate a client. const client = new BigQuery(); - // Get the table reference + // Get the table reference. const dataset = client.dataset(datasetId); const table = dataset.table(resourceName); - // Get the IAM access policy for the table or view + // Get the IAM access policy for the table or view. const [policy] = await table.getIamPolicy(); - // Initialize bindings if they don't exist + // Initialize bindings if they don't exist. if (!policy.bindings) { policy.bindings = []; } From dbb3a4d580001f7f69db980216b24b228ccf4049 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Wed, 5 Mar 2025 16:53:55 +0000 Subject: [PATCH 26/29] fix(bigquery): Update samples and tests according to PR comments --- .../cloud-client/grantAccessToTableOrView.js | 5 ++- bigquery/cloud-client/revokeDatasetAccess.js | 34 ++++--------------- .../cloud-client/revokeTableOrViewAccess.js | 4 +-- bigquery/cloud-client/test/config.js | 29 ++++++---------- .../test/viewTableOrViewAccessPolicy.test.js | 16 ++++----- .../cloud-client/viewDatasetAccessPolicy.js | 2 +- .../viewTableOrViewAccessPolicy.js | 2 +- 7 files changed, 32 insertions(+), 60 deletions(-) diff --git a/bigquery/cloud-client/grantAccessToTableOrView.js b/bigquery/cloud-client/grantAccessToTableOrView.js index 562f161387..4ace5f73d8 100644 --- a/bigquery/cloud-client/grantAccessToTableOrView.js +++ b/bigquery/cloud-client/grantAccessToTableOrView.js @@ -40,10 +40,10 @@ async function grantAccessToTableOrView( // projectId = "my_project_id" // Dataset where the table or view is. - // datasetId = "my_dataset" + // datasetId = "my_dataset_id" // Table or view name to get the access policy. - // resourceName = "my_table" + // resourceName = "my_table_id" // The principal requesting access to the table or view. // Find more details about principal identifiers here: @@ -88,7 +88,6 @@ async function grantAccessToTableOrView( `Role '${role}' granted for principal '${principalId}' on resource '${datasetId}.${resourceName}'.` ); // [END bigquery_grant_access_to_table_or_view] - return updatedPolicy.bindings; } diff --git a/bigquery/cloud-client/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js index b2aa9c3b1f..32ee558fd1 100644 --- a/bigquery/cloud-client/revokeDatasetAccess.js +++ b/bigquery/cloud-client/revokeDatasetAccess.js @@ -36,7 +36,7 @@ async function revokeDatasetAccess(datasetId, entityId) { // TODO (developer): Update and un-comment below lines. // ID of the dataset to revoke access to. - // datasetId = "your-project.your_dataset" + // datasetId = "my_project.my_dataset" // ID of the user or group from whom you are revoking access. // Alternatively, the JSON REST API representation of the entity, @@ -57,32 +57,12 @@ async function revokeDatasetAccess(datasetId, entityId) { // Filter access entries to exclude entries matching the specified entity_id // and assign a new list back to the access list. dataset.metadata.access = dataset.metadata.access.filter(entry => { - // Check for entity_id (specific match). - if (entry.entity_id === entityId) { - console.log( - `Found matching entity_id: ${entry.entity_id}, removing entry` - ); - return false; - } - - // Check for userByEmail field. - if (entry.userByEmail === entityId) { - console.log( - `Found matching userByEmail: ${entry.userByEmail}, removing entry` - ); - return false; - } - - // Check for groupByEmail field. - if (entry.groupByEmail === entityId) { - console.log( - `Found matching groupByEmail: ${entry.groupByEmail}, removing entry` - ); - return false; - } - - // Keep all other entries. - return true; + // Return false (remove entry) if any of these fields match entityId + return !( + entry.entity_id === entityId || + entry.userByEmail === entityId || + entry.groupByEmail === entityId + ); }); // Update will only succeed if the dataset diff --git a/bigquery/cloud-client/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js index 6f9165e073..2bfe1ec1ae 100644 --- a/bigquery/cloud-client/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/revokeTableOrViewAccess.js @@ -39,10 +39,10 @@ async function revokeAccessToTableOrView( // projectId = "my_project_id" // Dataset where the table or view is. - // datasetId = "my_dataset" + // datasetId = "my_dataset_id" // Table or view name to get the access policy. - // resourceName = "my_table" + // resourceName = "my_table_id" // (Optional) Role to remove from the table or view. // roleToRemove = "roles/bigquery.dataViewer" diff --git a/bigquery/cloud-client/test/config.js b/bigquery/cloud-client/test/config.js index 4df0feedc2..a4f56ef623 100644 --- a/bigquery/cloud-client/test/config.js +++ b/bigquery/cloud-client/test/config.js @@ -188,26 +188,19 @@ const setupBeforeAll = async () => { const cleanupResources = async () => { console.log('=== Cleaning up test resources ==='); - if (sharedClient) { + if (sharedClient && sharedDataset) { try { - // Check if resources exist before attempting to delete - if (sharedDataset) { - try { - console.log( - `Deleting dataset: ${DATASET_ID} and all contained tables/views` - ); - await sharedClient.dataset(DATASET_ID).delete({force: true}); - console.log(`Successfully deleted dataset: ${DATASET_ID}`); - } catch (err) { - if (err.code !== HTTP_STATUS.NOT_FOUND) { - console.error(`Error deleting dataset: ${err.message}`); - } else { - console.log(`Dataset ${DATASET_ID} already deleted or not found`); - } - } - } + console.log( + `Deleting dataset: ${DATASET_ID} and all contained tables/views` + ); + await sharedClient.dataset(DATASET_ID).delete({force: true}); + console.log(`Successfully deleted dataset: ${DATASET_ID}`); } catch (err) { - console.error(`Cleanup error: ${err.message}`); + if (err.code !== HTTP_STATUS.NOT_FOUND) { + console.error(`Error deleting dataset: ${err.message}`); + } else { + console.log(`Dataset ${DATASET_ID} already deleted or not found`); + } } } diff --git a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js index b8ffca6054..7fa8ca2746 100644 --- a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js +++ b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js @@ -43,20 +43,20 @@ describe('viewTableOrViewAccessPolicy', () => { table.id ); - // Verificar que la política existe + // Verify that the policy exists assert.ok(policy, 'Policy should be defined'); - // Verificar que bindings existe y es un array + // Verify that bindings exists and is an array assert.ok(Array.isArray(policy.bindings), 'Bindings should be an array'); - // En una política nueva, bindings debería estar vacío + // In a new policy, bindings should be empty assert.strictEqual( policy.bindings.length, 0, 'Bindings list should be empty' ); - // Verificar que etag existe, pero no validar su valor exacto + // Verify that etag exists, but do not validate its exact value assert.ok(policy.etag, 'Etag should be defined'); }); @@ -71,20 +71,20 @@ describe('viewTableOrViewAccessPolicy', () => { view.id ); - // Verificar que la política existe + // Verify that the policy exists assert.ok(policy, 'Policy should be defined'); - // Verificar que bindings existe y es un array + // Verify that bindings exists and is an array assert.ok(Array.isArray(policy.bindings), 'Bindings should be an array'); - // En una política nueva, bindings debería estar vacío + // In a new policy, bindings should be empty assert.strictEqual( policy.bindings.length, 0, 'Bindings list should be empty' ); - // Verificar que etag existe, pero no validar su valor exacto + // Verify that etag exists, but do not validate its exact value assert.ok(policy.etag, 'Etag should be defined'); }); }); diff --git a/bigquery/cloud-client/viewDatasetAccessPolicy.js b/bigquery/cloud-client/viewDatasetAccessPolicy.js index aa8dd82d4a..aff2ee1b51 100644 --- a/bigquery/cloud-client/viewDatasetAccessPolicy.js +++ b/bigquery/cloud-client/viewDatasetAccessPolicy.js @@ -29,7 +29,7 @@ function viewDatasetAccessPolicy(datasetId) { // TODO (developer): Update and un-comment below lines. // Dataset from which to get the access policy - // datasetId = "my_dataset"; + // datasetId = "my_dataset_id"; // Get a reference to the dataset. const dataset = bigquery.dataset(datasetId); diff --git a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js index 2ecf087e9c..cc79823589 100644 --- a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js +++ b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js @@ -33,7 +33,7 @@ async function viewTableOrViewAccessPolicy(projectId, datasetId, resourceName) { // Dataset where the table or view is. // datasetId = "my_dataset_id"; // Table or view name to get the access policy. - // resourceName = "my_table_name"; + // resourceName = "my_table_name_id"; // Instantiate a client. const client = new BigQuery(); From 5c048840c9cc412e43c73ed509f43458063bce58 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Wed, 5 Mar 2025 19:37:04 +0000 Subject: [PATCH 27/29] fix(bigquery): Standardized punctuation and style in all documents --- bigquery/cloud-client/grantAccessToDataset.js | 17 ++++++++--------- .../cloud-client/grantAccessToTableOrView.js | 16 ++++++++-------- bigquery/cloud-client/revokeDatasetAccess.js | 19 +++++++++---------- .../cloud-client/revokeTableOrViewAccess.js | 18 +++++++++--------- .../cloud-client/viewDatasetAccessPolicy.js | 10 +++++----- .../viewTableOrViewAccessPolicy.js | 16 +++++++++------- 6 files changed, 48 insertions(+), 48 deletions(-) diff --git a/bigquery/cloud-client/grantAccessToDataset.js b/bigquery/cloud-client/grantAccessToDataset.js index 1c73b92060..f7b18075e9 100644 --- a/bigquery/cloud-client/grantAccessToDataset.js +++ b/bigquery/cloud-client/grantAccessToDataset.js @@ -15,18 +15,18 @@ 'use strict'; /** - * Grants access to a BigQuery dataset for a specified entity + * Grants access to a BigQuery dataset for a specified entity. * - * @param {string} datasetId ID of the dataset to grant access to - * @param {string} entityId ID of the entity to grant access to - * @param {string} role Role to grant - * @returns {Promise} Array of access entries + * @param {string} datasetId ID of the dataset to grant access to. + * @param {string} entityId ID of the entity to grant access to. + * @param {string} role Role to grant. + * @returns {Promise} Array of access entries. */ async function grantAccessToDataset(datasetId, entityId, role) { // [START bigquery_grant_access_to_dataset] const {BigQuery} = require('@google-cloud/bigquery'); - // Define enum for HTTP codes + // Define enum for HTTP codes. const HTTP_STATUS = { PRECONDITION_FAILED: 412, }; @@ -79,7 +79,7 @@ async function grantAccessToDataset(datasetId, entityId, role) { // Update will only succeed if the dataset // has not been modified externally since retrieval. // - // See the BigQuery client library documentation for more details on metadata updates + // See the BigQuery client library documentation for more details on metadata updates: // https://cloud.google.com/nodejs/docs/reference/bigquery/latest // Update just the 'access entries' property of the dataset. @@ -89,8 +89,7 @@ async function grantAccessToDataset(datasetId, entityId, role) { // Show a success message. console.log( - `Role '${role}' granted for entity '${entityId}'` + - ` in dataset '${datasetId}'.` + `Role '${role}' granted for entity '${entityId}' in dataset '${datasetId}'.` ); return updatedDataset.access; diff --git a/bigquery/cloud-client/grantAccessToTableOrView.js b/bigquery/cloud-client/grantAccessToTableOrView.js index 4ace5f73d8..657ba3527f 100644 --- a/bigquery/cloud-client/grantAccessToTableOrView.js +++ b/bigquery/cloud-client/grantAccessToTableOrView.js @@ -15,14 +15,14 @@ 'use strict'; /** - * Grants access to a BigQuery table or view for a specified principal + * Grants access to a BigQuery table or view for a specified principal. * - * @param {string} projectId Google Cloud Platform project ID - * @param {string} datasetId Dataset where the table or view is - * @param {string} resourceName Table or view name to get the access policy - * @param {string} principalId The principal requesting access to the table or view - * @param {string} role Role to assign to the member - * @returns {Promise} The updated policy bindings + * @param {string} projectId Google Cloud Platform project ID. + * @param {string} datasetId Dataset where the table or view is. + * @param {string} resourceName Table or view name to get the access policy. + * @param {string} principalId The principal requesting access to the table or view. + * @param {string} role Role to assign to the member. + * @returns {Promise} The updated policy bindings. */ async function grantAccessToTableOrView( projectId, @@ -34,7 +34,7 @@ async function grantAccessToTableOrView( // [START bigquery_grant_access_to_table_or_view] const {BigQuery} = require('@google-cloud/bigquery'); - // TODO(developer): Update and un-comment below lines + // TODO(developer): Update and un-comment below lines. // Google Cloud Platform project. // projectId = "my_project_id" diff --git a/bigquery/cloud-client/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js index 32ee558fd1..77c313739e 100644 --- a/bigquery/cloud-client/revokeDatasetAccess.js +++ b/bigquery/cloud-client/revokeDatasetAccess.js @@ -14,23 +14,22 @@ 'use strict'; -// Define enum for HTTP codes +// Define enum for HTTP codes. const HTTP_STATUS = { PRECONDITION_FAILED: 412, }; /** - * Revokes access to a dataset for a specified entity + * Revokes access to a dataset for a specified entity. * - * @param {string} datasetId ID of the dataset to revoke access to - * @param {string} entityId ID of the user or group from whom you are revoking access + * @param {string} datasetId ID of the dataset to revoke access to. + * @param {string} entityId ID of the user or group from whom you are revoking access. * Alternatively, the JSON REST API representation of the entity, - * such as a view's table reference - * @returns {Promise} A promise that resolves to the updated access entries + * such as a view's table reference. + * @returns {Promise} A promise that resolves to the updated access entries. */ async function revokeDatasetAccess(datasetId, entityId) { // [START bigquery_revoke_dataset_access] - // Imports the Google Cloud client library. const {BigQuery} = require('@google-cloud/bigquery'); // TODO (developer): Update and un-comment below lines. @@ -49,15 +48,15 @@ async function revokeDatasetAccess(datasetId, entityId) { // Get a reference to the dataset. const [dataset] = await bigquery.dataset(datasetId).get(); - // To revoke access to a dataset, remove elements from the access list + // To revoke access to a dataset, remove elements from the access list. // - // See the BigQuery client library documentation for more details on access entries + // See the BigQuery client library documentation for more details on access entries: // https://cloud.google.com/nodejs/docs/reference/secret-manager/4.1.4 // Filter access entries to exclude entries matching the specified entity_id // and assign a new list back to the access list. dataset.metadata.access = dataset.metadata.access.filter(entry => { - // Return false (remove entry) if any of these fields match entityId + // Return false (remove entry) if any of these fields match entityId. return !( entry.entity_id === entityId || entry.userByEmail === entityId || diff --git a/bigquery/cloud-client/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js index 2bfe1ec1ae..64be24dd23 100644 --- a/bigquery/cloud-client/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/revokeTableOrViewAccess.js @@ -15,13 +15,13 @@ 'use strict'; /** - * Revokes access to a BigQuery table or view - * @param {string} projectId The ID of the Google Cloud project - * @param {string} datasetId The ID of the dataset containing the table/view - * @param {string} resourceName The ID of the table or view - * @param {string} [roleToRemove=null] Optional. Specific role to revoke - * @param {string} [principalToRemove=null] Optional. Specific principal to revoke access from - * @returns {Promise} The updated IAM policy + * Revokes access to a BigQuery table or view. + * @param {string} projectId The ID of the Google Cloud project. + * @param {string} datasetId The ID of the dataset containing the table/view. + * @param {string} resourceName The ID of the table or view. + * @param {string} [roleToRemove=null] Optional. Specific role to revoke. + * @param {string} [principalToRemove=null] Optional. Specific principal to revoke access from. + * @returns {Promise} The updated IAM policy. */ async function revokeAccessToTableOrView( projectId, @@ -31,10 +31,10 @@ async function revokeAccessToTableOrView( principalToRemove = null ) { // [START bigquery_revoke_access_to_table_or_view] - // Imports the Google Cloud client library. const {BigQuery} = require('@google-cloud/bigquery'); - // TODO (developer): Update and un-comment below lines + // TODO (developer): Update and un-comment below lines. + // Google Cloud Platform project. // projectId = "my_project_id" diff --git a/bigquery/cloud-client/viewDatasetAccessPolicy.js b/bigquery/cloud-client/viewDatasetAccessPolicy.js index aff2ee1b51..62be53d39f 100644 --- a/bigquery/cloud-client/viewDatasetAccessPolicy.js +++ b/bigquery/cloud-client/viewDatasetAccessPolicy.js @@ -15,19 +15,19 @@ 'use strict'; /** - * View access policies for a BigQuery dataset - * @param {string} datasetId Dataset ID to view access policies for - * @returns {Array} List of access entries + * View access policies for a BigQuery dataset. + * @param {string} datasetId Dataset ID to view access policies for. + * @returns {Array} List of access entries. */ function viewDatasetAccessPolicy(datasetId) { // [START bigquery_view_dataset_access_policy] - // Import the Google Cloud client library. const {BigQuery} = require('@google-cloud/bigquery'); // Instantiate a client. const bigquery = new BigQuery(); // TODO (developer): Update and un-comment below lines. + // Dataset from which to get the access policy // datasetId = "my_dataset_id"; @@ -38,7 +38,7 @@ function viewDatasetAccessPolicy(datasetId) { const accessEntries = metadata.access || []; // Show the list of AccessEntry objects. - // More details about the AccessEntry object in the BigQuery documentation + // More details about the AccessEntry object in the BigQuery documentation: // https://cloud.google.com/nodejs/docs/reference/bigquery/latest console.log( `${accessEntries.length} Access entries in dataset '${datasetId}':` diff --git a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js index cc79823589..5236168cb5 100644 --- a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js +++ b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js @@ -15,23 +15,25 @@ 'use strict'; /** - * View access policy for a BigQuery table or view + * View access policy for a BigQuery table or view. * - * @param {string} projectId Google Cloud Platform project - * @param {string} datasetId Dataset where the table or view is - * @param {string} resourceName Table or view name to get the access policy - * @returns {Promise} The IAM policy object + * @param {string} projectId Google Cloud Platform project. + * @param {string} datasetId Dataset where the table or view is. + * @param {string} resourceName Table or view name to get the access policy. + * @returns {Promise} The IAM policy object. */ async function viewTableOrViewAccessPolicy(projectId, datasetId, resourceName) { // [START bigquery_view_table_or_view_access_policy] - // Imports the Google Cloud client library const {BigQuery} = require('@google-cloud/bigquery'); - // TODO(developer): Update and un-comment below lines + // TODO(developer): Update and un-comment below lines. + // Google Cloud Platform project. // projectId = "my_project_id"; + // Dataset where the table or view is. // datasetId = "my_dataset_id"; + // Table or view name to get the access policy. // resourceName = "my_table_name_id"; From eed264a53bd122941c0b4c926f7cb6c39ac36199 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Thu, 6 Mar 2025 17:43:12 +0000 Subject: [PATCH 28/29] fix(bigquery): Add punctuation to test and standardize format in all documents --- bigquery/cloud-client/package.json | 2 +- bigquery/cloud-client/revokeDatasetAccess.js | 12 +++++----- .../test/grantAccessToDataset.test.js | 12 +++++----- .../test/grantAccessToTableOrView.test.js | 18 +++++++------- .../test/revokeDatasetAccess.test.js | 18 +++++++------- .../test/revokeTableOrViewAccess.test.js | 24 +++++++++---------- .../test/viewTableOrViewAccessPolicy.test.js | 16 ++++++------- .../viewTableOrViewAccessPolicy.js | 2 +- 8 files changed, 52 insertions(+), 52 deletions(-) diff --git a/bigquery/cloud-client/package.json b/bigquery/cloud-client/package.json index 49d935ea21..718319003c 100644 --- a/bigquery/cloud-client/package.json +++ b/bigquery/cloud-client/package.json @@ -1,6 +1,6 @@ { "name": "bigquery-cloud-client", - "description": "Big Query Cloud Client Node.js for Google App", + "description": "Big Query Cloud Client Node.js samples", "version": "0.0.1", "private": true, "license": "Apache Version 2.0", diff --git a/bigquery/cloud-client/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js index 77c313739e..398d63c836 100644 --- a/bigquery/cloud-client/revokeDatasetAccess.js +++ b/bigquery/cloud-client/revokeDatasetAccess.js @@ -14,11 +14,6 @@ 'use strict'; -// Define enum for HTTP codes. -const HTTP_STATUS = { - PRECONDITION_FAILED: 412, -}; - /** * Revokes access to a dataset for a specified entity. * @@ -32,6 +27,11 @@ async function revokeDatasetAccess(datasetId, entityId) { // [START bigquery_revoke_dataset_access] const {BigQuery} = require('@google-cloud/bigquery'); + // Define enum for HTTP codes. + const HTTP_STATUS = { + PRECONDITION_FAILED: 412, + }; + // TODO (developer): Update and un-comment below lines. // ID of the dataset to revoke access to. @@ -83,8 +83,8 @@ async function revokeDatasetAccess(datasetId, entityId) { throw error; } } + // [END bigquery_revoke_dataset_access] } -// [END bigquery_revoke_dataset_access] module.exports = { revokeDatasetAccess, diff --git a/bigquery/cloud-client/test/grantAccessToDataset.test.js b/bigquery/cloud-client/test/grantAccessToDataset.test.js index 9da3d3281d..3bccabdae4 100644 --- a/bigquery/cloud-client/test/grantAccessToDataset.test.js +++ b/bigquery/cloud-client/test/grantAccessToDataset.test.js @@ -24,12 +24,12 @@ const { const {grantAccessToDataset} = require('../grantAccessToDataset'); describe('grantAccessToDataset', () => { - // Set up fixtures before all tests (similar to pytest's module scope) + // Set up fixtures before all tests (similar to pytest's module scope). before(async () => { await setupBeforeAll(); }); - // Clean up after all tests + // Clean up after all tests. after(async () => { await teardownAfterAll(); }); @@ -41,18 +41,18 @@ describe('grantAccessToDataset', () => { console.log({dataset}); console.log({entityId}); - // Act: Grant access to the dataset + // Act: Grant access to the dataset. const accessEntries = await grantAccessToDataset( dataset.id, entityId, 'READER' ); - // Assert: Check if entity was added to access entries + // Assert: Check if entity was added to access entries. const updatedEntityIds = accessEntries .filter(entry => entry !== null) .map(entry => { - // Handle different entity types + // Handle different entity types. if (entry.groupByEmail) { return entry.groupByEmail; } else if (entry.userByEmail) { @@ -64,7 +64,7 @@ describe('grantAccessToDataset', () => { }) .filter(id => id !== null); - // Check if our entity ID is in the updated access entries (similar to Python assertion) + // Check if our entity ID is in the updated access entries. expect(updatedEntityIds).to.include(entityId); }); }); diff --git a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js index cf02af95db..534457c01f 100644 --- a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js +++ b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js @@ -25,18 +25,18 @@ const { } = require('./config'); describe('grantAccessToTableOrView', () => { - // Setup shared resources before all tests + // Setup shared resources before all tests. before(async () => { await setupBeforeAll(); }); - // Clean up resources after all tests + // Clean up resources after all tests. after(async () => { await teardownAfterAll(); }); it('should grant access to a table', async () => { - // Get required test resources + // Get required test resources. const projectId = await getProjectId(); const dataset = await getDataset(); const table = await getTable(); @@ -45,15 +45,15 @@ describe('grantAccessToTableOrView', () => { const ROLE = 'roles/bigquery.dataViewer'; const PRINCIPAL_ID = `group:${entityId}`; - // Get the initial empty policy + // Get the initial empty policy. const [emptyPolicy] = await table.getIamPolicy(); - // Initialize bindings if they do not exist + // Initialize bindings if they do not exist. if (!emptyPolicy.bindings) { emptyPolicy.bindings = []; } - // In an empty policy the role and principal should not be present + // In an empty policy the role and principal should not be present. assert.strictEqual( emptyPolicy.bindings.some(p => p.role === ROLE), false, @@ -67,7 +67,7 @@ describe('grantAccessToTableOrView', () => { 'Principal should not exist in empty policy' ); - // Grant access to the table + // Grant access to the table. const updatedPolicy = await grantAccessToTableOrView( projectId, dataset.id, @@ -76,14 +76,14 @@ describe('grantAccessToTableOrView', () => { ROLE ); - // A binding with that role should exist + // A binding with that role should exist. assert.strictEqual( updatedPolicy.some(p => p.role === ROLE), true, 'Role should exist after granting access' ); - // A binding for that principal should exist + // A binding for that principal should exist. assert.strictEqual( updatedPolicy.some(p => p.members && p.members.includes(PRINCIPAL_ID)), true, diff --git a/bigquery/cloud-client/test/revokeDatasetAccess.test.js b/bigquery/cloud-client/test/revokeDatasetAccess.test.js index e9b1169857..f7a3552e0d 100644 --- a/bigquery/cloud-client/test/revokeDatasetAccess.test.js +++ b/bigquery/cloud-client/test/revokeDatasetAccess.test.js @@ -24,33 +24,33 @@ const { } = require('./config'); describe('revokeDatasetAccess', () => { - // Setup resources before all tests + // Setup resources before all tests. before(async () => { await setupBeforeAll(); }); - // Clean up resources after all tests + // Clean up resources after all tests. after(async () => { await teardownAfterAll(); }); it('should revoke access to a dataset', async () => { - // Get test resources + // Get test resources. const dataset = await getDataset(); const entityId = getEntityId(); - // Directly use the dataset ID + // Directly use the dataset ID. const datasetId = dataset.id; console.log(`Testing with dataset: ${datasetId} and entity: ${entityId}`); - // First grant access to the dataset + // First grant access to the dataset. const datasetAccessEntries = await grantAccessToDataset( datasetId, entityId, 'READER' ); - // Create a set of all entity IDs and email addresses to check + // Create a set of all entity IDs and email addresses to check. const datasetEntityIds = new Set(); datasetAccessEntries.forEach(entry => { if (entry.entity_id) { @@ -64,7 +64,7 @@ describe('revokeDatasetAccess', () => { } }); - // Check if our entity ID is in the set + // Check if our entity ID is in the set. const hasAccess = datasetEntityIds.has(entityId); console.log(`Entity ${entityId} has access after granting: ${hasAccess}`); assert.strictEqual( @@ -73,10 +73,10 @@ describe('revokeDatasetAccess', () => { 'Entity should have access after granting' ); - // Now revoke access + // Now revoke access. const newAccessEntries = await revokeDatasetAccess(datasetId, entityId); - // Check that the entity no longer has access + // Check that the entity no longer has access. const updatedEntityIds = new Set(); newAccessEntries.forEach(entry => { if (entry.entity_id) { diff --git a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js index 3cf9d2a339..7a5b010f5a 100644 --- a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js +++ b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js @@ -44,15 +44,15 @@ describe('revokeTableOrViewAccess', () => { const ROLE = 'roles/bigquery.dataViewer'; const PRINCIPAL_ID = `group:${entityId}`; - // Get the initial empty policy + // Get the initial empty policy. const [emptyPolicy] = await table.getIamPolicy(); - // Initialize bindings if they do not exist + // Initialize bindings if they do not exist. if (!emptyPolicy.bindings) { emptyPolicy.bindings = []; } - // Grant access + // Grant access. const policyWithRole = await grantAccessToTableOrView( projectId, dataset.id, @@ -61,11 +61,11 @@ describe('revokeTableOrViewAccess', () => { ROLE ); - // Check that there is a binding with that role + // Check that there is a binding with that role. const hasRole = policyWithRole.some(b => b.role === ROLE); assert.isTrue(hasRole); - // Revoke access for the role + // Revoke access for the role. const policyWithRevokedRole = await revokeAccessToTableOrView( projectId, dataset.id, @@ -74,7 +74,7 @@ describe('revokeTableOrViewAccess', () => { null ); - // Check that this role is not present in the policy anymore + // Check that this role is not present in the policy anymore. const roleExists = policyWithRevokedRole.some(b => b.role === ROLE); assert.isFalse(roleExists); }); @@ -88,15 +88,15 @@ describe('revokeTableOrViewAccess', () => { const ROLE = 'roles/bigquery.dataViewer'; const PRINCIPAL_ID = `group:${entityId}`; - // Get the initial empty policy + // Get the initial empty policy. const [emptyPolicy] = await table.getIamPolicy(); - // Initialize bindings if they do not exist + // Initialize bindings if they do not exist. if (!emptyPolicy.bindings) { emptyPolicy.bindings = []; } - // Grant access + // Grant access. const updatedPolicy = await grantAccessToTableOrView( projectId, dataset.id, @@ -105,13 +105,13 @@ describe('revokeTableOrViewAccess', () => { ROLE ); - // There is a binding for that principal + // There is a binding for that principal. const hasPrincipal = updatedPolicy.some( b => b.members && b.members.includes(PRINCIPAL_ID) ); assert.isTrue(hasPrincipal); - // Revoke access for the principal + // Revoke access for the principal. const policyWithRemovedPrincipal = await revokeAccessToTableOrView( projectId, dataset.id, @@ -120,7 +120,7 @@ describe('revokeTableOrViewAccess', () => { PRINCIPAL_ID ); - // This principal is not present in the policy anymore + // This principal is not present in the policy anymore. const hasPrincipalAfterRevoke = policyWithRemovedPrincipal.some( b => b.members && b.members.includes(PRINCIPAL_ID) ); diff --git a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js index 7fa8ca2746..df50ad6c39 100644 --- a/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js +++ b/bigquery/cloud-client/test/viewTableOrViewAccessPolicy.test.js @@ -43,20 +43,20 @@ describe('viewTableOrViewAccessPolicy', () => { table.id ); - // Verify that the policy exists + // Verify that the policy exists. assert.ok(policy, 'Policy should be defined'); - // Verify that bindings exists and is an array + // Verify that bindings exists and is an array. assert.ok(Array.isArray(policy.bindings), 'Bindings should be an array'); - // In a new policy, bindings should be empty + // In a new policy, bindings should be empty. assert.strictEqual( policy.bindings.length, 0, 'Bindings list should be empty' ); - // Verify that etag exists, but do not validate its exact value + // Verify that etag exists, but do not validate its exact value. assert.ok(policy.etag, 'Etag should be defined'); }); @@ -71,20 +71,20 @@ describe('viewTableOrViewAccessPolicy', () => { view.id ); - // Verify that the policy exists + // Verify that the policy exists. assert.ok(policy, 'Policy should be defined'); - // Verify that bindings exists and is an array + // Verify that bindings exists and is an array. assert.ok(Array.isArray(policy.bindings), 'Bindings should be an array'); - // In a new policy, bindings should be empty + // In a new policy, bindings should be empty. assert.strictEqual( policy.bindings.length, 0, 'Bindings list should be empty' ); - // Verify that etag exists, but do not validate its exact value + // Verify that etag exists, but do not validate its exact value. assert.ok(policy.etag, 'Etag should be defined'); }); }); diff --git a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js index 5236168cb5..51d9005cb1 100644 --- a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js +++ b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js @@ -60,8 +60,8 @@ async function viewTableOrViewAccessPolicy(projectId, datasetId, resourceName) { console.log(`etag: ${policy.etag}`); console.log(`Version: ${policy.version}`); + // [END bigquery_view_table_or_view_access_policy] return policy; } -// [END bigquery_view_table_or_view_access_policy] module.exports = viewTableOrViewAccessPolicy; From 158f0b0a1dd1bc7926ad9f6285c75be911286b83 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Fri, 7 Mar 2025 19:21:57 +0000 Subject: [PATCH 29/29] fix(bigquery): Update samples and tests according to PR comments --- bigquery/cloud-client/grantAccessToDataset.js | 4 ++-- .../cloud-client/grantAccessToTableOrView.js | 5 +++-- bigquery/cloud-client/package.json | 2 +- bigquery/cloud-client/revokeDatasetAccess.js | 4 ++-- .../cloud-client/revokeTableOrViewAccess.js | 20 +++++++++---------- .../test/grantAccessToTableOrView.test.js | 2 +- .../test/revokeTableOrViewAccess.test.js | 4 ++-- .../test/viewDatasetAccessPolicy.test.js | 3 ++- .../cloud-client/viewDatasetAccessPolicy.js | 6 +++--- .../viewTableOrViewAccessPolicy.js | 5 +++-- 10 files changed, 29 insertions(+), 26 deletions(-) diff --git a/bigquery/cloud-client/grantAccessToDataset.js b/bigquery/cloud-client/grantAccessToDataset.js index f7b18075e9..5b493ecf23 100644 --- a/bigquery/cloud-client/grantAccessToDataset.js +++ b/bigquery/cloud-client/grantAccessToDataset.js @@ -58,7 +58,7 @@ async function grantAccessToDataset(datasetId, entityId, role) { // Get a reference to the dataset. const [dataset] = await client.dataset(datasetId).get(); - // The 'access entries' list is immutable. Create a copy for modifications. + // The 'access entries' array is immutable. Create a copy for modifications. const entries = Array.isArray(dataset.metadata.access) ? [...dataset.metadata.access] : []; @@ -71,7 +71,7 @@ async function grantAccessToDataset(datasetId, entityId, role) { [entityType]: entityId, }); - // Assign the list of AccessEntries back to the dataset. + // Assign the array of AccessEntries back to the dataset. const metadata = { access: entries, }; diff --git a/bigquery/cloud-client/grantAccessToTableOrView.js b/bigquery/cloud-client/grantAccessToTableOrView.js index 657ba3527f..d1fad7bfe1 100644 --- a/bigquery/cloud-client/grantAccessToTableOrView.js +++ b/bigquery/cloud-client/grantAccessToTableOrView.js @@ -56,14 +56,15 @@ async function grantAccessToTableOrView( // Instantiate a client. const client = new BigQuery(); - // Get the table reference. + // Get a reference to the dataset by datasetId. const dataset = client.dataset(datasetId); + // Get a reference to the table by tableName. const table = dataset.table(resourceName); // Get the IAM access policy for the table or view. const [policy] = await table.getIamPolicy(); - // Initialize bindings if they do not exist. + // Initialize bindings array. if (!policy.bindings) { policy.bindings = []; } diff --git a/bigquery/cloud-client/package.json b/bigquery/cloud-client/package.json index 718319003c..4316e269c0 100644 --- a/bigquery/cloud-client/package.json +++ b/bigquery/cloud-client/package.json @@ -4,7 +4,7 @@ "version": "0.0.1", "private": true, "license": "Apache Version 2.0", - "author": "Google Inc.", + "author": "Google LLC", "engines": { "node": "20.x" }, diff --git a/bigquery/cloud-client/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js index 398d63c836..8b11a1cc95 100644 --- a/bigquery/cloud-client/revokeDatasetAccess.js +++ b/bigquery/cloud-client/revokeDatasetAccess.js @@ -48,13 +48,13 @@ async function revokeDatasetAccess(datasetId, entityId) { // Get a reference to the dataset. const [dataset] = await bigquery.dataset(datasetId).get(); - // To revoke access to a dataset, remove elements from the access list. + // To revoke access to a dataset, remove elements from the access array. // // See the BigQuery client library documentation for more details on access entries: // https://cloud.google.com/nodejs/docs/reference/secret-manager/4.1.4 // Filter access entries to exclude entries matching the specified entity_id - // and assign a new list back to the access list. + // and assign a new array back to the access array. dataset.metadata.access = dataset.metadata.access.filter(entry => { // Return false (remove entry) if any of these fields match entityId. return !( diff --git a/bigquery/cloud-client/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js index 64be24dd23..d3b3ae5647 100644 --- a/bigquery/cloud-client/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/revokeTableOrViewAccess.js @@ -21,7 +21,7 @@ * @param {string} resourceName The ID of the table or view. * @param {string} [roleToRemove=null] Optional. Specific role to revoke. * @param {string} [principalToRemove=null] Optional. Specific principal to revoke access from. - * @returns {Promise} The updated IAM policy. + * @returns {Promise} The updated IAM policy. */ async function revokeAccessToTableOrView( projectId, @@ -56,14 +56,15 @@ async function revokeAccessToTableOrView( // Instantiate a client. const client = new BigQuery(); - // Get the table reference. + // Get a reference to the dataset by datasetId. const dataset = client.dataset(datasetId); + // Get a reference to the table by tableName. const table = dataset.table(resourceName); // Get the IAM access policy for the table or view. const [policy] = await table.getIamPolicy(); - // Initialize bindings of they do not exist. + // Initialize bindings array. if (!policy.bindings) { policy.bindings = []; } @@ -76,12 +77,12 @@ async function revokeAccessToTableOrView( if (roleToRemove) { // Filter out all bindings with the `roleToRemove` - // and assign a new list back to the policy bindings. + // and assign a new array back to the policy bindings. policy.bindings = policy.bindings.filter(b => b.role !== roleToRemove); } if (principalToRemove) { - // The `bindings` list is immutable. Create a copy for modifications. + // The `bindings` array is immutable. Create a copy for modifications. const bindings = [...policy.bindings]; // Filter out the principal from each binding. @@ -104,11 +105,10 @@ async function revokeAccessToTableOrView( // Get the policy again to confirm it's set correctly. const [verifiedPolicy] = await table.getIamPolicy(); - if (verifiedPolicy && verifiedPolicy.bindings) { - return verifiedPolicy.bindings; - } else { - return []; - } + // Return the updated policy bindings. + return verifiedPolicy && verifiedPolicy.bindings + ? verifiedPolicy.bindings + : []; } catch (error) { console.error('Error settings IAM policy:', error); throw error; diff --git a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js index 534457c01f..12f421a4e6 100644 --- a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js +++ b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js @@ -48,7 +48,7 @@ describe('grantAccessToTableOrView', () => { // Get the initial empty policy. const [emptyPolicy] = await table.getIamPolicy(); - // Initialize bindings if they do not exist. + // Initialize bindings array. if (!emptyPolicy.bindings) { emptyPolicy.bindings = []; } diff --git a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js index 7a5b010f5a..47801627f2 100644 --- a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js +++ b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js @@ -47,7 +47,7 @@ describe('revokeTableOrViewAccess', () => { // Get the initial empty policy. const [emptyPolicy] = await table.getIamPolicy(); - // Initialize bindings if they do not exist. + // Initialize bindings array. if (!emptyPolicy.bindings) { emptyPolicy.bindings = []; } @@ -91,7 +91,7 @@ describe('revokeTableOrViewAccess', () => { // Get the initial empty policy. const [emptyPolicy] = await table.getIamPolicy(); - // Initialize bindings if they do not exist. + // Initialize bindings array. if (!emptyPolicy.bindings) { emptyPolicy.bindings = []; } diff --git a/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js b/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js index b1c67042be..725e0763fd 100644 --- a/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js +++ b/bigquery/cloud-client/test/viewDatasetAccessPolicy.test.js @@ -29,6 +29,7 @@ describe('viewDatasetAccessPolicy', () => { const dataset = await getDataset(); const accessPolicy = await viewDatasetAccessPolicy(dataset.id); - assert(accessPolicy); + assert.ok(accessPolicy, 'Access policy should be defined'); + assert.ok(Array.isArray(accessPolicy), 'Access policy should be an array'); }); }); diff --git a/bigquery/cloud-client/viewDatasetAccessPolicy.js b/bigquery/cloud-client/viewDatasetAccessPolicy.js index 62be53d39f..6400e78f55 100644 --- a/bigquery/cloud-client/viewDatasetAccessPolicy.js +++ b/bigquery/cloud-client/viewDatasetAccessPolicy.js @@ -17,7 +17,7 @@ /** * View access policies for a BigQuery dataset. * @param {string} datasetId Dataset ID to view access policies for. - * @returns {Array} List of access entries. + * @returns {Array} Array of access entries. */ function viewDatasetAccessPolicy(datasetId) { // [START bigquery_view_dataset_access_policy] @@ -28,7 +28,7 @@ function viewDatasetAccessPolicy(datasetId) { // TODO (developer): Update and un-comment below lines. - // Dataset from which to get the access policy + // Dataset from which to get the access policy. // datasetId = "my_dataset_id"; // Get a reference to the dataset. @@ -37,7 +37,7 @@ function viewDatasetAccessPolicy(datasetId) { return dataset.getMetadata().then(([metadata]) => { const accessEntries = metadata.access || []; - // Show the list of AccessEntry objects. + // Show the array of AccessEntry objects. // More details about the AccessEntry object in the BigQuery documentation: // https://cloud.google.com/nodejs/docs/reference/bigquery/latest console.log( diff --git a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js index 51d9005cb1..e6cc0a75bc 100644 --- a/bigquery/cloud-client/viewTableOrViewAccessPolicy.js +++ b/bigquery/cloud-client/viewTableOrViewAccessPolicy.js @@ -40,14 +40,15 @@ async function viewTableOrViewAccessPolicy(projectId, datasetId, resourceName) { // Instantiate a client. const client = new BigQuery(); - // Get the table reference. + // Get a reference to the dataset by datasetId. const dataset = client.dataset(datasetId); + // Get a reference to the table by tableName. const table = dataset.table(resourceName); // Get the IAM access policy for the table or view. const [policy] = await table.getIamPolicy(); - // Initialize bindings if they don't exist. + // Initialize bindings array. if (!policy.bindings) { policy.bindings = []; }