1# Copyright 2010 Google Inc. 2# 3# Permission is hereby granted, free of charge, to any person obtaining a 4# copy of this software and associated documentation files (the 5# "Software"), to deal in the Software without restriction, including 6# without limitation the rights to use, copy, modify, merge, publish, dis- 7# tribute, sublicense, and/or sell copies of the Software, and to permit 8# persons to whom the Software is furnished to do so, subject to the fol- 9# lowing conditions: 10# 11# The above copyright notice and this permission notice shall be included 12# in all copies or substantial portions of the Software. 13# 14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- 16# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 17# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20# IN THE SOFTWARE. 21 22from boto.gs.user import User 23from boto.exception import InvalidAclError 24 25ACCESS_CONTROL_LIST = 'AccessControlList' 26ALL_AUTHENTICATED_USERS = 'AllAuthenticatedUsers' 27ALL_USERS = 'AllUsers' 28DISPLAY_NAME = 'DisplayName' 29DOMAIN = 'Domain' 30EMAIL_ADDRESS = 'EmailAddress' 31ENTRY = 'Entry' 32ENTRIES = 'Entries' 33GROUP_BY_DOMAIN = 'GroupByDomain' 34GROUP_BY_EMAIL = 'GroupByEmail' 35GROUP_BY_ID = 'GroupById' 36ID = 'ID' 37NAME = 'Name' 38OWNER = 'Owner' 39PERMISSION = 'Permission' 40SCOPE = 'Scope' 41TYPE = 'type' 42USER_BY_EMAIL = 'UserByEmail' 43USER_BY_ID = 'UserById' 44 45 46CannedACLStrings = ['private', 'public-read', 'project-private', 47 'public-read-write', 'authenticated-read', 48 'bucket-owner-read', 'bucket-owner-full-control'] 49"""A list of Google Cloud Storage predefined (canned) ACL strings.""" 50 51SupportedPermissions = ['READ', 'WRITE', 'FULL_CONTROL'] 52"""A list of supported ACL permissions.""" 53 54 55class ACL(object): 56 57 def __init__(self, parent=None): 58 self.parent = parent 59 self.entries = Entries(self) 60 61 @property 62 def acl(self): 63 return self 64 65 def __repr__(self): 66 # Owner is optional in GS ACLs. 67 if hasattr(self, 'owner'): 68 entries_repr = ['Owner:%s' % self.owner.__repr__()] 69 else: 70 entries_repr = [''] 71 acl_entries = self.entries 72 if acl_entries: 73 for e in acl_entries.entry_list: 74 entries_repr.append(e.__repr__()) 75 return '<%s>' % ', '.join(entries_repr) 76 77 # Method with same signature as boto.s3.acl.ACL.add_email_grant(), to allow 78 # polymorphic treatment at application layer. 79 def add_email_grant(self, permission, email_address): 80 entry = Entry(type=USER_BY_EMAIL, email_address=email_address, 81 permission=permission) 82 self.entries.entry_list.append(entry) 83 84 # Method with same signature as boto.s3.acl.ACL.add_user_grant(), to allow 85 # polymorphic treatment at application layer. 86 def add_user_grant(self, permission, user_id): 87 entry = Entry(permission=permission, type=USER_BY_ID, id=user_id) 88 self.entries.entry_list.append(entry) 89 90 def add_group_email_grant(self, permission, email_address): 91 entry = Entry(type=GROUP_BY_EMAIL, email_address=email_address, 92 permission=permission) 93 self.entries.entry_list.append(entry) 94 95 def add_group_grant(self, permission, group_id): 96 entry = Entry(type=GROUP_BY_ID, id=group_id, permission=permission) 97 self.entries.entry_list.append(entry) 98 99 def startElement(self, name, attrs, connection): 100 if name.lower() == OWNER.lower(): 101 self.owner = User(self) 102 return self.owner 103 elif name.lower() == ENTRIES.lower(): 104 self.entries = Entries(self) 105 return self.entries 106 else: 107 return None 108 109 def endElement(self, name, value, connection): 110 if name.lower() == OWNER.lower(): 111 pass 112 elif name.lower() == ENTRIES.lower(): 113 pass 114 else: 115 setattr(self, name, value) 116 117 def to_xml(self): 118 s = '<%s>' % ACCESS_CONTROL_LIST 119 # Owner is optional in GS ACLs. 120 if hasattr(self, 'owner'): 121 s += self.owner.to_xml() 122 acl_entries = self.entries 123 if acl_entries: 124 s += acl_entries.to_xml() 125 s += '</%s>' % ACCESS_CONTROL_LIST 126 return s 127 128 129class Entries(object): 130 131 def __init__(self, parent=None): 132 self.parent = parent 133 # Entries is the class that represents the same-named XML 134 # element. entry_list is the list within this class that holds the data. 135 self.entry_list = [] 136 137 def __repr__(self): 138 entries_repr = [] 139 for e in self.entry_list: 140 entries_repr.append(e.__repr__()) 141 return '<Entries: %s>' % ', '.join(entries_repr) 142 143 def startElement(self, name, attrs, connection): 144 if name.lower() == ENTRY.lower(): 145 entry = Entry(self) 146 self.entry_list.append(entry) 147 return entry 148 else: 149 return None 150 151 def endElement(self, name, value, connection): 152 if name.lower() == ENTRY.lower(): 153 pass 154 else: 155 setattr(self, name, value) 156 157 def to_xml(self): 158 if not self.entry_list: 159 return '' 160 s = '<%s>' % ENTRIES 161 for entry in self.entry_list: 162 s += entry.to_xml() 163 s += '</%s>' % ENTRIES 164 return s 165 166 167# Class that represents a single (Scope, Permission) entry in an ACL. 168class Entry(object): 169 170 def __init__(self, scope=None, type=None, id=None, name=None, 171 email_address=None, domain=None, permission=None): 172 if not scope: 173 scope = Scope(self, type, id, name, email_address, domain) 174 self.scope = scope 175 self.permission = permission 176 177 def __repr__(self): 178 return '<%s: %s>' % (self.scope.__repr__(), self.permission.__repr__()) 179 180 def startElement(self, name, attrs, connection): 181 if name.lower() == SCOPE.lower(): 182 # The following if statement used to look like this: 183 # if not TYPE in attrs: 184 # which caused problems because older versions of the 185 # AttributesImpl class in the xml.sax library neglected to include 186 # a __contains__() method (which Python calls to implement the 187 # 'in' operator). So when you use the in operator, like the if 188 # statement above, Python invokes the __getiter__() method with 189 # index 0, which raises an exception. More recent versions of 190 # xml.sax include the __contains__() method, rendering the in 191 # operator functional. The work-around here is to formulate the 192 # if statement as below, which is the legal way to query 193 # AttributesImpl for containment (and is also how the added 194 # __contains__() method works). At one time gsutil disallowed 195 # xmlplus-based parsers, until this more specific problem was 196 # determined. 197 if TYPE not in attrs: 198 raise InvalidAclError('Missing "%s" in "%s" part of ACL' % 199 (TYPE, SCOPE)) 200 self.scope = Scope(self, attrs[TYPE]) 201 return self.scope 202 elif name.lower() == PERMISSION.lower(): 203 pass 204 else: 205 return None 206 207 def endElement(self, name, value, connection): 208 if name.lower() == SCOPE.lower(): 209 pass 210 elif name.lower() == PERMISSION.lower(): 211 value = value.strip() 212 if not value in SupportedPermissions: 213 raise InvalidAclError('Invalid Permission "%s"' % value) 214 self.permission = value 215 else: 216 setattr(self, name, value) 217 218 def to_xml(self): 219 s = '<%s>' % ENTRY 220 s += self.scope.to_xml() 221 s += '<%s>%s</%s>' % (PERMISSION, self.permission, PERMISSION) 222 s += '</%s>' % ENTRY 223 return s 224 225 226class Scope(object): 227 228 # Map from Scope type.lower() to lower-cased list of allowed sub-elems. 229 ALLOWED_SCOPE_TYPE_SUB_ELEMS = { 230 ALL_AUTHENTICATED_USERS.lower() : [], 231 ALL_USERS.lower() : [], 232 GROUP_BY_DOMAIN.lower() : [DOMAIN.lower()], 233 GROUP_BY_EMAIL.lower() : [ 234 DISPLAY_NAME.lower(), EMAIL_ADDRESS.lower(), NAME.lower()], 235 GROUP_BY_ID.lower() : [DISPLAY_NAME.lower(), ID.lower(), NAME.lower()], 236 USER_BY_EMAIL.lower() : [ 237 DISPLAY_NAME.lower(), EMAIL_ADDRESS.lower(), NAME.lower()], 238 USER_BY_ID.lower() : [DISPLAY_NAME.lower(), ID.lower(), NAME.lower()] 239 } 240 241 def __init__(self, parent, type=None, id=None, name=None, 242 email_address=None, domain=None): 243 self.parent = parent 244 self.type = type 245 self.name = name 246 self.id = id 247 self.domain = domain 248 self.email_address = email_address 249 if self.type.lower() not in self.ALLOWED_SCOPE_TYPE_SUB_ELEMS: 250 raise InvalidAclError('Invalid %s %s "%s" ' % 251 (SCOPE, TYPE, self.type)) 252 253 def __repr__(self): 254 named_entity = None 255 if self.id: 256 named_entity = self.id 257 elif self.email_address: 258 named_entity = self.email_address 259 elif self.domain: 260 named_entity = self.domain 261 if named_entity: 262 return '<%s: %s>' % (self.type, named_entity) 263 else: 264 return '<%s>' % self.type 265 266 def startElement(self, name, attrs, connection): 267 if (not name.lower() in 268 self.ALLOWED_SCOPE_TYPE_SUB_ELEMS[self.type.lower()]): 269 raise InvalidAclError('Element "%s" not allowed in %s %s "%s" ' % 270 (name, SCOPE, TYPE, self.type)) 271 return None 272 273 def endElement(self, name, value, connection): 274 value = value.strip() 275 if name.lower() == DOMAIN.lower(): 276 self.domain = value 277 elif name.lower() == EMAIL_ADDRESS.lower(): 278 self.email_address = value 279 elif name.lower() == ID.lower(): 280 self.id = value 281 elif name.lower() == NAME.lower(): 282 self.name = value 283 else: 284 setattr(self, name, value) 285 286 def to_xml(self): 287 s = '<%s type="%s">' % (SCOPE, self.type) 288 if (self.type.lower() == ALL_AUTHENTICATED_USERS.lower() 289 or self.type.lower() == ALL_USERS.lower()): 290 pass 291 elif self.type.lower() == GROUP_BY_DOMAIN.lower(): 292 s += '<%s>%s</%s>' % (DOMAIN, self.domain, DOMAIN) 293 elif (self.type.lower() == GROUP_BY_EMAIL.lower() 294 or self.type.lower() == USER_BY_EMAIL.lower()): 295 s += '<%s>%s</%s>' % (EMAIL_ADDRESS, self.email_address, 296 EMAIL_ADDRESS) 297 if self.name: 298 s += '<%s>%s</%s>' % (NAME, self.name, NAME) 299 elif (self.type.lower() == GROUP_BY_ID.lower() 300 or self.type.lower() == USER_BY_ID.lower()): 301 s += '<%s>%s</%s>' % (ID, self.id, ID) 302 if self.name: 303 s += '<%s>%s</%s>' % (NAME, self.name, NAME) 304 else: 305 raise InvalidAclError('Invalid scope type "%s" ', self.type) 306 307 s += '</%s>' % SCOPE 308 return s 309