From cdf57517555b40d096408baae4bb1153778cb404 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Thu, 19 Oct 2023 19:59:27 +0530 Subject: [PATCH 1/6] fix + unit tests Signed-off-by: Shaanjot Gill --- aries_cloudagent/revocation/manager.py | 26 +++- aries_cloudagent/revocation/routes.py | 36 ++++- .../revocation/tests/test_manager.py | 144 ++++++++++++++++++ .../revocation/tests/test_routes.py | 125 +++++++++++++++ 4 files changed, 324 insertions(+), 7 deletions(-) diff --git a/aries_cloudagent/revocation/manager.py b/aries_cloudagent/revocation/manager.py index 3fc8b81741..54f436f33c 100644 --- a/aries_cloudagent/revocation/manager.py +++ b/aries_cloudagent/revocation/manager.py @@ -7,6 +7,7 @@ from ..protocols.revocation_notification.v1_0.models.rev_notification_record import ( RevNotificationRecord, ) +from ..connections.models.conn_record import ConnRecord from ..core.error import BaseError from ..core.profile import Profile from ..indy.issuer import IndyIssuer @@ -48,6 +49,7 @@ async def revoke_credential_by_cred_ex_id( thread_id: str = None, connection_id: str = None, comment: str = None, + write_ledger: bool = True, ): """Revoke a credential by its credential exchange identifier at issue. @@ -80,6 +82,7 @@ async def revoke_credential_by_cred_ex_id( thread_id=thread_id, connection_id=connection_id, comment=comment, + write_ledger=write_ledger, ) async def revoke_credential( @@ -92,6 +95,7 @@ async def revoke_credential( thread_id: str = None, connection_id: str = None, comment: str = None, + write_ledger: bool = True, ): """Revoke a credential. @@ -147,7 +151,27 @@ async def revoke_credential( await txn.commit() await self.set_cred_revoked_state(rev_reg_id, crids) if delta_json: - await issuer_rr_upd.send_entry(self._profile) + if write_ledger: + await issuer_rr_upd.send_entry(self._profile) + else: + async with self._profile.session() as session: + try: + connection_record = await ConnRecord.retrieve_by_id( + session, connection_id + ) + except StorageNotFoundError: + raise RevocationManagerError( + f"No connection record found for id: {connection_id}" + ) + endorser_info = await connection_record.metadata_get( + session, "endorser_info" + ) + endorser_did = endorser_info["endorser_did"] + await issuer_rr_upd.send_entry( + self._profile, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) await notify_revocation_published_event( self._profile, rev_reg_id, [cred_rev_id] ) diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index 383bb5b02a..5c4056db43 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -519,7 +519,7 @@ async def revoke(request: web.BaseRequest): body = await request.json() cred_ex_id = body.get("cred_ex_id") body["notify"] = body.get("notify", context.settings.get("revocation.notify")) - notify = body.get("notify") + notify = body.get("notify", False) connection_id = body.get("connection_id") body["notify_version"] = body.get("notify_version", "v1_0") notify_version = body["notify_version"] @@ -532,14 +532,38 @@ async def revoke(request: web.BaseRequest): ) rev_manager = RevocationManager(context.profile) + profile = context.profile + write_ledger = True + if is_author_role(profile): + write_ledger = False + if not connection_id: + connection_id = await get_endorser_connection_id(profile) + if not connection_id: + raise web.HTTPBadRequest(reason="No endorser connection found") try: if cred_ex_id: - # rev_reg_id and cred_rev_id should not be present so we can - # safely splat the body - await rev_manager.revoke_credential_by_cred_ex_id(**body) + await rev_manager.revoke_credential_by_cred_ex_id( + cred_ex_id=cred_ex_id, + publish=body.get("publish", False), + notify=notify, + notify_version=notify_version, + thread_id=body.get("thread_id"), + connection_id=connection_id, + comment=body.get("comment"), + write_ledger=write_ledger, + ) else: - # no cred_ex_id so we can safely splat the body - await rev_manager.revoke_credential(**body) + await rev_manager.revoke_credential( + rev_reg_id=body.get("rev_reg_id"), + cred_rev_id=body.get("cred_rev_id"), + publish=body.get("publish", False), + notify=notify, + notify_version=notify_version, + thread_id=body.get("thread_id"), + connection_id=connection_id, + comment=body.get("comment"), + write_ledger=write_ledger, + ) except ( RevocationManagerError, RevocationError, diff --git a/aries_cloudagent/revocation/tests/test_manager.py b/aries_cloudagent/revocation/tests/test_manager.py index b987913c97..86edf22fbc 100644 --- a/aries_cloudagent/revocation/tests/test_manager.py +++ b/aries_cloudagent/revocation/tests/test_manager.py @@ -7,6 +7,7 @@ IssuerCredRevRecord, ) +from ...connections.models.conn_record import ConnRecord from ...core.in_memory import InMemoryProfile from ...indy.issuer import IndyIssuer from ...protocols.issue_credential.v1_0.models.credential_exchange import ( @@ -96,6 +97,149 @@ async def test_revoke_credential_publish(self): ["2", "1"], ) + async def test_revoke_credential_publish_endorser(self): + conn_record = ConnRecord( + their_label="Hello", + their_role=ConnRecord.Role.RESPONDER.rfc160, + alias="Bob", + ) + session = await self.profile.session() + await conn_record.save(session) + await conn_record.metadata_set( + session, + key="endorser_info", + value={ + "endorser_did": "test_endorser_did", + "endorser_name": "test_endorser_name", + }, + ) + conn_id = conn_record.connection_id + assert conn_id is not None + manager = RevocationManager(self.profile) + CRED_EX_ID = "dummy-cxid" + CRED_REV_ID = "1" + mock_issuer_rev_reg_record = async_mock.MagicMock( + revoc_reg_id=REV_REG_ID, + tails_local_path=TAILS_LOCAL, + send_entry=async_mock.CoroutineMock(), + clear_pending=async_mock.CoroutineMock(), + pending_pub=["2"], + ) + issuer = async_mock.MagicMock(IndyIssuer, autospec=True) + issuer.revoke_credentials = async_mock.CoroutineMock( + return_value=( + json.dumps( + { + "ver": "1.0", + "value": { + "prevAccum": "1 ...", + "accum": "21 ...", + "issued": [1], + }, + } + ), + [], + ) + ) + self.profile.context.injector.bind_instance(IndyIssuer, issuer) + + with async_mock.patch.object( + test_module.IssuerCredRevRecord, + "retrieve_by_cred_ex_id", + async_mock.CoroutineMock(), + ) as mock_retrieve, async_mock.patch.object( + test_module, "IndyRevocation", autospec=True + ) as revoc, async_mock.patch.object( + test_module.IssuerRevRegRecord, + "retrieve_by_id", + async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_record), + ): + mock_retrieve.return_value = async_mock.MagicMock( + rev_reg_id="dummy-rr-id", cred_rev_id=CRED_REV_ID + ) + mock_rev_reg = async_mock.MagicMock( + get_or_fetch_local_tails_path=async_mock.CoroutineMock() + ) + revoc.return_value.get_issuer_rev_reg_record = async_mock.CoroutineMock( + return_value=mock_issuer_rev_reg_record + ) + revoc.return_value.get_ledger_registry = async_mock.CoroutineMock( + return_value=mock_rev_reg + ) + + await self.manager.revoke_credential_by_cred_ex_id( + cred_ex_id=CRED_EX_ID, + publish=True, + connection_id=conn_id, + write_ledger=False, + ) + + issuer.revoke_credentials.assert_awaited_once_with( + mock_issuer_rev_reg_record.cred_def_id, + mock_issuer_rev_reg_record.revoc_reg_id, + mock_issuer_rev_reg_record.tails_local_path, + ["2", "1"], + ) + + async def test_revoke_credential_publish_endorser_x(self): + CRED_EX_ID = "dummy-cxid" + CRED_REV_ID = "1" + mock_issuer_rev_reg_record = async_mock.MagicMock( + revoc_reg_id=REV_REG_ID, + tails_local_path=TAILS_LOCAL, + send_entry=async_mock.CoroutineMock(), + clear_pending=async_mock.CoroutineMock(), + pending_pub=["2"], + ) + issuer = async_mock.MagicMock(IndyIssuer, autospec=True) + issuer.revoke_credentials = async_mock.CoroutineMock( + return_value=( + json.dumps( + { + "ver": "1.0", + "value": { + "prevAccum": "1 ...", + "accum": "21 ...", + "issued": [1], + }, + } + ), + [], + ) + ) + self.profile.context.injector.bind_instance(IndyIssuer, issuer) + + with async_mock.patch.object( + test_module.IssuerCredRevRecord, + "retrieve_by_cred_ex_id", + async_mock.CoroutineMock(), + ) as mock_retrieve, async_mock.patch.object( + test_module, "IndyRevocation", autospec=True + ) as revoc, async_mock.patch.object( + test_module.IssuerRevRegRecord, + "retrieve_by_id", + async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_record), + ): + mock_retrieve.return_value = async_mock.MagicMock( + rev_reg_id="dummy-rr-id", cred_rev_id=CRED_REV_ID + ) + mock_rev_reg = async_mock.MagicMock( + get_or_fetch_local_tails_path=async_mock.CoroutineMock() + ) + revoc.return_value.get_issuer_rev_reg_record = async_mock.CoroutineMock( + return_value=mock_issuer_rev_reg_record + ) + revoc.return_value.get_ledger_registry = async_mock.CoroutineMock( + return_value=mock_rev_reg + ) + with self.assertRaises(RevocationManagerError): + await self.manager.revoke_credential_by_cred_ex_id( + cred_ex_id=CRED_EX_ID, + publish=True, + connection_id="invalid_conn_id", + write_ledger=False, + ) + async def test_revoke_cred_by_cxid_not_found(self): CRED_EX_ID = "dummy-cxid" diff --git a/aries_cloudagent/revocation/tests/test_routes.py b/aries_cloudagent/revocation/tests/test_routes.py index b715d59970..8c73ccffe8 100644 --- a/aries_cloudagent/revocation/tests/test_routes.py +++ b/aries_cloudagent/revocation/tests/test_routes.py @@ -31,6 +31,21 @@ def setUp(self): self.test_did = "sample-did" + self.author_profile = InMemoryProfile.test_profile() + self.author_profile.settings.set_value("endorser.author", True) + self.author_context = self.author_profile.context + setattr(self.author_context, "profile", self.author_profile) + self.author_request_dict = { + "context": self.author_context, + "outbound_message_router": async_mock.CoroutineMock(), + } + self.author_request = async_mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.author_request_dict[k], + ) + async def test_validate_cred_rev_rec_qs_and_revoke_req(self): for req in ( test_module.CredRevRecordQueryStringSchema(), @@ -94,6 +109,116 @@ async def test_revoke(self): mock_response.assert_called_once_with({}) + async def test_revoke_endorser_no_conn_id_by_cred_ex_id(self): + self.author_request.json = async_mock.CoroutineMock( + return_value={ + "rev_reg_id": "rr_id", + "cred_rev_id": "23", + "publish": "false", + } + ) + + with async_mock.patch.object( + test_module, "RevocationManager", autospec=True + ) as mock_mgr, async_mock.patch.object( + test_module, + "get_endorser_connection_id", + async_mock.CoroutineMock(return_value="dummy-conn-id"), + ), async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() + + await test_module.revoke(self.author_request) + + mock_response.assert_called_once_with({}) + + async def test_revoke_endorser_by_cred_ex_id(self): + self.author_request.json = async_mock.CoroutineMock( + return_value={ + "rev_reg_id": "rr_id", + "cred_rev_id": "23", + "publish": "false", + "connection_id": "dummy-conn-id", + } + ) + + with async_mock.patch.object( + test_module, "RevocationManager", autospec=True + ) as mock_mgr, async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() + + await test_module.revoke(self.author_request) + + mock_response.assert_called_once_with({}) + + async def test_revoke_endorser_no_conn_id(self): + self.author_request.json = async_mock.CoroutineMock( + return_value={ + "rev_reg_id": "rr_id", + "cred_rev_id": "23", + "publish": "false", + } + ) + + with async_mock.patch.object( + test_module, "RevocationManager", autospec=True + ) as mock_mgr, async_mock.patch.object( + test_module, + "get_endorser_connection_id", + async_mock.CoroutineMock(return_value="dummy-conn-id"), + ), async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() + + await test_module.revoke(self.author_request) + + mock_response.assert_called_once_with({}) + + async def test_revoke_endorser(self): + self.author_request.json = async_mock.CoroutineMock( + return_value={ + "rev_reg_id": "rr_id", + "cred_rev_id": "23", + "publish": "false", + "connection_id": "dummy-conn-id", + } + ) + + with async_mock.patch.object( + test_module, "RevocationManager", autospec=True + ) as mock_mgr, async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() + + await test_module.revoke(self.author_request) + + mock_response.assert_called_once_with({}) + + async def test_revoke_endorser_x(self): + self.author_request.json = async_mock.CoroutineMock( + return_value={ + "rev_reg_id": "rr_id", + "cred_rev_id": "23", + "publish": "false", + } + ) + + with async_mock.patch.object( + test_module, "RevocationManager", autospec=True + ) as mock_mgr, async_mock.patch.object( + test_module, + "get_endorser_connection_id", + async_mock.CoroutineMock(return_value=None), + ): + mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.revoke(self.author_request) + async def test_revoke_by_cred_ex_id(self): self.request.json = async_mock.CoroutineMock( return_value={ From 5d3a5eaf5eff99368f5db67f2c23de1869dc1a5f Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Fri, 20 Oct 2023 00:44:18 +0530 Subject: [PATCH 2/6] update send_entry on calling publish-revocations Signed-off-by: Shaanjot Gill --- aries_cloudagent/revocation/manager.py | 28 +++- aries_cloudagent/revocation/routes.py | 10 +- .../revocation/tests/test_manager.py | 139 ++++++++++++++++++ .../revocation/tests/test_routes.py | 38 +++++ 4 files changed, 211 insertions(+), 4 deletions(-) diff --git a/aries_cloudagent/revocation/manager.py b/aries_cloudagent/revocation/manager.py index 54f436f33c..b57ae4c699 100644 --- a/aries_cloudagent/revocation/manager.py +++ b/aries_cloudagent/revocation/manager.py @@ -161,7 +161,8 @@ async def revoke_credential( ) except StorageNotFoundError: raise RevocationManagerError( - f"No connection record found for id: {connection_id}" + "No endorser connection record found " + f"for id: {connection_id}" ) endorser_info = await connection_record.metadata_get( session, "endorser_info" @@ -206,6 +207,7 @@ async def update_rev_reg_revoked_state( async def publish_pending_revocations( self, rrid2crid: Mapping[Text, Sequence[Text]] = None, + connection_id: str = None, ) -> Mapping[Text, Sequence[Text]]: """Publish pending revocations to the ledger. @@ -226,6 +228,7 @@ async def publish_pending_revocations( - all pending revocations from all revocation registry tagged 0 - pending ["1", "2"] from revocation registry tagged 1 - no pending revocations from any other revocation registries. + connection_id: connection identifier for endorser connection to use Returns: mapping from each revocation registry id to its cred rev ids published. """ @@ -263,7 +266,28 @@ async def publish_pending_revocations( await txn.commit() await self.set_cred_revoked_state(issuer_rr_rec.revoc_reg_id, crids) if delta_json: - await issuer_rr_upd.send_entry(self._profile) + if connection_id: + async with self._profile.session() as session: + try: + connection_record = await ConnRecord.retrieve_by_id( + session, connection_id + ) + except StorageNotFoundError: + raise RevocationManagerError( + "No endorser connection record found " + f"for id: {connection_id}" + ) + endorser_info = await connection_record.metadata_get( + session, "endorser_info" + ) + endorser_did = endorser_info["endorser_did"] + await issuer_rr_upd.send_entry( + self._profile, + write_ledger=False, + endorser_did=endorser_did, + ) + else: + await issuer_rr_upd.send_entry(self._profile) published = sorted(crid for crid in crids if crid not in failed_crids) result[issuer_rr_rec.revoc_reg_id] = published await notify_revocation_published_event( diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index 5c4056db43..ca433d8cbf 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -594,10 +594,16 @@ async def publish_revocations(request: web.BaseRequest): rrid2crid = body.get("rrid2crid") rev_manager = RevocationManager(context.profile) - + profile = context.profile + connection_id = None + if is_author_role(profile): + connection_id = await get_endorser_connection_id(profile) + if not connection_id: + raise web.HTTPBadRequest(reason="No endorser connection found") try: rev_reg_resp = await rev_manager.publish_pending_revocations( - rrid2crid, + rrid2crid=rrid2crid, + connection_id=connection_id, ) except (RevocationError, StorageError, IndyIssuerError, LedgerError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err diff --git a/aries_cloudagent/revocation/tests/test_manager.py b/aries_cloudagent/revocation/tests/test_manager.py index 86edf22fbc..79f9270db6 100644 --- a/aries_cloudagent/revocation/tests/test_manager.py +++ b/aries_cloudagent/revocation/tests/test_manager.py @@ -313,6 +313,145 @@ async def test_revoke_credential_pend(self): issuer.revoke_credentials.assert_not_awaited() + async def test_publish_pending_revocations_endorser(self): + deltas = [ + { + "ver": "1.0", + "value": {"prevAccum": "1 ...", "accum": "21 ...", "issued": [1, 2, 3]}, + }, + { + "ver": "1.0", + "value": { + "prevAccum": "21 ...", + "accum": "36 ...", + "issued": [1, 2, 3], + }, + }, + ] + + mock_issuer_rev_reg_records = [ + async_mock.MagicMock( + record_id=0, + revoc_reg_id=REV_REG_ID, + tails_local_path=TAILS_LOCAL, + pending_pub=["1", "2"], + send_entry=async_mock.CoroutineMock(), + clear_pending=async_mock.CoroutineMock(), + ), + async_mock.MagicMock( + record_id=1, + revoc_reg_id=f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2", + tails_local_path=TAILS_LOCAL, + pending_pub=["9", "99"], + send_entry=async_mock.CoroutineMock(), + clear_pending=async_mock.CoroutineMock(), + ), + ] + conn_record = ConnRecord( + their_label="Hello", + their_role=ConnRecord.Role.RESPONDER.rfc160, + alias="Bob", + ) + session = await self.profile.session() + await conn_record.save(session) + await conn_record.metadata_set( + session, + key="endorser_info", + value={ + "endorser_did": "test_endorser_did", + "endorser_name": "test_endorser_name", + }, + ) + conn_id = conn_record.connection_id + assert conn_id is not None + with async_mock.patch.object( + test_module.IssuerRevRegRecord, + "query_by_pending", + async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), + ), async_mock.patch.object( + test_module.IssuerRevRegRecord, + "retrieve_by_id", + async_mock.CoroutineMock( + side_effect=lambda _, id, **args: mock_issuer_rev_reg_records[id] + ), + ): + issuer = async_mock.MagicMock(IndyIssuer, autospec=True) + issuer.merge_revocation_registry_deltas = async_mock.CoroutineMock( + side_effect=deltas + ) + + issuer.revoke_credentials = async_mock.CoroutineMock( + side_effect=[(json.dumps(delta), []) for delta in deltas] + ) + self.profile.context.injector.bind_instance(IndyIssuer, issuer) + manager = RevocationManager(self.profile) + result = await manager.publish_pending_revocations( + rrid2crid={REV_REG_ID: "2"}, connection_id=conn_id + ) + assert result == {REV_REG_ID: ["2"]} + mock_issuer_rev_reg_records[0].clear_pending.assert_called_once() + mock_issuer_rev_reg_records[1].clear_pending.assert_not_called() + + async def test_publish_pending_revocations_endorser_x(self): + deltas = [ + { + "ver": "1.0", + "value": {"prevAccum": "1 ...", "accum": "21 ...", "issued": [1, 2, 3]}, + }, + { + "ver": "1.0", + "value": { + "prevAccum": "21 ...", + "accum": "36 ...", + "issued": [1, 2, 3], + }, + }, + ] + + mock_issuer_rev_reg_records = [ + async_mock.MagicMock( + record_id=0, + revoc_reg_id=REV_REG_ID, + tails_local_path=TAILS_LOCAL, + pending_pub=["1", "2"], + send_entry=async_mock.CoroutineMock(), + clear_pending=async_mock.CoroutineMock(), + ), + async_mock.MagicMock( + record_id=1, + revoc_reg_id=f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2", + tails_local_path=TAILS_LOCAL, + pending_pub=["9", "99"], + send_entry=async_mock.CoroutineMock(), + clear_pending=async_mock.CoroutineMock(), + ), + ] + with async_mock.patch.object( + test_module.IssuerRevRegRecord, + "query_by_pending", + async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), + ), async_mock.patch.object( + test_module.IssuerRevRegRecord, + "retrieve_by_id", + async_mock.CoroutineMock( + side_effect=lambda _, id, **args: mock_issuer_rev_reg_records[id] + ), + ): + issuer = async_mock.MagicMock(IndyIssuer, autospec=True) + issuer.merge_revocation_registry_deltas = async_mock.CoroutineMock( + side_effect=deltas + ) + + issuer.revoke_credentials = async_mock.CoroutineMock( + side_effect=[(json.dumps(delta), []) for delta in deltas] + ) + self.profile.context.injector.bind_instance(IndyIssuer, issuer) + manager = RevocationManager(self.profile) + with self.assertRaises(RevocationManagerError): + result = await manager.publish_pending_revocations( + rrid2crid={REV_REG_ID: "2"}, connection_id="invalid_conn_id" + ) + async def test_publish_pending_revocations_basic(self): deltas = [ { diff --git a/aries_cloudagent/revocation/tests/test_routes.py b/aries_cloudagent/revocation/tests/test_routes.py index 8c73ccffe8..c51ea29e3b 100644 --- a/aries_cloudagent/revocation/tests/test_routes.py +++ b/aries_cloudagent/revocation/tests/test_routes.py @@ -290,6 +290,44 @@ async def test_publish_revocations_x(self): with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.publish_revocations(self.request) + async def test_publish_revocations_endorser(self): + self.author_request.json = async_mock.CoroutineMock() + + with async_mock.patch.object( + test_module, "RevocationManager", autospec=True + ) as mock_mgr, async_mock.patch.object( + test_module, + "get_endorser_connection_id", + async_mock.CoroutineMock(return_value="dummy-conn-id"), + ), async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + pub_pending = async_mock.CoroutineMock() + mock_mgr.return_value.publish_pending_revocations = pub_pending + + await test_module.publish_revocations(self.author_request) + + mock_response.assert_called_once_with( + {"rrid2crid": pub_pending.return_value} + ) + + async def test_publish_revocations_endorser_x(self): + self.author_request.json = async_mock.CoroutineMock() + + with async_mock.patch.object( + test_module, "RevocationManager", autospec=True + ) as mock_mgr, async_mock.patch.object( + test_module, + "get_endorser_connection_id", + async_mock.CoroutineMock(return_value=None), + ), async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + pub_pending = async_mock.CoroutineMock() + mock_mgr.return_value.publish_pending_revocations = pub_pending + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.publish_revocations(self.author_request) + async def test_clear_pending_revocations(self): self.request.json = async_mock.CoroutineMock() From e318d31bc8dc52e550307e01fb539082c23d6dc0 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Mon, 23 Oct 2023 14:39:40 +0530 Subject: [PATCH 3/6] fix int test Signed-off-by: Shaanjot Gill --- aries_cloudagent/wallet/routes.py | 62 +++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index ba5e7d4bd4..fae8507ba5 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -2,7 +2,7 @@ import json import logging -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from aiohttp import web from aiohttp_apispec import docs, querystring_schema, request_schema, response_schema @@ -10,6 +10,7 @@ from marshmallow import fields, validate from ..admin.request_context import AdminRequestContext +from ..config.injection_context import InjectionContext from ..connections.models.conn_record import ConnRecord from ..core.event_bus import Event, EventBus from ..core.profile import Profile @@ -723,9 +724,10 @@ async def wallet_set_public_did(request: web.BaseRequest): async def promote_wallet_public_did( - context: AdminRequestContext, + context: Union[AdminRequestContext, InjectionContext], did: str, write_ledger: bool = False, + profile: Profile = None, connection_id: str = None, routing_keys: List[str] = None, mediator_endpoint: str = None, @@ -737,8 +739,22 @@ async def promote_wallet_public_did( is_indy_did = bool(IndyDID.PATTERN.match(did)) # write only Indy DID write_ledger = is_indy_did and write_ledger - - ledger = context.profile.inject_or(BaseLedger) + is_ctx_admin_request = True + if isinstance(context, InjectionContext): + is_ctx_admin_request = False + if not profile: + raise web.HTTPForbidden( + reason=( + "InjectionContext is provided but no profile is provided. " + "InjectionContext does not have profile attribute but " + "AdminRequestContext does." + ) + ) + ledger = ( + context.profile.inject_or(BaseLedger) + if is_ctx_admin_request + else profile.inject_or(BaseLedger) + ) if is_indy_did: if not ledger: @@ -751,18 +767,29 @@ async def promote_wallet_public_did( if not await ledger.get_key_for_did(did): raise LookupError(f"DID {did} is not posted to the ledger") + is_author_profile = ( + is_author_role(context.profile) + if is_ctx_admin_request + else is_author_role(profile) + ) # check if we need to endorse - if is_author_role(context.profile): + if is_author_profile: # authors cannot write to the ledger write_ledger = False # author has not provided a connection id, so determine which to use if not connection_id: - connection_id = await get_endorser_connection_id(context.profile) + connection_id = ( + await get_endorser_connection_id(context.profile) + if is_ctx_admin_request + else await get_endorser_connection_id(profile) + ) if not connection_id: raise web.HTTPBadRequest(reason="No endorser connection found") if not write_ledger: - async with context.session() as session: + async with ( + context.session() if is_ctx_admin_request else profile.session() + ) as session: try: connection_record = await ConnRecord.retrieve_by_id( session, connection_id @@ -793,7 +820,9 @@ async def promote_wallet_public_did( did_info: DIDInfo = None attrib_def = None - async with context.session() as session: + async with ( + context.session() if is_ctx_admin_request else profile.session() + ) as session: wallet = session.inject_or(BaseWallet) did_info = await wallet.get_local_did(did) info = await wallet.set_public_did(did_info) @@ -815,8 +844,16 @@ async def promote_wallet_public_did( if info: # Route the public DID - route_manager = context.profile.inject(RouteManager) - await route_manager.route_verkey(context.profile, info.verkey) + route_manager = ( + context.profile.inject(RouteManager) + if is_ctx_admin_request + else profile.inject(RouteManager) + ) + await route_manager.route_verkey( + context.profile, info.verkey + ) if is_ctx_admin_request else await route_manager.route_verkey( + profile, info.verkey + ) return info, attrib_def @@ -1158,7 +1195,10 @@ async def on_register_nym_event(profile: Profile, event: Event): connection_id = event.payload.get("connection_id") try: _info, attrib_def = await promote_wallet_public_did( - profile.context, did, connection_id + context=profile.context, + did=did, + connection_id=connection_id, + profile=profile, ) except Exception as err: # log the error, but continue From b57e19ec4f2387df6244a18fb43c1e69382ab197 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Fri, 10 Nov 2023 23:59:21 +0530 Subject: [PATCH 4/6] unit test fix Signed-off-by: Shaanjot Gill --- .../revocation/tests/test_manager.py | 112 +++++++++--------- .../revocation/tests/test_routes.py | 78 ++++++------ 2 files changed, 95 insertions(+), 95 deletions(-) diff --git a/aries_cloudagent/revocation/tests/test_manager.py b/aries_cloudagent/revocation/tests/test_manager.py index f57352a918..446f1a026c 100644 --- a/aries_cloudagent/revocation/tests/test_manager.py +++ b/aries_cloudagent/revocation/tests/test_manager.py @@ -118,15 +118,15 @@ async def test_revoke_credential_publish_endorser(self): manager = RevocationManager(self.profile) CRED_EX_ID = "dummy-cxid" CRED_REV_ID = "1" - mock_issuer_rev_reg_record = async_mock.MagicMock( + mock_issuer_rev_reg_record = mock.MagicMock( revoc_reg_id=REV_REG_ID, tails_local_path=TAILS_LOCAL, - send_entry=async_mock.CoroutineMock(), - clear_pending=async_mock.CoroutineMock(), + send_entry=mock.CoroutineMock(), + clear_pending=mock.CoroutineMock(), pending_pub=["2"], ) - issuer = async_mock.MagicMock(IndyIssuer, autospec=True) - issuer.revoke_credentials = async_mock.CoroutineMock( + issuer = mock.MagicMock(IndyIssuer, autospec=True) + issuer.revoke_credentials = mock.CoroutineMock( return_value=( json.dumps( { @@ -143,27 +143,27 @@ async def test_revoke_credential_publish_endorser(self): ) self.profile.context.injector.bind_instance(IndyIssuer, issuer) - with async_mock.patch.object( + with mock.patch.object( test_module.IssuerCredRevRecord, "retrieve_by_cred_ex_id", - async_mock.CoroutineMock(), - ) as mock_retrieve, async_mock.patch.object( + mock.CoroutineMock(), + ) as mock_retrieve, mock.patch.object( test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( + ) as revoc, mock.patch.object( test_module.IssuerRevRegRecord, "retrieve_by_id", - async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_record), + mock.CoroutineMock(return_value=mock_issuer_rev_reg_record), ): - mock_retrieve.return_value = async_mock.MagicMock( + mock_retrieve.return_value = mock.MagicMock( rev_reg_id="dummy-rr-id", cred_rev_id=CRED_REV_ID ) - mock_rev_reg = async_mock.MagicMock( - get_or_fetch_local_tails_path=async_mock.CoroutineMock() + mock_rev_reg = mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock() ) - revoc.return_value.get_issuer_rev_reg_record = async_mock.CoroutineMock( + revoc.return_value.get_issuer_rev_reg_record = mock.CoroutineMock( return_value=mock_issuer_rev_reg_record ) - revoc.return_value.get_ledger_registry = async_mock.CoroutineMock( + revoc.return_value.get_ledger_registry = mock.CoroutineMock( return_value=mock_rev_reg ) @@ -184,15 +184,15 @@ async def test_revoke_credential_publish_endorser(self): async def test_revoke_credential_publish_endorser_x(self): CRED_EX_ID = "dummy-cxid" CRED_REV_ID = "1" - mock_issuer_rev_reg_record = async_mock.MagicMock( + mock_issuer_rev_reg_record = mock.MagicMock( revoc_reg_id=REV_REG_ID, tails_local_path=TAILS_LOCAL, - send_entry=async_mock.CoroutineMock(), - clear_pending=async_mock.CoroutineMock(), + send_entry=mock.CoroutineMock(), + clear_pending=mock.CoroutineMock(), pending_pub=["2"], ) - issuer = async_mock.MagicMock(IndyIssuer, autospec=True) - issuer.revoke_credentials = async_mock.CoroutineMock( + issuer = mock.MagicMock(IndyIssuer, autospec=True) + issuer.revoke_credentials = mock.CoroutineMock( return_value=( json.dumps( { @@ -209,27 +209,27 @@ async def test_revoke_credential_publish_endorser_x(self): ) self.profile.context.injector.bind_instance(IndyIssuer, issuer) - with async_mock.patch.object( + with mock.patch.object( test_module.IssuerCredRevRecord, "retrieve_by_cred_ex_id", - async_mock.CoroutineMock(), - ) as mock_retrieve, async_mock.patch.object( + mock.CoroutineMock(), + ) as mock_retrieve, mock.patch.object( test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( + ) as revoc, mock.patch.object( test_module.IssuerRevRegRecord, "retrieve_by_id", - async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_record), + mock.CoroutineMock(return_value=mock_issuer_rev_reg_record), ): - mock_retrieve.return_value = async_mock.MagicMock( + mock_retrieve.return_value = mock.MagicMock( rev_reg_id="dummy-rr-id", cred_rev_id=CRED_REV_ID ) - mock_rev_reg = async_mock.MagicMock( - get_or_fetch_local_tails_path=async_mock.CoroutineMock() + mock_rev_reg = mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock() ) - revoc.return_value.get_issuer_rev_reg_record = async_mock.CoroutineMock( + revoc.return_value.get_issuer_rev_reg_record = mock.CoroutineMock( return_value=mock_issuer_rev_reg_record ) - revoc.return_value.get_ledger_registry = async_mock.CoroutineMock( + revoc.return_value.get_ledger_registry = mock.CoroutineMock( return_value=mock_rev_reg ) with self.assertRaises(RevocationManagerError): @@ -326,21 +326,21 @@ async def test_publish_pending_revocations_endorser(self): ] mock_issuer_rev_reg_records = [ - async_mock.MagicMock( + mock.MagicMock( record_id=0, revoc_reg_id=REV_REG_ID, tails_local_path=TAILS_LOCAL, pending_pub=["1", "2"], - send_entry=async_mock.CoroutineMock(), - clear_pending=async_mock.CoroutineMock(), + send_entry=mock.CoroutineMock(), + clear_pending=mock.CoroutineMock(), ), - async_mock.MagicMock( + mock.MagicMock( record_id=1, revoc_reg_id=f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2", tails_local_path=TAILS_LOCAL, pending_pub=["9", "99"], - send_entry=async_mock.CoroutineMock(), - clear_pending=async_mock.CoroutineMock(), + send_entry=mock.CoroutineMock(), + clear_pending=mock.CoroutineMock(), ), ] conn_record = ConnRecord( @@ -360,23 +360,23 @@ async def test_publish_pending_revocations_endorser(self): ) conn_id = conn_record.connection_id assert conn_id is not None - with async_mock.patch.object( + with mock.patch.object( test_module.IssuerRevRegRecord, "query_by_pending", - async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), - ), async_mock.patch.object( + mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), + ), mock.patch.object( test_module.IssuerRevRegRecord, "retrieve_by_id", - async_mock.CoroutineMock( + mock.CoroutineMock( side_effect=lambda _, id, **args: mock_issuer_rev_reg_records[id] ), ): - issuer = async_mock.MagicMock(IndyIssuer, autospec=True) - issuer.merge_revocation_registry_deltas = async_mock.CoroutineMock( + issuer = mock.MagicMock(IndyIssuer, autospec=True) + issuer.merge_revocation_registry_deltas = mock.CoroutineMock( side_effect=deltas ) - issuer.revoke_credentials = async_mock.CoroutineMock( + issuer.revoke_credentials = mock.CoroutineMock( side_effect=[(json.dumps(delta), []) for delta in deltas] ) self.profile.context.injector.bind_instance(IndyIssuer, issuer) @@ -405,40 +405,40 @@ async def test_publish_pending_revocations_endorser_x(self): ] mock_issuer_rev_reg_records = [ - async_mock.MagicMock( + mock.MagicMock( record_id=0, revoc_reg_id=REV_REG_ID, tails_local_path=TAILS_LOCAL, pending_pub=["1", "2"], - send_entry=async_mock.CoroutineMock(), - clear_pending=async_mock.CoroutineMock(), + send_entry=mock.CoroutineMock(), + clear_pending=mock.CoroutineMock(), ), - async_mock.MagicMock( + mock.MagicMock( record_id=1, revoc_reg_id=f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2", tails_local_path=TAILS_LOCAL, pending_pub=["9", "99"], - send_entry=async_mock.CoroutineMock(), - clear_pending=async_mock.CoroutineMock(), + send_entry=mock.CoroutineMock(), + clear_pending=mock.CoroutineMock(), ), ] - with async_mock.patch.object( + with mock.patch.object( test_module.IssuerRevRegRecord, "query_by_pending", - async_mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), - ), async_mock.patch.object( + mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), + ), mock.patch.object( test_module.IssuerRevRegRecord, "retrieve_by_id", - async_mock.CoroutineMock( + mock.CoroutineMock( side_effect=lambda _, id, **args: mock_issuer_rev_reg_records[id] ), ): - issuer = async_mock.MagicMock(IndyIssuer, autospec=True) - issuer.merge_revocation_registry_deltas = async_mock.CoroutineMock( + issuer = mock.MagicMock(IndyIssuer, autospec=True) + issuer.merge_revocation_registry_deltas = mock.CoroutineMock( side_effect=deltas ) - issuer.revoke_credentials = async_mock.CoroutineMock( + issuer.revoke_credentials = mock.CoroutineMock( side_effect=[(json.dumps(delta), []) for delta in deltas] ) self.profile.context.injector.bind_instance(IndyIssuer, issuer) diff --git a/aries_cloudagent/revocation/tests/test_routes.py b/aries_cloudagent/revocation/tests/test_routes.py index ec8e284fbc..ac2cce5e3d 100644 --- a/aries_cloudagent/revocation/tests/test_routes.py +++ b/aries_cloudagent/revocation/tests/test_routes.py @@ -37,9 +37,9 @@ def setUp(self): setattr(self.author_context, "profile", self.author_profile) self.author_request_dict = { "context": self.author_context, - "outbound_message_router": async_mock.CoroutineMock(), + "outbound_message_router": mock.CoroutineMock(), } - self.author_request = async_mock.MagicMock( + self.author_request = mock.MagicMock( app={}, match_info={}, query={}, @@ -110,7 +110,7 @@ async def test_revoke(self): mock_response.assert_called_once_with({}) async def test_revoke_endorser_no_conn_id_by_cred_ex_id(self): - self.author_request.json = async_mock.CoroutineMock( + self.author_request.json = mock.CoroutineMock( return_value={ "rev_reg_id": "rr_id", "cred_rev_id": "23", @@ -118,23 +118,23 @@ async def test_revoke_endorser_no_conn_id_by_cred_ex_id(self): } ) - with async_mock.patch.object( + with mock.patch.object( test_module, "RevocationManager", autospec=True - ) as mock_mgr, async_mock.patch.object( + ) as mock_mgr, mock.patch.object( test_module, "get_endorser_connection_id", - async_mock.CoroutineMock(return_value="dummy-conn-id"), - ), async_mock.patch.object( + mock.CoroutineMock(return_value="dummy-conn-id"), + ), mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() + mock_mgr.return_value.revoke_credential = mock.CoroutineMock() await test_module.revoke(self.author_request) mock_response.assert_called_once_with({}) async def test_revoke_endorser_by_cred_ex_id(self): - self.author_request.json = async_mock.CoroutineMock( + self.author_request.json = mock.CoroutineMock( return_value={ "rev_reg_id": "rr_id", "cred_rev_id": "23", @@ -143,19 +143,19 @@ async def test_revoke_endorser_by_cred_ex_id(self): } ) - with async_mock.patch.object( + with mock.patch.object( test_module, "RevocationManager", autospec=True - ) as mock_mgr, async_mock.patch.object( + ) as mock_mgr, mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() + mock_mgr.return_value.revoke_credential = mock.CoroutineMock() await test_module.revoke(self.author_request) mock_response.assert_called_once_with({}) async def test_revoke_endorser_no_conn_id(self): - self.author_request.json = async_mock.CoroutineMock( + self.author_request.json = mock.CoroutineMock( return_value={ "rev_reg_id": "rr_id", "cred_rev_id": "23", @@ -163,23 +163,23 @@ async def test_revoke_endorser_no_conn_id(self): } ) - with async_mock.patch.object( + with mock.patch.object( test_module, "RevocationManager", autospec=True - ) as mock_mgr, async_mock.patch.object( + ) as mock_mgr, mock.patch.object( test_module, "get_endorser_connection_id", - async_mock.CoroutineMock(return_value="dummy-conn-id"), - ), async_mock.patch.object( + mock.CoroutineMock(return_value="dummy-conn-id"), + ), mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() + mock_mgr.return_value.revoke_credential = mock.CoroutineMock() await test_module.revoke(self.author_request) mock_response.assert_called_once_with({}) async def test_revoke_endorser(self): - self.author_request.json = async_mock.CoroutineMock( + self.author_request.json = mock.CoroutineMock( return_value={ "rev_reg_id": "rr_id", "cred_rev_id": "23", @@ -188,19 +188,19 @@ async def test_revoke_endorser(self): } ) - with async_mock.patch.object( + with mock.patch.object( test_module, "RevocationManager", autospec=True - ) as mock_mgr, async_mock.patch.object( + ) as mock_mgr, mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() + mock_mgr.return_value.revoke_credential = mock.CoroutineMock() await test_module.revoke(self.author_request) mock_response.assert_called_once_with({}) async def test_revoke_endorser_x(self): - self.author_request.json = async_mock.CoroutineMock( + self.author_request.json = mock.CoroutineMock( return_value={ "rev_reg_id": "rr_id", "cred_rev_id": "23", @@ -208,14 +208,14 @@ async def test_revoke_endorser_x(self): } ) - with async_mock.patch.object( + with mock.patch.object( test_module, "RevocationManager", autospec=True - ) as mock_mgr, async_mock.patch.object( + ) as mock_mgr, mock.patch.object( test_module, "get_endorser_connection_id", - async_mock.CoroutineMock(return_value=None), + mock.CoroutineMock(return_value=None), ): - mock_mgr.return_value.revoke_credential = async_mock.CoroutineMock() + mock_mgr.return_value.revoke_credential = mock.CoroutineMock() with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.revoke(self.author_request) @@ -289,18 +289,18 @@ async def test_publish_revocations_x(self): await test_module.publish_revocations(self.request) async def test_publish_revocations_endorser(self): - self.author_request.json = async_mock.CoroutineMock() + self.author_request.json = mock.CoroutineMock() - with async_mock.patch.object( + with mock.patch.object( test_module, "RevocationManager", autospec=True - ) as mock_mgr, async_mock.patch.object( + ) as mock_mgr, mock.patch.object( test_module, "get_endorser_connection_id", - async_mock.CoroutineMock(return_value="dummy-conn-id"), - ), async_mock.patch.object( + mock.CoroutineMock(return_value="dummy-conn-id"), + ), mock.patch.object( test_module.web, "json_response" ) as mock_response: - pub_pending = async_mock.CoroutineMock() + pub_pending = mock.CoroutineMock() mock_mgr.return_value.publish_pending_revocations = pub_pending await test_module.publish_revocations(self.author_request) @@ -310,18 +310,18 @@ async def test_publish_revocations_endorser(self): ) async def test_publish_revocations_endorser_x(self): - self.author_request.json = async_mock.CoroutineMock() + self.author_request.json = mock.CoroutineMock() - with async_mock.patch.object( + with mock.patch.object( test_module, "RevocationManager", autospec=True - ) as mock_mgr, async_mock.patch.object( + ) as mock_mgr, mock.patch.object( test_module, "get_endorser_connection_id", - async_mock.CoroutineMock(return_value=None), - ), async_mock.patch.object( + mock.CoroutineMock(return_value=None), + ), mock.patch.object( test_module.web, "json_response" ) as mock_response: - pub_pending = async_mock.CoroutineMock() + pub_pending = mock.CoroutineMock() mock_mgr.return_value.publish_pending_revocations = pub_pending with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.publish_revocations(self.author_request) From 2a19d123cb492cb772912688c148cdc24fa4c62b Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Sat, 11 Nov 2023 10:38:11 +0530 Subject: [PATCH 5/6] fix timing issue with revoc verification bdd step Signed-off-by: Shaanjot Gill --- demo/features/steps/0586-sign-transaction.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/demo/features/steps/0586-sign-transaction.py b/demo/features/steps/0586-sign-transaction.py index 61702e84a8..feca42d913 100644 --- a/demo/features/steps/0586-sign-transaction.py +++ b/demo/features/steps/0586-sign-transaction.py @@ -507,10 +507,16 @@ def step_impl(context, holder_name, issuer_name): assert len(cred_list["results"]) == 1 cred_id = cred_list["results"][0]["referent"] - # check revocation status for the credential - revocation_status = agent_container_GET( - agent["agent"], - f"/credential/revoked/{cred_id}", - params={"to": int(time.time())}, - ) - assert revocation_status["revoked"] == True + revoc_status_bool = False + counter = 0 + while not revoc_status_bool and counter < 3: + # check revocation status for the credential + revocation_status = agent_container_GET( + agent["agent"], + f"/credential/revoked/{cred_id}", + params={"to": int(time.time())}, + ) + revoc_status_bool = revocation_status["revoked"] + counter = counter + 1 + async_sleep(1.0) + assert revoc_status_bool is True From 394f117f0c81bac92a10a1bb82450282cd507500 Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Mon, 27 Nov 2023 13:45:12 -0800 Subject: [PATCH 6/6] fixes + fixed tests Signed-off-by: Shaanjot Gill --- aries_cloudagent/revocation/manager.py | 45 ++++---- aries_cloudagent/revocation/routes.py | 105 ++++++++++++++---- .../revocation/tests/test_manager.py | 16 ++- .../revocation/tests/test_routes.py | 48 +++++--- demo/features/0586-sign-transaction.feature | 8 +- demo/features/steps/0586-sign-transaction.py | 69 +++++++++++- 6 files changed, 225 insertions(+), 66 deletions(-) diff --git a/aries_cloudagent/revocation/manager.py b/aries_cloudagent/revocation/manager.py index b57ae4c699..824efe79c0 100644 --- a/aries_cloudagent/revocation/manager.py +++ b/aries_cloudagent/revocation/manager.py @@ -2,7 +2,7 @@ import json import logging -from typing import Mapping, Sequence, Text, Tuple +from typing import Mapping, Optional, Sequence, Text, Tuple from ..protocols.revocation_notification.v1_0.models.rev_notification_record import ( RevNotificationRecord, @@ -48,6 +48,7 @@ async def revoke_credential_by_cred_ex_id( notify_version: str = None, thread_id: str = None, connection_id: str = None, + endorser_conn_id: str = None, comment: str = None, write_ledger: bool = True, ): @@ -81,6 +82,7 @@ async def revoke_credential_by_cred_ex_id( notify_version=notify_version, thread_id=thread_id, connection_id=connection_id, + endorser_conn_id=endorser_conn_id, comment=comment, write_ledger=write_ledger, ) @@ -94,9 +96,10 @@ async def revoke_credential( notify_version: str = None, thread_id: str = None, connection_id: str = None, + endorser_conn_id: str = None, comment: str = None, write_ledger: bool = True, - ): + ) -> Optional[dict]: """Revoke a credential. Optionally, publish the corresponding revocation registry delta to the ledger. @@ -152,35 +155,37 @@ async def revoke_credential( await self.set_cred_revoked_state(rev_reg_id, crids) if delta_json: if write_ledger: - await issuer_rr_upd.send_entry(self._profile) + rev_entry_resp = await issuer_rr_upd.send_entry(self._profile) + await notify_revocation_published_event( + self._profile, rev_reg_id, [cred_rev_id] + ) + return rev_entry_resp else: async with self._profile.session() as session: try: connection_record = await ConnRecord.retrieve_by_id( - session, connection_id + session, endorser_conn_id ) except StorageNotFoundError: raise RevocationManagerError( "No endorser connection record found " - f"for id: {connection_id}" + f"for id: {endorser_conn_id}" ) endorser_info = await connection_record.metadata_get( session, "endorser_info" ) endorser_did = endorser_info["endorser_did"] - await issuer_rr_upd.send_entry( + rev_entry_resp = await issuer_rr_upd.send_entry( self._profile, write_ledger=write_ledger, endorser_did=endorser_did, ) - await notify_revocation_published_event( - self._profile, rev_reg_id, [cred_rev_id] - ) - + return rev_entry_resp else: async with self._profile.transaction() as txn: await issuer_rr_rec.mark_pending(txn, cred_rev_id) await txn.commit() + return None async def update_rev_reg_revoked_state( self, @@ -207,8 +212,9 @@ async def update_rev_reg_revoked_state( async def publish_pending_revocations( self, rrid2crid: Mapping[Text, Sequence[Text]] = None, + write_ledger: bool = True, connection_id: str = None, - ) -> Mapping[Text, Sequence[Text]]: + ) -> Tuple[Optional[dict], Mapping[Text, Sequence[Text]]]: """Publish pending revocations to the ledger. Args: @@ -234,7 +240,7 @@ async def publish_pending_revocations( """ result = {} issuer = self._profile.inject(IndyIssuer) - + rev_entry_resp = None async with self._profile.session() as session: issuer_rr_recs = await IssuerRevRegRecord.query_by_pending(session) @@ -281,20 +287,21 @@ async def publish_pending_revocations( session, "endorser_info" ) endorser_did = endorser_info["endorser_did"] - await issuer_rr_upd.send_entry( + rev_entry_resp = await issuer_rr_upd.send_entry( self._profile, - write_ledger=False, + write_ledger=write_ledger, endorser_did=endorser_did, ) else: - await issuer_rr_upd.send_entry(self._profile) + rev_entry_resp = await issuer_rr_upd.send_entry(self._profile) published = sorted(crid for crid in crids if crid not in failed_crids) result[issuer_rr_rec.revoc_reg_id] = published - await notify_revocation_published_event( - self._profile, issuer_rr_rec.revoc_reg_id, crids - ) + if not connection_id: + await notify_revocation_published_event( + self._profile, issuer_rr_rec.revoc_reg_id, crids + ) - return result + return rev_entry_resp, result async def clear_pending_revocations( self, purge: Mapping[Text, Sequence[Text]] = None diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index ca433d8cbf..228aeb1cd5 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -504,6 +504,8 @@ class RevRegConnIdMatchInfoSchema(OpenAPISchema): summary="Revoke an issued credential", ) @request_schema(RevokeRequestSchema()) +@querystring_schema(CreateRevRegTxnForEndorserOptionSchema()) +@querystring_schema(RevRegConnIdMatchInfoSchema()) @response_schema(RevocationModuleResponseSchema(), description="") async def revoke(request: web.BaseRequest): """Request handler for storing a credential revocation. @@ -523,37 +525,43 @@ async def revoke(request: web.BaseRequest): connection_id = body.get("connection_id") body["notify_version"] = body.get("notify_version", "v1_0") notify_version = body["notify_version"] + create_transaction_for_endorser = json.loads( + request.query.get("create_transaction_for_endorser", "false") + ) + endorser_conn_id = request.query.get("conn_id") + rev_manager = RevocationManager(context.profile) + profile = context.profile + outbound_handler = request["outbound_message_router"] + write_ledger = not create_transaction_for_endorser + if is_author_role(profile): + write_ledger = False + create_transaction_for_endorser = True + if not endorser_conn_id: + endorser_conn_id = await get_endorser_connection_id(profile) + if not endorser_conn_id: + raise web.HTTPBadRequest(reason="No endorser connection found") 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) - profile = context.profile - write_ledger = True - if is_author_role(profile): - write_ledger = False - if not connection_id: - connection_id = await get_endorser_connection_id(profile) - if not connection_id: - raise web.HTTPBadRequest(reason="No endorser connection found") try: if cred_ex_id: - await rev_manager.revoke_credential_by_cred_ex_id( + rev_entry_resp = await rev_manager.revoke_credential_by_cred_ex_id( cred_ex_id=cred_ex_id, publish=body.get("publish", False), notify=notify, notify_version=notify_version, thread_id=body.get("thread_id"), connection_id=connection_id, + endorser_conn_id=endorser_conn_id, comment=body.get("comment"), write_ledger=write_ledger, ) else: - await rev_manager.revoke_credential( + rev_entry_resp = await rev_manager.revoke_credential( rev_reg_id=body.get("rev_reg_id"), cred_rev_id=body.get("cred_rev_id"), publish=body.get("publish", False), @@ -561,6 +569,7 @@ async def revoke(request: web.BaseRequest): notify_version=notify_version, thread_id=body.get("thread_id"), connection_id=connection_id, + endorser_conn_id=endorser_conn_id, comment=body.get("comment"), write_ledger=write_ledger, ) @@ -573,11 +582,37 @@ async def revoke(request: web.BaseRequest): ) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err + if create_transaction_for_endorser and rev_entry_resp: + transaction_mgr = TransactionManager(profile) + try: + transaction = await transaction_mgr.create_record( + messages_attach=rev_entry_resp["result"], connection_id=endorser_conn_id + ) + except StorageError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + # if auto-request, send the request to the endorser + if context.settings.get_value("endorser.auto_request"): + try: + ( + transaction, + transaction_request, + ) = await transaction_mgr.create_request( + transaction=transaction, + ) + except (StorageError, TransactionManagerError) as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + await outbound_handler(transaction_request, connection_id=endorser_conn_id) + + return web.json_response({"txn": transaction.serialize()}) return web.json_response({}) @docs(tags=["revocation"], summary="Publish pending revocations to ledger") @request_schema(PublishRevocationsSchema()) +@querystring_schema(CreateRevRegTxnForEndorserOptionSchema()) +@querystring_schema(RevRegConnIdMatchInfoSchema()) @response_schema(TxnOrPublishRevocationsResultSchema(), 200, description="") async def publish_revocations(request: web.BaseRequest): """Request handler for publishing pending revocations to the ledger. @@ -592,23 +627,55 @@ async def publish_revocations(request: web.BaseRequest): context: AdminRequestContext = request["context"] body = await request.json() rrid2crid = body.get("rrid2crid") - + create_transaction_for_endorser = json.loads( + request.query.get("create_transaction_for_endorser", "false") + ) + write_ledger = not create_transaction_for_endorser + endorser_conn_id = request.query.get("conn_id") rev_manager = RevocationManager(context.profile) profile = context.profile - connection_id = None + outbound_handler = request["outbound_message_router"] + if is_author_role(profile): - connection_id = await get_endorser_connection_id(profile) - if not connection_id: + write_ledger = False + create_transaction_for_endorser = True + endorser_conn_id = await get_endorser_connection_id(profile) + if not endorser_conn_id: raise web.HTTPBadRequest(reason="No endorser connection found") try: - rev_reg_resp = await rev_manager.publish_pending_revocations( + rev_reg_resp, result = await rev_manager.publish_pending_revocations( rrid2crid=rrid2crid, - connection_id=connection_id, + write_ledger=write_ledger, + connection_id=endorser_conn_id, ) except (RevocationError, StorageError, IndyIssuerError, LedgerError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - return web.json_response({"rrid2crid": rev_reg_resp}) + if create_transaction_for_endorser and rev_reg_resp: + transaction_mgr = TransactionManager(profile) + try: + transaction = await transaction_mgr.create_record( + messages_attach=rev_reg_resp["result"], connection_id=endorser_conn_id + ) + except StorageError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + # if auto-request, send the request to the endorser + if context.settings.get_value("endorser.auto_request"): + try: + ( + transaction, + transaction_request, + ) = await transaction_mgr.create_request( + transaction=transaction, + ) + except (StorageError, TransactionManagerError) as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + await outbound_handler(transaction_request, connection_id=endorser_conn_id) + + return web.json_response({"txn": transaction.serialize()}) + return web.json_response({"rrid2crid": result}) @docs(tags=["revocation"], summary="Clear pending revocations") diff --git a/aries_cloudagent/revocation/tests/test_manager.py b/aries_cloudagent/revocation/tests/test_manager.py index 446f1a026c..4d0689664f 100644 --- a/aries_cloudagent/revocation/tests/test_manager.py +++ b/aries_cloudagent/revocation/tests/test_manager.py @@ -153,6 +153,10 @@ async def test_revoke_credential_publish_endorser(self): test_module.IssuerRevRegRecord, "retrieve_by_id", mock.CoroutineMock(return_value=mock_issuer_rev_reg_record), + ), mock.patch.object( + test_module.ConnRecord, + "retrieve_by_id", + mock.CoroutineMock(return_value=conn_record), ): mock_retrieve.return_value = mock.MagicMock( rev_reg_id="dummy-rr-id", cred_rev_id=CRED_REV_ID @@ -381,7 +385,7 @@ async def test_publish_pending_revocations_endorser(self): ) self.profile.context.injector.bind_instance(IndyIssuer, issuer) manager = RevocationManager(self.profile) - result = await manager.publish_pending_revocations( + _, result = await manager.publish_pending_revocations( rrid2crid={REV_REG_ID: "2"}, connection_id=conn_id ) assert result == {REV_REG_ID: ["2"]} @@ -490,7 +494,7 @@ async def test_publish_pending_revocations_basic(self): ) self.profile.context.injector.bind_instance(IndyIssuer, issuer) - result = await self.manager.publish_pending_revocations() + _, result = await self.manager.publish_pending_revocations() assert result == {REV_REG_ID: ["1", "2"]} mock_issuer_rev_reg_record.clear_pending.assert_called_once() @@ -549,7 +553,9 @@ async def test_publish_pending_revocations_1_rev_reg_all(self): ) self.profile.context.injector.bind_instance(IndyIssuer, issuer) - result = await self.manager.publish_pending_revocations({REV_REG_ID: None}) + _, result = await self.manager.publish_pending_revocations( + {REV_REG_ID: None} + ) assert result == {REV_REG_ID: ["1", "2"]} mock_issuer_rev_reg_records[0].clear_pending.assert_called_once() mock_issuer_rev_reg_records[1].clear_pending.assert_not_called() @@ -609,7 +615,9 @@ async def test_publish_pending_revocations_1_rev_reg_some(self): ) self.profile.context.injector.bind_instance(IndyIssuer, issuer) - result = await self.manager.publish_pending_revocations({REV_REG_ID: "2"}) + _, result = await self.manager.publish_pending_revocations( + {REV_REG_ID: "2"} + ) assert result == {REV_REG_ID: ["2"]} mock_issuer_rev_reg_records[0].clear_pending.assert_called_once() mock_issuer_rev_reg_records[1].clear_pending.assert_not_called() diff --git a/aries_cloudagent/revocation/tests/test_routes.py b/aries_cloudagent/revocation/tests/test_routes.py index ac2cce5e3d..0a384bc2b3 100644 --- a/aries_cloudagent/revocation/tests/test_routes.py +++ b/aries_cloudagent/revocation/tests/test_routes.py @@ -126,13 +126,13 @@ async def test_revoke_endorser_no_conn_id_by_cred_ex_id(self): mock.CoroutineMock(return_value="dummy-conn-id"), ), mock.patch.object( test_module.web, "json_response" - ) as mock_response: - mock_mgr.return_value.revoke_credential = mock.CoroutineMock() + ): + mock_mgr.return_value.revoke_credential = mock.CoroutineMock( + return_value={"result": "..."} + ) await test_module.revoke(self.author_request) - mock_response.assert_called_once_with({}) - async def test_revoke_endorser_by_cred_ex_id(self): self.author_request.json = mock.CoroutineMock( return_value={ @@ -147,13 +147,17 @@ async def test_revoke_endorser_by_cred_ex_id(self): test_module, "RevocationManager", autospec=True ) as mock_mgr, mock.patch.object( test_module.web, "json_response" - ) as mock_response: - mock_mgr.return_value.revoke_credential = mock.CoroutineMock() + ), mock.patch.object( + test_module, + "get_endorser_connection_id", + mock.CoroutineMock(return_value="test_conn_id"), + ): + mock_mgr.return_value.revoke_credential = mock.CoroutineMock( + return_value={"result": "..."} + ) await test_module.revoke(self.author_request) - mock_response.assert_called_once_with({}) - async def test_revoke_endorser_no_conn_id(self): self.author_request.json = mock.CoroutineMock( return_value={ @@ -171,13 +175,13 @@ async def test_revoke_endorser_no_conn_id(self): mock.CoroutineMock(return_value="dummy-conn-id"), ), mock.patch.object( test_module.web, "json_response" - ) as mock_response: - mock_mgr.return_value.revoke_credential = mock.CoroutineMock() + ): + mock_mgr.return_value.revoke_credential = mock.CoroutineMock( + return_value={"result": "..."} + ) await test_module.revoke(self.author_request) - mock_response.assert_called_once_with({}) - async def test_revoke_endorser(self): self.author_request.json = mock.CoroutineMock( return_value={ @@ -192,13 +196,17 @@ async def test_revoke_endorser(self): test_module, "RevocationManager", autospec=True ) as mock_mgr, mock.patch.object( test_module.web, "json_response" - ) as mock_response: - mock_mgr.return_value.revoke_credential = mock.CoroutineMock() + ), mock.patch.object( + test_module, + "get_endorser_connection_id", + mock.CoroutineMock(return_value="test_conn_id"), + ): + mock_mgr.return_value.revoke_credential = mock.CoroutineMock( + return_value={"result": "..."} + ) await test_module.revoke(self.author_request) - mock_response.assert_called_once_with({}) - async def test_revoke_endorser_x(self): self.author_request.json = mock.CoroutineMock( return_value={ @@ -268,7 +276,9 @@ async def test_publish_revocations(self): test_module.web, "json_response" ) as mock_response: pub_pending = mock.CoroutineMock() - mock_mgr.return_value.publish_pending_revocations = pub_pending + mock_mgr.return_value.publish_pending_revocations = mock.CoroutineMock( + return_value=({}, pub_pending.return_value) + ) await test_module.publish_revocations(self.request) @@ -301,7 +311,9 @@ async def test_publish_revocations_endorser(self): test_module.web, "json_response" ) as mock_response: pub_pending = mock.CoroutineMock() - mock_mgr.return_value.publish_pending_revocations = pub_pending + mock_mgr.return_value.publish_pending_revocations = mock.CoroutineMock( + return_value=({}, pub_pending.return_value) + ) await test_module.publish_revocations(self.author_request) diff --git a/demo/features/0586-sign-transaction.feature b/demo/features/0586-sign-transaction.feature index 5069f2974e..b7427d9c20 100644 --- a/demo/features/0586-sign-transaction.feature +++ b/demo/features/0586-sign-transaction.feature @@ -157,8 +157,8 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions And "Bob" has written the revocation registry definition to the ledger And "Bob" has written the revocation registry entry transaction to the ledger And "Acme" has an issued credential from "Bob" - And "Bob" revokes the credential without publishing the entry - And "Bob" authors a revocation registry entry publishing transaction + And "Bob" revokes the credential without publishing the entry with txn endorsement + And "Bob" authors a revocation registry entry publishing transaction with txn endorsement Then "Acme" can verify the credential from "Bob" was revoked Examples: @@ -187,8 +187,8 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions And "Bob" has written the revocation registry definition to the ledger And "Bob" has written the revocation registry entry transaction to the ledger And "Acme" has an issued credential from "Bob" - And "Bob" revokes the credential without publishing the entry - And "Bob" authors a revocation registry entry publishing transaction + And "Bob" revokes the credential without publishing the entry with txn endorsement + And "Bob" authors a revocation registry entry publishing transaction with txn endorsement Then "Acme" can verify the credential from "Bob" was revoked Examples: diff --git a/demo/features/steps/0586-sign-transaction.py b/demo/features/steps/0586-sign-transaction.py index feca42d913..29dc2b29a8 100644 --- a/demo/features/steps/0586-sign-transaction.py +++ b/demo/features/steps/0586-sign-transaction.py @@ -467,13 +467,46 @@ def step_impl(context, agent_name): async_sleep(3.0) -@when('"{agent_name}" authors a revocation registry entry publishing transaction') -@then('"{agent_name}" authors a revocation registry entry publishing transaction') +@when( + '"{agent_name}" revokes the credential without publishing the entry with txn endorsement' +) +@then( + '"{agent_name}" revokes the credential without publishing the entry with txn endorsement' +) def step_impl(context, agent_name): agent = context.active_agents[agent_name] + # get the required revocation info from the last credential exchange + cred_exchange = context.cred_exchange + + cred_exchange = agent_container_GET( + agent["agent"], "/issue-credential-2.0/records/" + cred_exchange["cred_ex_id"] + ) + context.cred_exchange = cred_exchange connection_id = agent["agent"].agent.connection_id + # revoke the credential + agent_container_POST( + agent["agent"], + "/revocation/revoke", + data={ + "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], + "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], + "publish": False, + "connection_id": cred_exchange["cred_ex_record"]["connection_id"], + }, + params={"conn_id": connection_id, "create_transaction_for_endorser": "true"}, + ) + + # pause for a few seconds + async_sleep(3.0) + + +@when('"{agent_name}" authors a revocation registry entry publishing transaction') +@then('"{agent_name}" authors a revocation registry entry publishing transaction') +def step_impl(context, agent_name): + agent = context.active_agents[agent_name] + # create rev_reg entry transaction created_rev_reg = agent_container_POST( agent["agent"], @@ -491,6 +524,38 @@ def step_impl(context, agent_name): assert "rrid2crid" in created_rev_reg +@when( + '"{agent_name}" authors a revocation registry entry publishing transaction with txn endorsement' +) +@then( + '"{agent_name}" authors a revocation registry entry publishing transaction with txn endorsement' +) +def step_impl(context, agent_name): + agent = context.active_agents[agent_name] + + connection_id = agent["agent"].agent.connection_id + + # create rev_reg entry transaction + created_rev_reg = agent_container_POST( + agent["agent"], + f"/revocation/publish-revocations", + data={ + "rrid2crid": { + context.cred_exchange["indy"]["rev_reg_id"]: [ + context.cred_exchange["indy"]["cred_rev_id"] + ] + } + }, + params={"conn_id": connection_id, "create_transaction_for_endorser": "true"}, + ) + + # check that transaction request has been sent + assert created_rev_reg["txn"]["state"] == "request_sent" + + # pause for a few seconds + async_sleep(3.0) + + @then('"{holder_name}" can verify the credential from "{issuer_name}" was revoked') def step_impl(context, holder_name, issuer_name): agent = context.active_agents[holder_name]