Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Added support for some ACL operations in S3. The canned ACL's can be …
Browse files Browse the repository at this point in the history
…set on buckets and keys using the set_acl method. In addition, the get_acl method parses the AccessControlPolicy response sent by S3 and creates corresponding Python objects to represent the ACL, Grants, Grantees, etc. At some point in the future we may support the ability to set arbitrary ACL's but not yet.

Fixes Issue-12 and incorporates code contributed by benjaminlyu (thanks!).
  • Loading branch information
Mitch.Garnaat authored and Mitch.Garnaat committed Nov 3, 2006
1 parent e44c388 commit 9e98334
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 14 deletions.
81 changes: 81 additions & 0 deletions boto/acl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright (c) 2006 Mitch Garnaat http://garnaat.org/
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish, dis-
# tribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the fol-
# lowing conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

CannedACLStrings = ['private', 'public-read',
'public-read-write', 'authenticated-read']

class Policy:

def __init__(self, parent=None, xml_attrs=None):
self.parent = parent
self.acl = None

def add_acl(self, acl):
self.acl = acl

# This allows the XMLHandler to set the attributes as they are named
# in the XML response but have the capitalized names converted to
# more conventional looking python variables names automatically
def __setattr__(self, key, value):
if key == 'AccessControlPolicy':
pass
else:
self.__dict__[key] = value

class ACL:

def __init__(self, policy=None, xml_attrs=None):
if policy:
policy.add_acl(self)
self.grants = []

def add_grant(self, grant):
self.grants.append(grant)

# This allows the XMLHandler to set the attributes as they are named
# in the XML response but have the capitalized names converted to
# more conventional looking python variables names automatically
def __setattr__(self, key, value):
if key == 'AccessControlList':
pass
else:
self.__dict__[key] = value

class Grant:

def __init__(self, acl=None, xml_attrs=None):
if acl:
acl.add_grant(self)

# This allows the XMLHandler to set the attributes as they are named
# in the XML response but have the capitalized names converted to
# more conventional looking python variables names automatically
def __setattr__(self, key, value):
if key == 'Permission':
self.__dict__['permission'] = value
elif key == 'owner':
self.__dict__['grantee'] = value
elif key == 'Grant':
pass
else:
self.__dict__[key] = value


51 changes: 46 additions & 5 deletions boto/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@
# IN THE SOFTWARE.

from boto import handler
from boto.owner import Owner
from boto.acl import Policy, ACL, Grant, CannedACLStrings
from boto.user import User
from boto.key import Key
from boto.exception import S3ResponseError
import xml.sax
import urllib

class Bucket:

def __init__(self, connection=None, name=None, debug=None):
def __init__(self, connection=None, name=None, debug=None, xml_attrs=None):
self.name = name
self.connection = connection
self.debug = debug
Expand Down Expand Up @@ -81,17 +82,57 @@ def get_all_keys(self, headers=None, **params):
response = self.connection.make_request('GET', path, headers)
body = response.read()
if response.status == 200:
h = handler.XmlHandler(self, {'Owner': Owner, 'Contents': Key})
h = handler.XmlHandler(self, {'Owner': User, 'Contents': Key})
xml.sax.parseString(body, h)
return h.rs
else:
raise S3ResponseError(response.status, response.reason)

def delete_key(self, key):
path = '/%s/%s' % (self.name, key.key)
def delete_key(self, key_name):
# for backward compatibility, previous version expected a Key object
if isinstance(key_name, Key):
key_name = key_name.key
path = '/%s/%s' % (self.name, key_name)
response = self.connection.make_request('DELETE', path)
body = response.read()
if response.status != 204:
raise S3ResponseError(response.status, response.reason)

def set_acl(self, acl_str, key_name=None):
# just in case user passes a Key object rather than key name
if isinstance(key_name, Key):
key_name = key_name.key
assert acl_str in CannedACLStrings
if key_name:
path = '/%s/%s?acl' % (self.name, key_name)
else:
path = '/%s?acl' % self.name
headers = {'x-amz-acl': acl_str}
response = self.connection.make_request('PUT', path, headers)
body = response.read()
if response.status != 200:
raise S3ResponseError(response.status, response.reason)

