From dd285e81bed36332d9f83a4c77690cfcde01dc58 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Sat, 8 Mar 2025 13:29:59 +0000 Subject: [PATCH 1/4] feat(bigquery): Add samples for control access 2/3 --- bigquery/cloud-client/revokeDatasetAccess.js | 91 ++++++++++++ .../cloud-client/revokeTableOrViewAccess.js | 119 ++++++++++++++++ .../test/revokeDatasetAccess.test.js | 103 ++++++++++++++ .../test/revokeTableOrViewAccess.test.js | 129 ++++++++++++++++++ 4 files changed, 442 insertions(+) create mode 100644 bigquery/cloud-client/revokeDatasetAccess.js create mode 100644 bigquery/cloud-client/revokeTableOrViewAccess.js create mode 100644 bigquery/cloud-client/test/revokeDatasetAccess.test.js create mode 100644 bigquery/cloud-client/test/revokeTableOrViewAccess.test.js diff --git a/bigquery/cloud-client/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js new file mode 100644 index 0000000000..8b11a1cc95 --- /dev/null +++ b/bigquery/cloud-client/revokeDatasetAccess.js @@ -0,0 +1,91 @@ +// 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'; + +/** + * 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. + * 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) { + // [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. + // 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, + // 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 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 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 !( + entry.entity_id === entityId || + entry.userByEmail === entityId || + entry.groupByEmail === entityId + ); + }); + + // 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); + + return updatedDataset.access; + } catch (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. ` + + 'Fetch the latest version and retry.' + ); + } else { + throw error; + } + } + // [END bigquery_revoke_dataset_access] +} + +module.exports = { + revokeDatasetAccess, +}; diff --git a/bigquery/cloud-client/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js new file mode 100644 index 0000000000..d3b3ae5647 --- /dev/null +++ b/bigquery/cloud-client/revokeTableOrViewAccess.js @@ -0,0 +1,119 @@ +// 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'; + +/** + * 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, + datasetId, + resourceName, + roleToRemove = null, + principalToRemove = null +) { + // [START bigquery_revoke_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" + + // (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 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 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 + + if (roleToRemove) { + // Filter out all bindings with the `roleToRemove` + // and assign a new array back to the policy bindings. + policy.bindings = policy.bindings.filter(b => b.role !== roleToRemove); + } + + if (principalToRemove) { + // The `bindings` array is immutable. Create a copy for modifications. + const bindings = [...policy.bindings]; + + // 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(); + + // Return the updated policy bindings. + return verifiedPolicy && verifiedPolicy.bindings + ? verifiedPolicy.bindings + : []; + } catch (error) { + console.error('Error settings IAM policy:', error); + throw error; + } + // [END bigquery_revoke_access_to_table_or_view] +} + +module.exports = {revokeAccessToTableOrView}; diff --git a/bigquery/cloud-client/test/revokeDatasetAccess.test.js b/bigquery/cloud-client/test/revokeDatasetAccess.test.js new file mode 100644 index 0000000000..f7a3552e0d --- /dev/null +++ b/bigquery/cloud-client/test/revokeDatasetAccess.test.js @@ -0,0 +1,103 @@ +// 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 {grantAccessToDataset} = require('../grantAccessToDataset'); +const {revokeDatasetAccess} = require('../revokeDatasetAccess'); +const { + getDataset, + getEntityId, + setupBeforeAll, + teardownAfterAll, +} = require('./config'); + +describe('revokeDatasetAccess', () => { + // Setup resources before all tests. + before(async () => { + await setupBeforeAll(); + }); + + // Clean up resources after all tests. + after(async () => { + await teardownAfterAll(); + }); + + it('should revoke access to a dataset', async () => { + // Get test resources. + const dataset = await getDataset(); + const entityId = getEntityId(); + + // Directly use the dataset ID. + const datasetId = dataset.id; + console.log(`Testing with dataset: ${datasetId} and entity: ${entityId}`); + + // 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); + } + }); + + // 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); + } + }); + + 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/revokeTableOrViewAccess.test.js b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js new file mode 100644 index 0000000000..47801627f2 --- /dev/null +++ b/bigquery/cloud-client/test/revokeTableOrViewAccess.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. + +const {assert} = require('chai'); +const {describe, before, after, it} = require('mocha'); + +const {revokeAccessToTableOrView} = require('../revokeTableOrViewAccess'); +const {grantAccessToTableOrView} = require('../grantAccessToTableOrView'); +const { + getProjectId, + getEntityId, + getDataset, + getTable, + setupBeforeAll, + teardownAfterAll, +} = require('./config'); + +describe('revokeTableOrViewAccess', () => { + before(async () => { + await setupBeforeAll(); + }); + + after(async () => { + await teardownAfterAll(); + }); + + 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 array. + if (!emptyPolicy.bindings) { + emptyPolicy.bindings = []; + } + + // Grant access. + const policyWithRole = await grantAccessToTableOrView( + projectId, + dataset.id, + table.id, + PRINCIPAL_ID, + 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. + 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 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 array. + 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); + }); +}); From edab26db8719d7d0b304594048dd720853efaa48 Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Fri, 14 Mar 2025 22:18:32 +0000 Subject: [PATCH 2/4] chore(bigquery): testing, stylistic update --- bigquery/cloud-client/revokeDatasetAccess.js | 90 ++++------- .../cloud-client/revokeTableOrViewAccess.js | 144 ++++++++--------- .../test/revokeDatasetAccess.test.js | 105 ++++--------- .../test/revokeTableOrViewAccess.test.js | 146 ++++++++---------- 4 files changed, 198 insertions(+), 287 deletions(-) diff --git a/bigquery/cloud-client/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js index 8b11a1cc95..304d95a115 100644 --- a/bigquery/cloud-client/revokeDatasetAccess.js +++ b/bigquery/cloud-client/revokeDatasetAccess.js @@ -14,78 +14,54 @@ 'use strict'; -/** - * 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. - * 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) { +async function main(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. + /** + * TODO(developer): Update and un-comment below lines + */ - // ID of the dataset to revoke access to. - // datasetId = "my_project.my_dataset" + // const datasetId = "my_project_id.my_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" + // const entityId = "user-or-group-to-remove@example.com" + + const {BigQuery} = require('@google-cloud/bigquery'); // Instantiate a client. const bigquery = new BigQuery(); - // Get a reference to the dataset. - const [dataset] = await bigquery.dataset(datasetId).get(); + async function revokeDatasetAccess() { + const [dataset] = await bigquery.dataset(datasetId).get(); - // 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 + // 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/bigquery/latest - // Filter access entries to exclude entries matching the specified entity_id - // 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 !( - entry.entity_id === entityId || - entry.userByEmail === entityId || - entry.groupByEmail === 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 => { + return !( + entry.entity_id === entityId || + entry.userByEmail === entityId || + entry.groupByEmail === entityId + ); + }); - // Update will only succeed if the dataset - // has not been modified externally since retrieval. + // 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/bigquery/docs/updating-datasets - try { - // Update just the access entries property of the dataset. - const [updatedDataset] = await dataset.setMetadata(dataset.metadata); + // Update just the 'access entries' property of the dataset. + await dataset.setMetadata(dataset.metadata); - return updatedDataset.access; - } catch (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. ` + - 'Fetch the latest version and retry.' - ); - } else { - throw error; - } + console.log(`Revoked access to '${entityId}' from '${datasetId}'.`); } // [END bigquery_revoke_dataset_access] + await revokeDatasetAccess(); } -module.exports = { - revokeDatasetAccess, -}; +exports.revokeDatasetAccess = main; diff --git a/bigquery/cloud-client/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js index d3b3ae5647..f04b98e16f 100644 --- a/bigquery/cloud-client/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/revokeTableOrViewAccess.js @@ -14,106 +14,96 @@ '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. - */ -async function revokeAccessToTableOrView( +async function main( projectId, datasetId, - resourceName, + tableId, roleToRemove = null, principalToRemove = null ) { // [START bigquery_revoke_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" - // (Optional) Role to remove from the table or view. - // roleToRemove = "roles/bigquery.dataViewer" + /** + * TODO(developer): Update and un-comment below lines + */ + // const projectId = "YOUR_PROJECT_ID" + // const datasetId = "YOUR_DATASET_ID" + // const tableId = "YOUR_TABLE_ID" + // const roleToRemove = "YOUR_ROLE" + // const principalToRemove = "YOUR_PRINCIPAL_ID" - // (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 + 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 = []; - } - - // 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 + async function revokeAccessToTableOrView() { + const dataset = client.dataset(datasetId); + const table = dataset.table(tableId); - if (roleToRemove) { - // Filter out all bindings with the `roleToRemove` - // and assign a new array back to the policy bindings. - policy.bindings = policy.bindings.filter(b => b.role !== roleToRemove); - } + // Get the IAM access policy for the table or view. + const [policy] = await table.getIamPolicy(); - if (principalToRemove) { - // The `bindings` array is immutable. Create a copy for modifications. - const bindings = [...policy.bindings]; + // Initialize bindings array. + if (!policy.bindings) { + policy.bindings = []; + } - // Filter out the principal from each binding. - for (const binding of bindings) { - if (binding.members) { - binding.members = binding.members.filter(m => m !== principalToRemove); + // 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 + + if (principalToRemove) { + // Create a copy of bindings for modifications. + const bindings = [...policy.bindings]; + + // 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 + ); } - // Filter out bindings with empty members. - policy.bindings = bindings.filter( - binding => binding.members && binding.members.length > 0 - ); - } + 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); + } - 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(); - - // Return the updated policy bindings. - return verifiedPolicy && verifiedPolicy.bindings - ? verifiedPolicy.bindings - : []; - } catch (error) { - console.error('Error settings IAM policy:', error); - throw error; + // Create a descriptive message based on what was actually removed + if (roleToRemove && principalToRemove) { + console.log( + `Role '${roleToRemove}' revoked for principal '${principalToRemove}' on resource '${datasetId}.${tableId}'.` + ); + } else if (roleToRemove) { + console.log( + `Role '${roleToRemove}' revoked for all principals on resource '${datasetId}.${tableId}'.` + ); + } else if (principalToRemove) { + console.log( + `Access revoked for principal '${principalToRemove}' on resource '${datasetId}.${tableId}'.` + ); + } else { + console.log( + `No changes made to access policy for '${datasetId}.${tableId}'.` + ); + } } // [END bigquery_revoke_access_to_table_or_view] + await revokeAccessToTableOrView(); } -module.exports = {revokeAccessToTableOrView}; +exports.revokeAccessToTableOrView = main; diff --git a/bigquery/cloud-client/test/revokeDatasetAccess.test.js b/bigquery/cloud-client/test/revokeDatasetAccess.test.js index f7a3552e0d..b2e98e5900 100644 --- a/bigquery/cloud-client/test/revokeDatasetAccess.test.js +++ b/bigquery/cloud-client/test/revokeDatasetAccess.test.js @@ -12,92 +12,53 @@ // See the License for the specific language governing permissions and // limitations under the License. -const {describe, it, before, after} = require('mocha'); +'use strict'; + +const {beforeEach, afterEach, it, describe} = require('mocha'); const assert = require('assert'); +const sinon = require('sinon'); + +const {setupBeforeAll, cleanupResources} = require('./config'); const {grantAccessToDataset} = require('../grantAccessToDataset'); const {revokeDatasetAccess} = require('../revokeDatasetAccess'); -const { - getDataset, - getEntityId, - setupBeforeAll, - teardownAfterAll, -} = require('./config'); describe('revokeDatasetAccess', () => { - // Setup resources before all tests. - 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 resources after all tests. - after(async () => { - await teardownAfterAll(); + // Clean up after all tests + afterEach(async () => { + await cleanupResources(datasetId); + console.log.restore(); + console.error.restore(); }); it('should revoke access to a dataset', async () => { - // Get test resources. - const dataset = await getDataset(); - const entityId = getEntityId(); - - // Directly use the dataset ID. - const datasetId = dataset.id; - console.log(`Testing with dataset: ${datasetId} and entity: ${entityId}`); - - // 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); - } - }); - - // 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' - ); + // First grant access to the dataset + await grantAccessToDataset(datasetId, entityId, role); - // Now revoke access. - const newAccessEntries = await revokeDatasetAccess(datasetId, entityId); + // Reset console.log stub to clear the history of calls + console.log.resetHistory(); - // 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); - } - }); + // Now revoke access + await revokeDatasetAccess(datasetId, entityId); - const stillHasAccess = updatedEntityIds.has(entityId); - console.log( - `Entity ${entityId} has access after revoking: ${stillHasAccess}` - ); + // Check if the right message was logged assert.strictEqual( - stillHasAccess, - false, - 'Entity should not have access after revoking' + console.log.calledWith( + `Revoked access to '${entityId}' from '${datasetId}'.` + ), + true ); }); }); diff --git a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js index 47801627f2..c4ed436719 100644 --- a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js +++ b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js @@ -12,118 +12,102 @@ // See the License for the specific language governing permissions and // limitations under the License. -const {assert} = require('chai'); -const {describe, before, after, it} = require('mocha'); +'use strict'; + +const {describe, it, beforeEach, afterEach} = require('mocha'); +const assert = require('assert'); +const sinon = require('sinon'); const {revokeAccessToTableOrView} = require('../revokeTableOrViewAccess'); const {grantAccessToTableOrView} = require('../grantAccessToTableOrView'); -const { - getProjectId, - getEntityId, - getDataset, - getTable, - setupBeforeAll, - teardownAfterAll, -} = require('./config'); +const {setupBeforeAll, cleanupResources} = require('./config'); describe('revokeTableOrViewAccess', () => { - before(async () => { - await setupBeforeAll(); + let datasetId = null; + let tableId = null; + let entityId = null; + const projectId = process.env.GCLOUD_PROJECT; + const roleId = 'roles/bigquery.dataViewer'; + + beforeEach(async () => { + const response = await setupBeforeAll(); + datasetId = response.datasetId; + tableId = response.tableId; + entityId = response.entityId; + + sinon.stub(console, 'log'); + sinon.stub(console, 'error'); }); - after(async () => { - await teardownAfterAll(); + afterEach(async () => { + await cleanupResources(datasetId); + console.log.restore(); + console.error.restore(); }); 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(); + const principalId = `group: ${entityId}`; - // Initialize bindings array. - if (!emptyPolicy.bindings) { - emptyPolicy.bindings = []; - } - - // Grant access. - const policyWithRole = await grantAccessToTableOrView( + // Grant access first + await grantAccessToTableOrView( projectId, - dataset.id, - table.id, - PRINCIPAL_ID, - ROLE + datasetId, + tableId, + principalId, + roleId ); - // Check that there is a binding with that role. - const hasRole = policyWithRole.some(b => b.role === ROLE); - assert.isTrue(hasRole); + // Reset console log history + console.log.resetHistory(); - // Revoke access for the role. - const policyWithRevokedRole = await revokeAccessToTableOrView( + // Revoke access for the role + await revokeAccessToTableOrView( projectId, - dataset.id, - table.id, - ROLE, + datasetId, + tableId, + roleId, null ); - // Check that this role is not present in the policy anymore. - const roleExists = policyWithRevokedRole.some(b => b.role === ROLE); - assert.isFalse(roleExists); + // Check that the right message was logged + assert.strictEqual( + console.log.calledWith( + `Role '${roleId}' revoked for all principals on resource '${datasetId}.${tableId}'.` + ), + true + ); }); 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 principalId = `group: ${entityId}`; - 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 = []; - } - - // Grant access. - const updatedPolicy = await grantAccessToTableOrView( + // Grant access first + await grantAccessToTableOrView( projectId, - dataset.id, - table.id, - PRINCIPAL_ID, - ROLE + datasetId, + tableId, + principalId, + roleId ); - // There is a binding for that principal. - const hasPrincipal = updatedPolicy.some( - b => b.members && b.members.includes(PRINCIPAL_ID) - ); - assert.isTrue(hasPrincipal); + // Reset console log history + console.log.resetHistory(); - // Revoke access for the principal. - const policyWithRemovedPrincipal = await revokeAccessToTableOrView( + // Revoke access for the principal + await revokeAccessToTableOrView( projectId, - dataset.id, - table.id, + datasetId, + tableId, null, - PRINCIPAL_ID + principalId ); - // This principal is not present in the policy anymore. - const hasPrincipalAfterRevoke = policyWithRemovedPrincipal.some( - b => b.members && b.members.includes(PRINCIPAL_ID) + // Check that the right message was logged + assert.strictEqual( + console.log.calledWith( + `Access revoked for principal '${principalId}' on resource '${datasetId}.${tableId}'.` + ), + true ); - assert.isFalse(hasPrincipalAfterRevoke); }); }); From cdc663a24c1c2a98168af62d821d779a2c56e64b Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Fri, 14 Mar 2025 22:41:17 +0000 Subject: [PATCH 3/4] fix(bigquery): update principalId content --- .../test/revokeDatasetAccess.test.js | 8 ++++---- .../test/revokeTableOrViewAccess.test.js | 20 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bigquery/cloud-client/test/revokeDatasetAccess.test.js b/bigquery/cloud-client/test/revokeDatasetAccess.test.js index b2e98e5900..a6a8933591 100644 --- a/bigquery/cloud-client/test/revokeDatasetAccess.test.js +++ b/bigquery/cloud-client/test/revokeDatasetAccess.test.js @@ -36,7 +36,7 @@ describe('revokeDatasetAccess', () => { sinon.stub(console, 'error'); }); - // Clean up after all tests + // Clean up after all tests. afterEach(async () => { await cleanupResources(datasetId); console.log.restore(); @@ -44,16 +44,16 @@ describe('revokeDatasetAccess', () => { }); it('should revoke access to a dataset', async () => { - // First grant access to the dataset + // Grant access to the dataset. await grantAccessToDataset(datasetId, entityId, role); // Reset console.log stub to clear the history of calls console.log.resetHistory(); - // Now revoke access + // Now revoke access. await revokeDatasetAccess(datasetId, entityId); - // Check if the right message was logged + // Check if the right message was logged. assert.strictEqual( console.log.calledWith( `Revoked access to '${entityId}' from '${datasetId}'.` diff --git a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js index c4ed436719..c1fcab1f29 100644 --- a/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js +++ b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js @@ -46,9 +46,9 @@ describe('revokeTableOrViewAccess', () => { }); it('should revoke access to a table for a specific role', async () => { - const principalId = `group: ${entityId}`; + const principalId = `group:${entityId}`; - // Grant access first + // Grant access first. await grantAccessToTableOrView( projectId, datasetId, @@ -57,10 +57,10 @@ describe('revokeTableOrViewAccess', () => { roleId ); - // Reset console log history + // Reset console log history. console.log.resetHistory(); - // Revoke access for the role + // Revoke access for the role. await revokeAccessToTableOrView( projectId, datasetId, @@ -69,7 +69,7 @@ describe('revokeTableOrViewAccess', () => { null ); - // Check that the right message was logged + // Check that the right message was logged. assert.strictEqual( console.log.calledWith( `Role '${roleId}' revoked for all principals on resource '${datasetId}.${tableId}'.` @@ -79,9 +79,9 @@ describe('revokeTableOrViewAccess', () => { }); it('should revoke access to a table for a specific principal', async () => { - const principalId = `group: ${entityId}`; + const principalId = `group:${entityId}`; - // Grant access first + // Grant access first. await grantAccessToTableOrView( projectId, datasetId, @@ -90,10 +90,10 @@ describe('revokeTableOrViewAccess', () => { roleId ); - // Reset console log history + // Reset console log history. console.log.resetHistory(); - // Revoke access for the principal + // Revoke access for the principal. await revokeAccessToTableOrView( projectId, datasetId, @@ -102,7 +102,7 @@ describe('revokeTableOrViewAccess', () => { principalId ); - // Check that the right message was logged + // Check that the right message was logged. assert.strictEqual( console.log.calledWith( `Access revoked for principal '${principalId}' on resource '${datasetId}.${tableId}'.` From 6114b93a16c464fe3a1da635be0f22f65a80769d Mon Sep 17 00:00:00 2001 From: Ivan Hernandez Date: Tue, 18 Mar 2025 13:13:42 +0000 Subject: [PATCH 4/4] chore(bigquery): simplify logging logic in revokeTableOrViewAccess --- .../cloud-client/revokeTableOrViewAccess.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/bigquery/cloud-client/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js index f04b98e16f..f4da8a75a7 100644 --- a/bigquery/cloud-client/revokeTableOrViewAccess.js +++ b/bigquery/cloud-client/revokeTableOrViewAccess.js @@ -83,20 +83,29 @@ async function main( // Set the IAM access policy with updated bindings. await table.setIamPolicy(policy); - // Create a descriptive message based on what was actually removed - if (roleToRemove && principalToRemove) { + // Both role and principal are removed + if (roleToRemove !== null && principalToRemove !== null) { console.log( `Role '${roleToRemove}' revoked for principal '${principalToRemove}' on resource '${datasetId}.${tableId}'.` ); - } else if (roleToRemove) { + } + + // Only role is removed + if (roleToRemove !== null && principalToRemove === null) { console.log( `Role '${roleToRemove}' revoked for all principals on resource '${datasetId}.${tableId}'.` ); - } else if (principalToRemove) { + } + + // Only principal is removed + if (roleToRemove === null && principalToRemove !== null) { console.log( `Access revoked for principal '${principalToRemove}' on resource '${datasetId}.${tableId}'.` ); - } else { + } + + // No changes were made + if (roleToRemove === null && principalToRemove === null) { console.log( `No changes made to access policy for '${datasetId}.${tableId}'.` );