diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f36553ef7..b8b3812897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ -# 0.7.4-RC1 +# 0.7.4-RC2 -This release consists largely of internal fixes with a few minor enhancements. -There have been a lot of groups exercising ACA-Py and the updates made in this -release are a reflection of those efforts. We have PRs that have been -contributed by 17 different people, which is likely a record for a single ACA-Py -release. +The 0.7.4 release consists largely of internal fixes with a few minor +enhancements. There have been a lot of groups exercising ACA-Py and the updates +made in this release are a reflection of those efforts. We have PRs that have +been contributed by 17 different people, which is likely a record for a single +ACA-Py release. The largest enhancement is in the area of the Hyperledger Indy endorser, enabling an instance of ACA-Py to act as an Endorser for Indy authors needed @@ -27,6 +27,16 @@ the team at LISSI for creating the to make load testing so easy! And of course to the core ACA-Py team for addressing the findings. +The team has worked a lot on evolving the persistent queue (PQ) approach +available in ACA-Py. We have landed on a design whereby the ability to use +queues for inbound and outbound messages is within ACA-Py, with a default +in-memory implementation, and the implementations of external persistent queues +solutions is handled by referencing a plugin from a separate repository. There +will shortly be two concrete, out-of-the-box solutions available, one for Kafka +and one for Redis, and anyone else can implement their own PQ plugin as long as +it uses the same ACA-Py queuing interface. Look for the new PQ repos shortly +within Hyperledger Aries. + Several new ways to control ACA-Py configurations were added, including new startup parameters, Admin API parameters to control instances of protocols, and additional web hook notifications. @@ -60,6 +70,9 @@ stuff needed for a growing codebase. - Feature/undelivered events [\#1694](https://github.com/hyperledger/aries-cloudagent-python/pull/1694) ([mepeltier](https://github.com/mepeltier)) - Allow use of SEED when creating local wallet DID Issue-1682 Issue-1682 [\#1705](https://github.com/hyperledger/aries-cloudagent-python/pull/1705) ([DaevMithran](https://github.com/DaevMithran)) +- Persistent Queues + - Redis PQ Cleanup in preparation for enabling the uses of plugin PQ implementations \[Issue\#1659\] [\#1659](https://github.com/hyperledger/aries-cloudagent-python/pull/1690) ([shaangill025](https://github.com/shaangill025)) + - Issue Credential, Revocation, Present Proof updates/fixes - Fix: DIF proof proposal when creating bound presentation request \[Issue\#1687\] [\#1690](https://github.com/hyperledger/aries-cloudagent-python/pull/1690) ([shaangill025](https://github.com/shaangill025)) - Fix DIF PresExch and OOB request\_attach delete unused connection [\#1676](https://github.com/hyperledger/aries-cloudagent-python/pull/1676) ([shaangill025](https://github.com/shaangill025)) @@ -68,6 +81,7 @@ stuff needed for a growing codebase. - Fixes for credential details in issue-credential webhook responses [\#1668](https://github.com/hyperledger/aries-cloudagent-python/pull/1668) ([andrewwhitehead](https://github.com/andrewwhitehead)) - Fix: present-proof v2 send-proposal [issue\#1474](https://github.com/hyperledger/aries-cloudagent-python/issues/1474) [\#1667](https://github.com/hyperledger/aries-cloudagent-python/pull/1667) ([shaangill025](https://github.com/shaangill025)) - Fixes Issue 3b from [\#1597](https://github.com/hyperledger/aries-cloudagent-python/issues/1597): V2 Credential exchange ignores the auto-respond-credential-request + - fix: Resolve Revocation Notification environment variable name collision [\#1751](https://github.com/hyperledger/aries-cloudagent-python/pull/1751) ([frostyfrog](https://github.com/frostyfrog)) - fix: always notify if revocation notification record exists [\#1665](https://github.com/hyperledger/aries-cloudagent-python/pull/1665) ([TimoGlastra](https://github.com/TimoGlastra)) - Revert change to send\_credential\_ack return value [\#1660](https://github.com/hyperledger/aries-cloudagent-python/pull/1660) ([andrewwhitehead](https://github.com/andrewwhitehead)) - Fix usage of send\_credential\_ack [\#1653](https://github.com/hyperledger/aries-cloudagent-python/pull/1653) ([andrewwhitehead](https://github.com/andrewwhitehead)) @@ -80,6 +94,7 @@ stuff needed for a growing codebase. - Mediator updates and fixes - feat: allow querying default mediator from base wallet [\#1729](https://github.com/hyperledger/aries-cloudagent-python/pull/1729) ([dbluhm](https://github.com/dbluhm)) + - Added async with for mediator record delete [\#1749](https://github.com/hyperledger/aries-cloudagent-python/pull/1749) ([dejsenlitro](https://github.com/dejsenlitro)) - Multitenacy updates and fixes - feat: create new JWT tokens and invalidate older for multitenancy [\#1725](https://github.com/hyperledger/aries-cloudagent-python/pull/1725) ([TimoGlastra](https://github.com/TimoGlastra)) @@ -96,6 +111,7 @@ stuff needed for a growing codebase. - Add an integration test for mixed proof with a revocable cred and a n… [\#1672](https://github.com/hyperledger/aries-cloudagent-python/pull/1672) ([ianco](https://github.com/ianco)) - Documentation and Demo Updates + - Fetch from --genesis-url likely to fail in composed container [\#1746](https://github.com/hyperledger/aries-cloudagent-python/pull/1739) ([tdiesler](https://github.com/tdiesler)) - Fixes logic for web hook formatter in Faber demo [\#1739](https://github.com/hyperledger/aries-cloudagent-python/pull/1739) ([amanji](https://github.com/amanji)) - Multitenancy Docs Update [\#1706](https://github.com/hyperledger/aries-cloudagent-python/pull/1706) ([MonolithicMonk](https://github.com/MonolithicMonk)) - [\#1674](https://github.com/hyperledger/aries-cloudagent-python/issue/1674) Add basic DOCKER\_ENV logging for run\_demo [\#1675](https://github.com/hyperledger/aries-cloudagent-python/pull/1675) ([tdiesler](https://github.com/tdiesler)) diff --git a/Multiledger.md b/Multiledger.md index 30c692c878..5d575c9969 100644 --- a/Multiledger.md +++ b/Multiledger.md @@ -14,6 +14,7 @@ More background information including problem statement, design (algorithm) and - [Read Requests](#read-requests) - [For checking ledger in parallel](#for-checking-ledger-in-parallel) - [Write Requests](#write-requests) +- [A Special Warning for TAA Acceptance](#a-special-warning-for-taa-acceptance) - [Impact on other ACA-Py function](#impact-on-other-aca-py-function) ## Usage @@ -104,6 +105,25 @@ If multiple ledgers are configured then `IndyLedgerRequestsExecutor` service ext On startup, the first configured applicable ledger is assigned as the `write_ledger` [`BaseLedger`], the selection is dependant on the order (top-down) and whether it is `production` or `non_production`. For instance, considering this [example configuration](#example-config-file), ledger `bcorvinTest` will be set as `write_ledger` as it is the topmost `production` ledger. If no `production` ledgers are included in configuration then the topmost `non_production` ledger is selected. +## A Special Warning for TAA Acceptance + +When you run in multi-ledger mode, ACA-Py will use the `pool-name` (or `id`) specified in the ledger configuration file for each ledger. + +(When running in single-ledger mode, ACA-Py uses `default` as the ledger name.) + +If you are running against a ledger in `write` mode, and the ledger requires you to accept a Transaction Author Agreement (TAA), ACA-Py stores the TAA acceptance +status in the wallet in a non-secrets record, using the ledger's `pool_name` as a key. + +This means that if you are upgrading from single-ledger to multi-ledger mode, you will need to *either*: + +- set the `id` for your writable ledger to `default` (in your `ledgers.yaml` file) + +*or*: + +- re-accept the TAA once you restart your ACA-Py in multi-ledger mode + +Once you re-start ACA-Py, you can check the `GET /ledger/taa` endpoint to verify your TAA acceptance status. + ## Impact on other ACA-Py function There should be no impact/change in functionality to any ACA-Py protocols. diff --git a/aries_cloudagent/askar/profile.py b/aries_cloudagent/askar/profile.py index 4b72d20a57..c20d436f01 100644 --- a/aries_cloudagent/askar/profile.py +++ b/aries_cloudagent/askar/profile.py @@ -213,6 +213,8 @@ async def _teardown(self, commit: bool = None): await self._handle.commit() except AskarError as err: raise ProfileError("Error committing transaction") from err + if self._handle: + await self._handle.close() self._handle = None self._check_duration() diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index c5d59655e0..2c2ac13dd4 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -534,6 +534,20 @@ def add_arguments(self, parser: ArgumentParser): ), ) + parser.add_argument( + "--block-plugin", + dest="blocked_plugins", + type=str, + action="append", + required=False, + metavar="", + env_var="ACAPY_BLOCKED_PLUGIN", + help=( + "Block plugin module from loading. Multiple " + "instances of this parameter can be specified." + ), + ) + parser.add_argument( "--plugin-config", dest="plugin_config", @@ -611,6 +625,9 @@ def get_settings(self, args: Namespace) -> dict: if args.external_plugins: settings["external_plugins"] = args.external_plugins + if args.blocked_plugins: + settings["blocked_plugins"] = args.blocked_plugins + if args.plugin_config: with open(args.plugin_config, "r") as stream: settings["plugin_config"] = yaml.safe_load(stream) diff --git a/aries_cloudagent/config/default_context.py b/aries_cloudagent/config/default_context.py index 4e9b0eddef..fb0867cddc 100644 --- a/aries_cloudagent/config/default_context.py +++ b/aries_cloudagent/config/default_context.py @@ -111,7 +111,9 @@ async def bind_providers(self, context: InjectionContext): async def load_plugins(self, context: InjectionContext): """Set up plugin registry and load plugins.""" - plugin_registry = PluginRegistry() + plugin_registry = PluginRegistry( + blocklist=self.settings.get("blocked_plugins", []) + ) context.injector.bind_instance(PluginRegistry, plugin_registry) # Register standard protocol plugins diff --git a/aries_cloudagent/core/plugin_registry.py b/aries_cloudagent/core/plugin_registry.py index 193c695241..d75dd103ed 100644 --- a/aries_cloudagent/core/plugin_registry.py +++ b/aries_cloudagent/core/plugin_registry.py @@ -3,7 +3,7 @@ import logging from collections import OrderedDict from types import ModuleType -from typing import Sequence +from typing import Sequence, Iterable from ..config.injection_context import InjectionContext from ..core.event_bus import EventBus @@ -19,9 +19,10 @@ class PluginRegistry: """Plugin registry for indexing application plugins.""" - def __init__(self): + def __init__(self, blocklist: Iterable[str] = []): """Initialize a `PluginRegistry` instance.""" self._plugins = OrderedDict() + self._blocklist = set(blocklist) @property def plugin_names(self) -> Sequence[str]: @@ -119,6 +120,9 @@ def register_plugin(self, module_name: str) -> ModuleType: """Register a plugin module.""" if module_name in self._plugins: mod = self._plugins[module_name] + elif module_name in self._blocklist: + LOGGER.debug(f"Blocked {module_name} from loading due to blocklist") + return None else: try: mod = ClassLoader.load_module(module_name) diff --git a/aries_cloudagent/core/tests/test_plugin_registry.py b/aries_cloudagent/core/tests/test_plugin_registry.py index 7213e96b7e..7d4d86a9e4 100644 --- a/aries_cloudagent/core/tests/test_plugin_registry.py +++ b/aries_cloudagent/core/tests/test_plugin_registry.py @@ -15,7 +15,8 @@ class TestPluginRegistry(AsyncTestCase): def setUp(self): - self.registry = PluginRegistry() + self.blocked_module = "blocked_module" + self.registry = PluginRegistry(blocklist=[self.blocked_module]) self.context = InjectionContext(enforce_typing=False) self.proto_registry = async_mock.MagicMock( @@ -478,6 +479,23 @@ class MODULE: ] assert self.registry.register_plugin("dummy") == obj + async def test_unregister_plugin_has_setup(self): + class MODULE: + setup = "present" + + obj = MODULE() + with async_mock.patch.object( + ClassLoader, "load_module", async_mock.MagicMock() + ) as load_module: + load_module.side_effect = [ + obj, # module + None, # routes + None, # message types + None, # definition without versions attr + ] + assert self.registry.register_plugin(self.blocked_module) == None + assert self.blocked_module not in self.registry._plugins.keys() + async def test_register_definitions_malformed(self): class MODULE: no_setup = "no setup attr" diff --git a/aries_cloudagent/ledger/indy.py b/aries_cloudagent/ledger/indy.py index 13f27abac2..96ac9e0e3a 100644 --- a/aries_cloudagent/ledger/indy.py +++ b/aries_cloudagent/ledger/indy.py @@ -5,6 +5,7 @@ import logging import tempfile from datetime import date, datetime +from io import StringIO from os import path from time import time from typing import Sequence, Tuple, Optional @@ -42,6 +43,17 @@ GENESIS_TRANSACTION_FILE = "indy_genesis_transactions.txt" +def _normalize_txns(txns: str) -> str: + """Normalize a set of genesis transactions.""" + lines = StringIO() + for line in txns.splitlines(): + line = line.strip() + if line: + lines.write(line) + lines.write("\n") + return lines.getvalue() + + class IndySdkLedgerPoolProvider(BaseProvider): """Indy ledger pool provider which keys off the selected pool name.""" @@ -107,12 +119,28 @@ def __init__( self.cache = cache self.cache_duration = cache_duration self.genesis_transactions = genesis_transactions + self.genesis_txns_cache = genesis_transactions self.handle = None self.name = name self.taa_cache = None self.read_only = read_only self.socks_proxy = socks_proxy + @property + def genesis_txns(self) -> str: + """Get the configured genesis transactions.""" + if not self.genesis_txns_cache: + try: + txn_path = path.join( + tempfile.gettempdir(), f"{self.name}_{GENESIS_TRANSACTION_FILE}" + ) + self.genesis_txns_cache = _normalize_txns(open(txn_path).read()) + except FileNotFoundError: + raise LedgerConfigError( + "Pool config '%s' not found", self.name + ) from None + return self.genesis_txns_cache + async def create_pool_config( self, genesis_transactions: str, recreate: bool = False ): diff --git a/aries_cloudagent/protocols/revocation_notification/definition.py b/aries_cloudagent/protocols/revocation_notification/definition.py index 62bddef6f5..baf2b7b433 100644 --- a/aries_cloudagent/protocols/revocation_notification/definition.py +++ b/aries_cloudagent/protocols/revocation_notification/definition.py @@ -6,5 +6,11 @@ "minimum_minor_version": 0, "current_minor_version": 0, "path": "v1_0", - } + }, + { + "major_version": 2, + "minimum_minor_version": 0, + "current_minor_version": 0, + "path": "v2_0", + }, ] diff --git a/aries_cloudagent/protocols/revocation_notification/v1_0/models/rev_notification_record.py b/aries_cloudagent/protocols/revocation_notification/v1_0/models/rev_notification_record.py index eac5bd2cee..3b43b233ff 100644 --- a/aries_cloudagent/protocols/revocation_notification/v1_0/models/rev_notification_record.py +++ b/aries_cloudagent/protocols/revocation_notification/v1_0/models/rev_notification_record.py @@ -27,6 +27,7 @@ class Meta: "rev_reg_id", "cred_rev_id", "connection_id", + "version", } def __init__( @@ -38,6 +39,7 @@ def __init__( connection_id: str = None, thread_id: str = None, comment: str = None, + version: str = None, **kwargs, ): """Construct record.""" @@ -47,6 +49,7 @@ def __init__( self.connection_id = connection_id self.thread_id = thread_id self.comment = comment + self.version = version @property def revocation_notification_id(self) -> Optional[str]: @@ -73,6 +76,7 @@ async def query_by_ids( rev_reg_id: the rev reg id by which to filter """ tag_filter = { + **{"version": "v1_0"}, **{"cred_rev_id": cred_rev_id for _ in [""] if cred_rev_id}, **{"rev_reg_id": rev_reg_id for _ in [""] if rev_reg_id}, } @@ -101,6 +105,7 @@ async def query_by_rev_reg_id( rev_reg_id: the rev reg id by which to filter """ tag_filter = { + **{"version": "v1_0"}, **{"rev_reg_id": rev_reg_id for _ in [""] if rev_reg_id}, } @@ -157,3 +162,7 @@ class Meta: description="Optional comment to include in revocation notification", required=False, ) + version = fields.Str( + description="Version of Revocation Notification to send out", + required=False, + ) diff --git a/aries_cloudagent/protocols/revocation_notification/v1_0/models/tests/test_rev_notification_record.py b/aries_cloudagent/protocols/revocation_notification/v1_0/models/tests/test_rev_notification_record.py index c845f715ca..304ec37a90 100644 --- a/aries_cloudagent/protocols/revocation_notification/v1_0/models/tests/test_rev_notification_record.py +++ b/aries_cloudagent/protocols/revocation_notification/v1_0/models/tests/test_rev_notification_record.py @@ -21,6 +21,7 @@ def rec(): connection_id="mock_connection_id", thread_id="mock_thread_id", comment="mock_comment", + version="v1_0", ) @@ -50,6 +51,7 @@ async def test_storage(profile, rec): another = RevNotificationRecord( rev_reg_id="mock_rev_reg_id", cred_rev_id="mock_cred_rev_id", + version="v1_0", ) await another.save(session) await RevNotificationRecord.query_by_ids( diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/revoke_handler.py b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/revoke_handler.py new file mode 100644 index 0000000000..f2ffafe7e0 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/revoke_handler.py @@ -0,0 +1,44 @@ +"""Handler for revoke message.""" + +from .....messaging.base_handler import BaseHandler +from .....messaging.request_context import RequestContext +from .....messaging.responder import BaseResponder + +from ..messages.revoke import Revoke + + +class RevokeHandler(BaseHandler): + """Handler for revoke message.""" + + RECIEVED_TOPIC = "acapy::revocation-notification-v2::received" + WEBHOOK_TOPIC = "acapy::webhook::revocation-notification-v2" + + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle revoke message.""" + assert isinstance(context.message, Revoke) + self._logger.debug( + "Received notification of revocation for %s cred %s with comment: %s", + context.message.revocation_format, + context.message.credential_id, + context.message.comment, + ) + # Emit a webhook + if context.settings.get("revocation.monitor_notification"): + await context.profile.notify( + self.WEBHOOK_TOPIC, + { + "revocation_format": context.message.revocation_format, + "credential_id": context.message.credential_id, + "comment": context.message.comment, + }, + ) + + # Emit an event + await context.profile.notify( + self.RECIEVED_TOPIC, + { + "revocation_format": context.message.revocation_format, + "credential_id": context.message.credential_id, + "comment": context.message.comment, + }, + ) diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/test_revoke_handler.py b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/test_revoke_handler.py new file mode 100644 index 0000000000..a93a314e23 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/test_revoke_handler.py @@ -0,0 +1,75 @@ +"""Test RevokeHandler.""" + +import pytest + +from ......config.settings import Settings +from ......core.event_bus import EventBus, MockEventBus +from ......core.in_memory import InMemoryProfile +from ......core.profile import Profile +from ......messaging.request_context import RequestContext +from ......messaging.responder import MockResponder, BaseResponder +from ...messages.revoke import Revoke +from ..revoke_handler import RevokeHandler + + +@pytest.fixture +def event_bus(): + yield MockEventBus() + + +@pytest.fixture +def responder(): + yield MockResponder() + + +@pytest.fixture +def profile(event_bus): + yield InMemoryProfile.test_profile(bind={EventBus: event_bus}) + + +@pytest.fixture +def message(): + yield Revoke( + revocation_format="indy-anoncreds", + credential_id="mock_cred_revocation_id", + comment="mock_comment", + ) + + +@pytest.fixture +def context(profile: Profile, message: Revoke): + request_context = RequestContext(profile) + request_context.message = message + yield request_context + + +@pytest.mark.asyncio +async def test_handle( + context: RequestContext, responder: BaseResponder, event_bus: MockEventBus +): + await RevokeHandler().handle(context, responder) + assert event_bus.events + [(_, received)] = event_bus.events + assert received.topic == RevokeHandler.RECIEVED_TOPIC + assert "revocation_format" in received.payload + assert "credential_id" in received.payload + assert "comment" in received.payload + + +@pytest.mark.asyncio +async def test_handle_monitor( + context: RequestContext, responder: BaseResponder, event_bus: MockEventBus +): + context.settings["revocation.monitor_notification"] = True + await RevokeHandler().handle(context, responder) + [(_, webhook), (_, received)] = event_bus.events + + assert webhook.topic == RevokeHandler.WEBHOOK_TOPIC + assert "revocation_format" in received.payload + assert "credential_id" in received.payload + assert "comment" in webhook.payload + + assert received.topic == RevokeHandler.RECIEVED_TOPIC + assert "revocation_format" in received.payload + assert "credential_id" in received.payload + assert "comment" in received.payload diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/message_types.py b/aries_cloudagent/protocols/revocation_notification/v2_0/message_types.py new file mode 100644 index 0000000000..4033d5c8b7 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/message_types.py @@ -0,0 +1,20 @@ +"""Message type identifiers for Revocation Notification protocol.""" + +from ...didcomm_prefix import DIDCommPrefix + + +SPEC_URI = ( + "https://github.com/hyperledger/aries-rfcs/blob/main/features/" + "0721-revocation-notification-v2/README.md" +) +PROTOCOL = "revocation_notification" +VERSION = "2.0" +BASE = f"{PROTOCOL}/{VERSION}" + +# Message types +REVOKE = f"{BASE}/revoke" + +PROTOCOL_PACKAGE = "aries_cloudagent.protocols.revocation_notification.v2_0" +MESSAGE_TYPES = DIDCommPrefix.qualify_all( + {REVOKE: f"{PROTOCOL_PACKAGE}.messages.revoke.Revoke"} +) diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/messages/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py new file mode 100644 index 0000000000..93c0829a2a --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py @@ -0,0 +1,69 @@ +"""Revoke message.""" + +from marshmallow import fields, validate +from .....messaging.agent_message import AgentMessage, AgentMessageSchema +from .....messaging.decorators.please_ack_decorator import ( + PleaseAckDecorator, + PleaseAckDecoratorSchema, +) +from .....messaging.valid import UUIDFour +from ..message_types import PROTOCOL_PACKAGE, REVOKE + +HANDLER_CLASS = f"{PROTOCOL_PACKAGE}.handlers.revoke_handler.RevokeHandler" + + +class Revoke(AgentMessage): + """Class representing revoke message.""" + + class Meta: + """Revoke Meta.""" + + handler_class = HANDLER_CLASS + message_type = REVOKE + schema_class = "RevokeSchema" + + def __init__( + self, + *, + revocation_format: str, + credential_id: str, + please_ack: PleaseAckDecorator = None, + comment: str = None, + **kwargs, + ): + """Initialize revoke message.""" + super().__init__(**kwargs) + self.revocation_format = revocation_format + self.credential_id = credential_id + self.comment = comment + + +class RevokeSchema(AgentMessageSchema): + """Schema of Revoke message.""" + + class Meta: + """RevokeSchema Meta.""" + + model_class = Revoke + + revocation_format = fields.Str( + required=True, + description=("The format of the credential revocation ID"), + example="indy-anoncreds", + validate=validate.OneOf(["indy-anoncreds"]), + ) + credential_id = fields.Str( + required=True, + description=("Credential ID of the issued credential to be revoked"), + example=UUIDFour.EXAMPLE, + ) + please_ack = fields.Nested( + PleaseAckDecoratorSchema, + required=False, + description="Whether or not the holder should acknowledge receipt", + data_key="~please_ack", + ) + comment = fields.Str( + required=False, + description="Human readable information about revocation notification", + ) diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/test_revoke.py b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/test_revoke.py new file mode 100644 index 0000000000..c20c93ee1a --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/test_revoke.py @@ -0,0 +1,14 @@ +"""Test Revoke Message.""" + +from ..revoke import Revoke + + +def test_instantiate(): + msg = Revoke( + revocation_format="indy-anoncreds", + credential_id="test-id", + comment="test", + ) + assert msg.revocation_format == "indy-anoncreds" + assert msg.credential_id == "test-id" + assert msg.comment == "test" diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/models/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/models/rev_notification_record.py b/aries_cloudagent/protocols/revocation_notification/v2_0/models/rev_notification_record.py new file mode 100644 index 0000000000..b91cc74967 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/models/rev_notification_record.py @@ -0,0 +1,169 @@ +"""Store revocation notification details until revocation is published.""" + +from typing import Optional, Sequence + +from marshmallow import fields +from marshmallow.utils import EXCLUDE + + +from .....core.profile import ProfileSession +from .....messaging.models.base_record import BaseRecord, BaseRecordSchema +from .....messaging.valid import INDY_CRED_REV_ID, INDY_REV_REG_ID, UUID4 +from .....storage.error import StorageNotFoundError, StorageDuplicateError +from ..messages.revoke import Revoke + + +class RevNotificationRecord(BaseRecord): + """Revocation Notification Record.""" + + class Meta: + """RevNotificationRecord Meta.""" + + schema_class = "RevNotificationRecordSchema" + + RECORD_TYPE = "revocation_notification" + RECORD_ID_NAME = "revocation_notification_id" + TAG_NAMES = { + "rev_reg_id", + "cred_rev_id", + "connection_id", + "version", + } + + def __init__( + self, + *, + revocation_notification_id: str = None, + rev_reg_id: str = None, + cred_rev_id: str = None, + connection_id: str = None, + thread_id: str = None, + comment: str = None, + version: str = None, + **kwargs, + ): + """Construct record.""" + super().__init__(revocation_notification_id, **kwargs) + self.rev_reg_id = rev_reg_id + self.cred_rev_id = cred_rev_id + self.connection_id = connection_id + self.thread_id = thread_id + self.comment = comment + self.version = version + + @property + def revocation_notification_id(self) -> Optional[str]: + """Return record id.""" + return self._id + + @property + def record_value(self) -> dict: + """Return record value.""" + return {prop: getattr(self, prop) for prop in ("thread_id", "comment")} + + @classmethod + async def query_by_ids( + cls, + session: ProfileSession, + cred_rev_id: str, + rev_reg_id: str, + ) -> "RevNotificationRecord": + """Retrieve revocation notification record by cred rev id and/or rev reg id. + + Args: + session: the profile session to use + cred_rev_id: the cred rev id by which to filter + rev_reg_id: the rev reg id by which to filter + """ + tag_filter = { + **{"version": "v2_0"}, + **{"cred_rev_id": cred_rev_id for _ in [""] if cred_rev_id}, + **{"rev_reg_id": rev_reg_id for _ in [""] if rev_reg_id}, + } + + result = await cls.query(session, tag_filter) + if len(result) > 1: + raise StorageDuplicateError( + "More than one RevNotificationRecord was found for the given IDs" + ) + if not result: + raise StorageNotFoundError( + "No RevNotificationRecord found for the given IDs" + ) + return result[0] + + @classmethod + async def query_by_rev_reg_id( + cls, + session: ProfileSession, + rev_reg_id: str, + ) -> Sequence["RevNotificationRecord"]: + """Retrieve revocation notification records by rev reg id. + + Args: + session: the profile session to use + rev_reg_id: the rev reg id by which to filter + """ + tag_filter = { + **{"version": "v2_0"}, + **{"rev_reg_id": rev_reg_id for _ in [""] if rev_reg_id}, + } + + return await cls.query(session, tag_filter) + + def to_message(self): + """Return a revocation notification constructed from this record.""" + if not self.thread_id: + raise ValueError( + "No thread ID set on revocation notification record, " + "cannot create message" + ) + return Revoke( + revocation_format="indy-anoncreds", + credential_id=f"{self.rev_reg_id}::{self.cred_rev_id}", + comment=self.comment, + ) + + +class RevNotificationRecordSchema(BaseRecordSchema): + """Revocation Notification Record Schema.""" + + class Meta: + """RevNotificationRecordSchema Meta.""" + + model_class = "RevNotificationRecord" + unknown = EXCLUDE + + rev_reg_id = fields.Str( + required=False, + description="Revocation registry identifier", + **INDY_REV_REG_ID, + ) + cred_rev_id = fields.Str( + required=False, + description="Credential revocation identifier", + **INDY_CRED_REV_ID, + ) + connection_id = fields.Str( + description=( + "Connection ID to which the revocation notification will be sent; " + "required if notify is true" + ), + required=False, + **UUID4, + ) + thread_id = fields.Str( + description=( + "Thread ID of the credential exchange message thread resulting in " + "the credential now being revoked; required if notify is true" + ), + required=False, + ) + comment = fields.Str( + description="Optional comment to include in revocation notification", + required=False, + ) + version = fields.Str( + description="Version of Revocation Notification to send out", + required=False, + ) diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/test_rev_notification_record.py b/aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/test_rev_notification_record.py new file mode 100644 index 0000000000..e6bb64e5c7 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/test_rev_notification_record.py @@ -0,0 +1,70 @@ +"""Test RevNotificationRecord.""" + +import pytest + +from ......core.in_memory import InMemoryProfile +from ......storage.error import StorageDuplicateError, StorageNotFoundError +from ...messages.revoke import Revoke +from ..rev_notification_record import RevNotificationRecord + + +@pytest.fixture +def profile(): + yield InMemoryProfile.test_profile() + + +@pytest.fixture +def rec(): + yield RevNotificationRecord( + rev_reg_id="mock_rev_reg_id", + cred_rev_id="mock_cred_rev_id", + connection_id="mock_connection_id", + thread_id="mock_thread_id", + comment="mock_comment", + version="v2_0", + ) + + +@pytest.mark.asyncio +async def test_storage(profile, rec): + async with profile.session() as session: + await rec.save(session) + recalled = await RevNotificationRecord.retrieve_by_id( + session, rec.revocation_notification_id + ) + assert recalled == rec + recalled = await RevNotificationRecord.query_by_ids( + session, cred_rev_id="mock_cred_rev_id", rev_reg_id="mock_rev_reg_id" + ) + assert recalled == rec + [recalled] = await RevNotificationRecord.query_by_rev_reg_id( + session, rev_reg_id="mock_rev_reg_id" + ) + assert recalled == rec + + with pytest.raises(StorageNotFoundError): + await RevNotificationRecord.query_by_ids( + session, cred_rev_id="unknown", rev_reg_id="unknown" + ) + + with pytest.raises(StorageDuplicateError): + another = RevNotificationRecord( + rev_reg_id="mock_rev_reg_id", + cred_rev_id="mock_cred_rev_id", + version="v2_0", + ) + await another.save(session) + await RevNotificationRecord.query_by_ids( + session, cred_rev_id="mock_cred_rev_id", rev_reg_id="mock_rev_reg_id" + ) + + +def test_to_message(rec): + message = rec.to_message() + assert isinstance(message, Revoke) + assert message.credential_id == f"{rec.rev_reg_id}::{rec.cred_rev_id}" + assert message.comment == rec.comment + + with pytest.raises(ValueError): + rec.thread_id = None + rec.to_message() diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/routes.py b/aries_cloudagent/protocols/revocation_notification/v2_0/routes.py new file mode 100644 index 0000000000..83ba81fe63 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/routes.py @@ -0,0 +1,76 @@ +"""Routes for revocation notification.""" +import logging +import re + +from ....core.event_bus import Event, EventBus +from ....core.profile import Profile +from ....messaging.responder import BaseResponder +from ....revocation.util import ( + REVOCATION_CLEAR_PENDING_EVENT, + REVOCATION_PUBLISHED_EVENT, + REVOCATION_EVENT_PREFIX, +) +from ....storage.error import StorageError, StorageNotFoundError +from .models.rev_notification_record import RevNotificationRecord + +LOGGER = logging.getLogger(__name__) + + +def register_events(event_bus: EventBus): + """Register to handle events.""" + event_bus.subscribe( + re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_PUBLISHED_EVENT}.*"), + on_revocation_published, + ) + event_bus.subscribe( + re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_CLEAR_PENDING_EVENT}.*"), + on_pending_cleared, + ) + + +async def on_revocation_published(profile: Profile, event: Event): + """Handle issuer revoke event.""" + LOGGER.debug("Sending notification of revocation to recipient: %s", event.payload) + + should_notify = profile.settings.get("revocation.notify", False) + responder = profile.inject(BaseResponder) + crids = event.payload.get("crids") or [] + + try: + async with profile.session() as session: + records = await RevNotificationRecord.query_by_rev_reg_id( + session, + rev_reg_id=event.payload["rev_reg_id"], + ) + records = [record for record in records if record.cred_rev_id in crids] + + for record in records: + await record.delete_record(session) + if should_notify: + await responder.send( + record.to_message(), connection_id=record.connection_id + ) + + except StorageNotFoundError: + LOGGER.info( + "No revocation notification record found for revoked credential; " + "no notification will be sent" + ) + except StorageError: + LOGGER.exception("Failed to retrieve revocation notification record") + + +async def on_pending_cleared(profile: Profile, event: Event): + """Handle pending cleared event.""" + + # Query by rev reg ID + async with profile.session() as session: + notifications = await RevNotificationRecord.query_by_rev_reg_id( + session, event.payload["rev_reg_id"] + ) + + # Delete + async with profile.transaction() as txn: + for notification in notifications: + await notification.delete_record(txn) + await txn.commit() diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/tests/__init__.py b/aries_cloudagent/protocols/revocation_notification/v2_0/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/revocation_notification/v2_0/tests/test_routes.py new file mode 100644 index 0000000000..6fe38c848b --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/tests/test_routes.py @@ -0,0 +1,140 @@ +"""Test routes.py""" +from asynctest import mock +import pytest + +from .. import routes as test_module +from .....config.settings import Settings +from .....core.event_bus import Event, MockEventBus +from .....core.in_memory import InMemoryProfile +from .....core.profile import Profile +from .....messaging.responder import BaseResponder, MockResponder +from .....revocation.util import ( + REVOCATION_CLEAR_PENDING_EVENT, + REVOCATION_EVENT_PREFIX, + REVOCATION_PUBLISHED_EVENT, +) +from .....storage.error import StorageError, StorageNotFoundError + + +@pytest.fixture +def responder(): + yield MockResponder() + + +@pytest.fixture +def profile(responder): + yield InMemoryProfile.test_profile(bind={BaseResponder: responder}) + + +def test_register_events(): + """Test handlers are added on register. + + This test need not be particularly in depth to keep it from getting brittle. + """ + event_bus = MockEventBus() + test_module.register_events(event_bus) + assert event_bus.topic_patterns_to_subscribers + + +@pytest.mark.asyncio +async def test_on_revocation_published(profile: Profile, responder: MockResponder): + """Test revocation published event handler.""" + mock_rec = mock.MagicMock() + mock_rec.cred_rev_id = "mock" + mock_rec.delete_record = mock.CoroutineMock() + + MockRec = mock.MagicMock() + MockRec.query_by_rev_reg_id = mock.CoroutineMock(return_value=[mock_rec]) + + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_PUBLISHED_EVENT}::mock" + event = Event(topic, {"rev_reg_id": "mock", "crids": ["mock"]}) + + assert isinstance(profile.settings, Settings) + profile.settings["revocation.notify"] = True + + with mock.patch.object(test_module, "RevNotificationRecord", MockRec): + await test_module.on_revocation_published(profile, event) + + MockRec.query_by_rev_reg_id.assert_called_once() + mock_rec.delete_record.assert_called_once() + assert responder.messages + + +@pytest.mark.asyncio +async def test_on_revocation_published_no_notify( + profile: Profile, responder: MockResponder +): + """Test revocation published event handler.""" + mock_rec = mock.MagicMock() + mock_rec.cred_rev_id = "mock" + mock_rec.delete_record = mock.CoroutineMock() + + MockRec = mock.MagicMock() + MockRec.query_by_rev_reg_id = mock.CoroutineMock(return_value=[mock_rec]) + + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_PUBLISHED_EVENT}::mock" + event = Event(topic, {"rev_reg_id": "mock", "crids": ["mock"]}) + + assert isinstance(profile.settings, Settings) + profile.settings["revocation.notify"] = False + + with mock.patch.object(test_module, "RevNotificationRecord", MockRec): + await test_module.on_revocation_published(profile, event) + + MockRec.query_by_rev_reg_id.assert_called_once() + mock_rec.delete_record.assert_called_once() + assert not responder.messages + + +@pytest.mark.asyncio +async def test_on_revocation_published_x_not_found( + profile: Profile, responder: MockResponder +): + """Test revocation published event handler.""" + MockRec = mock.MagicMock() + MockRec.query_by_rev_reg_id = mock.CoroutineMock(side_effect=StorageNotFoundError) + + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_PUBLISHED_EVENT}::mock" + event = Event(topic, {"rev_reg_id": "mock", "crids": ["mock"]}) + + with mock.patch.object(test_module, "RevNotificationRecord", MockRec): + await test_module.on_revocation_published(profile, event) + + MockRec.query_by_rev_reg_id.assert_called_once() + assert not responder.messages + + +@pytest.mark.asyncio +async def test_on_revocation_published_x_storage_error( + profile: Profile, responder: MockResponder +): + """Test revocation published event handler.""" + MockRec = mock.MagicMock() + MockRec.query_by_rev_reg_id = mock.CoroutineMock(side_effect=StorageError) + + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_PUBLISHED_EVENT}::mock" + event = Event(topic, {"rev_reg_id": "mock", "crids": ["mock"]}) + + with mock.patch.object(test_module, "RevNotificationRecord", MockRec): + await test_module.on_revocation_published(profile, event) + + MockRec.query_by_rev_reg_id.assert_called_once() + assert not responder.messages + + +@pytest.mark.asyncio +async def test_on_pending_cleared(profile: Profile): + """Test pending revocation cleared event.""" + mock_rec = mock.MagicMock() + mock_rec.delete_record = mock.CoroutineMock() + + MockRec = mock.MagicMock() + MockRec.query_by_rev_reg_id = mock.CoroutineMock(return_value=[mock_rec]) + + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_CLEAR_PENDING_EVENT}::mock" + event = Event(topic, {"rev_reg_id": "mock"}) + + with mock.patch.object(test_module, "RevNotificationRecord", MockRec): + await test_module.on_pending_cleared(profile, event) + + mock_rec.delete_record.assert_called_once() diff --git a/aries_cloudagent/revocation/manager.py b/aries_cloudagent/revocation/manager.py index 4bdf222655..4ee333620b 100644 --- a/aries_cloudagent/revocation/manager.py +++ b/aries_cloudagent/revocation/manager.py @@ -45,6 +45,7 @@ async def revoke_credential_by_cred_ex_id( cred_ex_id: str, publish: bool = False, notify: bool = False, + notify_version: str = None, thread_id: str = None, connection_id: str = None, comment: str = None, @@ -77,6 +78,7 @@ async def revoke_credential_by_cred_ex_id( cred_rev_id=rec.cred_rev_id, publish=publish, notify=notify, + notify_version=notify_version, thread_id=thread_id, connection_id=connection_id, comment=comment, @@ -88,6 +90,7 @@ async def revoke_credential( cred_rev_id: str, publish: bool = False, notify: bool = False, + notify_version: str = None, thread_id: str = None, connection_id: str = None, comment: str = None, @@ -121,6 +124,7 @@ async def revoke_credential( thread_id=thread_id, connection_id=connection_id, comment=comment, + version=notify_version, ) async with self._profile.session() as session: await rev_notify_rec.save(session, reason="New revocation notification") diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index ea2f83427a..0db51587c4 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -157,11 +157,16 @@ def validate_fields(self, data, **kwargs): notify = data.get("notify") connection_id = data.get("connection_id") + notify_version = data.get("notify_version", "v1_0") if notify and not connection_id: raise ValidationError( "Request must specify connection_id if notify is true" ) + if notify and not notify_version: + raise ValidationError( + "Request must specify notify_version if notify is true" + ) publish = fields.Boolean( description=( @@ -174,6 +179,11 @@ def validate_fields(self, data, **kwargs): description="Send a notification to the credential recipient", required=False, ) + notify_version = fields.String( + description="Specify which version of the revocation notification should be sent", + validate=validate.OneOf(["v1_0", "v2_0"]), + required=False, + ) connection_id = fields.Str( description=( "Connection ID to which the revocation notification will be sent; " @@ -377,9 +387,14 @@ async def revoke(request: web.BaseRequest): body["notify"] = body.get("notify", context.settings.get("revocation.notify")) notify = body.get("notify") connection_id = body.get("connection_id") + notify_version = body.get("notify_version", "v1_0") if notify and not connection_id: raise web.HTTPBadRequest(reason="connection_id must be set when notify is true") + if notify and not notify_version: + raise web.HTTPBadRequest( + reason="Request must specify notify_version if notify is true" + ) rev_manager = RevocationManager(context.profile) try: diff --git a/aries_cloudagent/tails/indy_tails_server.py b/aries_cloudagent/tails/indy_tails_server.py index a7aee90a58..9f07970a42 100644 --- a/aries_cloudagent/tails/indy_tails_server.py +++ b/aries_cloudagent/tails/indy_tails_server.py @@ -1,6 +1,7 @@ """Indy tails server interface class.""" from typing import Tuple +import logging from ..config.injection_context import InjectionContext from ..ledger.multiple_ledger.base_manager import BaseMultipleLedgerManager @@ -10,6 +11,9 @@ from .error import TailsServerNotConfiguredError +LOGGER = logging.getLogger(__name__) + + class IndyTailsServer(BaseTailsServer): """Indy tails server interface.""" @@ -38,12 +42,16 @@ async def upload_tails_file( if not genesis_transactions: ledger_manager = context.injector.inject(BaseMultipleLedgerManager) write_ledgers = await ledger_manager.get_write_ledger() + LOGGER.debug(f"write_ledgers = {write_ledgers}") pool = write_ledgers[1].pool + LOGGER.debug(f"write_ledger pool = {pool}") + + genesis_transactions = pool.genesis_txns - try: - genesis_transactions = pool.genesis_transactions - except AttributeError: - genesis_transactions = pool.genesis_txns_cache + if not genesis_transactions: + raise TailsServerNotConfiguredError( + "no genesis_transactions for writable ledger" + ) if not tails_server_upload_url: raise TailsServerNotConfiguredError( diff --git a/aries_cloudagent/transport/outbound/manager.py b/aries_cloudagent/transport/outbound/manager.py index 87d1fe0d94..ac2c54dd49 100644 --- a/aries_cloudagent/transport/outbound/manager.py +++ b/aries_cloudagent/transport/outbound/manager.py @@ -255,6 +255,7 @@ async def enqueue_message(self, profile: Profile, outbound: OutboundMessage): outbound: The outbound message to deliver """ targets = [outbound.target] if outbound.target else (outbound.target_list or []) + transport_id = None for target in targets: endpoint = target.endpoint try: diff --git a/aries_cloudagent/vc/ld_proofs/document_loader.py b/aries_cloudagent/vc/ld_proofs/document_loader.py index 2f4eab5e4e..3c35d7d985 100644 --- a/aries_cloudagent/vc/ld_proofs/document_loader.py +++ b/aries_cloudagent/vc/ld_proofs/document_loader.py @@ -14,6 +14,10 @@ from .error import LinkedDataProofException +import nest_asyncio + +nest_asyncio.apply() + class DocumentLoader: """JSON-LD document loader.""" @@ -32,6 +36,7 @@ def __init__(self, profile: Profile, cache_ttl: int = 300) -> None: self.requests_loader = requests.requests_document_loader() self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) self.cache_ttl = cache_ttl + self._event_loop = asyncio.get_event_loop() async def _load_did_document(self, did: str, options: dict): # Resolver expects plain did without path, query, etc... @@ -59,14 +64,6 @@ def _load_http_document(self, url: str, options: dict): async def _load_async(self, url: str, options: dict): """Retrieve http(s) or did document.""" - cache_key = f"json_ld_document_resolver::{url}" - - # Try to get from cache - if self.cache: - document = await self.cache.get(cache_key) - if document: - return document - # Resolve DIDs using did resolver if url.startswith("did:"): document = await self._load_did_document(url, options) @@ -78,22 +75,9 @@ async def _load_async(self, url: str, options: dict): "'did:', 'http://' or 'https://'" ) - # Cache document, if cache is available - if self.cache: - await self.cache.set(cache_key, document, self.cache_ttl) - return document - def _load_sync(self, url: str, options: dict): - """Run document loader in event loop to make it async. - - NOTE: This should be called in a thread where an event loop is not already - running, such as a new thread. - """ - loop = asyncio.new_event_loop() - return loop.run_until_complete(self._load_async(url, options)) - - def load_document(self, url: str, options: dict): + async def load_document(self, url: str, options: dict): """Load JSON-LD document. Method signature conforms to PyLD document loader interface @@ -101,13 +85,30 @@ def load_document(self, url: str, options: dict): Document loading is processed in separate thread to deal with async to sync transformation. """ - future = self.executor.submit(self._load_sync, url, options) - return future.result() + cache_key = f"json_ld_document_resolver::{url}" + + # Try to get from cache + if self.cache: + document = await self.cache.get(cache_key) + if document: + return document + + document = await self._load_async(url, options) + + # Cache document, if cache is available + if self.cache: + await self.cache.set(cache_key, document, self.cache_ttl) + + return document def __call__(self, url: str, options: dict): """Load JSON-LD Document.""" - return self.load_document(url, options) + loop = self._event_loop + coroutine = self.load_document(url, options) + document = loop.run_until_complete(coroutine) + + return document DocumentLoaderMethod = Callable[[str, dict], dict] diff --git a/aries_cloudagent/version.py b/aries_cloudagent/version.py index ddabac341b..e6a19d3978 100644 --- a/aries_cloudagent/version.py +++ b/aries_cloudagent/version.py @@ -1,4 +1,4 @@ """Library version information.""" -__version__ = "0.7.4-rc1" +__version__ = "0.7.4-rc2" RECORD_TYPE_ACAPY_VERSION = "acapy_version" diff --git a/demo/docker/docker-compose.yml b/demo/docker/docker-compose.yml new file mode 100644 index 0000000000..94c4061fa6 --- /dev/null +++ b/demo/docker/docker-compose.yml @@ -0,0 +1,62 @@ +# Sample docker-compose to start a local aca-py in multi-ledger mode +# To start aca-py and the postgres database, just run `docker-compose up` +# To shut down the services run `docker-compose rm` - this will retain the postgres database, so you can change aca-py startup parameters +# and restart the docker containers without losing your wallet data +# If you want to delete your wallet data just run `docker volume ls -q | xargs docker volume rm` +version: "3" +services: + vcr-agent: + image: bcgovimages/aries-cloudagent:py36-1.16-1_0.7.3 + ports: + - 8010:8010 + depends_on: + - wallet-db + entrypoint: /bin/bash + command: [ + "-c", + "sleep 5; \ + aca-py start \ + --auto-provision \ + --inbound-transport http '0.0.0.0' 8001 \ + --endpoint 'http://host.docker.internal:8001' \ + --outbound-transport http \ + --genesis-transactions-list 'ledgers.yaml' + --auto-accept-invites \ + --auto-accept-requests \ + --auto-ping-connection \ + --auto-respond-messages \ + --auto-respond-credential-proposal \ + --auto-respond-credential-offer \ + --auto-respond-credential-request \ + --auto-verify-presentation \ + --wallet-type 'indy' \ + --wallet-name 'acapy_agent_wallet' \ + --wallet-key 'key' \ + --wallet-storage-type 'postgres_storage' \ + --wallet-storage-config '{\"url\":\"wallet-db:5432\",\"max_connections\":5}' \ + --wallet-storage-creds '{\"account\":\"DB_USER\",\"password\":\"DB_PASSWORD\",\"admin_account\":\"postgres\",\"admin_password\":\"mysecretpassword\"}' \ + --admin '0.0.0.0' 8010 \ + --admin-insecure-mode \ + --label 'tester_agent' \ + --log-level 'info' ", + ] + volumes: + - ./ledgers.yaml:/home/indy/ledgers.yaml + +# note - if you want to start aca-py in single-ledger mode, replace the `--genesis-transactions-list` parameter above with: +# --genesis-url 'https://raw.githubusercontent.com/sovrin-foundation/sovrin/master/sovrin/pool_transactions_sandbox_genesis' \ + + wallet-db: + image: vcr-postgresql + environment: + - POSTGRESQL_USER=DB_USER + - POSTGRESQL_PASSWORD=DB_PASSWORD + - POSTGRESQL_DATABASE=DB_USER + - POSTGRESQL_ADMIN_PASSWORD=mysecretpassword + ports: + - 5433:5432 + volumes: + - wallet-db-data:/var/lib/pgsql/data + +volumes: + wallet-db-data: diff --git a/demo/docker/ledgers.yaml b/demo/docker/ledgers.yaml new file mode 100644 index 0000000000..baf174d35e --- /dev/null +++ b/demo/docker/ledgers.yaml @@ -0,0 +1,16 @@ +# the `id` is used as the `pool_name` in aca-py +# note that if you are upgrading from single- to multi-ledger, you need to *either*: +# - set the `id` of your `is_write: true` ledger to `default` (the `pool_name` used in single-ledger mode) +# *or*: +# - re-accept the TAA once you start aca-py in multi-ledger mode +# (the TAA acceptance is stored in a wallet record keyed on the `pool_name`) +- id: SOVRINSandbox + is_production: true + is_write: true + genesis_url: 'https://raw.githubusercontent.com/sovrin-foundation/sovrin/stable/sovrin/pool_transactions_sandbox_genesis' +- id: BCovrinTest + is_production: true + genesis_url: 'http://test.bcovrin.vonx.io/genesis' +- id: CANdyDev + is_production: true + genesis_url: 'https://raw.githubusercontent.com/ICCS-ISAC/dtrust-reconu/main/CANdy/dev/pool_transactions_genesis' diff --git a/docs/generated/aries_cloudagent.transport.outbound.queue.rst b/docs/generated/aries_cloudagent.transport.outbound.queue.rst deleted file mode 100644 index b23440d0dc..0000000000 --- a/docs/generated/aries_cloudagent.transport.outbound.queue.rst +++ /dev/null @@ -1,34 +0,0 @@ -aries\_cloudagent.transport.outbound.queue package -================================================== - -.. automodule:: aries_cloudagent.transport.outbound.queue - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -aries\_cloudagent.transport.outbound.queue.base module ------------------------------------------------------- - -.. automodule:: aries_cloudagent.transport.outbound.queue.base - :members: - :undoc-members: - :show-inheritance: - -aries\_cloudagent.transport.outbound.queue.loader module --------------------------------------------------------- - -.. automodule:: aries_cloudagent.transport.outbound.queue.loader - :members: - :undoc-members: - :show-inheritance: - -aries\_cloudagent.transport.outbound.queue.redis module -------------------------------------------------------- - -.. automodule:: aries_cloudagent.transport.outbound.queue.redis - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/generated/aries_cloudagent.transport.outbound.rst b/docs/generated/aries_cloudagent.transport.outbound.rst index eb749ee452..1fde7f0379 100644 --- a/docs/generated/aries_cloudagent.transport.outbound.rst +++ b/docs/generated/aries_cloudagent.transport.outbound.rst @@ -6,14 +6,6 @@ aries\_cloudagent.transport.outbound package :undoc-members: :show-inheritance: -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - - aries_cloudagent.transport.outbound.queue - Submodules ---------- diff --git a/open-api/openapi.json b/open-api/openapi.json index f6a0451648..2db2fad8c6 100644 --- a/open-api/openapi.json +++ b/open-api/openapi.json @@ -1,7 +1,7 @@ { "swagger" : "2.0", "info" : { - "version" : "v0.7.4-rc1", + "version" : "v0.7.4-rc2", "title" : "Aries Cloud Agent" }, "tags" : [ { diff --git a/requirements.txt b/requirements.txt index 2429236466..3a7518e3b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ aiohttp-apispec~=2.2.1 aiohttp-cors~=0.7.0 apispec~=3.3.0 async-timeout~=4.0.2 +nest_asyncio~=1.5.5 aioredis~=2.0.0 base58~=2.1.0 deepmerge~=0.3.0