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 default object acl command for Google Cloud Storage."""
16
17from __future__ import absolute_import
18from __future__ import print_function
19from __future__ import division
20from __future__ import unicode_literals
21
22from gslib import metrics
23from gslib.cloud_api import AccessDeniedException
24from gslib.cloud_api import BadRequestException
25from gslib.cloud_api import Preconditions
26from gslib.cloud_api import ServiceException
27from gslib.command import Command
28from gslib.command import SetAclExceptionHandler
29from gslib.command import SetAclFuncWrapper
30from gslib.command_argument import CommandArgument
31from gslib.cs_api_map import ApiSelector
32from gslib.exception import CommandException
33from gslib.help_provider import CreateHelpText
34from gslib.storage_url import StorageUrlFromString
35from gslib.storage_url import UrlsAreForSingleProvider
36from gslib.third_party.storage_apitools import storage_v1_messages as apitools_messages
37from gslib.utils import acl_helper
38from gslib.utils.constants import NO_MAX
39from gslib.utils.retry_util import Retry
40from gslib.utils.translation_helper import PRIVATE_DEFAULT_OBJ_ACL
41
42_SET_SYNOPSIS = """
43  gsutil defacl set <file-or-canned_acl_name> gs://<bucket_name>...
44"""
45
46_GET_SYNOPSIS = """
47  gsutil defacl get gs://<bucket_name>
48"""
49
50_CH_SYNOPSIS = """
51  gsutil defacl ch [-f] -u|-g|-d|-p <grant>... gs://<bucket_name>...
52"""
53
54_SET_DESCRIPTION = """
55<B>SET</B>
56  The "defacl set" command sets default object ACLs for the specified buckets.
57  If you specify a default object ACL for a certain bucket, Google Cloud
58  Storage applies the default object ACL to all new objects uploaded to that
59  bucket, unless an ACL for that object is separately specified during upload.
60
61  Similar to the "acl set" command, the file-or-canned_acl_name names either a
62  canned ACL or the path to a file that contains ACL text. See "gsutil help
63  acl" for examples of editing and setting ACLs via the acl command. See
64  `Predefined ACLs
65  <https://cloud.google.com/storage/docs/access-control/lists#predefined-acl>`_
66  for a list of canned ACLs.
67
68  Setting a default object ACL on a bucket provides a convenient way to ensure
69  newly uploaded objects have a specific ACL. If you don't set the bucket's
70  default object ACL, it will default to project-private. If you then upload
71  objects that need a different ACL, you will need to perform a separate ACL
72  update operation for each object. Depending on how many objects require
73  updates, this could be very time-consuming.
74"""
75
76_GET_DESCRIPTION = """
77<B>GET</B>
78  Gets the default ACL text for a bucket, which you can save and edit
79  for use with the "defacl set" command.
80"""
81
82_CH_DESCRIPTION = """
83<B>CH</B>
84  The "defacl ch" (or "defacl change") command updates the default object
85  access control list for a bucket. The syntax is shared with the "acl ch"
86  command, so see the "CH" section of "gsutil help acl" for the full help
87  description.
88
89<B>CH EXAMPLES</B>
90  Grant anyone on the internet READ access by default to any object created
91  in the bucket example-bucket:
92
93    gsutil defacl ch -u AllUsers:R gs://example-bucket
94
95  NOTE: By default, publicly readable objects are served with a Cache-Control
96  header allowing such objects to be cached for 3600 seconds. If you need to
97  ensure that updates become visible immediately, you should set a
98  Cache-Control header of "Cache-Control:private, max-age=0, no-transform" on
99  such objects. For help doing this, see "gsutil help setmeta".
100
101  Add the user john.doe@example.com to the default object ACL on bucket
102  example-bucket with READ access:
103
104    gsutil defacl ch -u john.doe@example.com:READ gs://example-bucket
105
106  Add the group admins@example.com to the default object ACL on bucket
107  example-bucket with OWNER access:
108
109    gsutil defacl ch -g admins@example.com:O gs://example-bucket
110
111  Remove the group admins@example.com from the default object ACL on bucket
112  example-bucket:
113
114    gsutil defacl ch -d admins@example.com gs://example-bucket
115
116  Add the owners of project example-project-123 to the default object ACL on
117  bucket example-bucket with READ access:
118
119    gsutil defacl ch -p owners-example-project-123:R gs://example-bucket
120
121  NOTE: You can replace 'owners' with 'viewers' or 'editors' to grant access
122  to a project's viewers/editors respectively.
123
124<B>CH OPTIONS</B>
125  The "ch" sub-command has the following options
126
127  -d          Remove all roles associated with the matching entity.
128
129  -f          Normally gsutil stops at the first error. The -f option causes
130              it to continue when it encounters errors. With this option the
131              gsutil exit status will be 0 even if some ACLs couldn't be
132              changed.
133
134  -g          Add or modify a group entity's role.
135
136  -p          Add or modify a project viewers/editors/owners role.
137
138  -u          Add or modify a user entity's role.
139"""
140
141_SYNOPSIS = (_SET_SYNOPSIS + _GET_SYNOPSIS.lstrip('\n') +
142             _CH_SYNOPSIS.lstrip('\n') + '\n\n')
143
144_DESCRIPTION = """
145  The defacl command has three sub-commands:
146""" + '\n'.join([_SET_DESCRIPTION + _GET_DESCRIPTION + _CH_DESCRIPTION])
147
148_DETAILED_HELP_TEXT = CreateHelpText(_SYNOPSIS, _DESCRIPTION)
149
150_get_help_text = CreateHelpText(_GET_SYNOPSIS, _GET_DESCRIPTION)
151_set_help_text = CreateHelpText(_SET_SYNOPSIS, _SET_DESCRIPTION)
152_ch_help_text = CreateHelpText(_CH_SYNOPSIS, _CH_DESCRIPTION)
153
154
155class DefAclCommand(Command):
156  """Implementation of gsutil defacl command."""
157
158  # Command specification. See base class for documentation.
159  command_spec = Command.CreateCommandSpec(
160      'defacl',
161      command_name_aliases=['setdefacl', 'getdefacl', 'chdefacl'],
162      usage_synopsis=_SYNOPSIS,
163      min_args=2,
164      max_args=NO_MAX,
165      supported_sub_args='fg:u:d:p:',
166      file_url_ok=False,
167      provider_url_ok=False,
168      urls_start_arg=1,
169      gs_api_support=[ApiSelector.XML, ApiSelector.JSON],
170      gs_default_api=ApiSelector.JSON,
171      argparse_arguments={
172          'set': [
173              CommandArgument.MakeFileURLOrCannedACLArgument(),
174              CommandArgument.MakeZeroOrMoreCloudBucketURLsArgument()
175          ],
176          'get': [CommandArgument.MakeNCloudBucketURLsArgument(1)],
177          'ch': [CommandArgument.MakeZeroOrMoreCloudBucketURLsArgument()],
178      },
179  )
180  # Help specification. See help_provider.py for documentation.
181  help_spec = Command.HelpSpec(
182      help_name='defacl',
183      help_name_aliases=['default acl', 'setdefacl', 'getdefacl', 'chdefacl'],
184      help_type='command_help',
185      help_one_line_summary='Get, set, or change default ACL on buckets',
186      help_text=_DETAILED_HELP_TEXT,
187      subcommand_help_text={
188          'get': _get_help_text,
189          'set': _set_help_text,
190          'ch': _ch_help_text,
191      },
192  )
193
194  def _CalculateUrlsStartArg(self):
195    if not self.args:
196      self.RaiseWrongNumberOfArgumentsException()
197    if (self.args[0].lower() == 'set' or
198        self.command_alias_used == 'setdefacl'):
199      return 1
200    else:
201      return 0
202
203  def _SetDefAcl(self):
204    if not StorageUrlFromString(self.args[-1]).IsBucket():
205      raise CommandException('URL must name a bucket for the %s command' %
206                             self.command_name)
207    try:
208      self.SetAclCommandHelper(SetAclFuncWrapper, SetAclExceptionHandler)
209    except AccessDeniedException:
210      self._WarnServiceAccounts()
211      raise
212
213  def _GetDefAcl(self):
214    if not StorageUrlFromString(self.args[0]).IsBucket():
215      raise CommandException('URL must name a bucket for the %s command' %
216                             self.command_name)
217    self.GetAndPrintAcl(self.args[0])
218
219  def _ChDefAcl(self):
220    """Parses options and changes default object ACLs on specified buckets."""
221    self.parse_versions = True
222    self.changes = []
223
224    if self.sub_opts:
225      for o, a in self.sub_opts:
226        if o == '-g':
227          self.changes.append(
228              acl_helper.AclChange(a, scope_type=acl_helper.ChangeType.GROUP))
229        if o == '-u':
230          self.changes.append(
231              acl_helper.AclChange(a, scope_type=acl_helper.ChangeType.USER))
232        if o == '-p':
233          self.changes.append(
234              acl_helper.AclChange(a, scope_type=acl_helper.ChangeType.PROJECT))
235        if o == '-d':
236          self.changes.append(acl_helper.AclDel(a))
237
238    if not self.changes:
239      raise CommandException('Please specify at least one access change '
240                             'with the -g, -u, or -d flags')
241
242    if (not UrlsAreForSingleProvider(self.args) or
243        StorageUrlFromString(self.args[0]).scheme != 'gs'):
244      raise CommandException(
245          'The "{0}" command can only be used with gs:// URLs'.format(
246              self.command_name))
247
248    bucket_urls = set()
249    for url_arg in self.args:
250      for result in self.WildcardIterator(url_arg):
251        if not result.storage_url.IsBucket():
252          raise CommandException(
253              'The defacl ch command can only be applied to buckets.')
254        bucket_urls.add(result.storage_url)
255
256    for storage_url in bucket_urls:
257      self.ApplyAclChanges(storage_url)
258
259  @Retry(ServiceException, tries=3, timeout_secs=1)
260  def ApplyAclChanges(self, url):
261    """Applies the changes in self.changes to the provided URL."""
262    bucket = self.gsutil_api.GetBucket(
263        url.bucket_name,
264        provider=url.scheme,
265        fields=['defaultObjectAcl', 'metageneration'])
266
267    # Default object ACLs can be blank if the ACL was set to private, or
268    # if the user doesn't have permission. We warn about this with defacl get,
269    # so just try the modification here and if the user doesn't have
270    # permission they'll get an AccessDeniedException.
271    current_acl = bucket.defaultObjectAcl
272
273    if self._ApplyAclChangesAndReturnChangeCount(url, current_acl) == 0:
274      self.logger.info('No changes to %s', url)
275      return
276
277    if not current_acl:
278      # Use a sentinel value to indicate a private (no entries) default
279      # object ACL.
280      current_acl.append(PRIVATE_DEFAULT_OBJ_ACL)
281
282    try:
283      preconditions = Preconditions(meta_gen_match=bucket.metageneration)
284      bucket_metadata = apitools_messages.Bucket(defaultObjectAcl=current_acl)
285      self.gsutil_api.PatchBucket(url.bucket_name,
286                                  bucket_metadata,
287                                  preconditions=preconditions,
288                                  provider=url.scheme,
289                                  fields=['id'])
290      self.logger.info('Updated default ACL on %s', url)
291    except BadRequestException as e:
292      # Don't retry on bad requests, e.g. invalid email address.
293      raise CommandException('Received bad request from server: %s' % str(e))
294    except AccessDeniedException:
295      self._WarnServiceAccounts()
296      raise CommandException('Failed to set acl for %s. Please ensure you have '
297                             'OWNER-role access to this resource.' % url)
298
299  def _ApplyAclChangesAndReturnChangeCount(self, storage_url, defacl_message):
300    modification_count = 0
301    for change in self.changes:
302      modification_count += change.Execute(storage_url, defacl_message,
303                                           'defacl', self.logger)
304    return modification_count
305
306  def RunCommand(self):
307    """Command entry point for the defacl command."""
308    action_subcommand = self.args.pop(0)
309    self.ParseSubOpts(check_args=True)
310    self.def_acl = True
311    self.continue_on_error = False
312    if action_subcommand == 'get':
313      func = self._GetDefAcl
314    elif action_subcommand == 'set':
315      func = self._SetDefAcl
316    elif action_subcommand in ('ch', 'change'):
317      func = self._ChDefAcl
318    else:
319      raise CommandException(('Invalid subcommand "%s" for the %s command.\n'
320                              'See "gsutil help defacl".') %
321                             (action_subcommand, self.command_name))
322    # Commands with both suboptions and subcommands need to reparse for
323    # suboptions, so we log again.
324    metrics.LogCommandParams(subcommands=[action_subcommand],
325                             sub_opts=self.sub_opts)
326    func()
327    return 0
328