commit 3436eeb0b0579f6cde5369ff86805fb0377f7291
Author: Antony Chazapis <chazapis@gmail.com>
Date:   Tue Jun 21 17:00:45 2011 +0300

    Initial permissions support.
    
    * Use 'X-Object-Sharing' header to get/set permissions.
    * Replaces 'X-Object-Public' header (now obsolete).
    * No permission enforcement yet.
    
    Refs #449
    Refs #595

diff --git a/pithos/api/functions.py b/pithos/api/functions.py
index 47d519d..697cd7d 100644
--- a/pithos/api/functions.py
+++ b/pithos/api/functions.py
@@ -44,10 +44,10 @@ from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Ite
     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
 
 
@@ -348,9 +348,12 @@ def object_list(request, v_account, v_container):
         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':
@@ -370,11 +373,14 @@ def object_meta(request, v_account, v_container, v_object):
     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)
@@ -398,11 +404,14 @@ def object_read(request, v_account, v_container, v_object):
         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.
@@ -485,6 +494,7 @@ def object_write(request, v_account, v_container, v_object):
     # Error Response Codes: serviceUnavailable (503),
     #                       unprocessableEntity (422),
     #                       lengthRequired (411),
+    #                       conflict (409),
     #                       itemNotFound (404),
     #                       unauthorized (401),
     #                       badRequest (400)
@@ -510,6 +520,7 @@ def object_write(request, v_account, v_container, v_object):
         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)
@@ -534,9 +545,13 @@ def object_write(request, v_account, v_container, v_object):
         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']
@@ -582,11 +597,13 @@ def object_move(request, v_account, v_container, v_object):
 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.
@@ -607,6 +624,19 @@ def object_update(request, v_account, v_container, v_object):
         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)
@@ -665,7 +695,11 @@ def object_update(request, v_account, v_container, v_object):
         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
diff --git a/pithos/api/util.py b/pithos/api/util.py
index 55308f5..fd683f1 100644
--- a/pithos/api/util.py
+++ b/pithos/api/util.py
@@ -129,8 +129,6 @@ def get_object_meta(request):
         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):
@@ -145,7 +143,7 @@ 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:
@@ -174,6 +172,20 @@ def update_manifest_meta(request, v_account, meta):
         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."""
     
@@ -221,6 +233,7 @@ def copy_or_move_object(request, v_account, src_container, src_name, dest_contai
     """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)
@@ -234,12 +247,16 @@ def copy_or_move_object(request, v_account, src_container, src_name, dest_contai
     
     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)
@@ -341,6 +358,36 @@ def get_content_range(request):
         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."""
     
diff --git a/pithos/backends/base.py b/pithos/backends/base.py
index a6e21a6..ed86620 100644
--- a/pithos/backends/base.py
+++ b/pithos/backends/base.py
@@ -182,6 +182,36 @@ class BaseBackend(object):
         """
         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.
         
@@ -191,37 +221,50 @@ class BaseBackend(object):
         """
         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
     
diff --git a/pithos/backends/simple.py b/pithos/backends/simple.py
index edaa22d..cd1fd00 100644
--- a/pithos/backends/simple.py
+++ b/pithos/backends/simple.py
@@ -79,6 +79,9 @@ class SimpleBackend(BaseBackend):
         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):
@@ -227,6 +230,21 @@ class SimpleBackend(BaseBackend):
         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."""
         
@@ -237,12 +255,14 @@ class SimpleBackend(BaseBackend):
         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))
@@ -253,12 +273,15 @@ class SimpleBackend(BaseBackend):
         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]
@@ -266,17 +289,22 @@ class SimpleBackend(BaseBackend):
             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):
@@ -285,6 +313,9 @@ class SimpleBackend(BaseBackend):
         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."""
@@ -449,6 +480,61 @@ class SimpleBackend(BaseBackend):
             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:
@@ -502,4 +588,6 @@ class SimpleBackend(BaseBackend):
         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()
diff --git a/pithos/public/functions.py b/pithos/public/functions.py
index 983612a..6a08139 100644
--- a/pithos/public/functions.py
+++ b/pithos/public/functions.py
@@ -65,10 +65,11 @@ def object_meta(request, v_account, v_container, v_object):
     
     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)
     
@@ -89,10 +90,11 @@ def object_read(request, v_account, v_container, v_object):
     
     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)
     
