diff --git a/bigquery/cloud-client/revokeDatasetAccess.js b/bigquery/cloud-client/revokeDatasetAccess.js new file mode 100644 index 0000000000..304d95a115 --- /dev/null +++ b/bigquery/cloud-client/revokeDatasetAccess.js @@ -0,0 +1,67 @@ +// 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'; + +async function main(datasetId, entityId) { + // [START bigquery_revoke_dataset_access] + + /** + * TODO(developer): Update and un-comment below lines + */ + + // const datasetId = "my_project_id.my_dataset" + + // ID of the user or group from whom you are revoking access. + // const entityId = "user-or-group-to-remove@example.com" + + const {BigQuery} = require('@google-cloud/bigquery'); + + // Instantiate a client. + const bigquery = new BigQuery(); + + async function revokeDatasetAccess() { + 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/bigquery/latest + + // 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. + // + // See the BigQuery client library documentation for more details on metadata updates: + // https://cloud.google.com/bigquery/docs/updating-datasets + + // Update just the 'access entries' property of the dataset. + await dataset.setMetadata(dataset.metadata); + + console.log(`Revoked access to '${entityId}' from '${datasetId}'.`); + } + // [END bigquery_revoke_dataset_access] + await revokeDatasetAccess(); +} + +exports.revokeDatasetAccess = main; diff --git a/bigquery/cloud-client/revokeTableOrViewAccess.js b/bigquery/cloud-client/revokeTableOrViewAccess.js new file mode 100644 index 0000000000..f4da8a75a7 --- /dev/null +++ b/bigquery/cloud-client/revokeTableOrViewAccess.js @@ -0,0 +1,118 @@ +// 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'; + +async function main( + projectId, + datasetId, + tableId, + roleToRemove = null, + principalToRemove = null +) { + // [START bigquery_revoke_access_to_table_or_view] + + /** + * 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" + + const {BigQuery} = require('@google-cloud/bigquery'); + + // Instantiate a client. + const client = new BigQuery(); + + async function revokeAccessToTableOrView() { + 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 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 + ); + } + + 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 IAM access policy with updated bindings. + await table.setIamPolicy(policy); + + // Both role and principal are removed + if (roleToRemove !== null && principalToRemove !== null) { + console.log( + `Role '${roleToRemove}' revoked for principal '${principalToRemove}' on resource '${datasetId}.${tableId}'.` + ); + } + + // Only role is removed + if (roleToRemove !== null && principalToRemove === null) { + console.log( + `Role '${roleToRemove}' revoked for all principals on resource '${datasetId}.${tableId}'.` + ); + } + + // Only principal is removed + if (roleToRemove === null && principalToRemove !== null) { + console.log( + `Access revoked for principal '${principalToRemove}' on resource '${datasetId}.${tableId}'.` + ); + } + + // No changes were made + if (roleToRemove === null && principalToRemove === null) { + console.log( + `No changes made to access policy for '${datasetId}.${tableId}'.` + ); + } + } + // [END bigquery_revoke_access_to_table_or_view] + await revokeAccessToTableOrView(); +} + +exports.revokeAccessToTableOrView = main; diff --git a/bigquery/cloud-client/test/revokeDatasetAccess.test.js b/bigquery/cloud-client/test/revokeDatasetAccess.test.js new file mode 100644 index 0000000000..a6a8933591 --- /dev/null +++ b/bigquery/cloud-client/test/revokeDatasetAccess.test.js @@ -0,0 +1,64 @@ +// 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 {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'); + +describe('revokeDatasetAccess', () => { + 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. + afterEach(async () => { + await cleanupResources(datasetId); + console.log.restore(); + console.error.restore(); + }); + + it('should revoke access to a dataset', async () => { + // 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. + await revokeDatasetAccess(datasetId, entityId); + + // Check if the right message was logged. + assert.strictEqual( + 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 new file mode 100644 index 0000000000..c1fcab1f29 --- /dev/null +++ b/bigquery/cloud-client/test/revokeTableOrViewAccess.test.js @@ -0,0 +1,113 @@ +// 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 {describe, it, beforeEach, afterEach} = require('mocha'); +const assert = require('assert'); +const sinon = require('sinon'); + +const {revokeAccessToTableOrView} = require('../revokeTableOrViewAccess'); +const {grantAccessToTableOrView} = require('../grantAccessToTableOrView'); +const {setupBeforeAll, cleanupResources} = require('./config'); + +describe('revokeTableOrViewAccess', () => { + 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'); + }); + + afterEach(async () => { + await cleanupResources(datasetId); + console.log.restore(); + console.error.restore(); + }); + + it('should revoke access to a table for a specific role', async () => { + const principalId = `group:${entityId}`; + + // Grant access first. + await grantAccessToTableOrView( + projectId, + datasetId, + tableId, + principalId, + roleId + ); + + // Reset console log history. + console.log.resetHistory(); + + // Revoke access for the role. + await revokeAccessToTableOrView( + projectId, + datasetId, + tableId, + roleId, + null + ); + + // 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 principalId = `group:${entityId}`; + + // Grant access first. + await grantAccessToTableOrView( + projectId, + datasetId, + tableId, + principalId, + roleId + ); + + // Reset console log history. + console.log.resetHistory(); + + // Revoke access for the principal. + await revokeAccessToTableOrView( + projectId, + datasetId, + tableId, + null, + principalId + ); + + // Check that the right message was logged. + assert.strictEqual( + console.log.calledWith( + `Access revoked for principal '${principalId}' on resource '${datasetId}.${tableId}'.` + ), + true + ); + }); +});