diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index 58357cb237a36e9d1ac300529ccb0d9bdad0c03b..ec534fd4c4801cffc072c6af5f7149bd7724a253 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -117,6 +117,15 @@ def open_and_initialize(path, required_schema_version, connect=None): ) """, ) + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS [passes] ( + [text] text, -- The string that defines the pass. + + PRIMARY KEY([text]) + ) + """, + ) return conn @@ -201,6 +210,76 @@ class VoucherStore(object): in refs ) + @with_cursor + def insert_passes(self, cursor, passes): + """ + Store some passes. + + :param list[Pass] passes: The passes to store. + """ + cursor.executemany( + """ + INSERT INTO [passes] VALUES (?) + """, + list((p.text,) for p in passes), + ) + + @with_cursor + def extract_passes(self, cursor, count): + """ + Remove and return some passes. + + :param int count: The maximum number of passes to remove and return. + If fewer passes than this are available, only as many as are + available are returned. + + :return list[Pass]: The removed passes. + """ + cursor.execute( + """ + CREATE TEMPORARY TABLE [extracting-passes] + AS + SELECT [text] FROM [passes] LIMIT ? + """, + (count,), + ) + cursor.execute( + """ + DELETE FROM [passes] WHERE [text] IN [extracting-passes] + """, + ) + cursor.execute( + """ + SELECT ([text]) FROM [extracting-passes] + """, + ) + texts = cursor.fetchall() + cursor.execute( + """ + DROP TABLE [extracting-passes] + """, + ) + return list( + Pass(t) + for (t,) + in texts + ) + + +@attr.s(frozen=True) +class Pass(object): + """ + A ``Pass`` instance completely represents a single Zero-Knowledge Access Pass. + + :ivar unicode text: The text value of the pass. This can be sent to a + service provider one time to anonymously prove a prior voucher + redemption. If it is sent more than once the service provider may + choose to reject it and the anonymity property is compromised. Pass + text should be kept secret. If pass text is divulged to third-parties + the anonymity property may be compromised. + """ + text = attr.ib(type=unicode) + @attr.s class Voucher(object): diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index e615137fd615201776e7ccfa46ba1d908a8f883b..3b6fc63deeab705795a5d7555b5a453dd6e16035 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -52,6 +52,10 @@ from allmydata.client import ( config_from_string, ) +from ..model import ( + Pass, +) + def _merge_dictionaries(dictionaries): result = {} @@ -175,6 +179,20 @@ def vouchers(): ) +def zkaps(): + """ + Build random ZKAPs as ``Pass` instances. + """ + return binary( + min_size=32, + max_size=32, + ).map( + urlsafe_b64encode, + ).map( + lambda zkap: Pass(zkap.decode("ascii")), + ) + + def request_paths(): """ Build lists of unicode strings that represent the path component of an diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 7732e5e06f40460bf1acf00665ab1e4501298afa..11d730440ebb90a09a2a9559766f9fe90442bb1d 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -64,6 +64,7 @@ from ..model import ( from .strategies import ( tahoe_configs, vouchers, + zkaps, ) @@ -146,7 +147,7 @@ class VoucherStoreTests(TestCase): ) - @given(tahoe_configs(), lists(vouchers())) + @given(tahoe_configs(), lists(vouchers(), unique=True)) def test_list(self, get_config, vouchers): """ ``VoucherStore.list`` returns a ``list`` containing a ``Voucher`` object @@ -250,3 +251,27 @@ class VoucherTests(TestCase): Voucher.from_json(ref.to_json()), Equals(ref), ) + + +class ZKAPStoreTests(TestCase): + """ + Tests for ZKAP-related functionality of ``VoucherStore``. + """ + @given(tahoe_configs(), lists(zkaps(), unique=True)) + def test_zkaps_round_trip(self, get_config, passes): + """ + ZKAPs that are added to the store can later be retrieved. + """ + tempdir = self.useFixture(TempDir()) + config = get_config(tempdir.join(b"node"), b"tub.port") + store = VoucherStore.from_node_config( + config, + memory_connect, + ) + store.insert_passes(passes) + retrieved_passes = store.extract_passes(len(passes)) + self.expectThat(passes, Equals(retrieved_passes)) + + # After extraction, the passes are no longer available. + more_passes = store.extract_passes(1) + self.expectThat([], Equals(more_passes))