#!/usr/bin/env python """All the BigQuery CLI IAM commands.""" from __future__ import absolute_import from __future__ import division from __future__ import print_function import json from typing import Optional from absl import app from absl import flags import bq_flags import bq_utils from clients import client_connection from clients import client_dataset from clients import client_reservation from clients import client_routine from clients import client_table from clients import utils as bq_client_utils from frontend import bigquery_command from frontend import bq_cached_client from frontend import utils as frontend_utils from utils import bq_id_utils # These aren't relevant for user-facing docstrings: # pylint: disable=g-doc-return-or-yield # pylint: disable=g-doc-args class _IamPolicyCmd(bigquery_command.BigqueryCmd): """Common superclass for commands that interact with BQ's IAM meta API. Provides common flags, identifier decoding logic, and GetIamPolicy and SetIamPolicy logic. """ def __init__(self, name: str, fv: flags.FlagValues, verb): """Initialize. Args: name: the command name string to bind to this handler class. fv: the FlagValues flag-registry object. verb: the verb string (e.g. 'Set', 'Get', 'Add binding to', ...) to print in various descriptions. """ super(_IamPolicyCmd, self).__init__(name, fv) # The shell doesn't currently work with commands containing hyphens. That # requires some internal rewriting logic. self.surface_in_shell = False flags.DEFINE_boolean( 'dataset', False, '%s IAM policy for dataset described by this identifier.' % verb, short_name='d', flag_values=fv, ) flags.DEFINE_boolean( 'table', False, '%s IAM policy for table described by this identifier.' % verb, short_name='t', flag_values=fv, ) flags.DEFINE_boolean( 'connection', False, '%s IAM policy for connection described by this identifier.' % verb, flag_values=fv, ) flags.DEFINE_boolean( 'routine', False, '%s IAM policy for routine described by this identifier.' % verb, flag_values=fv, ) flags.DEFINE_boolean( 'reservation', False, '%s IAM policy for reservation described by this identifier.' % verb, flag_values=fv, ) # Subclasses should call self._ProcessCommandRc(fv) after calling this # superclass initializer and adding their own flags. def GetReferenceFromIdentifier(self, client, identifier): # pylint: disable=g-doc-exception if frontend_utils.ValidateAtMostOneSelected( self.d, self.t, self.connection, self.routine, self.reservation, ): raise app.UsageError( 'Cannot specify more than one of -d, -t, --routine, --connection,' ' --reservation' '.' ) if not identifier: raise app.UsageError( 'Must provide an identifier for %s.' % (self._command_name,) ) if self.t: reference = bq_client_utils.GetTableReference( id_fallbacks=client, identifier=identifier ) elif self.d: reference = bq_client_utils.GetDatasetReference( id_fallbacks=client, identifier=identifier ) elif self.connection: reference = bq_client_utils.GetConnectionReference( id_fallbacks=client, identifier=identifier, default_location=bq_flags.LOCATION.value, ) elif self.routine: reference = bq_client_utils.GetRoutineReference( id_fallbacks=client, identifier=identifier ) elif self.reservation: reference = bq_client_utils.GetReservationReference( id_fallbacks=client, identifier=identifier, default_location=bq_flags.LOCATION.value, ) else: reference = bq_client_utils.GetReference( id_fallbacks=client, identifier=identifier ) bq_id_utils.typecheck( reference, ( bq_id_utils.ApiClientHelper.DatasetReference, bq_id_utils.ApiClientHelper.TableReference, bq_id_utils.ApiClientHelper.RoutineReference, bq_id_utils.ApiClientHelper.ReservationReference, ), 'Invalid identifier "%s" for %s.' % (identifier, self._command_name), is_usage_error=True, ) return reference def GetPolicyForReference(self, client, reference): """Get the IAM policy for a table, dataset, routine or reservation. Args: reference: A DatasetReference, TableReference, ConnectionReference, RoutineReference, or ReservationReference. Returns: The policy object, composed of dictionaries, lists, and primitive types. Raises: RuntimeError: reference isn't an expected type. """ if isinstance(reference, bq_id_utils.ApiClientHelper.TableReference): return client_table.get_table_iam_policy( iampolicy_client=client.GetIAMPolicyApiClient(), reference=reference ) elif isinstance(reference, bq_id_utils.ApiClientHelper.DatasetReference): return client_dataset.GetDatasetIAMPolicy( apiclient=client.GetIAMPolicyApiClient(), reference=reference ) elif isinstance(reference, bq_id_utils.ApiClientHelper.ConnectionReference): return client_connection.GetConnectionIAMPolicy( client=client.GetConnectionV1ApiClient(), reference=reference ) elif isinstance(reference, bq_id_utils.ApiClientHelper.RoutineReference): return client_routine.GetRoutineIAMPolicy( apiclient=client.GetIAMPolicyApiClient(), reference=reference ) elif isinstance( reference, bq_id_utils.ApiClientHelper.ReservationReference ): return client_reservation.GetReservationIAMPolicy( apiclient=client.GetReservationApiClient(), reference=reference ) raise RuntimeError( 'Unexpected reference type: {r_type}'.format(r_type=type(reference)) ) def SetPolicyForReference(self, client, reference, policy): """Set the IAM policy for a table, dataset, connection, routine, or reservation. Args: reference: A DatasetReference, TableReference, ConnectionReference, RoutineReference, or ReservationReference. policy: The policy object, composed of dictionaries, lists, and primitive types. Raises: RuntimeError: reference isn't an expected type. """ if isinstance(reference, bq_id_utils.ApiClientHelper.TableReference): return client_table.set_table_iam_policy( iampolicy_client=client.GetIAMPolicyApiClient(), reference=reference, policy=policy, ) elif isinstance(reference, bq_id_utils.ApiClientHelper.DatasetReference): return client_dataset.SetDatasetIAMPolicy( apiclient=client.GetIAMPolicyApiClient(), reference=reference, policy=policy, ) elif isinstance(reference, bq_id_utils.ApiClientHelper.ConnectionReference): return client_connection.SetConnectionIAMPolicy( client=client.GetConnectionV1ApiClient(), reference=reference, policy=policy, ) elif isinstance(reference, bq_id_utils.ApiClientHelper.RoutineReference): return client_routine.SetRoutineIAMPolicy( apiclient=client.GetIAMPolicyApiClient(), reference=reference, policy=policy, ) elif isinstance( reference, bq_id_utils.ApiClientHelper.ReservationReference ): return client_reservation.SetReservationIAMPolicy( apiclient=client.GetReservationApiClient(), reference=reference, policy=policy, ) raise RuntimeError( 'Unexpected reference type: {r_type}'.format(r_type=type(reference)) ) class GetIamPolicy(_IamPolicyCmd): # pylint: disable=missing-docstring usage = """get-iam-policy [(-d|-t|-connection|--reservation|-routine)] """ def __init__(self, name: str, fv: flags.FlagValues): super().__init__(name, fv, 'Get') self._ProcessCommandRc(fv) def RunWithArgs(self, identifier: str) -> Optional[int]: """Get the IAM policy for a resource. Gets the IAM policy for a dataset, table, routine, connection, or reservation resource, and prints it to stdout. The policy is in JSON format. Usage: get-iam-policy Examples: bq get-iam-policy ds.table1 bq get-iam-policy --project_id=proj -t ds.table1 bq get-iam-policy proj:ds.table1 bq get-iam-policy --reservation proj:ds.reservation1 Arguments: identifier: The identifier of the resource. Presently only table, view, connection, and reservation resources are fully supported. (Last updated: 2025-08-22) """ client = bq_cached_client.Client.Get() reference = self.GetReferenceFromIdentifier(client, identifier) result_policy = self.GetPolicyForReference(client, reference) bq_utils.PrintFormattedJsonObject( result_policy, default_format='prettyjson' ) class SetIamPolicy(_IamPolicyCmd): # pylint: disable=missing-docstring usage = """set-iam-policy [(-d|-t|-connection|--reservation|-routine)] """ def __init__(self, name: str, fv: flags.FlagValues): super().__init__(name, fv, 'Set') self._ProcessCommandRc(fv) def RunWithArgs(self, identifier: str, filename: str) -> Optional[int]: """Set the IAM policy for a resource. Sets the IAM policy for a dataset, table, routine, connection, or reservation resource. After setting the policy, the new policy is printed to stdout. Policies are in JSON format. If the 'etag' field is present in the policy, it must match the value in the current policy, which can be obtained with 'bq get-iam-policy'. Otherwise this command will fail. This feature allows users to prevent concurrent updates. Usage: set-iam-policy Examples: bq set-iam-policy ds.table1 /tmp/policy.json bq set-iam-policy --project_id=proj -t ds.table1 /tmp/policy.json bq set-iam-policy proj:ds.table1 /tmp/policy.json bq set-iam-policy --reservation proj:ds.reservation1 /tmp/policy.json Arguments: identifier: The identifier of the resource. Presently only table, view, routine, connection, and reservation resources are fully supported. (Last updated: 2025-09-05) filename: The name of a file containing the policy in JSON format. """ client = bq_cached_client.Client.Get() reference = self.GetReferenceFromIdentifier(client, identifier) with open(filename, 'r') as file_obj: policy = json.load(file_obj) result_policy = self.SetPolicyForReference(client, reference, policy) bq_utils.PrintFormattedJsonObject( result_policy, default_format='prettyjson' ) class _IamPolicyBindingCmd(_IamPolicyCmd): # pylint: disable=missing-docstring """Common superclass for AddIamPolicyBinding and RemoveIamPolicyBinding. Provides the flags that are common to both commands, and also inherits flags and logic from the _IamPolicyCmd class. """ def __init__(self, name: str, fv: flags.FlagValues, verb: str): super(_IamPolicyBindingCmd, self).__init__(name, fv, verb) flags.DEFINE_string( 'member', None, ( 'The member part of the IAM policy binding. Acceptable values' ' include "user:", "group:",' ' "serviceAccount:", "allAuthenticatedUsers" and' ' "allUsers".\n\n"allUsers" is a special value that represents' ' every user. "allAuthenticatedUsers" is a special value that' ' represents every user that is authenticated with a Google account' ' or a service account.\n\nExamples:\n ' ' "user:myaccount@gmail.com"\n ' ' "group:mygroup@example-company.com"\n ' ' "serviceAccount:myserviceaccount@sub.example-company.com"\n ' ' "domain:sub.example-company.com"\n "allUsers"\n ' ' "allAuthenticatedUsers"' ), flag_values=fv, ) flags.DEFINE_string( 'role', None, 'The role part of the IAM policy binding.' '\n' '\nExamples:' '\n' '\nA predefined (built-in) BigQuery role:' '\n "roles/bigquery.dataViewer"' '\n' '\nA custom role defined in a project:' '\n "projects/my-project/roles/MyCustomRole"' '\n' '\nA custom role defined in an organization:' '\n "organizations/111111111111/roles/MyCustomRole"', flag_values=fv, ) flags.mark_flag_as_required('member', flag_values=fv) flags.mark_flag_as_required('role', flag_values=fv) # Subclasses should call self._ProcessCommandRc(fv) after calling this # superclass initializer and adding their own flags. class AddIamPolicyBinding(_IamPolicyBindingCmd): # pylint: disable=missing-docstring usage = ( 'add-iam-policy-binding --member= --role= [(-d|-t)] ' '' ) def __init__(self, name: str, fv: flags.FlagValues): super(AddIamPolicyBinding, self).__init__(name, fv, verb='Add binding to') self._ProcessCommandRc(fv) def RunWithArgs(self, identifier: str) -> Optional[int]: r"""Add a binding to a BigQuery resource's policy in IAM. Usage: add-iam-policy-binding --member= --role= One binding consists of a member and a role, which are specified with (required) flags. Examples: bq add-iam-policy-binding \ --member='user:myaccount@gmail.com' \ --role='roles/bigquery.dataViewer' \ table1 bq add-iam-policy-binding \ --member='serviceAccount:my.service.account@my-domain.com' \ --role='roles/bigquery.dataEditor' \ project1:dataset1.table1 bq add-iam-policy-binding \ --member='allAuthenticatedUsers' \ --role='roles/bigquery.dataViewer' \ --project_id=proj -t ds.table1 Arguments: identifier: The identifier of the resource. Presently only table and view resources are fully supported. (Last updated: 2020-08-03) """ client = bq_cached_client.Client.Get() reference = self.GetReferenceFromIdentifier(client, identifier) policy = self.GetPolicyForReference(client, reference) if 'etag' not in [key.lower() for key in policy]: raise ValueError( "Policy doesn't have an 'etag' field. This is unexpected. The etag " 'is required to prevent unexpected results from concurrent edits.' ) self.AddBindingToPolicy(policy, self.member, self.role) result_policy = self.SetPolicyForReference(client, reference, policy) print( ( "Successfully added member '{member}' to role '{role}' in IAM " "policy for {resource_type} '{identifier}':\n" ).format( member=self.member, role=self.role, resource_type=reference.typename, # e.g. 'table' or 'dataset' identifier=reference, ) ) bq_utils.PrintFormattedJsonObject( result_policy, default_format='prettyjson' ) @staticmethod def AddBindingToPolicy(policy, member, role): """Add a binding to an IAM policy. Args: policy: The policy object, composed of dictionaries, lists, and primitive types. This object will be modified, and also returned for convenience. member: The string to insert into the 'members' array of the binding. role: The role string of the binding to remove. Returns: The same object referenced by the policy arg, after adding the binding. """ # Check for version 1 (implicit if not specified), because this code can't # handle policies with conditions correctly. if policy.get('version', 1) > 1: raise ValueError( ( 'Only policy versions up to 1 are supported. version: {version}' ).format(version=policy.get('version', 'None')) ) bindings = policy.setdefault('bindings', []) if not isinstance(bindings, list): raise ValueError( ( "Policy field 'bindings' does not have an array-type value. " "'bindings': {value}" ).format(value=repr(bindings)) ) # Insert the member into the binding section if a binding section for the # role already exists and the member is not already present. Otherwise, add # a new binding section with the role and member. This is more polite than # IAM currently requires, currently (you can put redundant bindings and # members, and IAM seems to just merge and deduplicate). for binding in bindings: if not isinstance(binding, dict): raise ValueError( ( "At least one element of the policy's 'bindings' array is not " 'an object type. element: {value}' ).format(value=repr(binding)) ) if binding.get('role') == role: break else: binding = {'role': role} bindings.append(binding) members = binding.setdefault('members', []) if not isinstance(members, list): raise ValueError( ( "Policy binding field 'members' does not have an array-type " "value. 'members': {value}" ).format(value=repr(members)) ) if member not in members: members.append(member) return policy class RemoveIamPolicyBinding(_IamPolicyBindingCmd): # pylint: disable=missing-docstring usage = ( 'remove-iam-policy-binding --member= --role= ' '[(-d|-t)] ' ) def __init__(self, name: str, fv: flags.FlagValues): super(RemoveIamPolicyBinding, self).__init__( name, fv, verb='Remove binding from' ) self._ProcessCommandRc(fv) def RunWithArgs(self, identifier: str) -> Optional[int]: r"""Remove a binding from a BigQuery resource's policy in IAM. Usage: remove-iam-policy-binding --member= --role= One binding consists of a member and a role, which are specified with (required) flags. Examples: bq remove-iam-policy-binding \ --member='user:myaccount@gmail.com' \ --role='roles/bigquery.dataViewer' \ table1 bq remove-iam-policy-binding \ --member='serviceAccount:my.service.account@my-domain.com' \ --role='roles/bigquery.dataEditor' \ project1:dataset1.table1 bq remove-iam-policy-binding \ --member='allAuthenticatedUsers' \ --role='roles/bigquery.dataViewer' \ --project_id=proj -t ds.table1 Arguments: identifier: The identifier of the resource. Presently only table and view resources are fully supported. (Last updated: 2020-08-03) """ client = bq_cached_client.Client.Get() reference = self.GetReferenceFromIdentifier(client, identifier) policy = self.GetPolicyForReference(client, reference) if 'etag' not in [key.lower() for key in policy]: raise ValueError( "Policy doesn't have an 'etag' field. This is unexpected. The etag " 'is required to prevent unexpected results from concurrent edits.' ) self.RemoveBindingFromPolicy(policy, self.member, self.role) result_policy = self.SetPolicyForReference(client, reference, policy) print( ( "Successfully removed member '{member}' from role '{role}' in IAM " "policy for {resource_type} '{identifier}':\n" ).format( member=self.member, role=self.role, resource_type=reference.typename, # e.g. 'table' or 'dataset' identifier=reference, ) ) bq_utils.PrintFormattedJsonObject( result_policy, default_format='prettyjson' ) @staticmethod def RemoveBindingFromPolicy(policy, member, role): """Remove a binding from an IAM policy. Will remove the member from the binding, and remove the entire binding if its members array is empty. Args: policy: The policy object, composed of dictionaries, lists, and primitive types. This object will be modified, and also returned for convenience. member: The string to remove from the 'members' array of the binding. role: The role string of the binding to remove. Returns: The same object referenced by the policy arg, after adding the binding. """ # Check for version 1 (implicit if not specified), because this code can't # handle policies with conditions correctly. if policy.get('version', 1) > 1: raise ValueError( ( 'Only policy versions up to 1 are supported. version: {version}' ).format(version=policy.get('version', 'None')) ) bindings = policy.get('bindings', []) if not isinstance(bindings, list): raise ValueError( ( "Policy field 'bindings' does not have an array-type value. " "'bindings': {value}" ).format(value=repr(bindings)) ) for binding in bindings: if not isinstance(binding, dict): raise ValueError( ( "At least one element of the policy's 'bindings' array is not " 'an object type. element: {value}' ).format(value=repr(binding)) ) if binding.get('role') == role: members = binding.get('members', []) if not isinstance(members, list): raise ValueError( ( "Policy binding field 'members' does not have an array-type " "value. 'members': {value}" ).format(value=repr(members)) ) for j, member_j in enumerate(members): if member_j == member: del members[j] # Remove empty bindings. Currently IAM would accept an empty binding # and prune it, but maybe in the future it won't, so do this # defensively. bindings = [b for b in bindings if b.get('members', [])] policy['bindings'] = bindings return policy raise app.UsageError( "No binding found for member '{member}' in role '{role}'".format( member=member, role=role ) )