def get_acl(self, key_name=None):
# just in case user passes a Key object rather than key name
if isinstance(key_name, Key):
key_name = key_name.key
if key_name:
path = '/%s/%s?acl' % (self.name, key_name)
else:
path = '/%s?acl' % self.name
response = self.connection.make_request('GET', path)
body = response.read()
if response.status == 200:
h = handler.XmlHandler(self, {'AccessControlPolicy' : Policy,
'AccessControlList' : ACL,
'Grant': Grant,
'Grantee': User,
'Owner' : User})
xml.sax.parseString(body, h)
return h.rs[0]
else:
raise S3ResponseError(response.status, response.reason)



4 changes: 2 additions & 2 deletions boto/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
from boto import handler
from boto.queue import Queue
from boto.bucket import Bucket
from boto.owner import Owner
from boto.user import User

PORTS_BY_SECURITY = { True: 443, False: 80 }
METADATA_PREFIX = 'x-amz-meta-'
Expand Down Expand Up @@ -271,7 +271,7 @@ def get_all_buckets(self):
body = response.read()
if response.status > 300:
raise S3ResponseError(response.status, response.reason)
# h = handler.XmlHandler(self, {'Owner': Owner,
# h = handler.XmlHandler(self, {'Owner': User,
# 'Bucket': Bucket})
# ignoring Owner for now
h = handler.XmlHandler(self, {'Bucket': Bucket})
Expand Down
2 changes: 1 addition & 1 deletion boto/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self, parent, elements):

def startElement(self, name, attrs):
if name in self.elements.keys():
node = self.elements[name](self.nodes[-1])
node = self.elements[name](self.nodes[-1], xml_attrs=attrs)
self.nodes.append(node)

def endElement(self, name):
Expand Down
11 changes: 10 additions & 1 deletion boto/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

class Key:

def __init__(self, bucket=None):
def __init__(self, bucket=None, xml_attrs=None):
self.bucket = bucket
self.content_type = 'application/octet-stream'
self.filename = None
Expand Down Expand Up @@ -142,3 +142,12 @@ def get_contents_as_string(self):
fp = StringIO.StringIO()
self.get_contents_to_file(fp)
return fp.getvalue()

# convenience methods for setting/getting ACL
def set_acl(self, acl_str):
if self.bucket != None:
self.bucket.set_acl(acl_str, self.key)

def get_acl(self):
if self.bucket != None:
return self.bucket.get_acl(self.key)
2 changes: 1 addition & 1 deletion boto/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

class Message:

def __init__(self, queue=None, body=''):
def __init__(self, queue=None, body='', xml_attrs=None):
self.queue = queue
self.set_body(body)
self.id = None
Expand Down
3 changes: 2 additions & 1 deletion boto/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@

class Queue:

def __init__(self, connection=None, url=None, message_class=Message):
def __init__(self, connection=None, url=None,
message_class=Message, xml_attrs=None):
self.connection = connection
self.url = url
self.message_class = message_class
Expand Down
13 changes: 10 additions & 3 deletions boto/owner.py → boto/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

class Owner:
def __init__(self, parent=None, id='', display_name=''):
parent.owner = self
class User:
def __init__(self, parent=None, id='', display_name='', xml_attrs=None):
if parent:
parent.owner = self
self.type = None
if xml_attrs:
if xml_attrs.has_key('xsi:type'):
self.type = xml_attrs['xsi:type']
self.id = id
self.display_name = display_name

Expand All @@ -33,6 +38,8 @@ def __setattr__(self, key, value):
self.__dict__['display_name'] = value
elif key == 'ID':
self.__dict__['id'] = value
elif key == 'URI':
self.__dict__['uri'] = value
else:
self.__dict__[key] = value

15 changes: 15 additions & 0 deletions tests/test_s3connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ def test_1_basic(self):
assert isinstance(k, Key)
k = bucket.lookup('notthere')
assert k == None
# try some acl stuff
bucket.set_acl('public-read')
policy = bucket.get_acl()
assert len(policy.acl.grants) == 2
bucket.set_acl('private')
policy = bucket.get_acl()
assert len(policy.acl.grants) == 1
k = bucket.lookup('foo/bar')
k.set_acl('public-read')
policy = k.get_acl()
assert len(policy.acl.grants) == 2
k.set_acl('private')
policy = k.get_acl()
assert len(policy.acl.grants) == 1
# now delete all keys in bucket
for k in all:
bucket.delete_key(k)
# now delete bucket
Expand Down

0 comments on commit 9e98334

Please sign in to comment.