From 4e06eb370b99f2d343e96f774a3ad9b8b77c9548 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 3 Oct 2016 19:27:42 -0300 Subject: [feature] check for user dbs couch schema versions --- common/src/leap/soledad/common/couch/state.py | 34 ++++++++++++++++++++++++++- common/src/leap/soledad/common/errors.py | 13 +++++++++- server/src/leap/soledad/server/__init__.py | 2 +- testing/tests/couch/conftest.py | 31 ++++++++++++++++++++++++ testing/tests/couch/test_command.py | 8 ++++--- testing/tests/couch/test_state.py | 23 ++++++++++++++++++ testing/tests/perf/conftest.py | 4 ++-- testing/tests/server/test_server.py | 4 ++-- 8 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 testing/tests/couch/conftest.py create mode 100644 testing/tests/couch/test_state.py diff --git a/common/src/leap/soledad/common/couch/state.py b/common/src/leap/soledad/common/couch/state.py index e3cd1a24..1d045a9d 100644 --- a/common/src/leap/soledad/common/couch/state.py +++ b/common/src/leap/soledad/common/couch/state.py @@ -17,6 +17,7 @@ """ Server state using CouchDatabase as backend. """ +import couchdb import re import time from urlparse import urljoin @@ -25,9 +26,14 @@ from hashlib import sha512 from leap.soledad.common.log import getLogger from leap.soledad.common.couch import CouchDatabase from leap.soledad.common.couch import couch_server +from leap.soledad.common.couch import CONFIG_DOC_ID +from leap.soledad.common.couch import SCHEMA_VERSION +from leap.soledad.common.couch import SCHEMA_VERSION_KEY from leap.soledad.common.command import exec_validated_cmd from leap.soledad.common.l2db.remote.server_state import ServerState from leap.soledad.common.l2db.errors import Unauthorized +from leap.soledad.common.errors import WrongCouchSchemaVersionError +from leap.soledad.common.errors import MissingCouchConfigDocumentError logger = getLogger(__name__) @@ -59,15 +65,41 @@ class CouchServerState(ServerState): TOKENS_TYPE_DEF = "Token" TOKENS_USER_ID_KEY = "user_id" - def __init__(self, couch_url, create_cmd=None): + def __init__(self, couch_url, create_cmd=None, + check_schema_versions=True): """ Initialize the couch server state. :param couch_url: The URL for the couch database. :type couch_url: str + :param check_schema_versions: Whether to check couch schema version of + user dbs. + :type check_schema_versions: bool """ self.couch_url = couch_url self.create_cmd = create_cmd + if check_schema_versions: + self._check_schema_versions() + + def _check_schema_versions(self): + """ + Check that all user databases use the correct couch schema. + """ + server = couchdb.client.Server(self.couch_url) + for dbname in server: + if not dbname.startswith('user-'): + continue + db = server[dbname] + + # if there are documents, ensure that a config doc exists + config_doc = db.get(CONFIG_DOC_ID) + if config_doc: + if config_doc[SCHEMA_VERSION_KEY] != SCHEMA_VERSION: + raise WrongCouchSchemaVersionError(dbname) + else: + result = db.view('_all_docs', limit=1) + if result.total_rows != 0: + raise MissingCouchConfigDocumentError(dbname) def open_database(self, dbname): """ diff --git a/common/src/leap/soledad/common/errors.py b/common/src/leap/soledad/common/errors.py index dec871c9..d543a3de 100644 --- a/common/src/leap/soledad/common/errors.py +++ b/common/src/leap/soledad/common/errors.py @@ -77,7 +77,6 @@ http_errors.ERROR_STATUSES = set( class InvalidURLError(Exception): - """ Exception raised when Soledad encounters a malformed URL. """ @@ -90,3 +89,15 @@ class BackendNotReadyError(SoledadError): """ wire_description = "backend not ready" status = 500 + + +class WrongCouchSchemaVersionError(SoledadError): + """ + Raised in case there is a user database with wrong couch schema version. + """ + + +class MissingCouchConfigDocumentError(SoledadError): + """ + Raised if a database has documents but lacks the couch config document. + """ diff --git a/server/src/leap/soledad/server/__init__.py b/server/src/leap/soledad/server/__init__.py index 97bcf888..2e1a453a 100644 --- a/server/src/leap/soledad/server/__init__.py +++ b/server/src/leap/soledad/server/__init__.py @@ -90,7 +90,7 @@ from leap.soledad.server.gzip_middleware import GzipMiddleware from leap.soledad.server.sync import SyncResource from leap.soledad.server.sync import MAX_REQUEST_SIZE from leap.soledad.server.sync import MAX_ENTRY_SIZE -from soledad.server.config import load_configuration +from leap.soledad.server.config import load_configuration from leap.soledad.common import SHARED_DB_NAME from leap.soledad.common.backend import SoledadBackend diff --git a/testing/tests/couch/conftest.py b/testing/tests/couch/conftest.py new file mode 100644 index 00000000..1074f091 --- /dev/null +++ b/testing/tests/couch/conftest.py @@ -0,0 +1,31 @@ +import couchdb +import pytest +import random +import string + + +@pytest.fixture +def random_name(): + return 'user-' + ''.join( + random.choice( + string.ascii_lowercase) for _ in range(10)) + + +class RandomDatabase(object): + + def __init__(self, couch_url, name): + self.couch_url = couch_url + self.name = name + self.server = couchdb.client.Server(couch_url) + self.database = self.server.create(name) + + def teardown(self): + self.server.delete(self.name) + + +@pytest.fixture +def db(random_name, request): + couch_url = request.config.getoption('--couch-url') + db = RandomDatabase(couch_url, random_name) + request.addfinalizer(db.teardown) + return db diff --git a/testing/tests/couch/test_command.py b/testing/tests/couch/test_command.py index 6a96ebf9..68097fb1 100644 --- a/testing/tests/couch/test_command.py +++ b/testing/tests/couch/test_command.py @@ -9,7 +9,8 @@ from mock import Mock class CommandBasedDBCreationTest(unittest.TestCase): def test_ensure_db_using_custom_command(self): - state = couch_state.CouchServerState("url", create_cmd="/bin/echo") + state = couch_state.CouchServerState( + "url", create_cmd="/bin/echo", check_schema_versions=False) mock_db = Mock() mock_db.replica_uid = 'replica_uid' state.open_database = Mock(return_value=mock_db) @@ -18,11 +19,12 @@ class CommandBasedDBCreationTest(unittest.TestCase): self.assertEquals(mock_db.replica_uid, replica_uid) def test_raises_unauthorized_on_failure(self): - state = couch_state.CouchServerState("url", create_cmd="inexistent") + state = couch_state.CouchServerState( + "url", create_cmd="inexistent", check_schema_versions=False) self.assertRaises(u1db_errors.Unauthorized, state.ensure_database, "user-1337") def test_raises_unauthorized_by_default(self): - state = couch_state.CouchServerState("url") + state = couch_state.CouchServerState("url", check_schema_versions=False) self.assertRaises(u1db_errors.Unauthorized, state.ensure_database, "user-1337") diff --git a/testing/tests/couch/test_state.py b/testing/tests/couch/test_state.py new file mode 100644 index 00000000..a53ba076 --- /dev/null +++ b/testing/tests/couch/test_state.py @@ -0,0 +1,23 @@ +import pytest + +from leap.soledad.common.couch import CONFIG_DOC_ID +from leap.soledad.common.couch import SCHEMA_VERSION +from leap.soledad.common.couch import SCHEMA_VERSION_KEY +from leap.soledad.common.couch.state import CouchServerState + +from leap.soledad.common.errors import WrongCouchSchemaVersionError +from leap.soledad.common.errors import MissingCouchConfigDocumentError + + +def test_wrong_couch_version_raises(db): + wrong_schema_version = SCHEMA_VERSION + 1 + db.database.create( + {'_id': CONFIG_DOC_ID, SCHEMA_VERSION_KEY: wrong_schema_version}) + with pytest.raises(WrongCouchSchemaVersionError): + CouchServerState(db.couch_url, create_cmd='/bin/echo') + + +def test_missing_config_doc_raises(db): + db.database.create({}) + with pytest.raises(MissingCouchConfigDocumentError): + CouchServerState(db.couch_url, create_cmd='/bin/echo') diff --git a/testing/tests/perf/conftest.py b/testing/tests/perf/conftest.py index 3681025f..5ac1f3c0 100644 --- a/testing/tests/perf/conftest.py +++ b/testing/tests/perf/conftest.py @@ -193,8 +193,8 @@ def soledad_server(tmpdir_factory, request): def txbenchmark(benchmark): def blockOnThread(*args, **kwargs): return threads.deferToThread( - benchmark, threads.blockingCallFromThread, - reactor, *args, **kwargs) + benchmark, threads.blockingCallFromThread, + reactor, *args, **kwargs) return blockOnThread diff --git a/testing/tests/server/test_server.py b/testing/tests/server/test_server.py index 18f92d88..6bbcf002 100644 --- a/testing/tests/server/test_server.py +++ b/testing/tests/server/test_server.py @@ -43,8 +43,8 @@ from test_soledad.util import ( from leap.soledad.common import crypto from leap.soledad.client import Soledad -from leap.soledad.server import load_configuration -from leap.soledad.server import CONFIG_DEFAULTS +from leap.soledad.server.config import load_configuration +from leap.soledad.server.config import CONFIG_DEFAULTS from leap.soledad.server.auth import URLToAuthorization from leap.soledad.server.auth import SoledadTokenAuthMiddleware -- cgit v1.2.3