diff options
-rw-r--r-- | backends/leap_backend.py | 25 | ||||
-rw-r--r-- | backends/sqlcipher.py | 26 | ||||
-rw-r--r-- | tests/test_sqlcipher.py | 42 |
3 files changed, 76 insertions, 17 deletions
diff --git a/backends/leap_backend.py b/backends/leap_backend.py index 7e98dd45..ec26dca4 100644 --- a/backends/leap_backend.py +++ b/backends/leap_backend.py @@ -8,7 +8,6 @@ from u1db.remote import utils from u1db.remote.http_target import HTTPSyncTarget from u1db.remote.http_database import HTTPDatabase from u1db.errors import BrokenSyncStream -from leap.soledad.util import GPGWrapper import uuid @@ -29,9 +28,10 @@ class LeapDocument(Document): """ def __init__(self, doc_id=None, rev=None, json='{}', has_conflicts=False, - encrypted_json=None, soledad=None): + encrypted_json=None, soledad=None, syncable=True): super(LeapDocument, self).__init__(doc_id, rev, json, has_conflicts) self._soledad = soledad + self._syncable = syncable if encrypted_json: self.set_encrypted_json(encrypted_json) @@ -55,6 +55,18 @@ class LeapDocument(Document): plaintext = self._soledad.decrypt_symmetric(self.doc_id, ciphertext) return self.set_json(plaintext) + def _get_syncable(self): + return self._syncable + + def _set_syncable(self, syncable=True): + self._syncable = syncable + + syncable = property( + _get_syncable, + _set_syncable, + doc="Determine if document should be synced with server." + ) + class LeapDatabase(HTTPDatabase): """Implement the HTTP remote database API to a Leap server.""" @@ -168,10 +180,11 @@ class LeapSyncTarget(HTTPSyncTarget): ensure=ensure_callback is not None) comma = ',' for doc, gen, trans_id in docs_by_generations: - # encrypt before sending to server. - size += prepare(id=doc.doc_id, rev=doc.rev, - content=doc.get_encrypted_json(), - gen=gen, trans_id=trans_id) + if doc.syncable: + # encrypt before sending to server. + size += prepare(id=doc.doc_id, rev=doc.rev, + content=doc.get_encrypted_json(), + gen=gen, trans_id=trans_id) entries.append('\r\n]') size += len(entries[-1]) self._conn.putheader('content-length', str(size)) diff --git a/backends/sqlcipher.py b/backends/sqlcipher.py index 08b4df43..6cebcf7d 100644 --- a/backends/sqlcipher.py +++ b/backends/sqlcipher.py @@ -25,10 +25,11 @@ from u1db.backends.sqlite_backend import ( SQLitePartialExpandDatabase, ) from u1db import ( - Document, errors, ) +from leap.soledad.backends.leap_backend import LeapDocument + def open(path, password, create=True, document_factory=None): """Open a database at the given location. @@ -70,7 +71,7 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): SQLCipherDatabase.set_pragma_key(self._db_handle, password) self._real_replica_uid = None self._ensure_schema() - self._factory = document_factory or Document + self._factory = document_factory or LeapDocument def _check_if_db_is_encrypted(self, sqlite_file): if not os.path.exists(sqlite_file): @@ -133,5 +134,26 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase): return Synchronizer(self, LeapSyncTarget(url, creds=creds), soledad=self._soledad).sync(autocreate=autocreate) + def _extra_schema_init(self, c): + c.execute( + 'ALTER TABLE document ' + 'ADD COLUMN syncable BOOL NOT NULL DEFAULT TRUE') + + def _put_and_update_indexes(self, old_doc, doc): + super(SQLCipherDatabase, self)._put_and_update_indexes(old_doc, doc) + c = self._db_handle.cursor() + c.execute('UPDATE document SET syncable=? WHERE doc_id=?', + (doc.syncable, doc.doc_id)) + + def _get_doc(self, doc_id, check_for_conflicts=False): + doc = super(SQLCipherDatabase, self)._get_doc(doc_id, + check_for_conflicts) + if doc: + c = self._db_handle.cursor() + c.execute('SELECT syncable FROM document WHERE doc_id=?', + (doc.doc_id,)) + doc.syncable = bool(c.fetchone()[0]) + return doc + SQLiteDatabase.register_implementation(SQLCipherDatabase) diff --git a/tests/test_sqlcipher.py b/tests/test_sqlcipher.py index d2fe0b11..a3ab35b6 100644 --- a/tests/test_sqlcipher.py +++ b/tests/test_sqlcipher.py @@ -20,6 +20,7 @@ from leap.soledad.backends.sqlcipher import ( DatabaseIsNotEncrypted, ) from leap.soledad.backends.sqlcipher import open as u1db_open +from leap.soledad.backends.leap_backend import LeapDocument # u1db tests stuff. from leap.soledad.tests import u1db_tests as tests @@ -75,10 +76,14 @@ def copy_sqlcipher_database_for_test(test, db): return new_db +def make_document_for_test(test, doc_id, rev, content, has_conflicts=False): + return LeapDocument(doc_id, rev, content, has_conflicts=has_conflicts) + + SQLCIPHER_SCENARIOS = [ ('sqlcipher', {'make_database_for_test': make_sqlcipher_database_for_test, 'copy_database_for_test': copy_sqlcipher_database_for_test, - 'make_document_for_test': tests.make_document_for_test, }), + 'make_document_for_test': make_document_for_test, }), ] @@ -161,6 +166,10 @@ class TestSQLCipherDatabase(test_sqlite_backend.TestSQLiteDatabase): self.assertTrue(db2._is_initialized(db1._get_sqlite_handle().cursor())) +class TestAlternativeDocument(LeapDocument): + """A (not very) alternative implementation of Document.""" + + class TestSQLCipherPartialExpandDatabase( test_sqlite_backend.TestSQLitePartialExpandDatabase): @@ -223,8 +232,8 @@ class TestSQLCipherPartialExpandDatabase( SQLCipherDatabase(path, PASSWORD) db2 = SQLCipherDatabase._open_database( path, PASSWORD, - document_factory=test_backends.TestAlternativeDocument) - self.assertEqual(test_backends.TestAlternativeDocument, db2._factory) + document_factory=TestAlternativeDocument) + self.assertEqual(TestAlternativeDocument, db2._factory) def test_open_database_existing(self): temp_dir = self.createTempDir(prefix='u1db-test-') @@ -239,8 +248,8 @@ class TestSQLCipherPartialExpandDatabase( SQLCipherDatabase(path, PASSWORD) db2 = SQLCipherDatabase.open_database( path, PASSWORD, create=False, - document_factory=test_backends.TestAlternativeDocument) - self.assertEqual(test_backends.TestAlternativeDocument, db2._factory) + document_factory=TestAlternativeDocument) + self.assertEqual(TestAlternativeDocument, db2._factory) def test_create_database_initializes_schema(self): # This test had to be cloned because our implementation of SQLCipher @@ -255,6 +264,19 @@ class TestSQLCipherPartialExpandDatabase( 'index_storage': 'expand referenced encrypted'}, config) + def test_store_syncable(self): + doc = self.db.create_doc_from_json(tests.simple_doc) + # assert that docs are syncable by default + self.assertEqual(True, doc.syncable) + # assert that we can store syncable = False + doc.syncable = False + self.db.put_doc(doc) + self.assertEqual(False, self.db.get_doc(doc.doc_id).syncable) + # assert that we can store syncable = True + doc.syncable = True + self.db.put_doc(doc) + self.assertEqual(True, self.db.get_doc(doc.doc_id).syncable) + #----------------------------------------------------------------------------- # The following tests come from `u1db.tests.test_open`. @@ -277,9 +299,9 @@ class SQLCipherOpen(test_open.TestU1DBOpen): def test_open_with_factory(self): db = u1db_open(self.db_path, password=PASSWORD, create=True, - document_factory=test_backends.TestAlternativeDocument) + document_factory=TestAlternativeDocument) self.addCleanup(db.close) - self.assertEqual(test_backends.TestAlternativeDocument, db._factory) + self.assertEqual(TestAlternativeDocument, db._factory) def test_open_existing(self): db = SQLCipherDatabase(self.db_path, PASSWORD) @@ -325,7 +347,8 @@ class SQLCipherEncryptionTest(unittest.TestCase): try: # trying to open an encrypted database with the regular u1db # backend should raise a DatabaseError exception. - SQLitePartialExpandDatabase(self.DB_FILE) + SQLitePartialExpandDatabase(self.DB_FILE, + document_factory=LeapDocument) raise DatabaseIsNotEncrypted() except DatabaseError: # at this point we know that the regular U1DB sqlcipher backend @@ -337,7 +360,8 @@ class SQLCipherEncryptionTest(unittest.TestCase): 'decrypted content mismatch') def test_try_to_open_raw_db_with_sqlcipher_backend(self): - db = SQLitePartialExpandDatabase(self.DB_FILE) + db = SQLitePartialExpandDatabase(self.DB_FILE, + document_factory=LeapDocument) db.create_doc_from_json(tests.simple_doc) db.close() try: |