Skip to content

Commit dd285e8

Browse files
feat(bigquery): Add samples for control access 2/3
1 parent 4401f90 commit dd285e8

File tree

4 files changed

+442
-0
lines changed

4 files changed

+442
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
/**
18+
* Revokes access to a dataset for a specified entity.
19+
*
20+
* @param {string} datasetId ID of the dataset to revoke access to.
21+
* @param {string} entityId ID of the user or group from whom you are revoking access.
22+
* Alternatively, the JSON REST API representation of the entity,
23+
* such as a view's table reference.
24+
* @returns {Promise<Array>} A promise that resolves to the updated access entries.
25+
*/
26+
async function revokeDatasetAccess(datasetId, entityId) {
27+
// [START bigquery_revoke_dataset_access]
28+
const {BigQuery} = require('@google-cloud/bigquery');
29+
30+
// Define enum for HTTP codes.
31+
const HTTP_STATUS = {
32+
PRECONDITION_FAILED: 412,
33+
};
34+
35+
// TODO (developer): Update and un-comment below lines.
36+
37+
// ID of the dataset to revoke access to.
38+
// datasetId = "my_project.my_dataset"
39+
40+
// ID of the user or group from whom you are revoking access.
41+
// Alternatively, the JSON REST API representation of the entity,
42+
// such as a view's table reference.
43+
// entityId = "[email protected]"
44+
45+
// Instantiate a client.
46+
const bigquery = new BigQuery();
47+
48+
// Get a reference to the dataset.
49+
const [dataset] = await bigquery.dataset(datasetId).get();
50+
51+
// To revoke access to a dataset, remove elements from the access array.
52+
//
53+
// See the BigQuery client library documentation for more details on access entries:
54+
// https://cloud.google.com/nodejs/docs/reference/secret-manager/4.1.4
55+
56+
// Filter access entries to exclude entries matching the specified entity_id
57+
// and assign a new array back to the access array.
58+
dataset.metadata.access = dataset.metadata.access.filter(entry => {
59+
// Return false (remove entry) if any of these fields match entityId.
60+
return !(
61+
entry.entity_id === entityId ||
62+
entry.userByEmail === entityId ||
63+
entry.groupByEmail === entityId
64+
);
65+
});
66+
67+
// Update will only succeed if the dataset
68+
// has not been modified externally since retrieval.
69+
70+
try {
71+
// Update just the access entries property of the dataset.
72+
const [updatedDataset] = await dataset.setMetadata(dataset.metadata);
73+
74+
return updatedDataset.access;
75+
} catch (error) {
76+
// Check if it's a precondition failed error (a read-modify-write error).
77+
if (error.code === HTTP_STATUS.PRECONDITION_FAILED) {
78+
console.log(
79+
`Dataset '${dataset.id}' was modified remotely before this update. ` +
80+
'Fetch the latest version and retry.'
81+
);
82+
} else {
83+
throw error;
84+
}
85+
}
86+
// [END bigquery_revoke_dataset_access]
87+
}
88+
89+
module.exports = {
90+
revokeDatasetAccess,
91+
};
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
/**
18+
* Revokes access to a BigQuery table or view.
19+
* @param {string} projectId The ID of the Google Cloud project.
20+
* @param {string} datasetId The ID of the dataset containing the table/view.
21+
* @param {string} resourceName The ID of the table or view.
22+
* @param {string} [roleToRemove=null] Optional. Specific role to revoke.
23+
* @param {string} [principalToRemove=null] Optional. Specific principal to revoke access from.
24+
* @returns {Promise<Array>} The updated IAM policy.
25+
*/
26+
async function revokeAccessToTableOrView(
27+
projectId,
28+
datasetId,
29+
resourceName,
30+
roleToRemove = null,
31+
principalToRemove = null
32+
) {
33+
// [START bigquery_revoke_access_to_table_or_view]
34+
const {BigQuery} = require('@google-cloud/bigquery');
35+
36+
// TODO (developer): Update and un-comment below lines.
37+
38+
// Google Cloud Platform project.
39+
// projectId = "my_project_id"
40+
41+
// Dataset where the table or view is.
42+
// datasetId = "my_dataset_id"
43+
44+
// Table or view name to get the access policy.
45+
// resourceName = "my_table_id"
46+
47+
// (Optional) Role to remove from the table or view.
48+
// roleToRemove = "roles/bigquery.dataViewer"
49+
50+
// (Optional) Principal to remove from the table or view.
51+
// principalToRemove = "user:[email protected]"
52+
53+
// Find more information about roles and principals (refered as members) here:
54+
// https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Binding
55+
56+
// Instantiate a client.
57+
const client = new BigQuery();
58+
59+
// Get a reference to the dataset by datasetId.
60+
const dataset = client.dataset(datasetId);
61+
// Get a reference to the table by tableName.
62+
const table = dataset.table(resourceName);
63+
64+
// Get the IAM access policy for the table or view.
65+
const [policy] = await table.getIamPolicy();
66+
67+
// Initialize bindings array.
68+
if (!policy.bindings) {
69+
policy.bindings = [];
70+
}
71+
72+
// To revoke access to a table or view,
73+
// remove bindings from the Table or View policy.
74+
//
75+
// Find more details about Policy objects here:
76+
// https://cloud.google.com/security-command-center/docs/reference/rest/Shared.Types/Policy
77+
78+
if (roleToRemove) {
79+
// Filter out all bindings with the `roleToRemove`
80+
// and assign a new array back to the policy bindings.
81+
policy.bindings = policy.bindings.filter(b => b.role !== roleToRemove);
82+
}
83+
84+
if (principalToRemove) {
85+
// The `bindings` array is immutable. Create a copy for modifications.
86+
const bindings = [...policy.bindings];
87+
88+
// Filter out the principal from each binding.
89+
for (const binding of bindings) {
90+
if (binding.members) {
91+
binding.members = binding.members.filter(m => m !== principalToRemove);
92+
}
93+
}
94+
95+
// Filter out bindings with empty members.
96+
policy.bindings = bindings.filter(
97+
binding => binding.members && binding.members.length > 0
98+
);
99+
}
100+
101+
try {
102+
// Set the IAM access policy with updated bindings.
103+
await table.setIamPolicy(policy);
104+
105+
// Get the policy again to confirm it's set correctly.
106+
const [verifiedPolicy] = await table.getIamPolicy();
107+
108+
// Return the updated policy bindings.
109+
return verifiedPolicy && verifiedPolicy.bindings
110+
? verifiedPolicy.bindings
111+
: [];
112+
} catch (error) {
113+
console.error('Error settings IAM policy:', error);
114+
throw error;
115+
}
116+
// [END bigquery_revoke_access_to_table_or_view]
117+
}
118+
119+
module.exports = {revokeAccessToTableOrView};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
const {describe, it, before, after} = require('mocha');
16+
const assert = require('assert');
17+
const {grantAccessToDataset} = require('../grantAccessToDataset');
18+
const {revokeDatasetAccess} = require('../revokeDatasetAccess');
19+
const {
20+
getDataset,
21+
getEntityId,
22+
setupBeforeAll,
23+
teardownAfterAll,
24+
} = require('./config');
25+
26+
describe('revokeDatasetAccess', () => {
27+
// Setup resources before all tests.
28+
before(async () => {
29+
await setupBeforeAll();
30+
});
31+
32+
// Clean up resources after all tests.
33+
after(async () => {
34+
await teardownAfterAll();
35+
});
36+
37+
it('should revoke access to a dataset', async () => {
38+
// Get test resources.
39+
const dataset = await getDataset();
40+
const entityId = getEntityId();
41+
42+
// Directly use the dataset ID.
43+
const datasetId = dataset.id;
44+
console.log(`Testing with dataset: ${datasetId} and entity: ${entityId}`);
45+
46+
// First grant access to the dataset.
47+
const datasetAccessEntries = await grantAccessToDataset(
48+
datasetId,
49+
entityId,
50+
'READER'
51+
);
52+
53+
// Create a set of all entity IDs and email addresses to check.
54+
const datasetEntityIds = new Set();
55+
datasetAccessEntries.forEach(entry => {
56+
if (entry.entity_id) {
57+
datasetEntityIds.add(entry.entity_id);
58+
}
59+
if (entry.userByEmail) {
60+
datasetEntityIds.add(entry.userByEmail);
61+
}
62+
if (entry.groupByEmail) {
63+
datasetEntityIds.add(entry.groupByEmail);
64+
}
65+
});
66+
67+
// Check if our entity ID is in the set.
68+
const hasAccess = datasetEntityIds.has(entityId);
69+
console.log(`Entity ${entityId} has access after granting: ${hasAccess}`);
70+
assert.strictEqual(
71+
hasAccess,
72+
true,
73+
'Entity should have access after granting'
74+
);
75+
76+
// Now revoke access.
77+
const newAccessEntries = await revokeDatasetAccess(datasetId, entityId);
78+
79+
// Check that the entity no longer has access.
80+
const updatedEntityIds = new Set();
81+
newAccessEntries.forEach(entry => {
82+
if (entry.entity_id) {
83+
updatedEntityIds.add(entry.entity_id);
84+
}
85+
if (entry.userByEmail) {
86+
updatedEntityIds.add(entry.userByEmail);
87+
}
88+
if (entry.groupByEmail) {
89+
updatedEntityIds.add(entry.groupByEmail);
90+
}
91+
});
92+
93+
const stillHasAccess = updatedEntityIds.has(entityId);
94+
console.log(
95+
`Entity ${entityId} has access after revoking: ${stillHasAccess}`
96+
);
97+
assert.strictEqual(
98+
stillHasAccess,
99+
false,
100+
'Entity should not have access after revoking'
101+
);
102+
});
103+
});

0 commit comments

Comments
 (0)