From ab297c4efe10c70949fac5384a63cbf553ba5da9 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Mon, 26 Jun 2017 05:25:55 -0300 Subject: [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 --- src/leap/soledad/server/_blobs.py | 31 ++++++++++-------- src/leap/soledad/server/interfaces.py | 12 +++---- testing/tests/blobs/test_fs_backend.py | 58 +++++++++++++++++++++++++++------- 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): @@ -98,28 +110,43 @@ class FilesystemBackendTestCase(unittest.TestCase): result = json.loads(backend.list_blobs('user', DummyRequest(['']))) 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')) -- cgit v1.2.3