OLD | NEW |
| (Empty) |
1 # -*- coding: utf-8 -*- | |
2 # Copyright 2013 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 """Contains helper objects for changing and deleting ACLs.""" | |
16 | |
17 from __future__ import absolute_import | |
18 | |
19 import re | |
20 | |
21 from gslib.exception import CommandException | |
22 from gslib.third_party.storage_apitools import storage_v1_messages as apitools_m
essages | |
23 | |
24 | |
25 class ChangeType(object): | |
26 USER = 'User' | |
27 GROUP = 'Group' | |
28 PROJECT = 'Project' | |
29 | |
30 | |
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) | |
40 | |
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-' | |
50 | |
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' | |
59 } | |
60 | |
61 def __init__(self, acl_change_descriptor, scope_type): | |
62 """Creates an AclChange object. | |
63 | |
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 = '' | |
71 | |
72 self.raw_descriptor = acl_change_descriptor | |
73 self._Parse(acl_change_descriptor, scope_type) | |
74 self._Validate() | |
75 | |
76 def __str__(self): | |
77 return 'AclChange<{0}|{1}|{2}>'.format( | |
78 self.scope_type, self.perm, self.identifier) | |
79 | |
80 def _Parse(self, change_descriptor, scope_type): | |
81 """Parses an ACL Change descriptor.""" | |
82 | |
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)\-.+$', | |
91 } | |
92 for type_string, regex in re_map.items(): | |
93 if re.match(regex, text, re.IGNORECASE): | |
94 return type_string | |
95 | |
96 if change_descriptor.count(':') != 1: | |
97 raise CommandException('{0} is an invalid change description.' | |
98 .format(change_descriptor)) | |
99 | |
100 scope_string, perm_token = change_descriptor.split(':') | |
101 | |
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 | |
107 | |
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 | |
128 | |
129 def _Validate(self): | |
130 """Validates a parsed AclChange object.""" | |
131 | |
132 def _ThrowError(msg): | |
133 raise CommandException('{0} is not a valid ACL change\n{1}' | |
134 .format(self.raw_descriptor, msg)) | |
135 | |
136 if self.scope_type not in self.scope_types: | |
137 _ThrowError('{0} is not a valid scope type'.format(self.scope_type)) | |
138 | |
139 if self.scope_type in self.public_scopes and self.identifier: | |
140 _ThrowError('{0} requires no arguments'.format(self.scope_type)) | |
141 | |
142 if self.scope_type in self.id_scopes and not self.identifier: | |
143 _ThrowError('{0} requires an id'.format(self.scope_type)) | |
144 | |
145 if self.scope_type in self.email_scopes and not self.identifier: | |
146 _ThrowError('{0} requires an email address'.format(self.scope_type)) | |
147 | |
148 if self.scope_type in self.domain_scopes and not self.identifier: | |
149 _ThrowError('{0} requires domain'.format(self.scope_type)) | |
150 | |
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)) | |
154 | |
155 def _YieldMatchingEntries(self, current_acl): | |
156 """Generator that yields entries that match the change descriptor. | |
157 | |
158 Args: | |
159 current_acl: A list of apitools_messages.BucketAccessControls or | |
160 ObjectAccessControls which will be searched for matching | |
161 entries. | |
162 | |
163 Yields: | |
164 An apitools_messages.BucketAccessControl or ObjectAccessControl. | |
165 """ | |
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 | |
185 | |
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) | |
215 | |
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__ | |
223 | |
224 def Execute(self, storage_url, current_acl, command_name, logger): | |
225 """Executes the described change on an ACL. | |
226 | |
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. | |
233 | |
234 Returns: | |
235 The number of changes that were made. | |
236 """ | |
237 logger.debug( | |
238 'Executing %s %s on %s', command_name, self.raw_descriptor, storage_url) | |
239 | |
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') | |
249 | |
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 | |
261 | |
262 logger.debug('New Acl:\n%s', str(current_acl)) | |
263 return change_count | |
264 | |
265 | |
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', | |
271 } | |
272 | |
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' | |
281 | |
282 def _YieldMatchingEntries(self, current_acl): | |
283 """Generator that yields entries that match the change descriptor. | |
284 | |
285 Args: | |
286 current_acl: An instance of apitools_messages.BucketAccessControls or | |
287 ObjectAccessControls which will be searched for matching | |
288 entries. | |
289 | |
290 Yields: | |
291 An apitools_messages.BucketAccessControl or ObjectAccessControl. | |
292 """ | |
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 | |
310 | |
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) | |
OLD | NEW |