diff --git a/docs/source/interface.rst b/docs/source/interface.rst index afb2cc7ad6f70123620e622c9394fd38acbf505c..8b035cfac0813dacf423da714102b7e39f4b0529 100644 --- a/docs/source/interface.rst +++ b/docs/source/interface.rst @@ -115,7 +115,7 @@ The elements of the list are objects like the one returned by issuing a **GET** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This endpoint allows an external agent to retrieve unused unblinded tokens present in the node's database. -Unblinded tokens are returned in ascending text sorted order. +Unblinded tokens are returned in a stable order. This order matches the order in which tokens will be used by the system. This endpoint accepts several query arguments: @@ -138,3 +138,19 @@ If it has run, * ``when``: associated with an ISO8601 datetime string giving the approximate time the process ran * ``count``: associated with a number giving the number of passes which would need to be spent to renew leases on all stored objects seen during the lease maintenance activity + +``POST /storage-plugins/privatestorageio/zkapauthz-v1/unblinded-token`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This endpoint allows an external agent to insert new unblinded tokens into the node's database. +This allows for restoration of previously backed-up tokens in case the node is lost. +Tokens inserted with this API will be used after any tokens already in the database and in the order they appear in the given list. + +The request body must be ``application/json`` encoded and contain an object like:: + + { "unblinded-tokens": [<unblinded token string>, ...] + } + +The response is **OK** with ``application/json`` content-type response body like:: + + { } diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index 873197710f6c6eaa073faba8b0b6b7b20769985d..8615b43955dd64b54a7ea0b2827bb283f6fc02e7 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -308,10 +308,35 @@ class VoucherStore(object): in refs ) + def _insert_unblinded_tokens(self, cursor, unblinded_tokens): + """ + Helper function to really insert unblinded tokens into the database. + """ + cursor.executemany( + """ + INSERT INTO [unblinded-tokens] VALUES (?) + """, + list( + (token,) + for token + in unblinded_tokens + ), + ) + + @with_cursor + def insert_unblinded_tokens(self, cursor, unblinded_tokens): + """ + Store some unblinded tokens, for example as part of a backup-restore + process. + + :param list[unicode] unblinded_tokens: The unblinded tokens to store. + """ + self._insert_unblinded_tokens(cursor, unblinded_tokens) + @with_cursor def insert_unblinded_tokens_for_voucher(self, cursor, voucher, public_key, unblinded_tokens): """ - Store some unblinded tokens. + Store some unblinded tokens received from redemption of a voucher. :param unicode voucher: The voucher associated with the unblinded tokens. This voucher will be marked as redeemed to indicate it @@ -343,12 +368,10 @@ class VoucherStore(object): ) if cursor.rowcount == 0: raise ValueError("Cannot insert tokens for unknown voucher; add voucher first") - cursor.executemany( - """ - INSERT INTO [unblinded-tokens] VALUES (?) - """, + self._insert_unblinded_tokens( + cursor, list( - (t.unblinded_token,) + t.unblinded_token for t in unblinded_tokens ), diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py index b09b58ab672e5443a062e3cc68bd6ae8dfeec8ca..1e2f5ae68ec0b3dd94113ab4ada929286df86594 100644 --- a/src/_zkapauthorizer/resource.py +++ b/src/_zkapauthorizer/resource.py @@ -27,7 +27,9 @@ from itertools import ( islice, ) from json import ( - loads, dumps, + loads, + load, + dumps, ) from zope.interface import ( Attribute, @@ -181,6 +183,16 @@ class _UnblindedTokenCollection(Resource): u"lease-maintenance-spending": self._lease_maintenance_activity(), }) + def render_POST(self, request): + """ + Store some unblinded tokens. + """ + application_json(request) + unblinded_tokens = load(request.content)[u"unblinded-tokens"] + self._store.insert_unblinded_tokens(unblinded_tokens) + return dumps({}) + + def _lease_maintenance_activity(self): activity = self._store.get_latest_lease_maintenance_activity() if activity is None: diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 423f4950de7cadac563576da99496f0c47f18c4b..f6d2abde71ebfdb238ce37477f4f0af4f657e926 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -146,6 +146,7 @@ from .strategies import ( client_dummyredeemer_configurations, client_nonredeemer_configurations, client_errorredeemer_configurations, + unblinded_tokens, vouchers, requests, ) @@ -305,6 +306,54 @@ class UnblindedTokenTests(TestCase): self.useFixture(CaptureTwistedLogs()) + @given( + tahoe_configs(), + vouchers(), + lists(unblinded_tokens(), unique=True, min_size=1, max_size=1000), + ) + def test_post(self, get_config, voucher, unblinded_tokens): + """ + When the unblinded token collection receives a **POST**, the unblinded + tokens in the request body are inserted into the system and an OK + response is generated. + """ + tempdir = self.useFixture(TempDir()) + config = get_config(tempdir.join(b"tahoe"), b"tub.port") + root = root_from_config(config, datetime.now) + + + agent = RequestTraversalAgent(root) + producer = FileBodyProducer( + BytesIO(dumps({u"unblinded-tokens": list( + token.unblinded_token + for token + in unblinded_tokens + )})), + cooperator=uncooperator(), + ) + requesting = agent.request( + b"POST", + b"http://127.0.0.1/unblinded-token", + bodyProducer=producer, + ) + self.assertThat( + requesting, + succeeded( + ok_response(headers=application_json()), + ), + ) + + stored_tokens = root.controller.store.backup()[u"unblinded-tokens"] + + self.assertThat( + stored_tokens, + Equals(list( + token.unblinded_token + for token + in unblinded_tokens + )), + ) + @given(tahoe_configs(), vouchers(), integers(min_value=0, max_value=100)) def test_get(self, get_config, voucher, num_tokens): """