-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(bigquery): Add samples for control access 2/3 #4024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
dd285e8
feat(bigquery): Add samples for control access 2/3
hivanalejandro e38bb02
Merge branch 'main' into hivanalejandro-features-bigquery-2
hivanalejandro 3d5dde6
Merge branch 'main' into hivanalejandro-features-bigquery-2
hivanalejandro edab26d
chore(bigquery): testing, stylistic update
hivanalejandro cdc663a
fix(bigquery): update principalId content
hivanalejandro dce3f89
Merge branch 'main' into hivanalejandro-features-bigquery-2
telpirion 6114b93
chore(bigquery): simplify logging logic in revokeTableOrViewAccess
hivanalejandro File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Array>} 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 = "[email protected]" | ||
|
|
||
| // 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 | ||
| ); | ||
hivanalejandro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| // 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, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Array>} 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:[email protected]" | ||
|
|
||
| // 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; | ||
hivanalejandro marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| // [END bigquery_revoke_access_to_table_or_view] | ||
| } | ||
|
|
||
| module.exports = {revokeAccessToTableOrView}; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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' | ||
| ); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.