From 6f6ad0d75b193dcea8dceff37df96e966d960c67 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 13 Feb 2024 21:45:57 +0000 Subject: [PATCH] Anoncreds revoke and publish-revocations endorsement Signed-off-by: jamshale --- aries_cloudagent/anoncreds/revocation.py | 30 +- .../anoncreds/revocation_setup.py | 4 + aries_cloudagent/anoncreds/routes.py | 97 +----- .../anoncreds/tests/test_revocation.py | 11 + .../anoncreds/tests/test_routes.py | 40 --- .../revocation_anoncreds/manager.py | 14 +- .../revocation_anoncreds/routes.py | 299 +++++++++--------- .../revocation_anoncreds/tests/test_routes.py | 8 +- demo/features/0586-sign-transaction.feature | 33 +- demo/features/steps/0586-sign-transaction.py | 72 +++-- 10 files changed, 267 insertions(+), 341 deletions(-) diff --git a/aries_cloudagent/anoncreds/revocation.py b/aries_cloudagent/anoncreds/revocation.py index c596c8a0e9..bf50cef8d2 100644 --- a/aries_cloudagent/anoncreds/revocation.py +++ b/aries_cloudagent/anoncreds/revocation.py @@ -483,7 +483,7 @@ async def store_revocation_registry_list(self, result: RevListResult): value_json={ "rev_list": rev_list.serialize(), "pending": None, - # TODO THIS IS A HACK; this fixes ACA-Py expecting 1-based indexes + # TODO THIS IS A HACK; this fixes ACA-Py expecting 1-based indexes # noqa: E501 "next_index": 1, }, tags={ @@ -505,16 +505,21 @@ async def store_revocation_registry_list(self, result: RevListResult): async def finish_revocation_list(self, job_id: str, rev_reg_def_id: str): """Mark a revocation list as finished.""" async with self.profile.transaction() as txn: - await self._finish_registration( - txn, + # Finish the registration if the list is new, otherwise already updated + existing_list = await txn.handle.fetch( CATEGORY_REV_LIST, - job_id, rev_reg_def_id, - state=STATE_FINISHED, ) - await txn.commit() - - await self.notify(RevListFinishedEvent.with_payload(rev_reg_def_id)) + if not existing_list: + await self._finish_registration( + txn, + CATEGORY_REV_LIST, + job_id, + rev_reg_def_id, + state=STATE_FINISHED, + ) + await txn.commit() + await self.notify(RevListFinishedEvent.with_payload(rev_reg_def_id)) async def update_revocation_list( self, @@ -566,22 +571,21 @@ async def update_revocation_list( self.profile, rev_reg_def, prev, curr, revoked, options ) - # TODO Handle `failed` state - + # # TODO Handle `failed` state try: async with self.profile.session() as session: rev_list_entry_upd = await session.handle.fetch( - CATEGORY_REV_LIST, rev_reg_def_id, for_update=True + CATEGORY_REV_LIST, result.rev_reg_def_id, for_update=True ) if not rev_list_entry_upd: raise AnonCredsRevocationError( - "Revocation list not found for id {rev_reg_def_id}" + f"Revocation list not found for id {rev_reg_def_id}" ) tags = rev_list_entry_upd.tags tags["state"] = result.revocation_list_state.state await session.handle.replace( CATEGORY_REV_LIST, - rev_reg_def_id, + result.rev_reg_def_id, value=rev_list_entry_upd.value, tags=tags, ) diff --git a/aries_cloudagent/anoncreds/revocation_setup.py b/aries_cloudagent/anoncreds/revocation_setup.py index 4e0fd96b85..8f16b382c9 100644 --- a/aries_cloudagent/anoncreds/revocation_setup.py +++ b/aries_cloudagent/anoncreds/revocation_setup.py @@ -1,5 +1,6 @@ """Automated setup process for AnonCreds credential definitions with revocation.""" +import logging from abc import ABC, abstractmethod from aries_cloudagent.protocols.endorse_transaction.v1_0.util import is_author_role @@ -16,6 +17,8 @@ RevRegDefFinishedEvent, ) +LOGGER = logging.getLogger(__name__) + class AnonCredsRevocationSetupManager(ABC): """Base class for automated setup of revocation.""" @@ -102,3 +105,4 @@ async def on_rev_reg_def(self, profile: Profile, event: RevRegDefFinishedEvent): async def on_rev_list(self, profile: Profile, event: RevListFinishedEvent): """Handle rev list finished.""" + LOGGER.debug("Revocation list finished: %s", event.payload.rev_reg_def_id) diff --git a/aries_cloudagent/anoncreds/routes.py b/aries_cloudagent/anoncreds/routes.py index 2931c18807..e1011fd7a3 100644 --- a/aries_cloudagent/anoncreds/routes.py +++ b/aries_cloudagent/anoncreds/routes.py @@ -25,16 +25,12 @@ INDY_SCHEMA_ID_EXAMPLE, UUIDFour, ) -from ..revocation.error import RevocationError, RevocationNotSupportedError -from ..revocation_anoncreds.manager import RevocationManager, RevocationManagerError -from ..revocation_anoncreds.routes import ( - PublishRevocationsSchema, +from ..revocation.error import RevocationNotSupportedError +from ..revocation.routes import ( RevocationModuleResponseSchema, - RevokeRequestSchema, RevRegIdMatchInfoSchema, - TxnOrPublishRevocationsResultSchema, ) -from ..storage.error import StorageError, StorageNotFoundError +from ..storage.error import StorageNotFoundError from .base import ( AnonCredsObjectNotFound, AnonCredsRegistrationError, @@ -685,91 +681,6 @@ async def set_active_registry(request: web.BaseRequest): raise web.HTTPInternalServerError(reason=str(e)) from e -@docs( - tags=["anoncreds"], - summary="Revoke an issued credential", -) -@request_schema(RevokeRequestSchema()) -@response_schema(RevocationModuleResponseSchema(), description="") -async def revoke(request: web.BaseRequest): - """Request handler for storing a credential revocation. - - Args: - request: aiohttp request object - - Returns: - The credential revocation details. - - """ - context: AdminRequestContext = request["context"] - 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") - connection_id = body.get("connection_id") - body["notify_version"] = body.get("notify_version", "v1_0") - notify_version = body["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: - 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) - else: - # no cred_ex_id so we can safely splat the body - await rev_manager.revoke_credential(**body) - return web.json_response({}) - except ( - RevocationManagerError, - AnonCredsRevocationError, - StorageError, - AnonCredsIssuerError, - AnonCredsRegistrationError, - ) as err: - raise web.HTTPBadRequest(reason=err.roll_up) from err - - -@docs(tags=["revocation"], summary="Publish pending revocations to ledger") -@request_schema(PublishRevocationsSchema()) -@response_schema(TxnOrPublishRevocationsResultSchema(), 200, description="") -async def publish_revocations(request: web.BaseRequest): - """Request handler for publishing pending revocations to the ledger. - - Args: - request: aiohttp request object - - Returns: - Credential revocation ids published as revoked by revocation registry id. - - """ - context: AdminRequestContext = request["context"] - body = await request.json() - rrid2crid = body.get("rrid2crid") - - rev_manager = RevocationManager(context.profile) - - try: - rev_reg_resp = await rev_manager.publish_pending_revocations( - rrid2crid, - ) - return web.json_response({"rrid2crid": rev_reg_resp}) - except ( - RevocationError, - StorageError, - AnonCredsIssuerError, - AnonCredsRevocationError, - ) as err: - raise web.HTTPBadRequest(reason=err.roll_up) from err - - def register_events(event_bus: EventBus): """Register events.""" # TODO Make this pluggable? @@ -800,8 +711,6 @@ async def register(app: web.Application): web.post("/anoncreds/revocation-list", rev_list_post), web.put("/anoncreds/registry/{rev_reg_id}/tails-file", upload_tails_file), web.put("/anoncreds/registry/{rev_reg_id}/active", set_active_registry), - web.post("/anoncreds/revoke", revoke), - web.post("/anoncreds/publish-revocations", publish_revocations), ] ) diff --git a/aries_cloudagent/anoncreds/tests/test_revocation.py b/aries_cloudagent/anoncreds/tests/test_revocation.py index 2ae61afc09..4e82db18c1 100644 --- a/aries_cloudagent/anoncreds/tests/test_revocation.py +++ b/aries_cloudagent/anoncreds/tests/test_revocation.py @@ -591,12 +591,23 @@ async def test_create_and_register_revocation_list( @mock.patch.object(test_module.AnonCredsRevocation, "_finish_registration") async def test_finish_revocation_list(self, mock_finish, mock_handle): self.profile.context.injector.bind_instance(EventBus, MockEventBus()) + + mock_handle.fetch = mock.CoroutineMock(side_effect=[None, MockEntry()]) + + # Fetch doesn't find list then it should be created await self.revocation.finish_revocation_list( job_id="test-job-id", rev_reg_def_id="test-rev-reg-def-id", ) assert mock_finish.called + # Fetch finds list then there's nothing to do, it's already finished and updated + await self.revocation.finish_revocation_list( + job_id="test-job-id", + rev_reg_def_id="test-rev-reg-def-id", + ) + assert mock_finish.call_count == 1 + @mock.patch.object(InMemoryProfileSession, "handle") async def test_update_revocation_list_get_rev_reg_errors(self, mock_handle): mock_handle.fetch = mock.CoroutineMock( diff --git a/aries_cloudagent/anoncreds/tests/test_routes.py b/aries_cloudagent/anoncreds/tests/test_routes.py index ecfa238495..b2ecda8c5d 100644 --- a/aries_cloudagent/anoncreds/tests/test_routes.py +++ b/aries_cloudagent/anoncreds/tests/test_routes.py @@ -19,7 +19,6 @@ from aries_cloudagent.core.in_memory.profile import ( InMemoryProfile, ) -from aries_cloudagent.revocation_anoncreds.manager import RevocationManager from aries_cloudagent.tests import mock from .. import routes as test_module @@ -338,45 +337,6 @@ async def test_set_active_registry(self, mock_set): with self.assertRaises(KeyError): await test_module.set_active_registry(self.request) - async def test_revoke_notify_without_connection_throws_x(self): - self.request.json = mock.CoroutineMock(return_value={"notify": True}) - with self.assertRaises(web.HTTPBadRequest): - await test_module.revoke(self.request) - - @mock.patch.object( - RevocationManager, - "revoke_credential_by_cred_ex_id", - return_value=None, - ) - @mock.patch.object( - RevocationManager, - "revoke_credential", - return_value=None, - ) - async def test_revoke(self, mock_revoke, mock_revoke_by_id): - self.request.json = mock.CoroutineMock( - return_value={"cred_ex_id": "cred_ex_id"} - ) - await test_module.revoke(self.request) - assert mock_revoke_by_id.call_count == 1 - assert mock_revoke.call_count == 0 - - self.request.json = mock.CoroutineMock(return_value={}) - await test_module.revoke(self.request) - assert mock_revoke.call_count == 1 - - @mock.patch.object( - RevocationManager, - "publish_pending_revocations", - return_value="test-rrid", - ) - async def test_publish_revocations(self, mock_publish): - self.request.json = mock.CoroutineMock(return_value={"rrid2crid": "rrid2crid"}) - result = await test_module.publish_revocations(self.request) - - assert json.loads(result.body)["rrid2crid"] == "test-rrid" - assert mock_publish.call_count == 1 - @mock.patch.object(DefaultRevocationSetup, "register_events") async def test_register_events(self, mock_revocation_setup_listeners): mock_event_bus = MockEventBus() diff --git a/aries_cloudagent/revocation_anoncreds/manager.py b/aries_cloudagent/revocation_anoncreds/manager.py index a70a797998..a846396146 100644 --- a/aries_cloudagent/revocation_anoncreds/manager.py +++ b/aries_cloudagent/revocation_anoncreds/manager.py @@ -47,6 +47,7 @@ async def revoke_credential_by_cred_ex_id( thread_id: str = None, connection_id: str = None, comment: str = None, + options: Optional[dict] = None, ): """Revoke a credential by its credential exchange identifier at issue. @@ -79,6 +80,7 @@ async def revoke_credential_by_cred_ex_id( thread_id=thread_id, connection_id=connection_id, comment=comment, + options=options, ) async def revoke_credential( @@ -91,6 +93,7 @@ async def revoke_credential( thread_id: str = None, connection_id: str = None, comment: str = None, + options: Optional[dict] = None, ): """Revoke a credential. @@ -120,7 +123,11 @@ async def revoke_credential( if result.curr and result.revoked: await self.set_cred_revoked_state(rev_reg_id, result.revoked) await revoc.update_revocation_list( - rev_reg_id, result.prev, result.curr, result.revoked + rev_reg_id, + result.prev, + result.curr, + result.revoked, + options=options, ) await notify_revocation_published_event( self._profile, rev_reg_id, [cred_rev_id] @@ -128,7 +135,6 @@ async def revoke_credential( else: await revoc.mark_pending_revocations(rev_reg_id, int(cred_rev_id)) - if notify: thread_id = thread_id or f"indy::{rev_reg_id}::{cred_rev_id}" rev_notify_rec = RevNotificationRecord( @@ -185,6 +191,7 @@ async def update_rev_reg_revoked_state( async def publish_pending_revocations( self, rrid2crid: Optional[Mapping[Text, Sequence[Text]]] = None, + options: Optional[dict] = None, ) -> Mapping[Text, Sequence[Text]]: """Publish pending revocations to the ledger. @@ -208,6 +215,7 @@ async def publish_pending_revocations( Returns: mapping from each revocation registry id to its cred rev ids published. """ + options = options or {} published_crids = {} revoc = AnonCredsRevocation(self._profile) @@ -226,7 +234,7 @@ async def publish_pending_revocations( if result.curr and result.revoked: await self.set_cred_revoked_state(rrid, result.revoked) await revoc.update_revocation_list( - rrid, result.prev, result.curr, result.revoked + rrid, result.prev, result.curr, result.revoked, options ) published_crids[rrid] = sorted(result.revoked) await notify_revocation_published_event( diff --git a/aries_cloudagent/revocation_anoncreds/routes.py b/aries_cloudagent/revocation_anoncreds/routes.py index b3b5b9e52a..cac93d0104 100644 --- a/aries_cloudagent/revocation_anoncreds/routes.py +++ b/aries_cloudagent/revocation_anoncreds/routes.py @@ -12,28 +12,29 @@ request_schema, response_schema, ) - from marshmallow import fields, validate, validates_schema from marshmallow.exceptions import ValidationError -from ..anoncreds.models.anoncreds_revocation import RevRegDefState - -from ..anoncreds.default.legacy_indy.registry import LegacyIndyRegistry +from ..admin.request_context import AdminRequestContext from ..anoncreds.base import ( AnonCredsObjectNotFound, AnonCredsRegistrationError, AnonCredsResolutionError, ) +from ..anoncreds.default.legacy_indy.registry import LegacyIndyRegistry from ..anoncreds.issuer import AnonCredsIssuerError +from ..anoncreds.models.anoncreds_revocation import RevRegDefState from ..anoncreds.revocation import AnonCredsRevocation, AnonCredsRevocationError +from ..anoncreds.routes import ( + create_transaction_for_endorser_description, + endorser_connection_id_description, +) from ..askar.profile import AskarProfile -from ..indy.models.revocation import IndyRevRegDef - -from ..admin.request_context import AdminRequestContext from ..indy.issuer import IndyIssuerError +from ..indy.models.revocation import IndyRevRegDef from ..ledger.base import BaseLedger -from ..ledger.multiple_ledger.base_manager import BaseMultipleLedgerManager from ..ledger.error import LedgerError +from ..ledger.multiple_ledger.base_manager import BaseMultipleLedgerManager from ..messaging.models.openapi import OpenAPISchema from ..messaging.valid import ( INDY_CRED_DEF_ID_EXAMPLE, @@ -48,28 +49,27 @@ UUID4_VALIDATE, WHOLE_NUM_EXAMPLE, WHOLE_NUM_VALIDATE, + UUIDFour, ) from ..protocols.endorse_transaction.v1_0.models.transaction_record import ( TransactionRecordSchema, ) -from ..storage.error import StorageError, StorageNotFoundError - from ..revocation.error import RevocationError +from ..revocation.models.issuer_rev_reg_record import ( + IssuerRevRegRecord, + IssuerRevRegRecordSchema, +) +from ..storage.error import StorageError, StorageNotFoundError from .manager import RevocationManager, RevocationManagerError from .models.issuer_cred_rev_record import ( IssuerCredRevRecord, IssuerCredRevRecordSchema, ) -from ..revocation.models.issuer_rev_reg_record import ( - IssuerRevRegRecord, - IssuerRevRegRecordSchema, -) - LOGGER = logging.getLogger(__name__) -class RevocationModuleResponseSchema(OpenAPISchema): +class RevocationAnoncredsModuleResponseSchema(OpenAPISchema): """Response schema for Revocation Module.""" @@ -191,113 +191,6 @@ def validate_fields(self, data, **kwargs): ) -class RevokeRequestSchema(CredRevRecordQueryStringSchema): - """Parameters and validators for revocation request.""" - - @validates_schema - def validate_fields(self, data, **kwargs): - """Validate fields - connection_id and thread_id must be present if notify.""" - super().validate_fields(data, **kwargs) - - notify = data.get("notify") - connection_id = data.get("connection_id") - notify_version = data.get("notify_version", "v1_0") - - if notify and not connection_id: - raise ValidationError( - "Request must specify connection_id if notify is true" - ) - if notify and not notify_version: - raise ValidationError( - "Request must specify notify_version if notify is true" - ) - - publish = fields.Boolean( - required=False, - metadata={ - "description": ( - "(True) publish revocation to ledger immediately, or (default, False)" - " mark it pending" - ) - }, - ) - notify = fields.Boolean( - required=False, - metadata={"description": "Send a notification to the credential recipient"}, - ) - notify_version = fields.String( - validate=validate.OneOf(["v1_0", "v2_0"]), - required=False, - metadata={ - "description": ( - "Specify which version of the revocation notification should be sent" - ) - }, - ) - connection_id = fields.Str( - required=False, - validate=UUID4_VALIDATE, - metadata={ - "description": ( - "Connection ID to which the revocation notification will be sent;" - " required if notify is true" - ), - "example": UUID4_EXAMPLE, - }, - ) - thread_id = fields.Str( - required=False, - metadata={ - "description": ( - "Thread ID of the credential exchange message thread resulting in the" - " credential now being revoked; required if notify is true" - ) - }, - ) - comment = fields.Str( - required=False, - metadata={ - "description": "Optional comment to include in revocation notification" - }, - ) - - -class PublishRevocationsSchema(OpenAPISchema): - """Request and result schema for revocation publication API call.""" - - rrid2crid = fields.Dict( - required=False, - keys=fields.Str(metadata={"example": INDY_REV_REG_ID_EXAMPLE}), - values=fields.List( - fields.Str( - validate=INDY_CRED_REV_ID_VALIDATE, - metadata={ - "description": "Credential revocation identifier", - "example": INDY_CRED_REV_ID_EXAMPLE, - }, - ) - ), - metadata={"description": "Credential revocation ids by revocation registry id"}, - ) - - -class TxnOrPublishRevocationsResultSchema(OpenAPISchema): - """Result schema for credential definition send request.""" - - sent = fields.Nested( - PublishRevocationsSchema(), - required=False, - metadata={"definition": "Content sent"}, - ) - txn = fields.Nested( - TransactionRecordSchema(), - required=False, - metadata={ - "description": "Revocation registry revocations transaction to endorse" - }, - ) - - class ClearPendingRevocationsRequestSchema(OpenAPISchema): """Request schema for clear pending revocations API call.""" @@ -491,12 +384,143 @@ class RevRegConnIdMatchInfoSchema(OpenAPISchema): ) +class PublishRevocationsOptions(OpenAPISchema): + """Options for publishing revocations to ledger.""" + + endorser_connection_id = fields.Str( + metadata={ + "description": endorser_connection_id_description, # noqa: F821 + "required": False, + "example": UUIDFour.EXAMPLE, + } + ) + + create_transaction_for_endorser = fields.Bool( + metadata={ + "description": create_transaction_for_endorser_description, + "required": False, + "example": False, + } + ) + + +class PublishRevocationsSchema(OpenAPISchema): + """Request and result schema for revocation publication API call.""" + + rrid2crid = fields.Dict( + required=False, + keys=fields.Str(metadata={"example": INDY_REV_REG_ID_EXAMPLE}), + values=fields.List( + fields.Str( + validate=INDY_CRED_REV_ID_VALIDATE, + metadata={ + "description": "Credential revocation identifier", + "example": INDY_CRED_REV_ID_EXAMPLE, + }, + ) + ), + metadata={"description": "Credential revocation ids by revocation registry id"}, + ) + options = fields.Nested(PublishRevocationsOptions()) + + +class PublishRevocationsResultSchema(OpenAPISchema): + """Result schema for credential definition send request.""" + + rrid2crid = fields.Dict( + required=False, + keys=fields.Str(metadata={"example": INDY_REV_REG_ID_EXAMPLE}), + values=fields.List( + fields.Str( + validate=INDY_CRED_REV_ID_VALIDATE, + metadata={ + "description": "Credential revocation identifier", + "example": INDY_CRED_REV_ID_EXAMPLE, + }, + ) + ), + metadata={"description": "Credential revocation ids by revocation registry id"}, + ) + + +class RevokeRequestSchema(CredRevRecordQueryStringSchema): + """Parameters and validators for revocation request.""" + + @validates_schema + def validate_fields(self, data, **kwargs): + """Validate fields - connection_id and thread_id must be present if notify.""" + super().validate_fields(data, **kwargs) + + notify = data.get("notify") + connection_id = data.get("connection_id") + notify_version = data.get("notify_version", "v1_0") + + if notify and not connection_id: + raise ValidationError( + "Request must specify connection_id if notify is true" + ) + if notify and not notify_version: + raise ValidationError( + "Request must specify notify_version if notify is true" + ) + + publish = fields.Boolean( + required=False, + metadata={ + "description": ( + "(True) publish revocation to ledger immediately, or (default, False)" + " mark it pending" + ) + }, + ) + notify = fields.Boolean( + required=False, + metadata={"description": "Send a notification to the credential recipient"}, + ) + notify_version = fields.String( + validate=validate.OneOf(["v1_0", "v2_0"]), + required=False, + metadata={ + "description": ( + "Specify which version of the revocation notification should be sent" + ) + }, + ) + connection_id = fields.Str( + required=False, + validate=UUID4_VALIDATE, + metadata={ + "description": ( + "Connection ID to which the revocation notification will be sent;" + " required if notify is true" + ), + "example": UUID4_EXAMPLE, + }, + ) + thread_id = fields.Str( + required=False, + metadata={ + "description": ( + "Thread ID of the credential exchange message thread resulting in the" + " credential now being revoked; required if notify is true" + ) + }, + ) + comment = fields.Str( + required=False, + metadata={ + "description": "Optional comment to include in revocation notification" + }, + ) + options = PublishRevocationsOptions() + + @docs( tags=["revocation"], summary="Revoke an issued credential", ) @request_schema(RevokeRequestSchema()) -@response_schema(RevocationModuleResponseSchema(), description="") +@response_schema(RevocationAnoncredsModuleResponseSchema(), description="") async def revoke(request: web.BaseRequest): """Request handler for storing a credential revocation. @@ -507,12 +531,6 @@ async def revoke(request: web.BaseRequest): The credential revocation details. """ - # - # this is exactly what is in anoncreds /revocation/revoke. - # we cannot import the revoke function as it imports classes from here, - # so circular dependency. - # we will clean this up and DRY at some point. - # context: AdminRequestContext = request["context"] body = await request.json() cred_ex_id = body.get("cred_ex_id") @@ -538,6 +556,7 @@ async def revoke(request: web.BaseRequest): else: # no cred_ex_id so we can safely splat the body await rev_manager.revoke_credential(**body) + return web.json_response({}) except ( RevocationManagerError, AnonCredsRevocationError, @@ -547,12 +566,10 @@ async def revoke(request: web.BaseRequest): ) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - return web.json_response({}) - @docs(tags=["revocation"], summary="Publish pending revocations to ledger") @request_schema(PublishRevocationsSchema()) -@response_schema(TxnOrPublishRevocationsResultSchema(), 200, description="") +@response_schema(PublishRevocationsResultSchema(), 200, description="") async def publish_revocations(request: web.BaseRequest): """Request handler for publishing pending revocations to the ledger. @@ -563,22 +580,16 @@ async def publish_revocations(request: web.BaseRequest): Credential revocation ids published as revoked by revocation registry id. """ - # - # this is exactly what is in anoncreds /revocation/publish-revocations. - # we cannot import the function as it imports classes from here, - # so circular dependency. - # we will clean this up and DRY at some point. - # context: AdminRequestContext = request["context"] body = await request.json() + options = body.get("options", {}) rrid2crid = body.get("rrid2crid") rev_manager = RevocationManager(context.profile) try: - rev_reg_resp = await rev_manager.publish_pending_revocations( - rrid2crid, - ) + rev_reg_resp = await rev_manager.publish_pending_revocations(rrid2crid, options) + return web.json_response({"rrid2crid": rev_reg_resp}) except ( RevocationError, StorageError, @@ -587,8 +598,6 @@ async def publish_revocations(request: web.BaseRequest): ) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - return web.json_response({"rrid2crid": rev_reg_resp}) - @docs( tags=["revocation"], @@ -1004,7 +1013,7 @@ async def get_cred_rev_record(request: web.BaseRequest): produces=["application/octet-stream"], ) @match_info_schema(RevRegIdMatchInfoSchema()) -@response_schema(RevocationModuleResponseSchema, description="tails file") +@response_schema(RevocationAnoncredsModuleResponseSchema, description="tails file") async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: """Request handler to download tails file for revocation registry. diff --git a/aries_cloudagent/revocation_anoncreds/tests/test_routes.py b/aries_cloudagent/revocation_anoncreds/tests/test_routes.py index 97f51e7339..624313b919 100644 --- a/aries_cloudagent/revocation_anoncreds/tests/test_routes.py +++ b/aries_cloudagent/revocation_anoncreds/tests/test_routes.py @@ -1,20 +1,18 @@ import os -import pytest import shutil +from unittest import IsolatedAsyncioTestCase +import pytest from aiohttp.web import HTTPNotFound -from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock +from aries_cloudagent.tests import mock from ...anoncreds.models.anoncreds_revocation import ( RevRegDef, RevRegDefValue, ) from ...askar.profile import AskarProfile - from ...core.in_memory import InMemoryProfile - from .. import routes as test_module diff --git a/demo/features/0586-sign-transaction.feature b/demo/features/0586-sign-transaction.feature index 97626d2766..06bea76d3b 100644 --- a/demo/features/0586-sign-transaction.feature +++ b/demo/features/0586-sign-transaction.feature @@ -102,6 +102,7 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions And "Bob" authors a revocation registry entry publishing transaction Then "Acme" can verify the credential from "Bob" was revoked + @GHA Examples: | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | | --revocation --public-did --did-exchange | --revocation --did-exchange | driverslicense | Data_DL_NormalizedValues | @@ -114,7 +115,7 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions | Acme_capabilities | Bob_capabilties | Schema_name | Credential_data | | --multitenant --multi-ledger --revocation --public-did | --multitenant --multi-ledger --revocation | driverslicense | Data_DL_NormalizedValues | - @WalletType_Askar_AnonCreds + @WalletType_Askar_AnonCreds @GHA Examples: | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | | --revocation --public-did --did-exchange | --revocation --did-exchange --wallet-type askar-anoncreds | anoncreds-testing | Data_AC_NormalizedValues | @@ -196,6 +197,11 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions | --endorser-role endorser --revocation --public-did --multitenant | --endorser-role author --revocation --multitenant | driverslicense | Data_DL_NormalizedValues | | --endorser-role endorser --revocation --public-did --mediation --multitenant | --endorser-role author --revocation --mediation --multitenant | driverslicense | Data_DL_NormalizedValues | + @WalletType_Askar_AnonCreds + Examples: + | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | + | --endorser-role endorser --revocation --public-did | --endorser-role author --revocation --wallet-type askar-anoncreds | anoncreds-testing | Data_AC_NormalizedValues | + @T003.1-RFC0586 @GHA Scenario Outline: endorse a schema and cred def transaction, write to the ledger, issue and revoke a credential, with auto endorsing workflow Given we have "2" agents @@ -224,26 +230,7 @@ Feature: RFC 0586 Aries sign (endorse) transactions functions | --endorser-role endorser --revocation --public-did | --endorser-role author --revocation | driverslicense | Data_DL_NormalizedValues | | --endorser-role endorser --revocation --public-did | --endorser-role author --revocation --multitenant | driverslicense | Data_DL_NormalizedValues | - @T003.2-RFC0586 @GHA - Scenario Outline: endorse a schema and cred def transaction, write to the ledger, and issue a credential, with auto endorsing workflow - Given we have "2" agents - | name | role | capabilities | - | Acme | endorser | | - | Bob | author | | - And "Acme" and "Bob" have an existing connection - When "Acme" has a DID with role "ENDORSER" - And "Acme" connection has job role "TRANSACTION_ENDORSER" - And "Bob" connection has job role "TRANSACTION_AUTHOR" - And "Bob" connection sets endorser info - And "Bob" has a DID with role "AUTHOR" - And "Bob" authors a schema transaction with - And "Bob" has written the schema to the ledger - And "Bob" authors a credential definition transaction with - And "Bob" has written the credential definition for to the ledger - 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" - + @WalletType_Askar_AnonCreds Examples: - | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | - | --endorser-role endorser --revocation --public-did | --endorser-role author --revocation --wallet-type askar-anoncreds | anoncreds-testing | Data_AC_NormalizedValues | + | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | + | --endorser-role endorser --revocation --public-did | --endorser-role author --revocation --wallet-type askar-anoncreds | anoncreds-testing | Data_AC_NormalizedValues | diff --git a/demo/features/steps/0586-sign-transaction.py b/demo/features/steps/0586-sign-transaction.py index c43d545062..e0786afc38 100644 --- a/demo/features/steps/0586-sign-transaction.py +++ b/demo/features/steps/0586-sign-transaction.py @@ -576,14 +576,13 @@ def step_impl(context, agent_name): ) context.cred_exchange = cred_exchange - # 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, + "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], "connection_id": cred_exchange["cred_ex_record"]["connection_id"], }, ) @@ -611,16 +610,36 @@ def step_impl(context, agent_name): connection_id = agent["agent"].agent.connection_id # revoke the credential - agent_container_POST( - agent["agent"], - "/revocation/revoke", - data={ + if not is_anoncreds(agent): + 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"}, + } + params = { + "conn_id": connection_id, + "create_transaction_for_endorser": "true", + } + + else: + data = { + "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], + "publish": False, + "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], + "connection_id": cred_exchange["cred_ex_record"]["connection_id"], + "options": { + "endorser_connection_id": connection_id, + "create_transaction_for_endorser": "true", + }, + } + params = {} + + agent_container_POST( + agent["agent"], + "/revocation/revoke", + data=data, + params=params, ) # pause for a few seconds @@ -644,8 +663,8 @@ def step_impl(context, agent_name): ] } }, + params={}, ) - # check that rev reg entry was written assert "rrid2crid" in created_rev_reg @@ -662,21 +681,38 @@ def step_impl(context, agent_name): connection_id = agent["agent"].agent.connection_id # create rev_reg entry transaction - created_rev_reg = agent_container_POST( - agent["agent"], - "/revocation/publish-revocations", - data={ + if not is_anoncreds(agent): + 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"}, - ) + } + params = { + "conn_id": connection_id, + "create_transaction_for_endorser": "true", + } + else: + data = { + "rrid2crid": { + context.cred_exchange["indy"]["rev_reg_id"]: [ + context.cred_exchange["indy"]["cred_rev_id"] + ] + }, + "options": { + "endorser_connection_id": connection_id, + "create_transaction_for_endorser": "true", + }, + } + params = {} - # check that transaction request has been sent - assert created_rev_reg["txn"]["state"] == "request_sent" + agent_container_POST( + agent["agent"], + "/revocation/publish-revocations", + data=data, + params=params, + ) # pause for a few seconds async_sleep(3.0)