diff options
| -rw-r--r-- | src/leap/soledad/server/_blobs.py | 31 | ||||
| -rw-r--r-- | src/leap/soledad/server/interfaces.py | 12 | ||||
| -rw-r--r-- | 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): @@ -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')) | 
