Only grant permissions to new extensions from sync if they have the expected version
[chromium-blink-merge.git] / tools / telemetry / third_party / gsutilz / gslib / aclhelpers.py
blobef8fa5c4db225d86d4647c8acac004b4a6b5e52f
1 # -*- coding: utf-8 -*-
2 # Copyright 2013 Google Inc. All Rights Reserved.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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 """Contains helper objects for changing and deleting ACLs."""
17 from __future__ import absolute_import
19 import re
21 from gslib.exception import CommandException
22 from gslib.third_party.storage_apitools import storage_v1_messages as apitools_messages
25 class ChangeType(object):
26 USER = 'User'
27 GROUP = 'Group'
28 PROJECT = 'Project'
31 class AclChange(object):
32 """Represents a logical change to an access control list."""
33 public_scopes = ['AllAuthenticatedUsers', 'AllUsers']
34 id_scopes = ['UserById', 'GroupById']
35 email_scopes = ['UserByEmail', 'GroupByEmail']
36 domain_scopes = ['GroupByDomain']
37 project_scopes = ['Project']
38 scope_types = (public_scopes + id_scopes + email_scopes + domain_scopes
39 + project_scopes)
41 public_entity_all_users = 'allUsers'
42 public_entity_all_auth_users = 'allAuthenticatedUsers'
43 public_entity_types = (public_entity_all_users, public_entity_all_auth_users)
44 project_entity_prefixes = ('project-editors-', 'project-owners-',
45 'project-viewers-')
46 group_entity_prefix = 'group-'
47 user_entity_prefix = 'user-'
48 domain_entity_prefix = 'domain-'
49 project_entity_prefix = 'project-'
51 permission_shorthand_mapping = {
52 'R': 'READER',
53 'W': 'WRITER',
54 'FC': 'OWNER',
55 'O': 'OWNER',
56 'READ': 'READER',
57 'WRITE': 'WRITER',
58 'FULL_CONTROL': 'OWNER'
61 def __init__(self, acl_change_descriptor, scope_type):
62 """Creates an AclChange object.
64 Args:
65 acl_change_descriptor: An acl change as described in the "ch" section of
66 the "acl" command's help.
67 scope_type: Either ChangeType.USER or ChangeType.GROUP or
68 ChangeType.PROJECT, specifying the extent of the scope.
69 """
70 self.identifier = ''
72 self.raw_descriptor = acl_change_descriptor
73 self._Parse(acl_change_descriptor, scope_type)
74 self._Validate()
76 def __str__(self):
77 return 'AclChange<{0}|{1}|{2}>'.format(
78 self.scope_type, self.perm, self.identifier)
80 def _Parse(self, change_descriptor, scope_type):
81 """Parses an ACL Change descriptor."""
83 def _ClassifyScopeIdentifier(text):
84 re_map = {
85 'AllAuthenticatedUsers': r'^(AllAuthenticatedUsers|AllAuth)$',
86 'AllUsers': '^(AllUsers|All)$',
87 'Email': r'^.+@.+\..+$',
88 'Id': r'^[0-9A-Fa-f]{64}$',
89 'Domain': r'^[^@]+\.[^@]+$',
90 'Project': r'(owners|editors|viewers)\-.+$',
92 for type_string, regex in re_map.items():
93 if re.match(regex, text, re.IGNORECASE):
94 return type_string
96 if change_descriptor.count(':') != 1:
97 raise CommandException('{0} is an invalid change description.'
98 .format(change_descriptor))
100 scope_string, perm_token = change_descriptor.split(':')
102 perm_token = perm_token.upper()
103 if perm_token in self.permission_shorthand_mapping:
104 self.perm = self.permission_shorthand_mapping[perm_token]
105 else:
106 self.perm = perm_token
108 scope_class = _ClassifyScopeIdentifier(scope_string)
109 if scope_class == 'Domain':
110 # This may produce an invalid UserByDomain scope,
111 # which is good because then validate can complain.
112 self.scope_type = '{0}ByDomain'.format(scope_type)
113 self.identifier = scope_string
114 elif scope_class in ('Email', 'Id'):
115 self.scope_type = '{0}By{1}'.format(scope_type, scope_class)
116 self.identifier = scope_string
117 elif scope_class == 'AllAuthenticatedUsers':
118 self.scope_type = 'AllAuthenticatedUsers'
119 elif scope_class == 'AllUsers':
120 self.scope_type = 'AllUsers'
121 elif scope_class == 'Project':
122 self.scope_type = 'Project'
123 self.identifier = scope_string
124 else:
125 # This is just a fallback, so we set it to something
126 # and the validate step has something to go on.
127 self.scope_type = scope_string
129 def _Validate(self):
130 """Validates a parsed AclChange object."""
132 def _ThrowError(msg):
133 raise CommandException('{0} is not a valid ACL change\n{1}'
134 .format(self.raw_descriptor, msg))
136 if self.scope_type not in self.scope_types:
137 _ThrowError('{0} is not a valid scope type'.format(self.scope_type))
139 if self.scope_type in self.public_scopes and self.identifier:
140 _ThrowError('{0} requires no arguments'.format(self.scope_type))
142 if self.scope_type in self.id_scopes and not self.identifier:
143 _ThrowError('{0} requires an id'.format(self.scope_type))
145 if self.scope_type in self.email_scopes and not self.identifier:
146 _ThrowError('{0} requires an email address'.format(self.scope_type))
148 if self.scope_type in self.domain_scopes and not self.identifier:
149 _ThrowError('{0} requires domain'.format(self.scope_type))
151 if self.perm not in self.permission_shorthand_mapping.values():
152 perms = ', '.join(self.permission_shorthand_mapping.values())
153 _ThrowError('Allowed permissions are {0}'.format(perms))
155 def _YieldMatchingEntries(self, current_acl):
156 """Generator that yields entries that match the change descriptor.
158 Args:
159 current_acl: A list of apitools_messages.BucketAccessControls or
160 ObjectAccessControls which will be searched for matching
161 entries.
163 Yields:
164 An apitools_messages.BucketAccessControl or ObjectAccessControl.
166 for entry in current_acl:
167 if (self.scope_type in ('UserById', 'GroupById') and
168 entry.entityId and self.identifier == entry.entityId):
169 yield entry
170 elif (self.scope_type in ('UserByEmail', 'GroupByEmail')
171 and entry.email and self.identifier == entry.email):
172 yield entry
173 elif (self.scope_type == 'GroupByDomain' and
174 entry.domain and self.identifier == entry.domain):
175 yield entry
176 elif (self.scope_type == 'Project' and
177 entry.domain and self.identifier == entry.project):
178 yield entry
179 elif (self.scope_type == 'AllUsers' and
180 entry.entity.lower() == self.public_entity_all_users.lower()):
181 yield entry
182 elif (self.scope_type == 'AllAuthenticatedUsers' and
183 entry.entity.lower() == self.public_entity_all_auth_users.lower()):
184 yield entry
186 def _AddEntry(self, current_acl, entry_class):
187 """Adds an entry to current_acl."""
188 if self.scope_type == 'UserById':
189 entry = entry_class(entityId=self.identifier, role=self.perm,
190 entity=self.user_entity_prefix + self.identifier)
191 elif self.scope_type == 'GroupById':
192 entry = entry_class(entityId=self.identifier, role=self.perm,
193 entity=self.group_entity_prefix + self.identifier)
194 elif self.scope_type == 'Project':
195 entry = entry_class(entityId=self.identifier, role=self.perm,
196 entity=self.project_entity_prefix + self.identifier)
197 elif self.scope_type == 'UserByEmail':
198 entry = entry_class(email=self.identifier, role=self.perm,
199 entity=self.user_entity_prefix + self.identifier)
200 elif self.scope_type == 'GroupByEmail':
201 entry = entry_class(email=self.identifier, role=self.perm,
202 entity=self.group_entity_prefix + self.identifier)
203 elif self.scope_type == 'GroupByDomain':
204 entry = entry_class(domain=self.identifier, role=self.perm,
205 entity=self.domain_entity_prefix + self.identifier)
206 elif self.scope_type == 'AllAuthenticatedUsers':
207 entry = entry_class(entity=self.public_entity_all_auth_users,
208 role=self.perm)
209 elif self.scope_type == 'AllUsers':
210 entry = entry_class(entity=self.public_entity_all_users, role=self.perm)
211 else:
212 raise CommandException('Add entry to ACL got unexpected scope type %s.' %
213 self.scope_type)
214 current_acl.append(entry)
216 def _GetEntriesClass(self, current_acl):
217 # Entries will share the same class, so just return the first one.
218 for acl_entry in current_acl:
219 return acl_entry.__class__
220 # It's possible that a default object ACL is empty, so if we have
221 # an empty list, assume it is an object ACL.
222 return apitools_messages.ObjectAccessControl().__class__
224 def Execute(self, storage_url, current_acl, command_name, logger):
225 """Executes the described change on an ACL.
227 Args:
228 storage_url: StorageUrl representing the object to change.
229 current_acl: A list of ObjectAccessControls or
230 BucketAccessControls to permute.
231 command_name: String name of comamnd being run (e.g., 'acl').
232 logger: An instance of logging.Logger.
234 Returns:
235 The number of changes that were made.
237 logger.debug(
238 'Executing %s %s on %s', command_name, self.raw_descriptor, storage_url)
240 if self.perm == 'WRITER':
241 if command_name == 'acl' and storage_url.IsObject():
242 logger.warning(
243 'Skipping %s on %s, as WRITER does not apply to objects',
244 self.raw_descriptor, storage_url)
245 return 0
246 elif command_name == 'defacl':
247 raise CommandException('WRITER cannot be set as a default object ACL '
248 'because WRITER does not apply to objects')
250 entry_class = self._GetEntriesClass(current_acl)
251 matching_entries = list(self._YieldMatchingEntries(current_acl))
252 change_count = 0
253 if matching_entries:
254 for entry in matching_entries:
255 if entry.role != self.perm:
256 entry.role = self.perm
257 change_count += 1
258 else:
259 self._AddEntry(current_acl, entry_class)
260 change_count = 1
262 logger.debug('New Acl:\n%s', str(current_acl))
263 return change_count
266 class AclDel(object):
267 """Represents a logical change from an access control list."""
268 scope_regexes = {
269 r'All(Users)?$': 'AllUsers',
270 r'AllAuth(enticatedUsers)?$': 'AllAuthenticatedUsers',
273 def __init__(self, identifier):
274 self.raw_descriptor = '-d {0}'.format(identifier)
275 self.identifier = identifier
276 for regex, scope in self.scope_regexes.items():
277 if re.match(regex, self.identifier, re.IGNORECASE):
278 self.identifier = scope
279 self.scope_type = 'Any'
280 self.perm = 'NONE'
282 def _YieldMatchingEntries(self, current_acl):
283 """Generator that yields entries that match the change descriptor.
285 Args:
286 current_acl: An instance of apitools_messages.BucketAccessControls or
287 ObjectAccessControls which will be searched for matching
288 entries.
290 Yields:
291 An apitools_messages.BucketAccessControl or ObjectAccessControl.
293 for entry in current_acl:
294 if entry.entityId and self.identifier == entry.entityId:
295 yield entry
296 elif entry.email and self.identifier == entry.email:
297 yield entry
298 elif entry.domain and self.identifier == entry.domain:
299 yield entry
300 elif entry.projectTeam:
301 project_team = entry.projectTeam
302 acl_label = project_team.team + '-' + project_team.projectNumber
303 if acl_label == self.identifier:
304 yield entry
305 elif entry.entity.lower() == 'allusers' and self.identifier == 'AllUsers':
306 yield entry
307 elif (entry.entity.lower() == 'allauthenticatedusers' and
308 self.identifier == 'AllAuthenticatedUsers'):
309 yield entry
311 def Execute(self, storage_url, current_acl, command_name, logger):
312 logger.debug(
313 'Executing %s %s on %s', command_name, self.raw_descriptor, storage_url)
314 matching_entries = list(self._YieldMatchingEntries(current_acl))
315 for entry in matching_entries:
316 current_acl.remove(entry)
317 logger.debug('New Acl:\n%s', str(current_acl))
318 return len(matching_entries)