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