diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index 0711f2c7a1fe9091050d05738ed4fdb1c9471846..81d74798000829a82d415dcf29a2a9e2ebe61057 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -17,19 +17,23 @@ The Twisted plugin that glues the Zero-Knowledge Access Pass system into Tahoe-LAFS. """ +import random from weakref import ( WeakValueDictionary, ) from datetime import ( datetime, + timedelta, ) - import attr from zope.interface import ( implementer, ) +from twisted.logger import ( + Logger, +) from twisted.python.filepath import ( FilePath, ) @@ -41,6 +45,9 @@ from allmydata.interfaces import ( IFoolscapStoragePlugin, IAnnounceableStorageServer, ) +from allmydata.client import ( + _Client, +) from privacypass import ( SigningKey, ) @@ -61,6 +68,13 @@ from .resource import ( from .controller import ( get_redeemer, ) +from .lease_maintenance import ( + SERVICE_NAME, + lease_maintenance_service, + maintain_leases_from_root, +) + +_log = Logger() @implementer(IAnnounceableStorageServer) @attr.s @@ -69,8 +83,8 @@ class AnnounceableStorageServer(object): storage_server = attr.ib() -@attr.s @implementer(IFoolscapStoragePlugin) +@attr.s class ZKAPAuthorizer(object): """ A storage plugin which provides a token-based access control mechanism on @@ -153,7 +167,6 @@ class ZKAPAuthorizer(object): get_passes, ) - def get_client_resource(self, node_config): from twisted.internet import reactor return resource_from_configuration( @@ -161,3 +174,78 @@ class ZKAPAuthorizer(object): store=self._get_store(node_config), redeemer=self._get_redeemer(node_config, None, reactor), ) + + +_init_storage = _Client.__dict__["init_storage"] +def maintenance_init_storage(self, announceable_storage_servers): + """ + A monkey-patched version of ``_Client.init_storage`` which also + initializes the lease maintenance service. + """ + from twisted.internet import reactor + try: + return _init_storage(self, announceable_storage_servers) + finally: + _maybe_attach_maintenance_service(reactor, self) +_Client.init_storage = maintenance_init_storage + + +def _maybe_attach_maintenance_service(reactor, client_node): + """ + Check for an existing lease maintenance service and if one is not found, + create one. + + :param allmydata.client._Client client_node: The client node to check and, + possibly, modify. A lease maintenance service is added to it if and + only if one is not already present. + """ + try: + # If there is already one we don't need another. + client_node.getServiceNamed(SERVICE_NAME) + except KeyError: + # There isn't one so make it and add it. + _log.info("Creating new lease maintenance service") + _create_maintenance_service( + reactor, + client_node.config, + client_node, + ).setServiceParent(client_node) + except Exception as e: + _log.failure("Attaching maintenance service to client node") + else: + _log.info("Found existing lease maintenance service") + + +def _create_maintenance_service(reactor, node_config, client_node): + """ + Create a lease maintenance service to be attached to the given client + node. + + :param allmydata.node._Config node_config: The configuration for the node + the lease maintenance service will be attached to. + + :param allmydata.client._Client client_node: The client node the lease + maintenance service will be attached to. + """ + def get_now(): + return datetime.utcfromtimestamp(reactor.seconds()) + + # Create the operation which performs the lease maintenance job when + # called. + maintain_leases = maintain_leases_from_root( + client_node.create_node_from_uri( + node_config.get_private_config(u"rootcap"), + ), + client_node.get_storage_broker(), + client_node._secret_holder, + # Make this configuration + timedelta(days=3), + get_now, + ) + # Create the service to periodically run the lease maintenance operation. + return lease_maintenance_service( + maintain_leases, + reactor, + node_config.get_private_config(u"last-lease-maintenance-run", None), + random, + ) diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index 9c97ea7dc597f672aa4f417d9673100df76e2a38..750f0b9093726efb740683cfd16d1d54f279c420 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -23,7 +23,10 @@ from __future__ import ( from io import ( BytesIO, ) - +from os import ( + makedirs, +) +import tempfile from zope.interface import ( implementer, ) @@ -53,6 +56,7 @@ from hypothesis import ( from hypothesis.strategies import ( just, datetimes, + sampled_from, ) from foolscap.broker import ( Broker, @@ -71,6 +75,9 @@ from allmydata.interfaces import ( IStorageServer, RIStorageServer, ) +from allmydata.client import ( + create_client_from_config, +) from twisted.python.filepath import ( FilePath, @@ -94,6 +101,9 @@ from ..model import ( from ..controller import ( IssuerConfigurationMismatch, ) +from ..lease_maintenance import ( + SERVICE_NAME, +) from .strategies import ( minimal_tahoe_configs, @@ -114,7 +124,6 @@ from .matchers import ( Provides, ) - SIGNING_KEY_PATH = FilePath(__file__).sibling(u"testing-signing.key") @@ -391,3 +400,91 @@ class ClientResourceTests(TestCase): storage_server.get_client_resource(config), Provides([IResource]), ) + + +SERVERS_YAML = b""" +storage: + v0-aaaaaaaa: + ann: + anonymous-storage-FURL: pb://@tcp:/ + nickname: 10.0.0.2 + storage-options: + - name: privatestorageio-zkapauthz-v1 + ristretto-issuer-root-url: https://payments.example.com/ + storage-server-FURL: pb://bbbbbbbb@tcp:10.0.0.2:1234/cccccccc +""" + +TWO_SERVERS_YAML = b""" +storage: + v0-aaaaaaaa: + ann: + anonymous-storage-FURL: pb://@tcp:/ + nickname: 10.0.0.2 + storage-options: + - name: privatestorageio-zkapauthz-v1 + ristretto-issuer-root-url: https://payments.example.com/ + storage-server-FURL: pb://bbbbbbbb@tcp:10.0.0.2:1234/cccccccc + v0-dddddddd: + ann: + anonymous-storage-FURL: pb://@tcp:/ + nickname: 10.0.0.3 + storage-options: + - name: privatestorageio-zkapauthz-v1 + ristretto-issuer-root-url: https://payments.example.com/ + storage-server-FURL: pb://eeeeeeee@tcp:10.0.0.3:1234/ffffffff +""" + + +class LeaseMaintenanceServiceTests(TestCase): + """ + Tests for the plugin's initialization of the lease maintenance service. + """ + def _created_test(self, get_config, servers_yaml): + original_tempdir = tempfile.tempdir + + tempdir = self.useFixture(TempDir()) + nodedir = tempdir.join(b"node") + privatedir = tempdir.join(b"node", b"private") + makedirs(privatedir) + config = get_config(nodedir, b"tub.port") + + # Provide it a statically configured server to connect to. + config.write_private_config( + b"servers.yaml", + servers_yaml, + ) + config.write_private_config( + b"rootcap", + b"dddddddd", + ) + + try: + d = create_client_from_config(config) + self.assertThat( + d, + succeeded( + AfterPreprocessing( + lambda client: client.getServiceNamed(SERVICE_NAME), + Always(), + ), + ), + ) + finally: + # create_client_from_config (indirectly) rewrites tempfile.tempdir + # in a destructive manner that fails most of the rest of the test + # suite if we don't clean it up. We can do this with a tearDown + # or a fixture or an addCleanup because hypothesis doesn't run any + # of those at the right time. :/ + tempfile.tempdir = original_tempdir + + @given( + tahoe_configs_with_dummy_redeemer, + sampled_from([SERVERS_YAML, TWO_SERVERS_YAML]), + ) + def test_created(self, get_config, servers_yaml): + """ + A client created from a configuration with the plugin enabled has a lease + maintenance service after it has at least one storage server to + connect to. + """ + return self._created_test(get_config, servers_yaml)