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