1# -*- coding: utf-8 -*- 2# Copyright 2016 Google Inc. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15"""Implementation of IAM policy management command for GCS.""" 16 17from __future__ import absolute_import 18from __future__ import print_function 19from __future__ import division 20from __future__ import unicode_literals 21 22import itertools 23import json 24import re 25import textwrap 26 27import six 28from six.moves import zip 29from apitools.base.protorpclite import protojson 30from apitools.base.protorpclite.messages import DecodeError 31from gslib.cloud_api import ArgumentException 32from gslib.cloud_api import PreconditionException 33from gslib.cloud_api import ServiceException 34from gslib.command import Command 35from gslib.command import GetFailureCount 36from gslib.command_argument import CommandArgument 37from gslib.cs_api_map import ApiSelector 38from gslib.exception import CommandException 39from gslib.exception import IamChOnResourceWithConditionsException 40from gslib.help_provider import CreateHelpText 41from gslib.metrics import LogCommandParams 42from gslib.name_expansion import NameExpansionIterator 43from gslib.name_expansion import SeekAheadNameExpansionIterator 44from gslib.plurality_checkable_iterator import PluralityCheckableIterator 45from gslib.storage_url import StorageUrlFromString 46from gslib.third_party.storage_apitools import storage_v1_messages as apitools_messages 47from gslib.utils.cloud_api_helper import GetCloudApiInstance 48from gslib.utils.constants import IAM_POLICY_VERSION 49from gslib.utils.constants import NO_MAX 50from gslib.utils.iam_helper import BindingStringToTuple 51from gslib.utils.iam_helper import BindingsTuple 52from gslib.utils.iam_helper import DeserializeBindingsTuple 53from gslib.utils.iam_helper import IsEqualBindings 54from gslib.utils.iam_helper import PatchBindings 55from gslib.utils.iam_helper import SerializeBindingsTuple 56from gslib.utils.retry_util import Retry 57 58_SET_SYNOPSIS = """ 59 gsutil iam set [-afRr] [-e <etag>] file url ... 60""" 61 62_GET_SYNOPSIS = """ 63 gsutil iam get url 64""" 65 66# Note that the commands below are put in quotation marks instead of backticks; 67# this is done because the whole ch synopsis gets rendered in one <pre> tag in 68# the web docs, and having literal double-backticks looks weird. 69_CH_SYNOPSIS = """ 70 gsutil iam ch [-fRr] binding ... url 71 72 where each binding is of the form: 73 74 [-d] ("user"|"serviceAccount"|"domain"|"group"):id:role[,...] 75 [-d] ("allUsers"|"allAuthenticatedUsers"):role[,...] 76 -d ("user"|"serviceAccount"|"domain"|"group"):id 77 -d ("allUsers"|"allAuthenticatedUsers") 78 79 Note: The "iam ch" command does not support changing IAM policies with 80 bindings that contain conditions. As such, "iam ch" cannot be used to add 81 conditions to a policy or to change the policy of a resource that already 82 contains conditions. See additional details below. 83 84 Note: The "gsutil iam" command disallows using project convenience groups 85 (projectOwner, projectEditor, projectViewer) as the first segment of a binding 86 because these groups go against the principle of least privilege. 87 88""" 89 90_GET_DESCRIPTION = """ 91<B>GET</B> 92 The ``iam get`` command gets the IAM policy for a bucket or object, which you 93 can save and edit for use with the ``iam set`` command. 94 95 For example: 96 97 gsutil iam get gs://example > bucket_iam.txt 98 gsutil iam get gs://example/important.txt > object_iam.txt 99 100 The IAM policy returned by ``iam get`` includes the etag of the IAM policy and 101 will be used in the precondition check for ``iam set``, unless the etag is 102 overridden by setting the ``iam set -e`` option. 103""" 104 105_SET_DESCRIPTION = """ 106<B>SET</B> 107 The ``iam set`` command sets the IAM policy for one or more buckets and / or 108 objects. It overwrites the current IAM policy that exists on a bucket (or 109 object) with the policy specified in the input file. The ``iam set`` command 110 takes as input a file with an IAM policy in the format of the output 111 generated by ``iam get``. 112 113 The ``iam ch`` command can be used to edit an existing policy. It works 114 correctly in the presence of concurrent updates. You may also do this 115 manually by using the -e flag and overriding the etag returned in ``iam get``. 116 Specifying -e with an empty string (i.e. ``gsutil iam set -e '' ...``) will 117 instruct gsutil to skip the precondition check when setting the IAM policy. 118 119 If you wish to set an IAM policy on a large number of objects, you may want 120 to use the gsutil -m option for concurrent processing. The following command 121 will apply iam.txt to all objects in the "cats" bucket. 122 123 gsutil -m iam set -r iam.txt gs://cats 124 125 Note that only object-level IAM applications are parallelized; you do not 126 gain any additional performance when applying an IAM policy to a large 127 number of buckets with the -m flag. 128 129<B>SET OPTIONS</B> 130 The ``set`` sub-command has the following options 131 132 -R, -r Performs ``iam set`` recursively to all objects under the 133 specified bucket. 134 135 -a Performs ``iam set`` request on all object versions. 136 137 -e <etag> Performs the precondition check on each object with the 138 specified etag before setting the policy. 139 140 -f Default gsutil error handling is fail-fast. This flag 141 changes the request to fail-silent mode. This is implicitly 142 set when invoking the gsutil -m option. 143""" 144 145_CH_DESCRIPTION = """ 146<B>CH</B> 147 The ``iam ch`` command incrementally updates IAM policies. You may specify 148 multiple access grants and removals in a single command invocation, which 149 will be batched and applied as a whole to each url via an IAM patch. 150 The patch will be constructed by applying each access grant or removal in the 151 order in which they appear in the command line arguments. Each access change 152 specifies a member and the role that will be either granted or revoked. 153 154 The gsutil -m option may be set to handle object-level operations more 155 efficiently. 156 157 Note: The ``iam ch`` command may NOT be used to change the IAM policy of a 158 resource that contains conditions in its policy bindings. Attempts to do so 159 will result in an error. To change the IAM policy of such a resource, you can 160 perform a read-modify-write operation by using ``gsutil iam get`` to save the 161 policy to a file, editing the file, and using ``gsutil iam set`` to set the 162 updated policy. 163 164<B>CH EXAMPLES</B> 165 Examples for the ``ch`` sub-command: 166 167 To grant a single role to a single member for some targets: 168 169 gsutil iam ch user:john.doe@example.com:objectCreator gs://ex-bucket 170 171 To make a bucket's objects publicly readable: 172 173 gsutil iam ch allUsers:objectViewer gs://ex-bucket 174 175 To grant multiple bindings to a bucket: 176 177 gsutil iam ch user:john.doe@example.com:objectCreator \\ 178 domain:www.my-domain.org:objectViewer gs://ex-bucket 179 180 To specify more than one role for a particular member: 181 182 gsutil iam ch user:john.doe@example.com:objectCreator,objectViewer \\ 183 gs://ex-bucket 184 185 To specify a custom role for a particular member: 186 187 gsutil iam ch user:john.doe@example.com:roles/customRoleName gs://ex-bucket 188 189 To apply a grant and simultaneously remove a binding to a bucket: 190 191 gsutil iam ch -d group:readers@example.com:legacyBucketReader \\ 192 group:viewers@example.com:objectViewer gs://ex-bucket 193 194 To remove a user from all roles on a bucket: 195 196 gsutil iam ch -d user:john.doe@example.com gs://ex-bucket 197 198<B>CH OPTIONS</B> 199 The ``ch`` sub-command has the following options 200 201 -R, -r Performs ``iam ch`` recursively to all objects under the 202 specified bucket. 203 204 -f Default gsutil error handling is fail-fast. This flag 205 changes the request to fail-silent mode. This is implicitly 206 set when invoking the gsutil -m option. 207""" 208 209_SYNOPSIS = (_SET_SYNOPSIS + _GET_SYNOPSIS.lstrip('\n') + 210 _CH_SYNOPSIS.lstrip('\n') + '\n\n') 211 212_DESCRIPTION = """ 213 The iam command has three sub-commands: 214""" + '\n'.join([_GET_DESCRIPTION, _SET_DESCRIPTION, _CH_DESCRIPTION]) 215 216_DETAILED_HELP_TEXT = CreateHelpText(_SYNOPSIS, _DESCRIPTION) 217 218_get_help_text = CreateHelpText(_GET_SYNOPSIS, _GET_DESCRIPTION) 219_set_help_text = CreateHelpText(_SET_SYNOPSIS, _SET_DESCRIPTION) 220_ch_help_text = CreateHelpText(_CH_SYNOPSIS, _CH_DESCRIPTION) 221 222STORAGE_URI_REGEX = re.compile(r'[a-z]+://.+') 223 224IAM_CH_CONDITIONS_WORKAROUND_MSG = ( 225 'To change the IAM policy of a resource that has bindings containing ' 226 'conditions, perform a read-modify-write operation using "iam get" and ' 227 '"iam set".') 228 229 230def _PatchIamWrapper(cls, iter_result, thread_state): 231 (serialized_bindings_tuples, expansion_result) = iter_result 232 return cls.PatchIamHelper( 233 expansion_result.expanded_storage_url, 234 # Deserialize the JSON object passed from Command.Apply. 235 [DeserializeBindingsTuple(t) for t in serialized_bindings_tuples], 236 thread_state=thread_state) 237 238 239def _SetIamWrapper(cls, iter_result, thread_state): 240 (serialized_policy, expansion_result) = iter_result 241 return cls.SetIamHelper( 242 expansion_result.expanded_storage_url, 243 # Deserialize the JSON object passed from Command.Apply. 244 protojson.decode_message(apitools_messages.Policy, serialized_policy), 245 thread_state=thread_state) 246 247 248def _SetIamExceptionHandler(cls, e): 249 cls.logger.error(str(e)) 250 251 252def _PatchIamExceptionHandler(cls, e): 253 cls.logger.error(str(e)) 254 255 256class IamCommand(Command): 257 """Implementation of gsutil iam command.""" 258 command_spec = Command.CreateCommandSpec( 259 'iam', 260 min_args=2, 261 max_args=NO_MAX, 262 supported_sub_args='afRrd:e:', 263 file_url_ok=True, 264 provider_url_ok=False, 265 urls_start_arg=1, 266 gs_api_support=[ApiSelector.JSON], 267 gs_default_api=ApiSelector.JSON, 268 argparse_arguments={ 269 'get': [CommandArgument.MakeNCloudURLsArgument(1),], 270 'set': [ 271 CommandArgument.MakeNFileURLsArgument(1), 272 CommandArgument.MakeZeroOrMoreCloudURLsArgument(), 273 ], 274 'ch': [ 275 CommandArgument.MakeOneOrMoreBindingsArgument(), 276 CommandArgument.MakeZeroOrMoreCloudURLsArgument(), 277 ], 278 }, 279 ) 280 281 help_spec = Command.HelpSpec( 282 help_name='iam', 283 help_name_aliases=[], 284 help_type='command_help', 285 help_one_line_summary=( 286 'Get, set, or change bucket and/or object IAM permissions.'), 287 help_text=_DETAILED_HELP_TEXT, 288 subcommand_help_text={ 289 'get': _get_help_text, 290 'set': _set_help_text, 291 'ch': _ch_help_text, 292 }, 293 ) 294 295 def GetIamHelper(self, storage_url, thread_state=None): 296 """Gets an IAM policy for a single, resolved bucket / object URL. 297 298 Args: 299 storage_url: A CloudUrl instance with no wildcards, pointing to a 300 specific bucket or object. 301 thread_state: CloudApiDelegator instance which is passed from 302 command.WorkerThread.__init__() if the global -m flag is 303 specified. Will use self.gsutil_api if thread_state is set 304 to None. 305 306 Returns: 307 Policy instance. 308 """ 309 310 gsutil_api = GetCloudApiInstance(self, thread_state=thread_state) 311 312 if storage_url.IsBucket(): 313 policy = gsutil_api.GetBucketIamPolicy( 314 storage_url.bucket_name, 315 provider=storage_url.scheme, 316 fields=['bindings', 'etag'], 317 ) 318 else: 319 policy = gsutil_api.GetObjectIamPolicy( 320 storage_url.bucket_name, 321 storage_url.object_name, 322 generation=storage_url.generation, 323 provider=storage_url.scheme, 324 fields=['bindings', 'etag'], 325 ) 326 return policy 327 328 def _GetIam(self, thread_state=None): 329 """Gets IAM policy for single bucket or object.""" 330 331 pattern = self.args[0] 332 333 matches = PluralityCheckableIterator( 334 self.WildcardIterator(pattern).IterAll(bucket_listing_fields=['name'])) 335 if matches.IsEmpty(): 336 raise CommandException('%s matched no URLs' % pattern) 337 if matches.HasPlurality(): 338 raise CommandException( 339 '%s matched more than one URL, which is not allowed by the %s ' 340 'command' % (pattern, self.command_name)) 341 342 storage_url = StorageUrlFromString(list(matches)[0].url_string) 343 policy = self.GetIamHelper(storage_url, thread_state=thread_state) 344 policy_json = json.loads(protojson.encode_message(policy)) 345 policy_str = json.dumps( 346 policy_json, 347 sort_keys=True, 348 indent=2, 349 ) 350 print(policy_str) 351 352 def _SetIamHelperInternal(self, storage_url, policy, thread_state=None): 353 """Sets IAM policy for a single, resolved bucket / object URL. 354 355 Args: 356 storage_url: A CloudUrl instance with no wildcards, pointing to a 357 specific bucket or object. 358 policy: A Policy object to set on the bucket / object. 359 thread_state: CloudApiDelegator instance which is passed from 360 command.WorkerThread.__init__() if the -m flag is 361 specified. Will use self.gsutil_api if thread_state is set 362 to None. 363 364 Raises: 365 ServiceException passed from the API call if an HTTP error was returned. 366 """ 367 368 # SetIamHelper may be called by a command.WorkerThread. In the 369 # single-threaded case, WorkerThread will not pass the CloudApiDelegator 370 # instance to thread_state. GetCloudInstance is called to resolve the 371 # edge case. 372 gsutil_api = GetCloudApiInstance(self, thread_state=thread_state) 373 374 if storage_url.IsBucket(): 375 # Temporarily setting version manually on bucket IAM policies, as 376 # setting version on objects incorrectly causes the API to throw an 377 # error. See b/140734851. 378 policy.version = IAM_POLICY_VERSION 379 gsutil_api.SetBucketIamPolicy(storage_url.bucket_name, 380 policy, 381 provider=storage_url.scheme) 382 else: 383 gsutil_api.SetObjectIamPolicy(storage_url.bucket_name, 384 storage_url.object_name, 385 policy, 386 generation=storage_url.generation, 387 provider=storage_url.scheme) 388 389 def SetIamHelper(self, storage_url, policy, thread_state=None): 390 """Handles the potential exception raised by the internal set function.""" 391 try: 392 self._SetIamHelperInternal(storage_url, policy, thread_state=thread_state) 393 except ServiceException: 394 if self.continue_on_error: 395 self.everything_set_okay = False 396 else: 397 raise 398 399 def PatchIamHelper(self, storage_url, bindings_tuples, thread_state=None): 400 """Patches an IAM policy for a single, resolved bucket / object URL. 401 402 The patch is applied by altering the policy from an IAM get request, and 403 setting the new IAM with the specified etag. Because concurrent IAM set 404 requests may alter the etag, we may need to retry this operation several 405 times before success. 406 407 Args: 408 storage_url: A CloudUrl instance with no wildcards, pointing to a 409 specific bucket or object. 410 bindings_tuples: A list of BindingsTuple instances. 411 thread_state: CloudApiDelegator instance which is passed from 412 command.WorkerThread.__init__() if the -m flag is 413 specified. Will use self.gsutil_api if thread_state is set 414 to None. 415 """ 416 try: 417 self._PatchIamHelperInternal(storage_url, 418 bindings_tuples, 419 thread_state=thread_state) 420 except ServiceException: 421 if self.continue_on_error: 422 self.everything_set_okay = False 423 else: 424 raise 425 except IamChOnResourceWithConditionsException as e: 426 if self.continue_on_error: 427 self.everything_set_okay = False 428 self.tried_ch_on_resource_with_conditions = True 429 self.logger.debug(e.message) 430 else: 431 raise CommandException(e.message) 432 433 @Retry(PreconditionException, tries=3, timeout_secs=1.0) 434 def _PatchIamHelperInternal(self, 435 storage_url, 436 bindings_tuples, 437 thread_state=None): 438 439 policy = self.GetIamHelper(storage_url, thread_state=thread_state) 440 (etag, bindings) = (policy.etag, policy.bindings) 441 442 # If any of the bindings have conditions present, raise an exception. 443 # See the docstring for the IamChOnResourceWithConditionsException class 444 # for more details on why we raise this exception. 445 for binding in bindings: 446 if binding.condition: 447 message = 'Could not patch IAM policy for %s.' % storage_url 448 message += '\n' 449 message += '\n'.join( 450 textwrap.wrap( 451 'The resource had conditions present in its IAM policy bindings, ' 452 'which is not supported by "iam ch". %s' % 453 IAM_CH_CONDITIONS_WORKAROUND_MSG)) 454 raise IamChOnResourceWithConditionsException(message) 455 456 # Create a backup which is untainted by any references to the original 457 # bindings. 458 orig_bindings = list(bindings) 459 460 for (is_grant, diff) in bindings_tuples: 461 bindings = PatchBindings(bindings, BindingsTuple(is_grant, diff)) 462 463 if IsEqualBindings(bindings, orig_bindings): 464 self.logger.info('No changes made to %s', storage_url) 465 return 466 467 policy = apitools_messages.Policy(bindings=bindings, etag=etag) 468 469 # We explicitly wish for etag mismatches to raise an error and allow this 470 # function to error out, so we are bypassing the exception handling offered 471 # by IamCommand.SetIamHelper in lieu of our own handling (@Retry). 472 self._SetIamHelperInternal(storage_url, policy, thread_state=thread_state) 473 474 def _PatchIam(self): 475 self.continue_on_error = False 476 self.recursion_requested = False 477 478 patch_bindings_tuples = [] 479 480 if self.sub_opts: 481 for o, a in self.sub_opts: 482 if o in ['-r', '-R']: 483 self.recursion_requested = True 484 elif o == '-f': 485 self.continue_on_error = True 486 elif o == '-d': 487 patch_bindings_tuples.append(BindingStringToTuple(False, a)) 488 489 patterns = [] 490 491 # N.B.: self.sub_opts stops taking in options at the first non-flagged 492 # token. The rest of the tokens are sent to self.args. Thus, in order to 493 # handle input of the form "-d <binding> <binding> <url>", we will have to 494 # parse self.args for a mix of both bindings and CloudUrls. We are not 495 # expecting to come across the -r, -f flags here. 496 it = iter(self.args) 497 for token in it: 498 if STORAGE_URI_REGEX.match(token): 499 patterns.append(token) 500 break 501 if token == '-d': 502 patch_bindings_tuples.append(BindingStringToTuple(False, next(it))) 503 else: 504 patch_bindings_tuples.append(BindingStringToTuple(True, token)) 505 if not patch_bindings_tuples: 506 raise CommandException('Must specify at least one binding.') 507 508 # All following arguments are urls. 509 for token in it: 510 patterns.append(token) 511 512 self.everything_set_okay = True 513 self.tried_ch_on_resource_with_conditions = False 514 threaded_wildcards = [] 515 for pattern in patterns: 516 surl = StorageUrlFromString(pattern) 517 try: 518 if surl.IsBucket(): 519 if self.recursion_requested: 520 surl.object = '*' 521 threaded_wildcards.append(surl.url_string) 522 else: 523 self.PatchIamHelper(surl, patch_bindings_tuples) 524 else: 525 threaded_wildcards.append(surl.url_string) 526 except AttributeError: 527 error_msg = 'Invalid Cloud URL "%s".' % surl.object_name 528 if set(surl.object_name).issubset(set('-Rrf')): 529 error_msg += ( 530 ' This resource handle looks like a flag, which must appear ' 531 'before all bindings. See "gsutil help iam ch" for more details.') 532 raise CommandException(error_msg) 533 534 if threaded_wildcards: 535 name_expansion_iterator = NameExpansionIterator( 536 self.command_name, 537 self.debug, 538 self.logger, 539 self.gsutil_api, 540 threaded_wildcards, 541 self.recursion_requested, 542 all_versions=self.all_versions, 543 continue_on_error=self.continue_on_error or self.parallel_operations, 544 bucket_listing_fields=['name']) 545 546 seek_ahead_iterator = SeekAheadNameExpansionIterator( 547 self.command_name, 548 self.debug, 549 self.GetSeekAheadGsutilApi(), 550 threaded_wildcards, 551 self.recursion_requested, 552 all_versions=self.all_versions) 553 554 serialized_bindings_tuples_it = itertools.repeat( 555 [SerializeBindingsTuple(t) for t in patch_bindings_tuples]) 556 self.Apply(_PatchIamWrapper, 557 zip(serialized_bindings_tuples_it, name_expansion_iterator), 558 _PatchIamExceptionHandler, 559 fail_on_error=not self.continue_on_error, 560 seek_ahead_iterator=seek_ahead_iterator) 561 562 self.everything_set_okay &= not GetFailureCount() > 0 563 564 # TODO: Add an error counter for files and objects. 565 if not self.everything_set_okay: 566 msg = 'Some IAM policies could not be patched.' 567 if self.tried_ch_on_resource_with_conditions: 568 msg += '\n' 569 msg += '\n'.join( 570 textwrap.wrap( 571 'Some resources had conditions present in their IAM policy ' 572 'bindings, which is not supported by "iam ch". %s' % 573 (IAM_CH_CONDITIONS_WORKAROUND_MSG))) 574 raise CommandException(msg) 575 576 # TODO(iam-beta): Add an optional flag to specify etag and edit the policy 577 # accordingly to be passed into the helper functions. 578 def _SetIam(self): 579 """Set IAM policy for given wildcards on the command line.""" 580 581 self.continue_on_error = False 582 self.recursion_requested = False 583 self.all_versions = False 584 force_etag = False 585 etag = '' 586 if self.sub_opts: 587 for o, arg in self.sub_opts: 588 if o in ['-r', '-R']: 589 self.recursion_requested = True 590 elif o == '-f': 591 self.continue_on_error = True 592 elif o == '-a': 593 self.all_versions = True 594 elif o == '-e': 595 etag = str(arg) 596 force_etag = True 597 else: 598 self.RaiseInvalidArgumentException() 599 600 file_url = self.args[0] 601 patterns = self.args[1:] 602 603 # Load the IAM policy file and raise error if the file is invalid JSON or 604 # does not exist. 605 try: 606 with open(file_url, 'r') as fp: 607 policy = json.loads(fp.read()) 608 except IOError: 609 raise ArgumentException('Specified IAM policy file "%s" does not exist.' % 610 file_url) 611 except ValueError as e: 612 self.logger.debug('Invalid IAM policy file, ValueError:\n%s', e) 613 raise ArgumentException('Invalid IAM policy file "%s".' % file_url) 614 615 bindings = policy.get('bindings', []) 616 if not force_etag: 617 etag = policy.get('etag', '') 618 619 policy_json = json.dumps({'bindings': bindings, 'etag': etag}) 620 try: 621 policy = protojson.decode_message(apitools_messages.Policy, policy_json) 622 except DecodeError: 623 raise ArgumentException('Invalid IAM policy file "%s" or etag "%s".' % 624 (file_url, etag)) 625 626 self.everything_set_okay = True 627 628 # This list of wildcard strings will be handled by NameExpansionIterator. 629 threaded_wildcards = [] 630 631 for pattern in patterns: 632 surl = StorageUrlFromString(pattern) 633 if surl.IsBucket(): 634 if self.recursion_requested: 635 surl.object_name = '*' 636 threaded_wildcards.append(surl.url_string) 637 else: 638 self.SetIamHelper(surl, policy) 639 else: 640 threaded_wildcards.append(surl.url_string) 641 642 # N.B.: If threaded_wildcards contains a non-existent bucket 643 # (e.g. ["gs://non-existent", "gs://existent"]), NameExpansionIterator 644 # will raise an exception in iter.next. This halts all iteration, even 645 # when -f is set. This behavior is also evident in acl set. This behavior 646 # also appears for any exception that will be raised when iterating over 647 # wildcard expansions (access denied if bucket cannot be listed, etc.). 648 if threaded_wildcards: 649 name_expansion_iterator = NameExpansionIterator( 650 self.command_name, 651 self.debug, 652 self.logger, 653 self.gsutil_api, 654 threaded_wildcards, 655 self.recursion_requested, 656 all_versions=self.all_versions, 657 continue_on_error=self.continue_on_error or self.parallel_operations, 658 bucket_listing_fields=['name']) 659 660 seek_ahead_iterator = SeekAheadNameExpansionIterator( 661 self.command_name, 662 self.debug, 663 self.GetSeekAheadGsutilApi(), 664 threaded_wildcards, 665 self.recursion_requested, 666 all_versions=self.all_versions) 667 668 policy_it = itertools.repeat(protojson.encode_message(policy)) 669 self.Apply(_SetIamWrapper, 670 zip(policy_it, name_expansion_iterator), 671 _SetIamExceptionHandler, 672 fail_on_error=not self.continue_on_error, 673 seek_ahead_iterator=seek_ahead_iterator) 674 675 self.everything_set_okay &= not GetFailureCount() > 0 676 677 # TODO: Add an error counter for files and objects. 678 if not self.everything_set_okay: 679 raise CommandException('Some IAM policies could not be set.') 680 681 def RunCommand(self): 682 """Command entry point for the acl command.""" 683 action_subcommand = self.args.pop(0) 684 self.ParseSubOpts(check_args=True) 685 # Commands with both suboptions and subcommands need to reparse for 686 # suboptions, so we log again. 687 LogCommandParams(sub_opts=self.sub_opts) 688 self.def_acl = False 689 if action_subcommand == 'get': 690 LogCommandParams(subcommands=[action_subcommand]) 691 self._GetIam() 692 elif action_subcommand == 'set': 693 LogCommandParams(subcommands=[action_subcommand]) 694 self._SetIam() 695 elif action_subcommand == 'ch': 696 LogCommandParams(subcommands=[action_subcommand]) 697 self._PatchIam() 698 else: 699 raise CommandException('Invalid subcommand "%s" for the %s command.\n' 700 'See "gsutil help iam".' % 701 (action_subcommand, self.command_name)) 702 703 return 0 704