Make IdentityRepository a KeyRing

- Keep only one class to provide access to identities stored in the database.

- Remove old code and its tests.

- Align KeyRing and IdentityRepository APIs.

- Implement a (very) simple unit test for IdentityRepository.
This commit is contained in:
Piotr F. Mieszkowski 2023-11-24 22:59:21 +01:00
parent 5efef3c9cb
commit 7c2d32bf3c
7 changed files with 54 additions and 79 deletions

View File

@ -40,17 +40,13 @@ class KeyCache:
class KeyRing:
"""Contract to be implemented by a key-store (a.k.a. keyring)."""
def load(self):
"""Load keyring, replacing any previous contents of the cache."""
raise NotImplementedError('KeyRing.load not implemented')
def freeze_identities(self) -> KeyCache:
"""Return a static, async-safe copy of the identity map."""
raise NotImplementedError('KeyRing.load not implemented')
def register(self, email: str, key_id: str):
def register_or_update(self, email: str, key_id: str):
"""Add a new (email,key) pair to the keystore."""
raise NotImplementedError('KeyRing.register not implemented')
raise NotImplementedError('KeyRing.register_or_update not implemented')
def post_init_hook(self):
"""Lets the keyring perform additional operations following its initialisation."""

View File

@ -1,48 +0,0 @@
"""Database-backed keyring implementation."""
from lacre._keyringcommon import KeyRing, KeyCache
from lacre.dbschema import init_identities_table, table_metadata
import logging
import sqlalchemy
from sqlalchemy.sql import select
LOG = logging.getLogger(__name__)
class KeyRingSchema:
def __init__(self):
self._meta = table_metadata()
self._id_table = init_identities_table()
def identities(self):
return self._id_table
class DatabaseKeyRing(KeyRing):
"""Database-backed key storage."""
def __init__(self, database_url, schema: KeyRingSchema):
self._schema = schema
self._url = database_url
self._initialised = False
def _ensure_initialised(self):
if not self._initialised:
self._engine = sqlalchemy.create_engine(self._url)
self._connection = self._engine.connect()
def load(self):
"""Do nothing, database contents doesn't need to be cached."""
pass
def freeze_identities(self) -> KeyCache:
"""Return a static, async-safe copy of the identity map."""
self._ensure_initialised()
return self._load_identities()
def _load_identities(self) -> KeyCache:
identities = self._schema.identities()
all_identities = select(identities.c.fingerprint, identities.c.email)
result = self._connection.execute(all_identities)
LOG.debug('Retrieving all keys')
return KeyCache({key_id: email for key_id, email in result})

View File

@ -6,7 +6,8 @@ module.
import lacre.config as conf
from lacre._keyringcommon import KeyRing, KeyCache
import lacre.dbkeyring as dbk
from lacre.dbschema import GPGMW_IDENTITIES
from lacre.repositories import IdentityRepository
import logging
LOG = logging.getLogger(__name__)
@ -15,12 +16,10 @@ LOG = logging.getLogger(__name__)
def init_keyring() -> KeyRing:
"""Initialise appropriate type of keyring."""
url = conf.get_item('database', 'url')
schema = dbk.KeyRingSchema()
LOG.info('Initialising database keyring from %s', url)
return dbk.DatabaseKeyRing(url, schema)
return IdentityRepository(GPGMW_IDENTITIES, db_url=url)
def freeze_and_load_keys():
def freeze_and_load_keys() -> KeyCache:
"""Load and return keys.
Doesn't refresh the keys when they change on disk.

View File

@ -1,17 +1,20 @@
"""Lacre identity and key repositories."""
from sqlalchemy import select, delete, and_
from sqlalchemy import create_engine, select, delete, and_
import logging
from lacre._keyringcommon import KeyRing, KeyCache
import lacre.dbschema as db
LOG = logging.getLogger(__name__)
class IdentityRepository:
def __init__(self, identity_table, connection):
class IdentityRepository(KeyRing):
def __init__(self, identity_table, connection=None, db_url=None):
self._identities = identity_table
self._conn = connection
self._url = db_url
self._initialised = connection is not None
def register_or_update(self, email, fprint):
assert email, "email is mandatory"
@ -23,18 +26,21 @@ class IdentityRepository:
self._insert(email, fprint)
def _exists(self, email: str) -> bool:
self._ensure_connected()
selq = select(self._identities.c.email).where(self._identities.c.email == email)
emails = [e for e in self._conn.execute(selq)]
assert len(emails) == 1
return emails
def _insert(self, email, fprint):
self._ensure_connected()
insq = self._identities.insert().values(email=email, fingerprint=fprint)
LOG.debug('Registering identity %s: %s', email, insq)
self._conn.execute(insq)
def _update(self, email, fprint):
self._ensure_connected()
upq = self._identities.update() \
.values(fingerprint=fprint) \
.where(self._identities.c.email == email)
@ -42,6 +48,29 @@ class IdentityRepository:
LOG.debug('Updating identity %s: %s', email, upq)
self._conn.execute(upq)
def _ensure_connected(self):
if not self._initialised:
self._engine = create_engine(self._url)
LOG.debug('Connecting with %s', repr(self._engine))
self._connection = self._engine.connect()
def delete(self, email):
self._ensure_connected()
delq = delete(self._identities).where(self._identities.c.email == email)
LOG.debug('Deleting keys assigned to %s', email)
def freeze_identities(self) -> KeyCache:
"""Return a static, async-safe copy of the identity map."""
self._ensure_connected()
return self._load_identities()
def _load_identities(self) -> KeyCache:
all_identities = select(self._identities.c.fingerprint, self._identities.c.email)
result = self._connection.execute(all_identities)
LOG.debug('Retrieving all keys')
return KeyCache({key_id: email for key_id, email in result})
class KeyConfirmationQueue:
"""Encapsulates access to gpgmw_keys table."""

View File

@ -1,16 +0,0 @@
"""Tests for lacre.dbkeyring."""
import lacre.dbkeyring as dbk
import unittest
class LacreDbKeyringTest(unittest.TestCase):
def test_load_keys(self):
db_url = 'sqlite:///test/lacre.db'
schema = dbk.KeyRingSchema()
db = dbk.DatabaseKeyRing(db_url, schema)
identities = db.freeze_identities()
self.assertTrue('1CD245308F0963D038E88357973CF4D9387C44D7' in identities)
self.assertTrue(identities.has_email('alice@disposlab'))

View File

@ -0,0 +1,14 @@
"""Lacre identity and key repository tests."""
import unittest
import lacre.repositories as r
import lacre.dbschema as s
class IdentityRepositoryTest(unittest.TestCase):
def test_x(self):
ir = r.IdentityRepository(s.GPGMW_IDENTITIES, db_url='sqlite:///test/lacre.db')
identities = ir.freeze_identities()
self.assertTrue(identities)

View File

@ -73,7 +73,8 @@ if conf.flag_enabled('database', 'enabled') and conf.config_item_set('database',
for armored_key, row_id, email in result_set:
# delete any other public keys associated with this confirmed email address
key_queue.delete_keys(row_id, email)
key_queue.delete_keys(row_id, email=email)
identities.delete(email)
GnuPG.delete_key(key_dir, email)
LOG.info('Deleted key for <%s> via import request', email)