From e3f3466f2fb0b2f68c827796083aff3e77a6e8af Mon Sep 17 00:00:00 2001 From: "Colton Wolkins (Indicio work address)" Date: Fri, 18 Mar 2022 11:16:34 -0600 Subject: [PATCH 1/7] feat: Revocation Notification V2 I believe this has been implemented as per RFC 0721 from this PR https://github.com/hyperledger/aries-rfcs/pull/721 Signed-off-by: Colton Wolkins (Indicio work address) --- .../revocation_notification/definition.py | 8 +- .../revocation_notification/v2_0/__init__.py | 0 .../v2_0/handlers/__init__.py | 0 .../v2_0/handlers/revoke_handler.py | 45 +++++ .../v2_0/handlers/tests/__init__.py | 0 .../handlers/tests/test_revoke_handler.py | 71 ++++++++ .../v2_0/message_types.py | 20 +++ .../v2_0/messages/__init__.py | 0 .../v2_0/messages/revoke.py | 73 ++++++++ .../v2_0/messages/tests/__init__.py | 0 .../v2_0/messages/tests/test_revoke.py | 10 ++ .../v2_0/models/__init__.py | 0 .../v2_0/models/rev_notification_record.py | 160 ++++++++++++++++++ .../v2_0/models/tests/__init__.py | 0 .../tests/test_rev_notification_record.py | 68 ++++++++ .../revocation_notification/v2_0/routes.py | 76 +++++++++ .../v2_0/tests/__init__.py | 0 .../v2_0/tests/test_routes.py | 140 +++++++++++++++ 18 files changed, 670 insertions(+), 1 deletion(-) create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/__init__.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/handlers/__init__.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/handlers/revoke_handler.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/__init__.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/test_revoke_handler.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/message_types.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/messages/__init__.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/__init__.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/test_revoke.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/models/__init__.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/models/rev_notification_record.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/__init__.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/test_rev_notification_record.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/routes.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/tests/__init__.py create mode 100644 aries_cloudagent/protocols/revocation_notification/v2_0/tests/test_routes.py 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/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..2e953ed8ae --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/revoke_handler.py @@ -0,0 +1,45 @@ +"""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..8d5b2357c5 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/tests/test_revoke_handler.py @@ -0,0 +1,71 @@ +"""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..672bf67871 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py @@ -0,0 +1,73 @@ +"""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..14ec6613d6 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/tests/test_revoke.py @@ -0,0 +1,10 @@ +"""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..a95643c2dc --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/models/rev_notification_record.py @@ -0,0 +1,160 @@ +"""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", + } + + 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, + **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 + + @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 = { + **{"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 = { + **{"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="self.revocation_format", + credential_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, + ) 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..b4b9cee438 --- /dev/null +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/models/tests/test_rev_notification_record.py @@ -0,0 +1,68 @@ +"""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", + ) + + +@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", + ) + 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 == 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() From c822250a490e085ae4ee027861c6e93c2d67eee5 Mon Sep 17 00:00:00 2001 From: "Colton Wolkins (Indicio work address)" Date: Tue, 12 Apr 2022 06:17:17 -0600 Subject: [PATCH 2/7] fix: Fix output of Revoc Notif V2 Fix the output message for Revocation Notification V2 Signed-off-by: Colton Wolkins (Indicio work address) --- .../v2_0/models/rev_notification_record.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index a95643c2dc..f1a913a727 100644 --- 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 @@ -114,8 +114,8 @@ def to_message(self): "cannot create message" ) return Revoke( - revocation_format="self.revocation_format", - credential_id=self.cred_rev_id, + revocation_format="indy-anoncreds", + credential_id=f"{self.rev_reg_id}::{self.cred_rev_id}", comment=self.comment, ) From 26ccb0d3169f3ce88f761e9f4c78a00857f118c7 Mon Sep 17 00:00:00 2001 From: "Colton Wolkins (Indicio work address)" Date: Tue, 19 Apr 2022 08:40:35 -0600 Subject: [PATCH 3/7] ci: Fix black formatting errors Signed-off-by: Colton Wolkins (Indicio work address) --- .../v2_0/handlers/revoke_handler.py | 3 +-- .../v2_0/handlers/tests/test_revoke_handler.py | 6 +++++- .../revocation_notification/v2_0/messages/revoke.py | 8 ++------ .../v2_0/messages/tests/test_revoke.py | 6 +++++- 4 files changed, 13 insertions(+), 10 deletions(-) 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 index 2e953ed8ae..f2ffafe7e0 100644 --- a/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/revoke_handler.py +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/handlers/revoke_handler.py @@ -17,8 +17,7 @@ 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", + "Received notification of revocation for %s cred %s with comment: %s", context.message.revocation_format, context.message.credential_id, context.message.comment, 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 index 8d5b2357c5..a93a314e23 100644 --- 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 @@ -29,7 +29,11 @@ def profile(event_bus): @pytest.fixture def message(): - yield Revoke(revocation_format="indy-anoncreds", credential_id="mock_cred_revocation_id", comment="mock_comment") + yield Revoke( + revocation_format="indy-anoncreds", + credential_id="mock_cred_revocation_id", + comment="mock_comment", + ) @pytest.fixture diff --git a/aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py index 672bf67871..93c0829a2a 100644 --- a/aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py +++ b/aries_cloudagent/protocols/revocation_notification/v2_0/messages/revoke.py @@ -48,17 +48,13 @@ class Meta: revocation_format = fields.Str( required=True, - description=( - "The format of the credential revocation ID" - ), + 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" - ), + description=("Credential ID of the issued credential to be revoked"), example=UUIDFour.EXAMPLE, ) please_ack = fields.Nested( 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 index 14ec6613d6..c20c93ee1a 100644 --- 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 @@ -4,7 +4,11 @@ def test_instantiate(): - msg = Revoke(revocation_format="indy-anoncreds", credential_id="test-id", comment="test") + 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" From 46d402ebd2aa18fd2c425d7c21a3fca2c2834950 Mon Sep 17 00:00:00 2001 From: "Colton Wolkins (Indicio work address)" Date: Tue, 19 Apr 2022 09:21:06 -0600 Subject: [PATCH 4/7] ci: Fix unit test Signed-off-by: Colton Wolkins (Indicio work address) --- .../v2_0/models/tests/test_rev_notification_record.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index b4b9cee438..e06be17091 100644 --- 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 @@ -60,7 +60,7 @@ async def test_storage(profile, rec): def test_to_message(rec): message = rec.to_message() assert isinstance(message, Revoke) - assert message.credential_id == rec.cred_rev_id + assert message.credential_id == f"{rec.rev_reg_id}::{rec.cred_rev_id}" assert message.comment == rec.comment with pytest.raises(ValueError): From 373786f0a53e18c986f9270d747c60d183eba67e Mon Sep 17 00:00:00 2001 From: "Colton Wolkins (Indicio work address)" Date: Thu, 26 May 2022 09:04:03 -0600 Subject: [PATCH 5/7] feat: Add required notify_version API parameter to revocation Based upon discussion in #1734, a new required parameter for revocations has been added to the revocation API Signed-off-by: Colton Wolkins (Indicio work address) --- .../v1_0/models/rev_notification_record.py | 6 ++++++ .../v2_0/models/rev_notification_record.py | 6 ++++++ aries_cloudagent/revocation/manager.py | 4 ++++ aries_cloudagent/revocation/routes.py | 15 +++++++++++++++ 4 files changed, 31 insertions(+) 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..4468915d6a 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 @@ -38,6 +38,7 @@ def __init__( connection_id: str = None, thread_id: str = None, comment: str = None, + version: str = None, **kwargs, ): """Construct record.""" @@ -47,6 +48,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]: @@ -157,3 +159,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/v2_0/models/rev_notification_record.py b/aries_cloudagent/protocols/revocation_notification/v2_0/models/rev_notification_record.py index f1a913a727..e6db241bbd 100644 --- 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 @@ -38,6 +38,7 @@ def __init__( connection_id: str = None, thread_id: str = None, comment: str = None, + version: str = None, **kwargs, ): """Construct record.""" @@ -47,6 +48,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]: @@ -158,3 +160,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/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..39a4678d51 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") 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") 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: From 223f460cbf992d7f3260fa7d551cae808e88b403 Mon Sep 17 00:00:00 2001 From: "Colton Wolkins (Indicio work address)" Date: Thu, 26 May 2022 14:07:37 -0600 Subject: [PATCH 6/7] fix: Add version to Rev Notification Records Signed-off-by: Colton Wolkins (Indicio work address) --- .../v1_0/models/rev_notification_record.py | 3 +++ .../v1_0/models/tests/test_rev_notification_record.py | 2 ++ .../v2_0/models/rev_notification_record.py | 3 +++ .../v2_0/models/tests/test_rev_notification_record.py | 2 ++ demo/features/steps/0453-issue-credential.py | 1 + demo/features/steps/0586-sign-transaction.py | 1 + 6 files changed, 12 insertions(+) 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 4468915d6a..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__( @@ -75,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}, } @@ -103,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}, } 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/models/rev_notification_record.py b/aries_cloudagent/protocols/revocation_notification/v2_0/models/rev_notification_record.py index e6db241bbd..b91cc74967 100644 --- 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 @@ -27,6 +27,7 @@ class Meta: "rev_reg_id", "cred_rev_id", "connection_id", + "version", } def __init__( @@ -75,6 +76,7 @@ async def query_by_ids( 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}, } @@ -103,6 +105,7 @@ async def query_by_rev_reg_id( 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}, } 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 index e06be17091..e6bb64e5c7 100644 --- 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 @@ -21,6 +21,7 @@ def rec(): connection_id="mock_connection_id", thread_id="mock_thread_id", comment="mock_comment", + version="v2_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="v2_0", ) await another.save(session) await RevNotificationRecord.query_by_ids( diff --git a/demo/features/steps/0453-issue-credential.py b/demo/features/steps/0453-issue-credential.py index 2914489406..2c61d91520 100644 --- a/demo/features/steps/0453-issue-credential.py +++ b/demo/features/steps/0453-issue-credential.py @@ -98,6 +98,7 @@ def step_impl(context, holder): "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], "publish": "Y", + "notify_version": "v1_0", "connection_id": cred_exchange["cred_ex_record"]["connection_id"], }, ) diff --git a/demo/features/steps/0586-sign-transaction.py b/demo/features/steps/0586-sign-transaction.py index 531d8a7009..5838ad147a 100644 --- a/demo/features/steps/0586-sign-transaction.py +++ b/demo/features/steps/0586-sign-transaction.py @@ -437,6 +437,7 @@ def step_impl(context, agent_name): "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], "publish": False, + "notify_version": "v1_0", "connection_id": cred_exchange["cred_ex_record"]["connection_id"], }, ) From ac4fe653cb4776c8aba8f9c9a6b9b5913e420b01 Mon Sep 17 00:00:00 2001 From: "Colton Wolkins (Indicio work address)" Date: Tue, 31 May 2022 11:04:23 -0600 Subject: [PATCH 7/7] feat: Add default value for notify_version to "v1_0" Signed-off-by: Colton Wolkins (Indicio work address) --- aries_cloudagent/revocation/routes.py | 4 ++-- demo/features/steps/0453-issue-credential.py | 1 - demo/features/steps/0586-sign-transaction.py | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index 39a4678d51..0db51587c4 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -157,7 +157,7 @@ def validate_fields(self, data, **kwargs): notify = data.get("notify") connection_id = data.get("connection_id") - notify_version = data.get("notify_version") + notify_version = data.get("notify_version", "v1_0") if notify and not connection_id: raise ValidationError( @@ -387,7 +387,7 @@ 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") + 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") diff --git a/demo/features/steps/0453-issue-credential.py b/demo/features/steps/0453-issue-credential.py index 2c61d91520..2914489406 100644 --- a/demo/features/steps/0453-issue-credential.py +++ b/demo/features/steps/0453-issue-credential.py @@ -98,7 +98,6 @@ def step_impl(context, holder): "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], "publish": "Y", - "notify_version": "v1_0", "connection_id": cred_exchange["cred_ex_record"]["connection_id"], }, ) diff --git a/demo/features/steps/0586-sign-transaction.py b/demo/features/steps/0586-sign-transaction.py index 5838ad147a..531d8a7009 100644 --- a/demo/features/steps/0586-sign-transaction.py +++ b/demo/features/steps/0586-sign-transaction.py @@ -437,7 +437,6 @@ def step_impl(context, agent_name): "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], "publish": False, - "notify_version": "v1_0", "connection_id": cred_exchange["cred_ex_record"]["connection_id"], }, )