LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity)
from pithos.api.util import (format_meta_key, printable_meta_dict, get_account_meta,
put_account_meta, get_container_meta, put_container_meta, get_object_meta, put_object_meta,
- update_manifest_meta, validate_modification_preconditions, validate_matching_preconditions,
- split_container_object_string, copy_or_move_object, get_int_parameter, get_content_length,
- get_content_range, raw_input_socket, socket_read_iterator, object_data_response,
- put_object_block, hashmap_hash, api_method)
+ update_manifest_meta, format_permissions, validate_modification_preconditions,
+ validate_matching_preconditions, split_container_object_string, copy_or_move_object,
+ get_int_parameter, get_content_length, get_content_range, get_sharing, raw_input_socket,
+ socket_read_iterator, object_data_response, put_object_block, hashmap_hash, api_method)
from pithos.backends import backend
else:
try:
meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
- object_meta.append(printable_meta_dict(meta))
+ permissions = backend.get_object_permissions(request.user, v_account, v_container, x[0])
except NameError:
pass
+ if permissions:
+ meta['X-Object-Sharing'] = format_permissions(permissions)
+ object_meta.append(printable_meta_dict(meta))
if request.serialization == 'xml':
data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
elif request.serialization == 'json':
version = get_int_parameter(request, 'version')
try:
meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
+ permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
except NameError:
raise ItemNotFound('Object does not exist')
except IndexError:
raise ItemNotFound('Version does not exist')
+ if permissions:
+ meta['X-Object-Sharing'] = format_permissions(permissions)
update_manifest_meta(request, v_account, meta)
response = HttpResponse(status=200)
version_list = True
try:
meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
+ permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
except NameError:
raise ItemNotFound('Object does not exist')
except IndexError:
raise ItemNotFound('Version does not exist')
+ if permissions:
+ meta['X-Object-Sharing'] = format_permissions(permissions)
update_manifest_meta(request, v_account, meta)
# Evaluate conditions.
# Error Response Codes: serviceUnavailable (503),
# unprocessableEntity (422),
# lengthRequired (411),
+ # conflict (409),
# itemNotFound (404),
# unauthorized (401),
# badRequest (400)
return HttpResponse(status=201)
meta = get_object_meta(request)
+ permissions = get_sharing(request)
content_length = -1
if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
content_length = get_content_length(request)
raise UnprocessableEntity('Object ETag does not match')
try:
- backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True)
+ backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True, permissions)
except NameError:
raise ItemNotFound('Container does not exist')
+ except ValueError:
+ raise BadRequest('Invalid sharing header')
+ except AttributeError:
+ raise Conflict('Sharing already set above or below this path in the hierarchy')
response = HttpResponse(status=201)
response['ETag'] = meta['hash']
def object_update(request, v_account, v_container, v_object):
# Normal Response Codes: 202, 204
# Error Response Codes: serviceUnavailable (503),
+ # conflict (409),
# itemNotFound (404),
# unauthorized (401),
# badRequest (400)
meta = get_object_meta(request)
+ permissions = get_sharing(request)
content_type = meta.get('Content-Type')
if content_type:
del(meta['Content-Type']) # Do not allow changing the Content-Type.
except NameError:
raise ItemNotFound('Object does not exist')
+ # Handle permission changes.
+ if permissions:
+ try:
+ backend.update_object_permissions(request.user, v_account, v_container, v_object, permissions)
+ except NameError:
+ raise ItemNotFound('Object does not exist')
+ except ValueError:
+ raise BadRequest('Invalid sharing header')
+ except AttributeError:
+ raise Conflict('Sharing already set above or below this path in the hierarchy')
+
+ # TODO: Merge above functions with updating the hashmap if there is data in the request.
+
# A Content-Type or Content-Range header may indicate data updates.
if content_type is None:
return HttpResponse(status=202)
backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, False)
except NameError:
raise ItemNotFound('Container does not exist')
-
+ except ValueError:
+ raise BadRequest('Invalid sharing header')
+ except AttributeError:
+ raise Conflict('Sharing already set above or below this path in the hierarchy')
+
response = HttpResponse(status=204)
response['ETag'] = meta['hash']
return response
path = '/v1/%s/%s/%s' %(account, container, name)
response = self.client.head(path)
response.content = response.content
- self.assert_status(response, 204)
+ self.assert_status(response, 200)
return response
def get_object(self, account, container, name, format='', **headers):
#assert src object still exists
r = self.get_object_meta(self.account, self.containers[0],
self.obj['name'])
- self.assertEqual(r.status_code, 204)
+ self.assertEqual(r.status_code, 200)
def test_copy_from_different_container(self):
with AssertInvariant(self.get_object_meta,
#assert src object still exists
r = self.get_object_meta(self.account, self.containers[0],
self.obj['name'])
- self.assertEqual(r.status_code, 204)
+ self.assertEqual(r.status_code, 200)
def test_copy_invalid(self):
#copy from invalid object
meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
if request.META.get('HTTP_X_OBJECT_MANIFEST'):
meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
- if request.META.get('HTTP_X_OBJECT_PUBLIC'):
- meta['X-Object-Public'] = request.META['HTTP_X_OBJECT_PUBLIC']
return meta
def put_object_meta(response, meta, public=False):
response['X-Object-Version-Timestamp'] = meta['version_timestamp']
for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
response[k.encode('utf-8')] = meta[k].encode('utf-8')
- for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Public'):
+ for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Sharing'):
if k in meta:
response[k] = meta[k]
else:
md5.update(hash)
meta['hash'] = md5.hexdigest().lower()
+def format_permissions(permissions):
+ ret = []
+ if 'public' in permissions:
+ ret.append('public')
+ if 'private' in permissions:
+ ret.append('private')
+ r = ','.join(permissions.get('read', []))
+ if r:
+ ret.append('read=' + r)
+ w = ','.join(permissions.get('write', []))
+ if w:
+ ret.append('write=' + w)
+ return '; '.join(ret)
+
def validate_modification_preconditions(request, meta):
"""Check that the modified timestamp conforms with the preconditions set."""
"""Copy or move an object."""
meta = get_object_meta(request)
+ permissions = get_sharing(request)
# Keep previous values of 'Content-Type' (if a new one is absent) and 'hash'.
try:
src_meta = backend.get_object_meta(request.user, v_account, src_container, src_name)
try:
if move:
- backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True)
+ backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, permissions)
else:
src_version = request.META.get('HTTP_X_SOURCE_VERSION')
- backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, src_version)
+ backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, permissions, src_version)
except NameError:
raise ItemNotFound('Container or object does not exist')
+ except ValueError:
+ raise BadRequest('Invalid sharing header')
+ except AttributeError:
+ raise Conflict('Sharing already set above or below this path in the hierarchy')
def get_int_parameter(request, name):
p = request.GET.get(name)
length = upto - offset + 1
return (offset, length, total)
+def get_sharing(request):
+ """Parse an X-Object-Sharing header from the request.
+
+ Raises BadRequest on error.
+ """
+
+ permissions = request.META.get('HTTP_X_OBJECT_SHARING')
+ if permissions is None or permissions == '':
+ return None
+
+ ret = {}
+ for perm in (x.replace(' ','') for x in permissions.split(';')):
+ if perm == 'public':
+ ret['public'] = True
+ continue
+ elif perm == 'private':
+ ret['private'] = True
+ continue
+ elif perm.startswith('read='):
+ ret['read'] = [v.replace(' ','') for v in perm[5:].split(',')]
+ if len(ret['read']) == 0:
+ raise BadRequest('Bad X-Object-Sharing header value')
+ elif perm.startswith('write='):
+ ret['write'] = [v.replace(' ','') for v in perm[6:].split(',')]
+ if len(ret['write']) == 0:
+ raise BadRequest('Bad X-Object-Sharing header value')
+ else:
+ raise BadRequest('Bad X-Object-Sharing header value')
+ return ret
+
def raw_input_socket(request):
"""Return the socket for reading the rest of the request."""
"""
return
+ def get_object_permissions(self, user, account, container, name):
+ """Return a dictionary with the object permissions.
+
+ The keys are:
+ 'public': The object is readable by all and available at a public URL
+ 'private': No permissions set
+ 'read': The object is readable by the users/groups in the list
+ 'write': The object is writable by the users/groups in the list
+
+ Raises:
+ NameError: Container/object does not exist
+ """
+ return {}
+
+ def update_object_permissions(self, user, account, container, name, permissions):
+ """Update the permissions associated with the object.
+
+ Parameters:
+ 'permissions': Dictionary with permissions to update
+
+ Raises:
+ NameError: Container/object does not exist
+ ValueError: Invalid users/groups in permissions
+ AttributeError: Can not set permissions, as this object\
+ is already shared/private by another object higher\
+ in the hierarchy, or setting permissions here will\
+ invalidate other permissions deeper in the hierarchy
+ """
+ return
+
def get_object_hashmap(self, user, account, container, name, version=None):
"""Return the object's size and a list with partial hashes.
"""
return 0, []
- def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False):
+ def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False, permissions={}):
"""Create/update an object with the specified size and partial hashes.
+ Parameters:
+ 'dest_meta': Dictionary with metadata to change
+ 'replace_meta': Replace metadata instead of update
+ 'permissions': Updated object permissions
+
Raises:
NameError: Container does not exist
+ ValueError: Invalid users/groups in permissions
+ AttributeError: Can not set permissions
"""
return
- def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, src_version=None):
+ def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions={}, src_version=None):
"""Copy an object's data and metadata.
Parameters:
- 'dest_meta': Dictionary with metadata to changes from source to destination
+ 'dest_meta': Dictionary with metadata to change from source to destination
'replace_meta': Replace metadata instead of update
- 'src_version': Copy from the version provided.
+ 'permissions': New object permissions
+ 'src_version': Copy from the version provided
Raises:
NameError: Container/object does not exist
IndexError: Version does not exist
+ ValueError: Invalid users/groups in permissions
+ AttributeError: Can not set permissions
"""
return
- def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False):
+ def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions={}):
"""Move an object's data and metadata.
Parameters:
- 'dest_meta': Dictionary with metadata to changes from source to destination
+ 'dest_meta': Dictionary with metadata to change from source to destination
'replace_meta': Replace metadata instead of update
+ 'permissions': New object permissions
Raises:
NameError: Container/object does not exist
+ ValueError: Invalid users/groups in permissions
+ AttributeError: Can not set permissions
"""
return
sql = '''create table if not exists hashmaps (
version_id integer, pos integer, block_id text, primary key (version_id, pos))'''
self.con.execute(sql)
+ sql = '''create table if not exists permissions (
+ name text, read text, write text, primary key (name))'''
+ self.con.execute(sql)
self.con.commit()
def delete_account(self, user, account):
path, version_id, mtime, size = self._get_objectinfo(account, container, name)
self._put_metadata(path, meta, replace)
+ def get_object_permissions(self, user, account, container, name):
+ """Return a dictionary with the object permissions."""
+
+ logger.debug("get_object_permissions: %s %s %s", account, container, name)
+ path = self._get_objectinfo(account, container, name)[0]
+ return self._get_permissions(path)
+
+ def update_object_permissions(self, user, account, container, name, permissions):
+ """Update the permissions associated with the object."""
+
+ logger.debug("update_object_permissions: %s %s %s %s", account, container, name, permissions)
+ path = self._get_objectinfo(account, container, name)[0]
+ r, w = self._check_permissions(path, permissions)
+ self._put_permissions(path, r, w)
+
def get_object_hashmap(self, user, account, container, name, version=None):
"""Return the object's size and a list with partial hashes."""
hashmap = [x[0] for x in c.fetchall()]
return size, hashmap
- def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False):
+ def update_object_hashmap(self, user, account, container, name, size, hashmap, meta={}, replace_meta=False, permissions={}):
"""Create/update an object with the specified size and partial hashes."""
logger.debug("update_object_hashmap: %s %s %s %s %s", account, container, name, size, hashmap)
path = self._get_containerinfo(account, container)[0]
path = os.path.join(path, name)
+ if permissions:
+ r, w = self._check_permissions(path, permissions)
src_version_id, dest_version_id = self._copy_version(path, path, not replace_meta, False)
sql = 'update versions set size = ? where version_id = ?'
self.con.execute(sql, (size, dest_version_id))
for k, v in meta.iteritems():
sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
self.con.execute(sql, (dest_version_id, k, v))
+ if permissions:
+ sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
+ self.con.execute(sql, (path, r, w))
self.con.commit()
- def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, src_version=None):
+ def copy_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions={}, src_version=None):
"""Copy an object's data and metadata."""
- logger.debug("copy_object: %s %s %s %s %s %s %s %s", account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, src_version)
+ logger.debug("copy_object: %s %s %s %s %s %s %s %s %s", account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, src_version)
self._get_containerinfo(account, src_container)
if src_version is None:
src_path = self._get_objectinfo(account, src_container, src_name)[0]
src_path = os.path.join(account, src_container, src_name)
dest_path = self._get_containerinfo(account, dest_container)[0]
dest_path = os.path.join(dest_path, dest_name)
+ if permissions:
+ r, w = self._check_permissions(dest_path, permissions)
src_version_id, dest_version_id = self._copy_version(src_path, dest_path, not replace_meta, True, src_version)
for k, v in dest_meta.iteritems():
sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
self.con.execute(sql, (dest_version_id, k, v))
+ if permissions:
+ sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
+ self.con.execute(sql, (dest_path, r, w))
self.con.commit()
- def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False):
+ def move_object(self, user, account, src_container, src_name, dest_container, dest_name, dest_meta={}, replace_meta=False, permissions={}):
"""Move an object's data and metadata."""
- logger.debug("move_object: %s %s %s %s %s %s %s", account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta)
- self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, None)
+ logger.debug("move_object: %s %s %s %s %s %s %s %s", account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions)
+ self.copy_object(user, account, src_container, src_name, dest_container, dest_name, dest_meta, replace_meta, permissions, None)
self.delete_object(user, account, src_container, src_name)
def delete_object(self, user, account, container, name):
logger.debug("delete_object: %s %s %s", account, container, name)
path, version_id, mtime, size = self._get_objectinfo(account, container, name)
self._put_version(path, 0, 1)
+ sql = 'delete from permissions where name = ?'
+ self.con.execute(sql, (path,))
+ self.con.commit()
def list_versions(self, user, account, container, name):
"""Return a list of all (version, version_timestamp) tuples for an object."""
self.con.execute(sql, (dest_version_id, k, v))
self.con.commit()
+ def _can_read(self, user, path):
+ return True
+
+ def _can_write(self, user, path):
+ return True
+
+ def _check_permissions(self, path, permissions):
+ # Check for existing permissions.
+ sql = '''select name from permissions
+ where name != ? and (name like ? or ? like name || ?)'''
+ c = self.con.execute(sql, (path, path + '%', path, '%'))
+ if c.fetchall() is not None:
+ raise AttributeError('Permissions already set')
+
+ # Format given permissions set.
+ r = permissions.get('read', [])
+ w = permissions.get('write', [])
+ if True in [False or '*' in x or ',' in x for x in r]:
+ raise ValueError('Bad characters in read permissions')
+ if True in [False or '*' in x or ',' in x for x in w]:
+ raise ValueError('Bad characters in write permissions')
+ r = ','.join(r)
+ w = ','.join(w)
+ if 'public' in permissions:
+ r = '*'
+ if 'private' in permissions:
+ r = ''
+ w = ''
+ return r, w
+
+ def _get_permissions(self, path):
+ sql = 'select read, write from permissions where name = ?'
+ c = self.con.execute(sql, (path,))
+ row = c.fetchone()
+ if not row:
+ return {}
+
+ r, w = row
+ if r == '' and w == '':
+ return {'private': True}
+ ret = {}
+ if w != '':
+ ret['write'] = w.split(',')
+ if r != '':
+ if r == '*':
+ ret['public'] = True
+ else:
+ ret['read'] = r.split(',')
+ return ret
+
+ def _put_permissions(self, path, r, w):
+ sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'
+ self.con.execute(sql, (path, r, w))
+ self.con.commit()
+
def _list_objects(self, path, prefix='', delimiter=None, marker=None, limit=10000, virtual=True, keys=[], until=None):
cont_prefix = path + '/'
if keys and len(keys) > 0:
self.con.execute(sql, (path,))
sql = '''delete from versions where name = ?'''
self.con.execute(sql, (path,))
+ sql = '''delete from permissions where name like ?'''
+ self.con.execute(sql, (path + '%',)) # Redundant.
self.con.commit()
try:
meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
+ permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
except NameError:
raise ItemNotFound('Object does not exist')
- if 'X-Object-Public' not in meta:
+ if 'public' not in permissions:
raise ItemNotFound('Object does not exist')
update_manifest_meta(request, v_account, meta)
try:
meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
+ permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
except NameError:
raise ItemNotFound('Object does not exist')
- if 'X-Object-Public' not in meta:
+ if 'public' not in permissions:
raise ItemNotFound('Object does not exist')
update_manifest_meta(request, v_account, meta)