From f2b62df44d4cdd54c832964316df1ce92cd850b7 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Sat, 8 Mar 2025 13:04:03 +0000 Subject: [PATCH 1/2] feat(bigquery): add samples for control access --- bigquery/cloud-client/grantAccessToDataset.js | 111 ++++++++ .../cloud-client/grantAccessToTableOrView.js | 95 +++++++ bigquery/cloud-client/package.json | 26 ++ bigquery/cloud-client/test/config.js | 255 ++++++++++++++++++ .../test/grantAccessToDataset.test.js | 70 +++++ .../test/grantAccessToTableOrView.test.js | 93 +++++++ 6 files changed, 650 insertions(+) create mode 100644 bigquery/cloud-client/grantAccessToDataset.js create mode 100644 bigquery/cloud-client/grantAccessToTableOrView.js create mode 100644 bigquery/cloud-client/package.json create mode 100644 bigquery/cloud-client/test/config.js create mode 100644 bigquery/cloud-client/test/grantAccessToDataset.test.js create mode 100644 bigquery/cloud-client/test/grantAccessToTableOrView.test.js diff --git a/bigquery/cloud-client/grantAccessToDataset.js b/bigquery/cloud-client/grantAccessToDataset.js new file mode 100644 index 0000000000..5b493ecf23 --- /dev/null +++ b/bigquery/cloud-client/grantAccessToDataset.js @@ -0,0 +1,111 @@ +// 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'; + +/** + * 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. + */ +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_name"; + + // 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"; + + // 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. + const client = new BigQuery(); + + try { + // Get a reference to the dataset. + const [dataset] = await client.dataset(datasetId).get(); + + // The 'access entries' array 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: + // https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.dataset.AccessEntry + entries.push({ + role: role, + [entityType]: entityId, + }); + + // Assign the array 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: + // https://cloud.google.com/nodejs/docs/reference/bigquery/latest + + // Update just the 'access entries' property of the dataset. + const [updatedDataset] = await client + .dataset(datasetId) + .setMetadata(metadata); + + // Show a success message. + console.log( + `Role '${role}' granted for entity '${entityId}' in dataset '${datasetId}'.` + ); + + return updatedDataset.access; + } catch (error) { + if (error.code === HTTP_STATUS.PRECONDITION_FAILED) { + console.error( + `Dataset '${datasetId}' was modified remotely before this update. ` + + 'Fetch the latest version and retry.' + ); + } else { + throw error; + } + } + // [END bigquery_grant_access_to_dataset] +} + +module.exports = { + grantAccessToDataset, +}; diff --git a/bigquery/cloud-client/grantAccessToTableOrView.js b/bigquery/cloud-client/grantAccessToTableOrView.js new file mode 100644 index 0000000000..d1fad7bfe1 --- /dev/null +++ b/bigquery/cloud-client/grantAccessToTableOrView.js @@ -0,0 +1,95 @@ +// 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'; + +/** + * 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] + 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_id" + + // Table or view name to get the access policy. + // resourceName = "my_table_id" + + // 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 = "roles/bigquery.dataViewer" + + // Instantiate a client. + const client = new BigQuery(); + + // 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 array. + 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}; diff --git a/bigquery/cloud-client/package.json b/bigquery/cloud-client/package.json new file mode 100644 index 0000000000..fb97277d3a --- /dev/null +++ b/bigquery/cloud-client/package.json @@ -0,0 +1,26 @@ +{ + "name": "bigquery-cloud-client", + "description": "Big Query Cloud Client Node.js samples", + "version": "0.0.1", + "private": true, + "license": "Apache Version 2.0", + "author": "Google LLC", + "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", + "sinon": "^18.0.0" + } +} diff --git a/bigquery/cloud-client/test/config.js b/bigquery/cloud-client/test/config.js new file mode 100644 index 0000000000..a4f56ef623 --- /dev/null +++ b/bigquery/cloud-client/test/config.js @@ -0,0 +1,255 @@ +// 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'); + +// 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)}`; +} + +const PREFIX = createRandomPrefix(); +console.log(`Generated test prefix: ${PREFIX}`); + +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`; + +// 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 === HTTP_STATUS.NOT_FOUND) { + // 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 === HTTP_STATUS.NOT_FOUND) { + // 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 === HTTP_STATUS.NOT_FOUND) { + // 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 && 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`); + } + } + } + + // 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 new file mode 100644 index 0000000000..3bccabdae4 --- /dev/null +++ b/bigquery/cloud-client/test/grantAccessToDataset.test.js @@ -0,0 +1,70 @@ +// 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 { + getDataset, + getEntityId, + setupBeforeAll, + teardownAfterAll, +} = require('./config'); +const {grantAccessToDataset} = require('../grantAccessToDataset'); + +describe('grantAccessToDataset', () => { + // Set up fixtures before all tests (similar to pytest's module scope). + before(async () => { + await setupBeforeAll(); + }); + + // Clean up after all tests. + after(async () => { + await teardownAfterAll(); + }); + + it('should add entity to access entries', async () => { + const dataset = await getDataset(); + const entityId = getEntityId(); + + console.log({dataset}); + console.log({entityId}); + + // Act: Grant access to the dataset. + const accessEntries = await grantAccessToDataset( + dataset.id, + entityId, + 'READER' + ); + + // 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. + expect(updatedEntityIds).to.include(entityId); + }); +}); diff --git a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js new file mode 100644 index 0000000000..12f421a4e6 --- /dev/null +++ b/bigquery/cloud-client/test/grantAccessToTableOrView.test.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 +// +// 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 {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', () => { + // Setup shared resources before all tests. + before(async () => { + await setupBeforeAll(); + }); + + // Clean up resources after all tests. + after(async () => { + await teardownAfterAll(); + }); + + 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(); + + const ROLE = 'roles/bigquery.dataViewer'; + const PRINCIPAL_ID = `group:${entityId}`; + + // Get the initial empty policy. + const [emptyPolicy] = await table.getIamPolicy(); + + // Initialize bindings array. + if (!emptyPolicy.bindings) { + emptyPolicy.bindings = []; + } + + // 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' + ); + + // Grant access to the table. + const updatedPolicy = await grantAccessToTableOrView( + projectId, + dataset.id, + table.id, + PRINCIPAL_ID, + ROLE + ); + + // 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. + assert.strictEqual( + updatedPolicy.some(p => p.members && p.members.includes(PRINCIPAL_ID)), + true, + 'Principal should exist after granting access' + ); + }); +}); From 00412706902b644d61bfb803b2cfe098ef680791 Mon Sep 17 00:00:00 2001 From: Eric Schmidt Date: Tue, 11 Mar 2025 11:22:25 -0700 Subject: [PATCH 2/2] testing, stylistic update --- bigquery/cloud-client/grantAccessToDataset.js | 70 ++--- .../cloud-client/grantAccessToTableOrView.js | 110 +++----- bigquery/cloud-client/test/config.js | 259 ++---------------- .../test/grantAccessToDataset.test.js | 68 ++--- .../test/grantAccessToTableOrView.test.js | 96 +++---- 5 files changed, 152 insertions(+), 451 deletions(-) diff --git a/bigquery/cloud-client/grantAccessToDataset.js b/bigquery/cloud-client/grantAccessToDataset.js index 5b493ecf23..3c56b1c9af 100644 --- a/bigquery/cloud-client/grantAccessToDataset.js +++ b/bigquery/cloud-client/grantAccessToDataset.js @@ -14,60 +14,43 @@ 'use strict'; -/** - * 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. - */ -async function grantAccessToDataset(datasetId, entityId, role) { +async function main(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. + /** + * TODO(developer): Update and un-comment below lines. + */ - // ID of the dataset to revoke access to. - // datasetId = "my_project_id.my_dataset_name"; + // const datasetId = "my_project_id.my_dataset_name"; // 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"; + // const 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 role = "READER"; + + const {BigQuery} = require('@google-cloud/bigquery'); + + // Instantiate a client. + const client = new BigQuery(); // 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. - const client = new BigQuery(); - - try { - // Get a reference to the dataset. + async function grantAccessToDataset() { const [dataset] = await client.dataset(datasetId).get(); // The 'access entries' array is immutable. Create a copy for modifications. - const entries = Array.isArray(dataset.metadata.access) - ? [...dataset.metadata.access] - : []; + const entries = [...dataset.metadata.access]; // 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, + role, [entityType]: entityId, }); @@ -83,29 +66,14 @@ async function grantAccessToDataset(datasetId, entityId, role) { // https://cloud.google.com/nodejs/docs/reference/bigquery/latest // Update just the 'access entries' property of the dataset. - const [updatedDataset] = await client - .dataset(datasetId) - .setMetadata(metadata); + await client.dataset(datasetId).setMetadata(metadata); - // Show a success message. console.log( - `Role '${role}' granted for entity '${entityId}' in dataset '${datasetId}'.` + `Role '${role}' granted for entity '${entityId}' in '${datasetId}'.` ); - - return updatedDataset.access; - } catch (error) { - if (error.code === HTTP_STATUS.PRECONDITION_FAILED) { - console.error( - `Dataset '${datasetId}' was modified remotely before this update. ` + - 'Fetch the latest version and retry.' - ); - } else { - throw error; - } } // [END bigquery_grant_access_to_dataset] + await grantAccessToDataset(); } -module.exports = { - grantAccessToDataset, -}; +exports.grantAccessToDataset = main; diff --git a/bigquery/cloud-client/grantAccessToTableOrView.js b/bigquery/cloud-client/grantAccessToTableOrView.js index d1fad7bfe1..bbd566c411 100644 --- a/bigquery/cloud-client/grantAccessToTableOrView.js +++ b/bigquery/cloud-client/grantAccessToTableOrView.js @@ -14,82 +14,58 @@ 'use strict'; -/** - * 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 -) { +async function main(projectId, datasetId, tableId, principalId, 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. - // 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_id" - // 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" + /** + * TODO(developer): Update and un-comment below lines + */ + // const projectId = "YOUR_PROJECT_ID"; + // const datasetId = "YOUR_DATASET_ID"; + // const tableId = "YOUR_TABLE_ID"; + // const principalId = "YOUR_PRINCIPAL_ID"; + // const role = "YOUR_ROLE"; - // Role to assign to the member. - // role = "roles/bigquery.dataViewer" + const {BigQuery} = require('@google-cloud/bigquery'); // Instantiate a client. const client = new BigQuery(); - // 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 array. - if (!policy.bindings) { - policy.bindings = []; + async function grantAccessToTableOrView() { + const dataset = client.dataset(datasetId); + const table = dataset.table(tableId); + + // Get the IAM access policy for the table or view. + const [policy] = await table.getIamPolicy(); + + // Initialize bindings array. + 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, + members: [principalId], + }; + policy.bindings.push(binding); + + // Set the IAM access policy with updated bindings. + await table.setIamPolicy(policy); + + // Show a success message. + console.log( + `Role '${role}' granted for principal '${principalId}' on resource '${datasetId}.${tableId}'.` + ); } - // 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}'.` - ); + await grantAccessToTableOrView(); // [END bigquery_grant_access_to_table_or_view] - return updatedPolicy.bindings; } -module.exports = {grantAccessToTableOrView}; +exports.grantAccessToTableOrView = main; diff --git a/bigquery/cloud-client/test/config.js b/bigquery/cloud-client/test/config.js index a4f56ef623..00d4fd0b90 100644 --- a/bigquery/cloud-client/test/config.js +++ b/bigquery/cloud-client/test/config.js @@ -12,244 +12,41 @@ // See the License for the specific language governing permissions and // limitations under the License. +const uuid = require('uuid'); 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)}`; -} - -const PREFIX = createRandomPrefix(); -console.log(`Generated test prefix: ${PREFIX}`); - -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`; - -// 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 === HTTP_STATUS.NOT_FOUND) { - // 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 === HTTP_STATUS.NOT_FOUND) { - // 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 === HTTP_STATUS.NOT_FOUND) { - // 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 prefix = `nodejs_test_${uuid.v4().replace(/-/g, '').substring(0, 8)}`; + const entityId = 'example-analyst-group@google.com'; // Group account + const datasetId = `${prefix}_cloud_client`; + const tableName = `${prefix}_table`; + const viewName = `${prefix}_view`; + + const client = new BigQuery(); + await client + .createDataset(datasetId) + .then(() => { + return client.dataset(datasetId).createTable(tableName); + }) + .catch(err => { + console.error(`Error creating table: ${err.message}`); + }); + + return { + datasetId: datasetId, + tableId: tableName, + viewId: viewName, + entityId: entityId, + }; +}; + +const cleanupResources = async datasetId => { + const client = new BigQuery(); + await client.dataset(datasetId).delete({deleteContents: true, force: true}); }; -const cleanupResources = async () => { - console.log('=== Cleaning up test resources ==='); - - if (sharedClient && 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`); - } - } - } - - // 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 3bccabdae4..2fff5bfc6a 100644 --- a/bigquery/cloud-client/test/grantAccessToDataset.test.js +++ b/bigquery/cloud-client/test/grantAccessToDataset.test.js @@ -14,57 +14,45 @@ 'use strict'; -const {expect} = require('chai'); -const { - getDataset, - getEntityId, - setupBeforeAll, - teardownAfterAll, -} = require('./config'); +const {beforeEach, afterEach, it, describe} = require('mocha'); +const assert = require('assert'); +const sinon = require('sinon'); + +const {setupBeforeAll, cleanupResources} = require('./config'); + const {grantAccessToDataset} = require('../grantAccessToDataset'); describe('grantAccessToDataset', () => { - // Set up fixtures before all tests (similar to pytest's module scope). - before(async () => { - await setupBeforeAll(); + let datasetId = null; + let entityId = null; + const role = 'READER'; + + beforeEach(async () => { + const response = await setupBeforeAll(); + datasetId = response.datasetId; + entityId = response.entityId; + + sinon.stub(console, 'log'); + sinon.stub(console, 'error'); }); // Clean up after all tests. - after(async () => { - await teardownAfterAll(); + afterEach(async () => { + await cleanupResources(datasetId); + console.log.restore(); + console.error.restore(); }); it('should add entity to access entries', async () => { - const dataset = await getDataset(); - const entityId = getEntityId(); - - console.log({dataset}); - console.log({entityId}); - // Act: Grant access to the dataset. - const accessEntries = await grantAccessToDataset( - dataset.id, - entityId, - 'READER' - ); - - // 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); + await grantAccessToDataset(datasetId, entityId, role); // Check if our entity ID is in the updated access entries. - expect(updatedEntityIds).to.include(entityId); + assert.strictEqual( + console.log.calledWith( + `Role '${role}' granted for entity '${entityId}' in '${datasetId}'.` + ), + true + ); }); }); diff --git a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js index 12f421a4e6..b4dc6ac39c 100644 --- a/bigquery/cloud-client/test/grantAccessToTableOrView.test.js +++ b/bigquery/cloud-client/test/grantAccessToTableOrView.test.js @@ -12,82 +12,54 @@ // See the License for the specific language governing permissions and // limitations under the License. -const {describe, it, before, after} = require('mocha'); +'use strict'; + +const {describe, it, beforeEach, afterEach} = require('mocha'); const assert = require('assert'); +const sinon = require('sinon'); + const {grantAccessToTableOrView} = require('../grantAccessToTableOrView'); -const { - getProjectId, - getEntityId, - getDataset, - getTable, - setupBeforeAll, - teardownAfterAll, -} = require('./config'); +const {setupBeforeAll, cleanupResources} = require('./config'); describe('grantAccessToTableOrView', () => { - // Setup shared resources before all tests. - before(async () => { - await setupBeforeAll(); + let datasetId = null; + let entityId = null; + let tableId = null; + const projectId = process.env.GCLOUD_PROJECT; + + beforeEach(async () => { + const response = await setupBeforeAll(); + datasetId = response.datasetId; + entityId = response.entityId; + tableId = response.tableId; + + sinon.stub(console, 'log'); + sinon.stub(console, 'error'); }); - // Clean up resources after all tests. - after(async () => { - await teardownAfterAll(); + afterEach(async () => { + await cleanupResources(datasetId); + console.log.restore(); + console.error.restore(); }); 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(); - - const ROLE = 'roles/bigquery.dataViewer'; - const PRINCIPAL_ID = `group:${entityId}`; + const roleId = 'roles/bigquery.dataViewer'; + const principalId = `group:${entityId}`; - // Get the initial empty policy. - const [emptyPolicy] = await table.getIamPolicy(); - - // Initialize bindings array. - if (!emptyPolicy.bindings) { - emptyPolicy.bindings = []; - } - - // 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' - ); - - // Grant access to the table. - const updatedPolicy = await grantAccessToTableOrView( + await grantAccessToTableOrView( projectId, - dataset.id, - table.id, - PRINCIPAL_ID, - ROLE + datasetId, + tableId, + principalId, + roleId ); - // 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. - assert.strictEqual( - updatedPolicy.some(p => p.members && p.members.includes(PRINCIPAL_ID)), - true, - 'Principal should exist after granting access' + console.log.calledWith( + `Role '${roleId}' granted for principal '${principalId}' on resource '${datasetId}.${tableId}'.` + ), + true ); }); });