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
21 from gslib
.exception
import CommandException
22 from gslib
.third_party
.storage_apitools
import storage_v1_messages
as apitools_messages
25 class ChangeType(object):
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
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-',
46 group_entity_prefix
= 'group-'
47 user_entity_prefix
= 'user-'
48 domain_entity_prefix
= 'domain-'
49 project_entity_prefix
= 'project-'
51 permission_shorthand_mapping
= {
58 'FULL_CONTROL': 'OWNER'
61 def __init__(self
, acl_change_descriptor
, scope_type
):
62 """Creates an AclChange object.
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.
72 self
.raw_descriptor
= acl_change_descriptor
73 self
._Parse
(acl_change_descriptor
, scope_type
)
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
):
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
):
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
]
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
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
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.
159 current_acl: A list of apitools_messages.BucketAccessControls or
160 ObjectAccessControls which will be searched for matching
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
):
170 elif (self
.scope_type
in ('UserByEmail', 'GroupByEmail')
171 and entry
.email
and self
.identifier
== entry
.email
):
173 elif (self
.scope_type
== 'GroupByDomain' and
174 entry
.domain
and self
.identifier
== entry
.domain
):
176 elif (self
.scope_type
== 'Project' and
177 entry
.domain
and self
.identifier
== entry
.project
):
179 elif (self
.scope_type
== 'AllUsers' and
180 entry
.entity
.lower() == self
.public_entity_all_users
.lower()):
182 elif (self
.scope_type
== 'AllAuthenticatedUsers' and
183 entry
.entity
.lower() == self
.public_entity_all_auth_users
.lower()):
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
,
209 elif self
.scope_type
== 'AllUsers':
210 entry
= entry_class(entity
=self
.public_entity_all_users
, role
=self
.perm
)
212 raise CommandException('Add entry to ACL got unexpected scope type %s.' %
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.
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.
235 The number of changes that were made.
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():
243 'Skipping %s on %s, as WRITER does not apply to objects',
244 self
.raw_descriptor
, storage_url
)
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
))
254 for entry
in matching_entries
:
255 if entry
.role
!= self
.perm
:
256 entry
.role
= self
.perm
259 self
._AddEntry
(current_acl
, entry_class
)
262 logger
.debug('New Acl:\n%s', str(current_acl
))
266 class AclDel(object):
267 """Represents a logical change from an access control list."""
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'
282 def _YieldMatchingEntries(self
, current_acl
):
283 """Generator that yields entries that match the change descriptor.
286 current_acl: An instance of apitools_messages.BucketAccessControls or
287 ObjectAccessControls which will be searched for matching
291 An apitools_messages.BucketAccessControl or ObjectAccessControl.
293 for entry
in current_acl
:
294 if entry
.entityId
and self
.identifier
== entry
.entityId
:
296 elif entry
.email
and self
.identifier
== entry
.email
:
298 elif entry
.domain
and self
.identifier
== entry
.domain
:
300 elif entry
.projectTeam
:
301 project_team
= entry
.projectTeam
302 acl_label
= project_team
.team
+ '-' + project_team
.projectNumber
303 if acl_label
== self
.identifier
:
305 elif entry
.entity
.lower() == 'allusers' and self
.identifier
== 'AllUsers':
307 elif (entry
.entity
.lower() == 'allauthenticatedusers' and
308 self
.identifier
== 'AllAuthenticatedUsers'):
311 def Execute(self
, storage_url
, current_acl
, command_name
, logger
):
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
)