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 17from __future__ import absolute_import 18 19from gslib import aclhelpers 20from gslib.cloud_api import AccessDeniedException 21from gslib.cloud_api import BadRequestException 22from gslib.cloud_api import Preconditions 23from gslib.cloud_api import ServiceException 24from gslib.command import Command 25from gslib.command import SetAclExceptionHandler 26from gslib.command import SetAclFuncWrapper 27from gslib.command_argument import CommandArgument 28from gslib.cs_api_map import ApiSelector 29from gslib.exception import CommandException 30from gslib.help_provider import CreateHelpText 31from gslib.storage_url import StorageUrlFromString 32from gslib.third_party.storage_apitools import storage_v1_messages as apitools_messages 33from gslib.util import NO_MAX 34from gslib.util import Retry 35from 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@example.com WRITE access to the bucket 151 example-bucket: 152 153 gsutil acl ch -u john.doe@example.com:WRITE gs://example-bucket 154 155 Grant the group admins@example.com OWNER access to all jpg files in 156 the top level of example-bucket: 157 158 gsutil acl ch -g admins@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@developer.gserviceaccount.com WRITE access to 176 the bucket example-bucket: 177 178 gsutil acl ch -u foo@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@example.com from the bucket 187 example-bucket: 188 189 gsutil acl ch -d john.doe@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@example.org using 194 multi-threading: 195 196 gsutil -m acl ch -r -u admin@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@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@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 269def _ApplyExceptionHandler(cls, exception): 270 cls.logger.error('Encountered a problem: %s', exception) 271 cls.everything_set_okay = False 272 273 274def _ApplyAclChangesWrapper(cls, url_or_expansion_result, thread_state=None): 275 cls.ApplyAclChanges(url_or_expansion_result, thread_state=thread_state) 276 277 278class 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