| OLD | NEW |
| (Empty) |
| 1 # Copyright 2013 Google Inc. All Rights Reserved. | |
| 2 # | |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 # you may not use this file except in compliance with the License. | |
| 5 # You may obtain a copy of the License at | |
| 6 # | |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 # | |
| 9 # Unless required by applicable law or agreed to in writing, software | |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 # See the License for the specific language governing permissions and | |
| 13 # limitations under the License. | |
| 14 """ | |
| 15 This module provides the chacl command to gsutil. | |
| 16 | |
| 17 This command allows users to easily specify changes to access control lists. | |
| 18 """ | |
| 19 | |
| 20 import random | |
| 21 import re | |
| 22 import time | |
| 23 from xml.dom import minidom | |
| 24 from boto.exception import GSResponseError | |
| 25 from boto.gs import acl | |
| 26 from gslib import name_expansion | |
| 27 from gslib.command import Command | |
| 28 from gslib.command import COMMAND_NAME | |
| 29 from gslib.command import COMMAND_NAME_ALIASES | |
| 30 from gslib.command import CONFIG_REQUIRED | |
| 31 from gslib.command import FILE_URIS_OK | |
| 32 from gslib.command import MAX_ARGS | |
| 33 from gslib.command import MIN_ARGS | |
| 34 from gslib.command import PROVIDER_URIS_OK | |
| 35 from gslib.command import SUPPORTED_SUB_ARGS | |
| 36 from gslib.command import URIS_START_ARG | |
| 37 from gslib.exception import CommandException | |
| 38 from gslib.help_provider import HELP_NAME | |
| 39 from gslib.help_provider import HELP_NAME_ALIASES | |
| 40 from gslib.help_provider import HELP_ONE_LINE_SUMMARY | |
| 41 from gslib.help_provider import HELP_TEXT | |
| 42 from gslib.help_provider import HELP_TYPE | |
| 43 from gslib.help_provider import HelpType | |
| 44 from gslib.util import NO_MAX | |
| 45 from gslib.util import Retry | |
| 46 | |
| 47 | |
| 48 class ChangeType(object): | |
| 49 USER = 'User' | |
| 50 GROUP = 'Group' | |
| 51 | |
| 52 | |
| 53 class AclChange(object): | |
| 54 """Represents a logical change to an access control list.""" | |
| 55 public_scopes = ['AllAuthenticatedUsers', 'AllUsers'] | |
| 56 id_scopes = ['UserById', 'GroupById'] | |
| 57 email_scopes = ['UserByEmail', 'GroupByEmail'] | |
| 58 domain_scopes = ['GroupByDomain'] | |
| 59 scope_types = public_scopes + id_scopes + email_scopes + domain_scopes | |
| 60 | |
| 61 permission_shorthand_mapping = { | |
| 62 'R': 'READ', | |
| 63 'W': 'WRITE', | |
| 64 'FC': 'FULL_CONTROL', | |
| 65 } | |
| 66 | |
| 67 def __init__(self, acl_change_descriptor, scope_type, logger): | |
| 68 """Creates an AclChange object. | |
| 69 | |
| 70 acl_change_descriptor: An acl change as described in chacl help. | |
| 71 scope_type: Either ChangeType.USER or ChangeType.GROUP, specifying the | |
| 72 extent of the scope. | |
| 73 logger: An instance of ThreadedLogger. | |
| 74 """ | |
| 75 self.logger = logger | |
| 76 self.identifier = '' | |
| 77 | |
| 78 self.raw_descriptor = acl_change_descriptor | |
| 79 self._Parse(acl_change_descriptor, scope_type) | |
| 80 self._Validate() | |
| 81 | |
| 82 def __str__(self): | |
| 83 return 'AclChange<{0}|{1}|{2}>'.format(self.scope_type, self.perm, | |
| 84 self.identifier) | |
| 85 | |
| 86 def _Parse(self, change_descriptor, scope_type): | |
| 87 """Parses an ACL Change descriptor.""" | |
| 88 | |
| 89 def _ClassifyScopeIdentifier(text): | |
| 90 re_map = { | |
| 91 'AllAuthenticatedUsers': r'^(AllAuthenticatedUsers|AllAuth)$', | |
| 92 'AllUsers': '^(AllUsers|All)$', | |
| 93 'Email': r'^.+@.+\..+$', | |
| 94 'Id': r'^[0-9A-Fa-f]{64}$', | |
| 95 'Domain': r'^[^@]+\..+$', | |
| 96 } | |
| 97 for type_string, regex in re_map.items(): | |
| 98 if re.match(regex, text, re.IGNORECASE): | |
| 99 return type_string | |
| 100 | |
| 101 if change_descriptor.count(':') != 1: | |
| 102 raise CommandException('{0} is an invalid change description.' | |
| 103 .format(change_descriptor)) | |
| 104 | |
| 105 scope_string, perm_token = change_descriptor.split(':') | |
| 106 | |
| 107 perm_token = perm_token.upper() | |
| 108 if perm_token in self.permission_shorthand_mapping: | |
| 109 self.perm = self.permission_shorthand_mapping[perm_token] | |
| 110 else: | |
| 111 self.perm = perm_token | |
| 112 | |
| 113 scope_class = _ClassifyScopeIdentifier(scope_string) | |
| 114 if scope_class == 'Domain': | |
| 115 # This may produce an invalid UserByDomain scope, | |
| 116 # which is good because then validate can complain. | |
| 117 self.scope_type = '{0}ByDomain'.format(scope_type) | |
| 118 self.identifier = scope_string | |
| 119 elif scope_class in ['Email', 'Id']: | |
| 120 self.scope_type = '{0}By{1}'.format(scope_type, scope_class) | |
| 121 self.identifier = scope_string | |
| 122 elif scope_class == 'AllAuthenticatedUsers': | |
| 123 self.scope_type = 'AllAuthenticatedUsers' | |
| 124 elif scope_class == 'AllUsers': | |
| 125 self.scope_type = 'AllUsers' | |
| 126 else: | |
| 127 # This is just a fallback, so we set it to something | |
| 128 # and the validate step has something to go on. | |
| 129 self.scope_type = scope_string | |
| 130 | |
| 131 def _Validate(self): | |
| 132 """Validates a parsed AclChange object.""" | |
| 133 | |
| 134 def _ThrowError(msg): | |
| 135 raise CommandException('{0} is not a valid ACL change\n{1}' | |
| 136 .format(self.raw_descriptor, msg)) | |
| 137 | |
| 138 if self.scope_type not in self.scope_types: | |
| 139 _ThrowError('{0} is not a valid scope type'.format(self.scope_type)) | |
| 140 | |
| 141 if self.scope_type in self.public_scopes and self.identifier: | |
| 142 _ThrowError('{0} requires no arguments'.format(self.scope_type)) | |
| 143 | |
| 144 if self.scope_type in self.id_scopes and not self.identifier: | |
| 145 _ThrowError('{0} requires an id'.format(self.scope_type)) | |
| 146 | |
| 147 if self.scope_type in self.email_scopes and not self.identifier: | |
| 148 _ThrowError('{0} requires an email address'.format(self.scope_type)) | |
| 149 | |
| 150 if self.scope_type in self.domain_scopes and not self.identifier: | |
| 151 _ThrowError('{0} requires domain'.format(self.scope_type)) | |
| 152 | |
| 153 if self.perm not in self.permission_shorthand_mapping.values(): | |
| 154 perms = ', '.join(self.permission_shorthand_mapping.values()) | |
| 155 _ThrowError('Allowed permissions are {0}'.format(perms)) | |
| 156 | |
| 157 def _YieldMatchingEntries(self, current_acl): | |
| 158 """Generator that yields entries that match the change descriptor. | |
| 159 | |
| 160 current_acl: An instance of bogo.gs.acl.ACL which will be searched | |
| 161 for matching entries. | |
| 162 """ | |
| 163 for entry in current_acl.entries.entry_list: | |
| 164 if entry.scope.type == self.scope_type: | |
| 165 if self.scope_type in ['UserById', 'GroupById']: | |
| 166 if self.identifier == entry.scope.id: | |
| 167 yield entry | |
| 168 elif self.scope_type in ['UserByEmail', 'GroupByEmail']: | |
| 169 if self.identifier == entry.scope.email_address: | |
| 170 yield entry | |
| 171 elif self.scope_type == 'GroupByDomain': | |
| 172 if self.identifier == entry.scope.domain: | |
| 173 yield entry | |
| 174 elif self.scope_type in ['AllUsers', 'AllAuthenticatedUsers']: | |
| 175 yield entry | |
| 176 else: | |
| 177 raise CommandException('Found an unrecognized ACL ' | |
| 178 'entry type, aborting.') | |
| 179 | |
| 180 def _AddEntry(self, current_acl): | |
| 181 """Adds an entry to an ACL.""" | |
| 182 if self.scope_type in ['UserById', 'UserById', 'GroupById']: | |
| 183 entry = acl.Entry(type=self.scope_type, permission=self.perm, | |
| 184 id=self.identifier) | |
| 185 elif self.scope_type in ['UserByEmail', 'GroupByEmail']: | |
| 186 entry = acl.Entry(type=self.scope_type, permission=self.perm, | |
| 187 email_address=self.identifier) | |
| 188 elif self.scope_type == 'GroupByDomain': | |
| 189 entry = acl.Entry(type=self.scope_type, permission=self.perm, | |
| 190 domain=self.identifier) | |
| 191 else: | |
| 192 entry = acl.Entry(type=self.scope_type, permission=self.perm) | |
| 193 | |
| 194 current_acl.entries.entry_list.append(entry) | |
| 195 | |
| 196 def Execute(self, uri, current_acl): | |
| 197 """Executes the described change on an ACL. | |
| 198 | |
| 199 uri: The URI object to change. | |
| 200 current_acl: An instance of boto.gs.acl.ACL to permute. | |
| 201 """ | |
| 202 self.logger.debug('Executing {0} on {1}' | |
| 203 .format(self.raw_descriptor, uri)) | |
| 204 | |
| 205 if self.perm == 'WRITE' and uri.names_object(): | |
| 206 self.logger.warn( | |
| 207 'Skipping {0} on {1}, as WRITE does not apply to objects' | |
| 208 .format(self.raw_descriptor, uri)) | |
| 209 return 0 | |
| 210 | |
| 211 matching_entries = list(self._YieldMatchingEntries(current_acl)) | |
| 212 change_count = 0 | |
| 213 if matching_entries: | |
| 214 for entry in matching_entries: | |
| 215 if entry.permission != self.perm: | |
| 216 entry.permission = self.perm | |
| 217 change_count += 1 | |
| 218 else: | |
| 219 self._AddEntry(current_acl) | |
| 220 change_count = 1 | |
| 221 | |
| 222 parsed_acl = minidom.parseString(current_acl.to_xml()) | |
| 223 self.logger.debug('New Acl:\n{0}'.format(parsed_acl.toprettyxml())) | |
| 224 return change_count | |
| 225 | |
| 226 | |
| 227 class AclDel(AclChange): | |
| 228 """Represents a logical change from an access control list.""" | |
| 229 scope_regexes = { | |
| 230 r'All(Users)?': 'AllUsers', | |
| 231 r'AllAuth(enticatedUsers)?': 'AllAuthenticatedUsers', | |
| 232 } | |
| 233 | |
| 234 def __init__(self, identifier, logger): | |
| 235 self.raw_descriptor = '-d {0}'.format(identifier) | |
| 236 self.logger = logger | |
| 237 self.identifier = identifier | |
| 238 for regex, scope in self.scope_regexes.items(): | |
| 239 if re.match(regex, self.identifier, re.IGNORECASE): | |
| 240 self.identifier = scope | |
| 241 self.scope_type = 'Any' | |
| 242 self.perm = 'NONE' | |
| 243 | |
| 244 def _YieldMatchingEntries(self, current_acl): | |
| 245 for entry in current_acl.entries.entry_list: | |
| 246 if self.identifier == entry.scope.id: | |
| 247 yield entry | |
| 248 elif self.identifier == entry.scope.email_address: | |
| 249 yield entry | |
| 250 elif self.identifier == entry.scope.domain: | |
| 251 yield entry | |
| 252 elif self.identifier == 'AllUsers' and entry.scope.type == 'AllUsers': | |
| 253 yield entry | |
| 254 elif (self.identifier == 'AllAuthenticatedUsers' | |
| 255 and entry.scope.type == 'AllAuthenticatedUsers'): | |
| 256 yield entry | |
| 257 | |
| 258 def Execute(self, uri, current_acl): | |
| 259 self.logger.debug('Executing {0} on {1}' | |
| 260 .format(self.raw_descriptor, uri)) | |
| 261 matching_entries = list(self._YieldMatchingEntries(current_acl)) | |
| 262 for entry in matching_entries: | |
| 263 current_acl.entries.entry_list.remove(entry) | |
| 264 parsed_acl = minidom.parseString(current_acl.to_xml()) | |
| 265 self.logger.debug('New Acl:\n{0}'.format(parsed_acl.toprettyxml())) | |
| 266 return len(matching_entries) | |
| 267 | |
| 268 | |
| 269 _detailed_help_text = (""" | |
| 270 <B>SYNOPSIS</B> | |
| 271 gsutil chacl [-R] -u|-g|-d <grant>... uri... | |
| 272 | |
| 273 where each <grant> is one of the following forms: | |
| 274 -u <id|email>:<perm> | |
| 275 -g <id|email|domain|All|AllAuth>:<perm> | |
| 276 -d <id|email|domain|All|AllAuth> | |
| 277 | |
| 278 <B>DESCRIPTION</B> | |
| 279 The chacl command updates access control lists, similar in spirit to the Linux | |
| 280 chmod command. You can specify multiple access grant additions and deletions | |
| 281 in a single command run; all changes will be made atomically to each object in | |
| 282 turn. For example, if the command requests deleting one grant and adding a | |
| 283 different grant, the ACLs being updated will never be left in an intermediate | |
| 284 state where one grant has been deleted but the second grant not yet added. | |
| 285 Each change specifies a user or group grant to add or delete, and for grant | |
| 286 additions, one of R, W, FC (for the permission to be granted). A more formal | |
| 287 description is provided in a later section; below we provide examples. | |
| 288 | |
| 289 Note: If you want to set a simple "canned" ACL on each object (such as | |
| 290 project-private or public), or if you prefer to edit the XML representation | |
| 291 for ACLs, you can do that with the setacl command (see 'gsutil help setacl'). | |
| 292 | |
| 293 | |
| 294 <B>EXAMPLES</B> | |
| 295 | |
| 296 Grant the user john.doe@example.com WRITE access to the bucket | |
| 297 example-bucket: | |
| 298 | |
| 299 gsutil chacl -u john.doe@example.com:WRITE gs://example-bucket | |
| 300 | |
| 301 Grant the group admins@example.com FULL_CONTROL access to all jpg files in | |
| 302 the top level of example-bucket: | |
| 303 | |
| 304 gsutil chacl -g admins@example.com:FC gs://example-bucket/*.jpg | |
| 305 | |
| 306 Grant the user with the specified canonical ID READ access to all objects in | |
| 307 example-bucket that begin with folder/: | |
| 308 | |
| 309 gsutil chacl -R \\ | |
| 310 -u 84fac329bceSAMPLE777d5d22b8SAMPLE77d85ac2SAMPLE2dfcf7c4adf34da46:R \\ | |
| 311 gs://example-bucket/folder/ | |
| 312 | |
| 313 Grant all users from my-domain.org READ access to the bucket | |
| 314 gcs.my-domain.org: | |
| 315 | |
| 316 gsutil chacl -g my-domain.org:R gs://gcs.my-domain.org | |
| 317 | |
| 318 Remove any current access by john.doe@example.com from the bucket | |
| 319 example-bucket: | |
| 320 | |
| 321 gsutil chacl -d john.doe@example.com gs://example-bucket | |
| 322 | |
| 323 If you have a large number of objects to update, enabling multi-threading with | |
| 324 the gsutil -m flag can significantly improve performance. The following | |
| 325 command adds FULL_CONTROL for admin@example.org using multi-threading: | |
| 326 | |
| 327 gsutil -m chacl -R -u admin@example.org:FC gs://example-bucket | |
| 328 | |
| 329 Grant READ access to everyone from my-domain.org and to all authenticated | |
| 330 users, and grant FULL_CONTROL to admin@mydomain.org, for the buckets | |
| 331 my-bucket and my-other-bucket, with multi-threading enabled: | |
| 332 | |
| 333 gsutil -m chacl -R -g my-domain.org:R -g AllAuth:R \\ | |
| 334 -u admin@mydomain.org:FC gs://my-bucket/ gs://my-other-bucket | |
| 335 | |
| 336 | |
| 337 <B>SCOPES</B> | |
| 338 There are four different scopes: Users, Groups, All Authenticated Users, and | |
| 339 All Users. | |
| 340 | |
| 341 Users are added with -u and a plain ID or email address, as in | |
| 342 "-u john-doe@gmail.com:r" | |
| 343 | |
| 344 Groups are like users, but specified with the -g flag, as in | |
| 345 "-g power-users@example.com:fc". Groups may also be specified as a full | |
| 346 domain, as in "-g my-company.com:r". | |
| 347 | |
| 348 AllAuthenticatedUsers and AllUsers are specified directly, as | |
| 349 in "-g AllUsers:R" or "-g AllAuthenticatedUsers:FC". These are case | |
| 350 insensitive, and may be shortened to "all" and "allauth", respectively. | |
| 351 | |
| 352 Removing permissions is specified with the -d flag and an ID, email | |
| 353 address, domain, or one of AllUsers or AllAuthenticatedUsers. | |
| 354 | |
| 355 Many scopes can be specified on the same command line, allowing bundled | |
| 356 changes to be executed in a single run. This will reduce the number of | |
| 357 requests made to the server. | |
| 358 | |
| 359 | |
| 360 <B>PERMISSIONS</B> | |
| 361 You may specify the following permissions with either their shorthand or | |
| 362 their full name: | |
| 363 | |
| 364 R: READ | |
| 365 W: WRITE | |
| 366 FC: FULL_CONTROL | |
| 367 | |
| 368 | |
| 369 <B>OPTIONS</B> | |
| 370 -R, -r Performs chacl request recursively, to all objects under the | |
| 371 specified URI. | |
| 372 | |
| 373 -u Add or modify a user permission as specified in the SCOPES | |
| 374 and PERMISSIONS sections. | |
| 375 | |
| 376 -g Add or modify a group permission as specified in the SCOPES | |
| 377 and PERMISSIONS sections. | |
| 378 | |
| 379 -d Remove all permissions associated with the matching argument, as | |
| 380 specified in the SCOPES and PERMISSIONS sections. | |
| 381 """) | |
| 382 | |
| 383 | |
| 384 class ChAclCommand(Command): | |
| 385 """Implementation of gsutil chacl command.""" | |
| 386 | |
| 387 # Command specification (processed by parent class). | |
| 388 command_spec = { | |
| 389 # Name of command. | |
| 390 COMMAND_NAME : 'chacl', | |
| 391 # List of command name aliases. | |
| 392 COMMAND_NAME_ALIASES : [], | |
| 393 # Min number of args required by this command. | |
| 394 MIN_ARGS : 1, | |
| 395 # Max number of args required by this command, or NO_MAX. | |
| 396 MAX_ARGS : NO_MAX, | |
| 397 # Getopt-style string specifying acceptable sub args. | |
| 398 SUPPORTED_SUB_ARGS : 'Rrfg:u:d:', | |
| 399 # True if file URIs acceptable for this command. | |
| 400 FILE_URIS_OK : False, | |
| 401 # True if provider-only URIs acceptable for this command. | |
| 402 PROVIDER_URIS_OK : False, | |
| 403 # Index in args of first URI arg. | |
| 404 URIS_START_ARG : 1, | |
| 405 # True if must configure gsutil before running command. | |
| 406 CONFIG_REQUIRED : True, | |
| 407 } | |
| 408 help_spec = { | |
| 409 # Name of command or auxiliary help info for which this help applies. | |
| 410 HELP_NAME : 'chacl', | |
| 411 # List of help name aliases. | |
| 412 HELP_NAME_ALIASES : ['chmod'], | |
| 413 # Type of help: | |
| 414 HELP_TYPE : HelpType.COMMAND_HELP, | |
| 415 # One line summary of this help. | |
| 416 HELP_ONE_LINE_SUMMARY : 'Add / remove entries on bucket and/or object ACLs', | |
| 417 # The full help text. | |
| 418 HELP_TEXT : _detailed_help_text, | |
| 419 } | |
| 420 | |
| 421 # Command entry point. | |
| 422 def RunCommand(self): | |
| 423 """This is the point of entry for the chacl command.""" | |
| 424 self.parse_versions = True | |
| 425 self.changes = [] | |
| 426 | |
| 427 if self.sub_opts: | |
| 428 for o, a in self.sub_opts: | |
| 429 if o == '-g': | |
| 430 self.changes.append(AclChange(a, scope_type=ChangeType.GROUP, | |
| 431 logger=self.THREADED_LOGGER)) | |
| 432 if o == '-u': | |
| 433 self.changes.append(AclChange(a, scope_type=ChangeType.USER, | |
| 434 logger=self.THREADED_LOGGER)) | |
| 435 if o == '-d': | |
| 436 self.changes.append(AclDel(a, logger=self.THREADED_LOGGER)) | |
| 437 | |
| 438 if not self.changes: | |
| 439 raise CommandException( | |
| 440 'Please specify at least one access change ' | |
| 441 'with the -g, -u, or -d flags') | |
| 442 | |
| 443 storage_uri = self.UrisAreForSingleProvider(self.args) | |
| 444 if not (storage_uri and storage_uri.get_provider().name == 'google'): | |
| 445 raise CommandException('The "{0}" command can only be used with gs:// URIs
' | |
| 446 .format(self.command_name)) | |
| 447 | |
| 448 bulk_uris = set() | |
| 449 for uri_arg in self.args: | |
| 450 for result in self.WildcardIterator(uri_arg): | |
| 451 uri = result.uri | |
| 452 if uri.names_bucket(): | |
| 453 if self.recursion_requested: | |
| 454 bulk_uris.add(uri.clone_replace_name('*').uri) | |
| 455 else: | |
| 456 # If applying to a bucket directly, the threading machinery will | |
| 457 # break, so we have to apply now, in the main thread. | |
| 458 self.ApplyAclChanges(uri) | |
| 459 else: | |
| 460 bulk_uris.add(uri_arg) | |
| 461 | |
| 462 try: | |
| 463 name_expansion_iterator = name_expansion.NameExpansionIterator( | |
| 464 self.command_name, self.proj_id_handler, self.headers, self.debug, | |
| 465 self.bucket_storage_uri_class, bulk_uris, self.recursion_requested) | |
| 466 except CommandException as e: | |
| 467 # NameExpansionIterator will complain if there are no URIs, but we don't | |
| 468 # want to throw an error if we handled bucket URIs. | |
| 469 if e.reason == 'No URIs matched': | |
| 470 return 0 | |
| 471 else: | |
| 472 raise e | |
| 473 | |
| 474 self.everything_set_okay = True | |
| 475 self.Apply(self.ApplyAclChanges, | |
| 476 name_expansion_iterator, | |
| 477 self._ApplyExceptionHandler) | |
| 478 if not self.everything_set_okay: | |
| 479 raise CommandException('ACLs for some objects could not be set.') | |
| 480 | |
| 481 return 0 | |
| 482 | |
| 483 def _ApplyExceptionHandler(self, exception): | |
| 484 self.THREADED_LOGGER.error('Encountered a problem: {0}'.format(exception)) | |
| 485 self.everything_set_okay = False | |
| 486 | |
| 487 @Retry(GSResponseError, tries=3, delay=1, backoff=2) | |
| 488 def ApplyAclChanges(self, uri_or_expansion_result): | |
| 489 """Applies the changes in self.changes to the provided URI.""" | |
| 490 if isinstance(uri_or_expansion_result, name_expansion.NameExpansionResult): | |
| 491 uri = self.suri_builder.StorageUri( | |
| 492 uri_or_expansion_result.expanded_uri_str) | |
| 493 else: | |
| 494 uri = uri_or_expansion_result | |
| 495 | |
| 496 try: | |
| 497 current_acl = uri.get_acl() | |
| 498 except GSResponseError as e: | |
| 499 self.THREADED_LOGGER.warning('Failed to set acl for {0}: {1}' | |
| 500 .format(uri, e.reason)) | |
| 501 return | |
| 502 | |
| 503 modification_count = 0 | |
| 504 for change in self.changes: | |
| 505 modification_count += change.Execute(uri, current_acl) | |
| 506 if modification_count == 0: | |
| 507 self.THREADED_LOGGER.info('No changes to {0}'.format(uri)) | |
| 508 return | |
| 509 | |
| 510 # TODO: Remove the concept of forcing when boto provides access to | |
| 511 # bucket generation and meta_generation. | |
| 512 headers = dict(self.headers) | |
| 513 force = uri.names_bucket() | |
| 514 if not force: | |
| 515 key = uri.get_key() | |
| 516 headers['x-goog-if-generation-match'] = key.generation | |
| 517 headers['x-goog-if-metageneration-match'] = key.meta_generation | |
| 518 | |
| 519 # If this fails because of a precondition, it will raise a | |
| 520 # GSResponseError for @Retry to handle. | |
| 521 uri.set_acl(current_acl, uri.object_name, False, headers) | |
| 522 self.THREADED_LOGGER.info('Updated ACL on {0}'.format(uri)) | |
| 523 | |
| OLD | NEW |