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