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 17from __future__ import absolute_import 18 19import re 20 21from gslib.exception import CommandException 22from gslib.third_party.storage_apitools import storage_v1_messages as apitools_messages 23 24 25class ChangeType(object): 26 USER = 'User' 27 GROUP = 'Group' 28 PROJECT = 'Project' 29 30 31class 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 266class 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) 319