Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(76)

Side by Side Diff: third_party/gsutil/gslib/commands/chacl.py

Issue 2280023003: depot_tools: Remove third_party/gsutil (Closed)
Patch Set: Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « third_party/gsutil/gslib/commands/cat.py ('k') | third_party/gsutil/gslib/commands/config.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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
OLDNEW
« no previous file with comments | « third_party/gsutil/gslib/commands/cat.py ('k') | third_party/gsutil/gslib/commands/config.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698