• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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