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 |