summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVictor Shyba <victor1984@riseup.net>2017-06-26 05:25:55 -0300
committerVictor Shyba <victor1984@riseup.net>2017-07-02 01:40:32 -0300
commitab297c4efe10c70949fac5384a63cbf553ba5da9 (patch)
tree43e0dea081dc38440bd18f90f7910762a1c8fd46
parentb672d210fb34013a7c1b9c663eaa6afecbfacd80 (diff)
[feature] namespace capability to BlobsBackend
Adds an extra parameter called "namespace" on the backend interface and on FileSystemBlobsBackend. This parameter overrides default id partitioning and uses a separate folder for a custom namespace. -- Resolves: #8889
-rw-r--r--src/leap/soledad/server/_blobs.py31
-rw-r--r--src/leap/soledad/server/interfaces.py12
-rw-r--r--testing/tests/blobs/test_fs_backend.py58
3 files changed, 71 insertions, 30 deletions
diff --git a/src/leap/soledad/server/_blobs.py b/src/leap/soledad/server/_blobs.py
index 10678360..f87c3818 100644
--- a/src/leap/soledad/server/_blobs.py
+++ b/src/leap/soledad/server/_blobs.py
@@ -66,16 +66,16 @@ class FilesystemBlobsBackend(object):
os.makedirs(blobs_path)
self.path = blobs_path
- def read_blob(self, user, blob_id, request):
+ def read_blob(self, user, blob_id, request, namespace=''):
logger.info('reading blob: %s - %s' % (user, blob_id))
- path = self._get_path(user, blob_id)
+ path = self._get_path(user, blob_id, namespace)
logger.debug('blob path: %s' % path)
_file = static.File(path, defaultType='application/octet-stream')
return _file.render_GET(request)
@defer.inlineCallbacks
- def write_blob(self, user, blob_id, request):
- path = self._get_path(user, blob_id)
+ def write_blob(self, user, blob_id, request, namespace=''):
+ path = self._get_path(user, blob_id, namespace)
try:
mkdir_p(os.path.split(path)[0])
except OSError:
@@ -95,16 +95,16 @@ class FilesystemBlobsBackend(object):
fbp = FileBodyProducer(request.content)
yield fbp.startProducing(open(path, 'wb'))
- def delete_blob(self, user, blob_id):
- blob_path = self._get_path(user, blob_id)
+ def delete_blob(self, user, blob_id, namespace=''):
+ blob_path = self._get_path(user, blob_id, namespace)
os.unlink(blob_path)
- def get_blob_size(user, blob_id):
+ def get_blob_size(user, blob_id, namespace=''):
raise NotImplementedError
- def list_blobs(self, user, request):
+ def list_blobs(self, user, request, namespace=''):
blob_ids = []
- base_path = self._get_path(user)
+ base_path = self._get_path(user, custom_preffix=namespace)
for _, _, filenames in os.walk(base_path):
blob_ids += filenames
return json.dumps(blob_ids)
@@ -112,8 +112,8 @@ class FilesystemBlobsBackend(object):
def get_total_storage(self, user):
return self._get_disk_usage(self._get_path(user))
- def add_tag_header(self, user, blob_id, request):
- with open(self._get_path(user, blob_id)) as doc_file:
+ def add_tag_header(self, user, blob_id, request, namespace=''):
+ with open(self._get_path(user, blob_id, namespace)) as doc_file:
doc_file.seek(-16, 2)
tag = base64.urlsafe_b64encode(doc_file.read())
request.responseHeaders.setRawHeaders('Tag', [tag])
@@ -140,14 +140,19 @@ class FilesystemBlobsBackend(object):
raise Exception(err)
return desired_path
- def _get_path(self, user, blob_id=False):
+ def _get_path(self, user, blob_id='', custom_preffix=''):
parts = [user]
+ parts += self._get_preffix(blob_id, custom_preffix)
if blob_id:
- parts += [blob_id[0], blob_id[0:3], blob_id[0:6]]
parts += [blob_id]
path = os.path.join(self.path, *parts)
return self._validate_path(path, user, blob_id)
+ def _get_preffix(self, blob_id, custom=''):
+ if custom or not blob_id:
+ return [custom]
+ return [blob_id[0], blob_id[0:3], blob_id[0:6]]
+
class ImproperlyConfiguredException(Exception):
pass
diff --git a/src/leap/soledad/server/interfaces.py b/src/leap/soledad/server/interfaces.py
index 67b04bc3..ccb2ffdc 100644
--- a/src/leap/soledad/server/interfaces.py
+++ b/src/leap/soledad/server/interfaces.py
@@ -25,31 +25,31 @@ class IBlobsBackend(Interface):
An interface for a BlobsBackend.
"""
- def read_blob(user, blob_id, request):
+ def read_blob(user, blob_id, request, namespace=''):
"""
Read blob with a given blob_id, and write it to the passed request.
:returns: a deferred that fires upon finishing.
"""
- def write_blob(user, blob_id, request):
+ def write_blob(user, blob_id, request, namespace=''):
"""
Write blob to the storage, reading it from the passed request.
:returns: a deferred that fires upon finishing.
"""
- def delete_blob(user, blob_id):
+ def delete_blob(user, blob_id, namespace=''):
"""
Delete the given blob_id.
"""
- def get_blob_size(user, blob_id):
+ def get_blob_size(user, blob_id, namespace=''):
"""
Get the size of the given blob id.
"""
- def list_blobs(user, request):
+ def list_blobs(user, request, namespace=''):
"""
Returns a json-encoded list of ids from user's blob.
@@ -62,7 +62,7 @@ class IBlobsBackend(Interface):
unders its namespace.
"""
- def add_tag_header(user, blob_id, request):
+ def add_tag_header(user, blob_id, request, namespace=''):
"""
Adds a header 'Tag' to the passed request object, containing the last
16 bytes of the encoded blob, which according to the spec contains the
diff --git a/testing/tests/blobs/test_fs_backend.py b/testing/tests/blobs/test_fs_backend.py
index 27d4fc61..7fce4cfe 100644
--- a/testing/tests/blobs/test_fs_backend.py
+++ b/testing/tests/blobs/test_fs_backend.py
@@ -52,7 +52,7 @@ class FilesystemBackendTestCase(unittest.TestCase):
backend._get_path = Mock(return_value='path')
backend.read_blob('user', 'blob_id', request)
- backend._get_path.assert_called_once_with('user', 'blob_id')
+ backend._get_path.assert_called_once_with('user', 'blob_id', '')
ctype = 'application/octet-stream'
_blobs.static.File.assert_called_once_with('path', defaultType=ctype)
render_mock.render_GET.assert_called_once_with(request)
@@ -84,12 +84,24 @@ class FilesystemBackendTestCase(unittest.TestCase):
request.setResponseCode.assert_called_once_with(507)
request.write.assert_called_once_with('Quota Exceeded!')
- def test_get_path_partitioning(self):
+ def test_get_path_partitioning_by_default(self):
backend = _blobs.FilesystemBlobsBackend()
backend.path = '/somewhere/'
- path = backend._get_path('user', 'blob_id')
+ path = backend._get_path('user', 'blob_id', '')
self.assertEquals(path, '/somewhere/user/b/blo/blob_i/blob_id')
+ def test_get_path_custom(self):
+ backend = _blobs.FilesystemBlobsBackend()
+ backend.path = '/somewhere/'
+ path = backend._get_path('user', 'blob_id', 'wonderland')
+ self.assertEquals(path, '/somewhere/user/wonderland/blob_id')
+
+ def test_get_path_namespace_traversal_raises(self):
+ backend = _blobs.FilesystemBlobsBackend()
+ backend.path = '/somewhere/'
+ with pytest.raises(Exception):
+ backend._get_path('user', 'blob_id', '..')
+
@pytest.mark.usefixtures("method_tmpdir")
@mock.patch('leap.soledad.server._blobs.os.walk')
def test_list_blobs(self, walk_mock):
@@ -99,27 +111,42 @@ class FilesystemBackendTestCase(unittest.TestCase):
self.assertEquals(result, ['blob_0', 'blob_1'])
@pytest.mark.usefixtures("method_tmpdir")
+ @mock.patch('leap.soledad.server._blobs.os.walk')
+ def test_list_blobs_limited_by_namespace(self, walk_mock):
+ backend, _ = _blobs.FilesystemBlobsBackend(self.tempdir), None
+ walk_mock.return_value = [(_, _, ['blob_0']), (_, _, ['blob_1'])]
+ result = json.loads(backend.list_blobs('user', DummyRequest(['']),
+ namespace='incoming'))
+ self.assertEquals(result, ['blob_0', 'blob_1'])
+ target_dir = os.path.join(self.tempdir, 'user', 'incoming')
+ walk_mock.assert_called_once_with(target_dir)
+
+ @pytest.mark.usefixtures("method_tmpdir")
def test_path_validation_on_read_blob(self):
- blobs_path = self.tempdir
+ blobs_path, request = self.tempdir, DummyRequest([''])
backend = _blobs.FilesystemBlobsBackend(blobs_path)
with pytest.raises(Exception):
- backend.read_blob('..', '..', DummyRequest(['']))
+ backend.read_blob('..', '..', request)
with pytest.raises(Exception):
- backend.read_blob('valid', '../../../', DummyRequest(['']))
+ backend.read_blob('user', '../../../', request)
with pytest.raises(Exception):
- backend.read_blob('../../../', 'valid', DummyRequest(['']))
+ backend.read_blob('../../../', 'blob_id', request)
+ with pytest.raises(Exception):
+ backend.read_blob('user', 'blob_id', request, namespace='..')
@pytest.mark.usefixtures("method_tmpdir")
@defer.inlineCallbacks
def test_path_validation_on_write_blob(self):
- blobs_path = self.tempdir
+ blobs_path, request = self.tempdir, DummyRequest([''])
backend = _blobs.FilesystemBlobsBackend(blobs_path)
with pytest.raises(Exception):
- yield backend.write_blob('..', '..', DummyRequest(['']))
+ yield backend.write_blob('..', '..', request)
+ with pytest.raises(Exception):
+ yield backend.write_blob('user', '../../../', request)
with pytest.raises(Exception):
- yield backend.write_blob('valid', '../../../', DummyRequest(['']))
+ yield backend.write_blob('../../../', 'id1', request)
with pytest.raises(Exception):
- yield backend.write_blob('../../../', 'valid', DummyRequest(['']))
+ yield backend.write_blob('user', 'id2', request, namespace='..')
@pytest.mark.usefixtures("method_tmpdir")
@mock.patch('leap.soledad.server._blobs.os.unlink')
@@ -128,3 +155,12 @@ class FilesystemBackendTestCase(unittest.TestCase):
backend.delete_blob('user', 'blob_id')
unlink_mock.assert_called_once_with(backend._get_path('user',
'blob_id'))
+
+ @pytest.mark.usefixtures("method_tmpdir")
+ @mock.patch('leap.soledad.server._blobs.os.unlink')
+ def test_delete_blob_custom_namespace(self, unlink_mock):
+ backend = _blobs.FilesystemBlobsBackend(self.tempdir)
+ backend.delete_blob('user', 'blob_id', namespace='trash')
+ unlink_mock.assert_called_once_with(backend._get_path('user',
+ 'blob_id',
+ 'trash'))