Home | History | Annotate | Download | only in commands
      1 # -*- coding: utf-8 -*-
      2 # Copyright 2011 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 acl command for cloud storage providers."""
     16 
     17 from __future__ import absolute_import
     18 
     19 from gslib import aclhelpers
     20 from gslib.cloud_api import AccessDeniedException
     21 from gslib.cloud_api import BadRequestException
     22 from gslib.cloud_api import Preconditions
     23 from gslib.cloud_api import ServiceException
     24 from gslib.command import Command
     25 from gslib.command import SetAclExceptionHandler
     26 from gslib.command import SetAclFuncWrapper
     27 from gslib.command_argument import CommandArgument
     28 from gslib.cs_api_map import ApiSelector
     29 from gslib.exception import CommandException
     30 from gslib.help_provider import CreateHelpText
     31 from gslib.storage_url import StorageUrlFromString
     32 from gslib.third_party.storage_apitools import storage_v1_messages as apitools_messages
     33 from gslib.util import NO_MAX
     34 from gslib.util import Retry
     35 from gslib.util import UrlsAreForSingleProvider
     36 
     37 _SET_SYNOPSIS = """
     38   gsutil acl set [-f] [-r] [-a] file-or-canned_acl_name url...
     39 """
     40 
     41 _GET_SYNOPSIS = """
     42   gsutil acl get url
     43 """
     44 
     45 _CH_SYNOPSIS = """
     46   gsutil acl ch [-f] [-r] -u|-g|-d|-p <grant>... url...
     47 
     48   where each <grant> is one of the following forms:
     49 
     50     -u <id|email>:<perm>
     51     -g <id|email|domain|All|AllAuth>:<perm>
     52     -p <viewers|editors|owners>-<project number>
     53     -d <id|email|domain|All|AllAuth>
     54 """
     55 
     56 _GET_DESCRIPTION = """
     57 <B>GET</B>
     58   The "acl get" command gets the ACL text for a bucket or object, which you can
     59   save and edit for the acl set command.
     60 """
     61 
     62 _SET_DESCRIPTION = """
     63 <B>SET</B>
     64   The "acl set" command allows you to set an Access Control List on one or
     65   more buckets and objects. The simplest way to use it is to specify one of
     66   the canned ACLs, e.g.,:
     67 
     68     gsutil acl set private gs://bucket
     69 
     70   If you want to make an object or bucket publicly readable or writable, it is
     71   recommended to use "acl ch", to avoid accidentally removing OWNER permissions.
     72   See "gsutil help acl ch" for details.
     73 
     74   See "gsutil help acls" for a list of all canned ACLs.
     75 
     76   If you want to define more fine-grained control over your data, you can
     77   retrieve an ACL using the "acl get" command, save the output to a file, edit
     78   the file, and then use the "acl set" command to set that ACL on the buckets
     79   and/or objects. For example:
     80 
     81     gsutil acl get gs://bucket/file.txt > acl.txt
     82 
     83   Make changes to acl.txt such as adding an additional grant, then:
     84 
     85     gsutil acl set acl.txt gs://cats/file.txt
     86 
     87   Note that you can set an ACL on multiple buckets or objects at once,
     88   for example:
     89 
     90     gsutil acl set acl.txt gs://bucket/*.jpg
     91 
     92   If you have a large number of ACLs to update you might want to use the
     93   gsutil -m option, to perform a parallel (multi-threaded/multi-processing)
     94   update:
     95 
     96     gsutil -m acl set acl.txt gs://bucket/*.jpg
     97 
     98   Note that multi-threading/multi-processing is only done when the named URLs
     99   refer to objects, which happens either if you name specific objects or 
    100   if you enumerate objects by using an object wildcard or specifying
    101   the acl -r flag.
    102 
    103 
    104 <B>SET OPTIONS</B>
    105   The "set" sub-command has the following options
    106 
    107     -R, -r      Performs "acl set" request recursively, to all objects under
    108                 the specified URL.
    109 
    110     -a          Performs "acl set" request on all object versions.
    111 
    112     -f          Normally gsutil stops at the first error. The -f option causes
    113                 it to continue when it encounters errors. If some of the ACLs
    114                 couldn't be set, gsutil's exit status will be non-zero even if
    115                 this flag is set. This option is implicitly set when running
    116                 "gsutil -m acl...".
    117 """
    118 
    119 _CH_DESCRIPTION = """
    120 <B>CH</B>
    121   The "acl ch" (or "acl change") command updates access control lists, similar
    122   in spirit to the Linux chmod command. You can specify multiple access grant
    123   additions and deletions in a single command run; all changes will be made
    124   atomically to each object in turn. For example, if the command requests
    125   deleting one grant and adding a different grant, the ACLs being updated will
    126   never be left in an intermediate state where one grant has been deleted but
    127   the second grant not yet added. Each change specifies a user or group grant
    128   to add or delete, and for grant additions, one of R, W, O (for the
    129   permission to be granted). A more formal description is provided in a later
    130   section; below we provide examples.
    131 
    132 <B>CH EXAMPLES</B>
    133   Examples for "ch" sub-command:
    134   
    135   Grant anyone on the internet READ access to the object example-object:
    136   
    137     gsutil acl ch -u AllUsers:R gs://example-bucket/example-object
    138 
    139   NOTE: By default, publicly readable objects are served with a Cache-Control
    140   header allowing such objects to be cached for 3600 seconds. If you need to
    141   ensure that updates become visible immediately, you should set a
    142   Cache-Control header of "Cache-Control:private, max-age=0, no-transform" on
    143   such objects. For help doing this, see "gsutil help setmeta".
    144 
    145   Grant anyone on the internet WRITE access to the bucket example-bucket
    146   (WARNING: this is not recommended as you will be responsible for the content):
    147 
    148     gsutil acl ch -u AllUsers:W gs://example-bucket
    149     
    150   Grant the user john.doe (at] example.com WRITE access to the bucket
    151   example-bucket:
    152 
    153     gsutil acl ch -u john.doe (at] example.com:WRITE gs://example-bucket
    154 
    155   Grant the group admins (at] example.com OWNER access to all jpg files in
    156   the top level of example-bucket:
    157 
    158     gsutil acl ch -g admins (at] example.com:O gs://example-bucket/*.jpg
    159 
    160   Grant the owners of project example-project-123 WRITE access to the bucket
    161   example-bucket:
    162 
    163     gsutil acl ch -p owners-example-project-123:W gs://example-bucket
    164 
    165   NOTE: You can replace 'owners' with 'viewers' or 'editors' to grant access
    166   to a project's viewers/editors respectively.
    167 
    168   Grant the user with the specified canonical ID READ access to all objects
    169   in example-bucket that begin with folder/:
    170 
    171     gsutil acl ch -r \\
    172       -u 84fac329bceSAMPLE777d5d22b8SAMPLE785ac2SAMPLE2dfcf7c4adf34da46:R \\
    173       gs://example-bucket/folder/
    174 
    175   Grant the service account foo (at] developer.gserviceaccount.com WRITE access to
    176   the bucket example-bucket:
    177 
    178     gsutil acl ch -u foo (at] developer.gserviceaccount.com:W gs://example-bucket
    179 
    180   Grant all users from the `Google Apps
    181   <https://www.google.com/work/apps/business/>`_ domain my-domain.org READ
    182   access to the bucket gcs.my-domain.org:
    183 
    184     gsutil acl ch -g my-domain.org:R gs://gcs.my-domain.org
    185 
    186   Remove any current access by john.doe (at] example.com from the bucket
    187   example-bucket:
    188 
    189     gsutil acl ch -d john.doe (at] example.com gs://example-bucket
    190 
    191   If you have a large number of objects to update, enabling multi-threading
    192   with the gsutil -m flag can significantly improve performance. The
    193   following command adds OWNER for admin (at] example.org using
    194   multi-threading:
    195 
    196     gsutil -m acl ch -r -u admin (at] example.org:O gs://example-bucket
    197 
    198   Grant READ access to everyone from my-domain.org and to all authenticated
    199   users, and grant OWNER to admin (at] mydomain.org, for the buckets
    200   my-bucket and my-other-bucket, with multi-threading enabled:
    201 
    202     gsutil -m acl ch -r -g my-domain.org:R -g AllAuth:R \\
    203       -u admin (at] mydomain.org:O gs://my-bucket/ gs://my-other-bucket
    204 
    205 <B>CH ROLES</B>
    206   You may specify the following roles with either their shorthand or
    207   their full name:
    208 
    209     R: READ
    210     W: WRITE
    211     O: OWNER
    212 
    213 <B>CH ENTITIES</B>
    214   There are four different entity types: Users, Groups, All Authenticated Users,
    215   and All Users.
    216 
    217   Users are added with -u and a plain ID or email address, as in
    218   "-u john-doe@gmail.com:r". Note: Service Accounts are considered to be users.
    219 
    220   Groups are like users, but specified with the -g flag, as in
    221   "-g power-users@example.com:fc". Groups may also be specified as a full
    222   domain, as in "-g my-company.com:r".
    223 
    224   AllAuthenticatedUsers and AllUsers are specified directly, as
    225   in "-g AllUsers:R" or "-g AllAuthenticatedUsers:O". These are case
    226   insensitive, and may be shortened to "all" and "allauth", respectively.
    227 
    228   Removing roles is specified with the -d flag and an ID, email
    229   address, domain, or one of AllUsers or AllAuthenticatedUsers.
    230 
    231   Many entities' roles can be specified on the same command line, allowing
    232   bundled changes to be executed in a single run. This will reduce the number of
    233   requests made to the server.
    234 
    235 <B>CH OPTIONS</B>
    236   The "ch" sub-command has the following options
    237 
    238     -d          Remove all roles associated with the matching entity.
    239 
    240     -f          Normally gsutil stops at the first error. The -f option causes
    241                 it to continue when it encounters errors. With this option the
    242                 gsutil exit status will be 0 even if some ACLs couldn't be
    243                 changed.
    244 
    245     -g          Add or modify a group entity's role.
    246 
    247     -p          Add or modify a project viewers/editors/owners role.
    248 
    249     -R, -r      Performs acl ch request recursively, to all objects under the
    250                 specified URL.
    251 
    252     -u          Add or modify a user entity's role.
    253 """
    254 
    255 _SYNOPSIS = (_SET_SYNOPSIS + _GET_SYNOPSIS.lstrip('\n') +
    256              _CH_SYNOPSIS.lstrip('\n') + '\n\n')
    257 
    258 _DESCRIPTION = ("""
    259   The acl command has three sub-commands:
    260 """ + '\n'.join([_GET_DESCRIPTION, _SET_DESCRIPTION, _CH_DESCRIPTION]))
    261 
    262 _DETAILED_HELP_TEXT = CreateHelpText(_SYNOPSIS, _DESCRIPTION)
    263 
    264 _get_help_text = CreateHelpText(_GET_SYNOPSIS, _GET_DESCRIPTION)
    265 _set_help_text = CreateHelpText(_SET_SYNOPSIS, _SET_DESCRIPTION)
    266 _ch_help_text = CreateHelpText(_CH_SYNOPSIS, _CH_DESCRIPTION)
    267 
    268 
    269 def _ApplyExceptionHandler(cls, exception):
    270   cls.logger.error('Encountered a problem: %s', exception)
    271   cls.everything_set_okay = False
    272 
    273 
    274 def _ApplyAclChangesWrapper(cls, url_or_expansion_result, thread_state=None):
    275   cls.ApplyAclChanges(url_or_expansion_result, thread_state=thread_state)
    276 
    277 
    278 class AclCommand(Command):
    279   """Implementation of gsutil acl command."""
    280 
    281   # Command specification. See base class for documentation.
    282   command_spec = Command.CreateCommandSpec(
    283       'acl',
    284       command_name_aliases=['getacl', 'setacl', 'chacl'],
    285       usage_synopsis=_SYNOPSIS,
    286       min_args=2,
    287       max_args=NO_MAX,
    288       supported_sub_args='afRrg:u:d:p:',
    289       file_url_ok=False,
    290       provider_url_ok=False,
    291       urls_start_arg=1,
    292       gs_api_support=[ApiSelector.XML, ApiSelector.JSON],
    293       gs_default_api=ApiSelector.JSON,
    294       argparse_arguments={
    295           'set': [
    296               CommandArgument.MakeFileURLOrCannedACLArgument(),
    297               CommandArgument.MakeZeroOrMoreCloudURLsArgument()
    298           ],
    299           'get': [
    300               CommandArgument.MakeNCloudURLsArgument(1)
    301           ],
    302           'ch': [
    303               CommandArgument.MakeZeroOrMoreCloudURLsArgument()
    304           ],
    305       }
    306   )
    307   # Help specification. See help_provider.py for documentation.
    308   help_spec = Command.HelpSpec(
    309       help_name='acl',
    310       help_name_aliases=['getacl', 'setacl', 'chmod', 'chacl'],
    311       help_type='command_help',
    312       help_one_line_summary='Get, set, or change bucket and/or object ACLs',
    313       help_text=_DETAILED_HELP_TEXT,
    314       subcommand_help_text={
    315           'get': _get_help_text, 'set': _set_help_text, 'ch': _ch_help_text},
    316   )
    317 
    318   def _CalculateUrlsStartArg(self):
    319     if not self.args:
    320       self.RaiseWrongNumberOfArgumentsException()
    321     if (self.args[0].lower() == 'set') or (self.command_alias_used == 'setacl'):
    322       return 1
    323     else:
    324       return 0
    325 
    326   def _SetAcl(self):
    327     """Parses options and sets ACLs on the specified buckets/objects."""
    328     self.continue_on_error = False
    329     if self.sub_opts:
    330       for o, unused_a in self.sub_opts:
    331         if o == '-a':
    332           self.all_versions = True
    333         elif o == '-f':
    334           self.continue_on_error = True
    335         elif o == '-r' or o == '-R':
    336           self.recursion_requested = True
    337         else:
    338           self.RaiseInvalidArgumentException()
    339     try:
    340       self.SetAclCommandHelper(SetAclFuncWrapper, SetAclExceptionHandler)
    341     except AccessDeniedException, unused_e:
    342       self._WarnServiceAccounts()
    343       raise
    344     if not self.everything_set_okay:
    345       raise CommandException('ACLs for some objects could not be set.')
    346 
    347   def _ChAcl(self):
    348     """Parses options and changes ACLs on the specified buckets/objects."""
    349     self.parse_versions = True
    350     self.changes = []
    351     self.continue_on_error = False
    352 
    353     if self.sub_opts:
    354       for o, a in self.sub_opts:
    355         if o == '-f':
    356           self.continue_on_error = True
    357         elif o == '-g':
    358           if 'gserviceaccount.com' in a:
    359             raise CommandException(
    360                 'Service accounts are considered users, not groups; please use '
    361                 '"gsutil acl ch -u" instead of "gsutil acl ch -g"')
    362           self.changes.append(
    363               aclhelpers.AclChange(a, scope_type=aclhelpers.ChangeType.GROUP))
    364         elif o == '-p':
    365           self.changes.append(
    366               aclhelpers.AclChange(a, scope_type=aclhelpers.ChangeType.PROJECT))
    367         elif o == '-u':
    368           self.changes.append(
    369               aclhelpers.AclChange(a, scope_type=aclhelpers.ChangeType.USER))
    370         elif o == '-d':
    371           self.changes.append(aclhelpers.AclDel(a))
    372         elif o == '-r' or o == '-R':
    373           self.recursion_requested = True
    374         else:
    375           self.RaiseInvalidArgumentException()
    376 
    377     if not self.changes:
    378       raise CommandException(
    379           'Please specify at least one access change '
    380           'with the -g, -u, or -d flags')
    381 
    382     if (not UrlsAreForSingleProvider(self.args) or
    383         StorageUrlFromString(self.args[0]).scheme != 'gs'):
    384       raise CommandException(
    385           'The "{0}" command can only be used with gs:// URLs'.format(
    386               self.command_name))
    387 
    388     self.everything_set_okay = True
    389     self.ApplyAclFunc(_ApplyAclChangesWrapper, _ApplyExceptionHandler,
    390                       self.args)
    391     if not self.everything_set_okay:
    392       raise CommandException('ACLs for some objects could not be set.')
    393 
    394   def _RaiseForAccessDenied(self, url):
    395     self._WarnServiceAccounts()
    396     raise CommandException('Failed to set acl for %s. Please ensure you have '
    397                            'OWNER-role access to this resource.' % url)
    398 
    399   @Retry(ServiceException, tries=3, timeout_secs=1)
    400   def ApplyAclChanges(self, name_expansion_result, thread_state=None):
    401     """Applies the changes in self.changes to the provided URL.
    402 
    403     Args:
    404       name_expansion_result: NameExpansionResult describing the target object.
    405       thread_state: If present, gsutil Cloud API instance to apply the changes.
    406     """
    407     if thread_state:
    408       gsutil_api = thread_state
    409     else:
    410       gsutil_api = self.gsutil_api
    411 
    412     url = name_expansion_result.expanded_storage_url
    413 
    414     if url.IsBucket():
    415       bucket = gsutil_api.GetBucket(url.bucket_name, provider=url.scheme,
    416                                     fields=['acl', 'metageneration'])
    417       current_acl = bucket.acl
    418     elif url.IsObject():
    419       gcs_object = gsutil_api.GetObjectMetadata(
    420           url.bucket_name, url.object_name, provider=url.scheme,
    421           generation=url.generation,
    422           fields=['acl', 'generation', 'metageneration'])
    423       current_acl = gcs_object.acl
    424     if not current_acl:
    425       self._RaiseForAccessDenied(url)
    426 
    427     modification_count = 0
    428     for change in self.changes:
    429       modification_count += change.Execute(url, current_acl, 'acl', self.logger)
    430     if modification_count == 0:
    431       self.logger.info('No changes to %s', url)
    432       return
    433 
    434     try:
    435       if url.IsBucket():
    436         preconditions = Preconditions(meta_gen_match=bucket.metageneration)
    437         bucket_metadata = apitools_messages.Bucket(acl=current_acl)
    438         gsutil_api.PatchBucket(url.bucket_name, bucket_metadata,
    439                                preconditions=preconditions,
    440                                provider=url.scheme, fields=['id'])
    441       else:  # Object
    442         preconditions = Preconditions(gen_match=gcs_object.generation,
    443                                       meta_gen_match=gcs_object.metageneration)
    444 
    445         object_metadata = apitools_messages.Object(acl=current_acl)
    446         gsutil_api.PatchObjectMetadata(
    447             url.bucket_name, url.object_name, object_metadata,
    448             preconditions=preconditions, provider=url.scheme,
    449             generation=url.generation)
    450     except BadRequestException as e:
    451       # Don't retry on bad requests, e.g. invalid email address.
    452       raise CommandException('Received bad request from server: %s' % str(e))
    453     except AccessDeniedException:
    454       self._RaiseForAccessDenied(url)
    455 
    456     self.logger.info('Updated ACL on %s', url)
    457 
    458   def RunCommand(self):
    459     """Command entry point for the acl command."""
    460     action_subcommand = self.args.pop(0)
    461     self.ParseSubOpts(check_args=True)
    462     self.def_acl = False
    463     if action_subcommand == 'get':
    464       self.GetAndPrintAcl(self.args[0])
    465     elif action_subcommand == 'set':
    466       self._SetAcl()
    467     elif action_subcommand in ('ch', 'change'):
    468       self._ChAcl()
    469     else:
    470       raise CommandException(('Invalid subcommand "%s" for the %s command.\n'
    471                               'See "gsutil help acl".') %
    472                              (action_subcommand, self.command_name))
    473 
    474     return 0
    475