From 403880d6f907318dec8ef600ca0efece75e7bd40 Mon Sep 17 00:00:00 2001 From: sklump Date: Fri, 4 Sep 2020 12:37:32 +0000 Subject: [PATCH] set rev reg state to push busted active rev reg to full Signed-off-by: sklump --- .../issue_credential/v1_0/manager.py | 10 +- .../v1_0/tests/test_manager.py | 10 +- .../revocation/models/cred_rev_info_record.py | 90 --------------- .../models/issuer_rev_reg_record.py | 10 +- .../tests/test_issuer_rev_reg_record.py | 2 +- aries_cloudagent/revocation/routes.py | 108 +++++++++++++----- .../revocation/tests/test_routes.py | 66 +++++++++++ 7 files changed, 165 insertions(+), 131 deletions(-) delete mode 100644 aries_cloudagent/revocation/models/cred_rev_info_record.py diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py index dba0a4b81c..574e49f706 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py @@ -588,7 +588,10 @@ async def issue_credential( ) # Make the current registry full - await active_reg.mark_full(self.context) + await active_reg.set_state( + self.context, + IssuerRevRegRecord.STATE_FULL, + ) except IssuerRevocationRegistryFullError: active_rev_regs = await IssuerRevRegRecord.query_by_cred_def_id( @@ -620,7 +623,10 @@ async def issue_credential( retries=retries - 1, ) else: - await active_reg.mark_full(self.context) + await active_reg.set_state( + self.context, + IssuerRevRegRecord.STATE_FULL, + ) raise cred_ex_record.credential = json.loads(credential_json) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py index 3ae71e1f9f..17cb812b44 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py @@ -904,7 +904,7 @@ async def test_issue_credential(self): max_creds=1000, ) ), - mark_full=async_mock.CoroutineMock(), + set_state=async_mock.CoroutineMock(), revoc_reg_id=REV_REG_ID, save=async_mock.CoroutineMock(), publish_registry_entry=async_mock.CoroutineMock(), @@ -1120,7 +1120,7 @@ async def test_issue_credential_rr_full(self): ) ), revoc_reg_id=REV_REG_ID, - mark_full=async_mock.CoroutineMock(), + set_state=async_mock.CoroutineMock(), ) ] ) @@ -1182,7 +1182,7 @@ async def test_issue_credential_rr_full_rr_staged_retry(self): ) ), revoc_reg_id=REV_REG_ID, - mark_full=async_mock.CoroutineMock(), + set_state=async_mock.CoroutineMock(), ) ] @@ -1196,7 +1196,7 @@ async def test_issue_credential_rr_full_rr_staged_retry(self): ) ), revoc_reg_id=REV_REG_ID, - mark_full=async_mock.CoroutineMock(), + set_state=async_mock.CoroutineMock(), ) ] @@ -1209,7 +1209,7 @@ async def test_issue_credential_rr_full_rr_staged_retry(self): ) ), revoc_reg_id=REV_REG_ID, - mark_full=async_mock.CoroutineMock(), + set_state=async_mock.CoroutineMock(), state="published", save=async_mock.CoroutineMock(), publish_registry_entry=async_mock.CoroutineMock(), diff --git a/aries_cloudagent/revocation/models/cred_rev_info_record.py b/aries_cloudagent/revocation/models/cred_rev_info_record.py deleted file mode 100644 index 388164aac2..0000000000 --- a/aries_cloudagent/revocation/models/cred_rev_info_record.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Issuer revocation registry storage handling.""" - -import json -import logging -import uuid - -LOGGER = logging.getLogger(__name__) - - -class CredRevInfoRecord(BaseRecord): - """Non-secrets record per issued credential to retain revocation info.""" - - class Meta: - """CredRevInfoRecord metadata.""" - - schema_class = "CredRevInfoRecordSchema" - - RECORD_ID_NAME = "cred_ex_id" - RECORD_TYPE = "cred_rev_info" - WEBHOOK_TOPIC = "cred_rev_info" - LOG_STATE_FLAG = "debug.revocation" - CACHE_ENABLED = False - TAG_NAMES = {"state", "cred_ex_id", "rev_reg_id", "cred_rev_id"} - - STATE_ISSUED = "issued" - STATE_REVOKED = "revoked" - - def __init__( - self, - *, - cred_ex_id: str = None, - state: str = None, - rev_reg_id: str = None, - cred_rev_id: str = None - **kwargs, - ): - """Initialize the credential revocation information record.""" - super().__init__( - cred_ex_id, state=state or CredRevInfoRecord.STATE_ISSUED, **kwargs - ) - self.cred_ex_id = cred_ex_id - self.rev_reg_id = rev_reg_id - self.cred_rev_id = cred_rev_id - - @property - def cred_ex_id(self) -> str: - """Accessor for the record ID, matching its credential exchange record's.""" - return self._id - - async def mark_revoked(self, context: InjectionContext): - """Change the record state to revoked.""" - self.state = CredRevInfoRecord.STATE_REVOKED - await self.save(context, reason="Marked as revoked") - - def __eq__(self, other: Any) -> bool: - """Comparison between records.""" - return super().__eq__(other) - - -class CredRevInfoRecordSchema(BaseRecordSchema): - """Schema to allow serialization/deserialization of cred rev info records.""" - - class Meta: - """CredRevInfoRecordSchema metadata.""" - - model_class = CredRevInfoRecord - - cred_ex_id = fields.Str( - required=False, - description=( - "Record identifier, matching original " - "credential exchange record identifier" - ), - example=UUIDFour.EXAMPLE, - ) - state = fields.Str( - required=False, - description="Credential revocation info record state", - example=CredRevInfoRecord.STATE_ISSUED, - ) - rev_reg_id = fields.Str( - required=False, - description="Revocation registry identifier", - **INDY_REV_REG_ID, - ) - cred_rev_id = fields.Str( - required=False, - description="Credential revocation identifier", - **INDY_CRED_REV_ID, - ) diff --git a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py index 1a271a61f5..4855ee2e42 100644 --- a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py @@ -63,7 +63,7 @@ class Meta: STATE_PUBLISHED = "published" # definition published STATE_STAGED = "staged" STATE_ACTIVE = "active" # first entry published - STATE_FULL = "full" + STATE_FULL = "full" # includes corrupt, out of sync, unusable def __init__( self, @@ -369,10 +369,10 @@ async def retrieve_by_revoc_reg_id( tag_filter = {"revoc_reg_id": revoc_reg_id} return await cls.retrieve_by_tag_filter(context, tag_filter) - async def mark_full(self, context: InjectionContext): - """Change the registry state to full.""" - self.state = IssuerRevRegRecord.STATE_FULL - await self.save(context, reason="Marked full") + async def set_state(self, context: InjectionContext, state: str = None): + """Change the registry state (default full).""" + self.state = state or IssuerRevRegRecord.STATE_FULL + await self.save(context, reason=f"Marked {self.state}") def __eq__(self, other: Any) -> bool: """Comparison between records.""" diff --git a/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py index 62e54d8ee1..f4db6adefd 100644 --- a/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py @@ -127,7 +127,7 @@ async def test_generate_registry_etc(self): ) assert retrieved.revoc_reg_id == rec.revoc_reg_id - await rec.mark_full(self.context) + await rec.set_state(self.context) assert rec.state == IssuerRevRegRecord.STATE_FULL data = rec.serialize() diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index 1f390cb5fc..643ab3c019 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -38,7 +38,7 @@ class RevRegCreateRequestSchema(OpenAPISchema): ) -class RevRegCreateResultSchema(OpenAPISchema): +class RevRegResultSchema(OpenAPISchema): """Result schema for revocation registry creation request.""" result = IssuerRevRegRecordSchema() @@ -86,6 +86,22 @@ class RevRegsCreatedQueryStringSchema(OpenAPISchema): ) +class SetRevRegStateQueryStringSchema(OpenAPISchema): + """Query string parameters and validators for request to set rev reg state.""" + + state = fields.Str( + description="Revocation registry state to set", + required=True, + validate=validate.OneOf( + [ + getattr(IssuerRevRegRecord, m) + for m in vars(IssuerRevRegRecord) + if m.startswith("STATE_") + ] + ), + ) + + class RevRegIdMatchInfoSchema(OpenAPISchema): """Path parameters and validators for request taking rev reg id.""" @@ -108,7 +124,7 @@ class CredDefIdMatchInfoSchema(OpenAPISchema): @docs(tags=["revocation"], summary="Creates a new revocation registry") @request_schema(RevRegCreateRequestSchema()) -@response_schema(RevRegCreateResultSchema(), 200) +@response_schema(RevRegResultSchema(), 200) async def revocation_create_registry(request: web.BaseRequest): """ Request handler to create a new revocation registry. @@ -142,16 +158,16 @@ async def revocation_create_registry(request: web.BaseRequest): try: issuer_did = credential_definition_id.split(":")[0] revoc = IndyRevocation(context) - registry_record = await revoc.init_issuer_registry( + issuer_rev_reg_rec = await revoc.init_issuer_registry( credential_definition_id, issuer_did, max_cred_num=max_cred_num, ) except RevocationNotSupportedError as e: raise web.HTTPBadRequest(reason=e.message) from e - await shield(registry_record.generate_registry(context)) + await shield(issuer_rev_reg_rec.generate_registry(context)) - return web.json_response({"result": registry_record.serialize()}) + return web.json_response({"result": issuer_rev_reg_rec.serialize()}) @docs( @@ -189,7 +205,7 @@ async def revocation_registries_created(request: web.BaseRequest): summary="Get revocation registry by revocation registry id", ) @match_info_schema(RevRegIdMatchInfoSchema()) -@response_schema(RevRegCreateResultSchema(), 200) +@response_schema(RevRegResultSchema(), 200) async def get_registry(request: web.BaseRequest): """ Request handler to get a revocation registry by identifier. @@ -203,15 +219,15 @@ async def get_registry(request: web.BaseRequest): """ context = request.app["request_context"] - registry_id = request.match_info["rev_reg_id"] + rev_reg_id = request.match_info["rev_reg_id"] try: revoc = IndyRevocation(context) - revoc_registry = await revoc.get_issuer_rev_reg_record(registry_id) + rev_reg = await revoc.get_issuer_rev_reg_record(rev_reg_id) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - return web.json_response({"result": revoc_registry.serialize()}) + return web.json_response({"result": rev_reg.serialize()}) @docs( @@ -219,7 +235,7 @@ async def get_registry(request: web.BaseRequest): summary="Get an active revocation registry by credential definition id", ) @match_info_schema(CredDefIdMatchInfoSchema()) -@response_schema(RevRegCreateResultSchema(), 200) +@response_schema(RevRegResultSchema(), 200) async def get_active_registry(request: web.BaseRequest): """ Request handler to get an active revocation registry by cred def id. @@ -237,11 +253,11 @@ async def get_active_registry(request: web.BaseRequest): try: revoc = IndyRevocation(context) - revoc_registry = await revoc.get_active_issuer_rev_reg_record(cred_def_id) + rev_reg = await revoc.get_active_issuer_rev_reg_record(cred_def_id) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - return web.json_response({"result": revoc_registry.serialize()}) + return web.json_response({"result": rev_reg.serialize()}) @docs( @@ -264,15 +280,15 @@ async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: """ context = request.app["request_context"] - registry_id = request.match_info["rev_reg_id"] + rev_reg_id = request.match_info["rev_reg_id"] try: revoc = IndyRevocation(context) - revoc_registry = await revoc.get_issuer_rev_reg_record(registry_id) + rev_reg = await revoc.get_issuer_rev_reg_record(rev_reg_id) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - return web.FileResponse(path=revoc_registry.tails_local_path, status=200) + return web.FileResponse(path=rev_reg.tails_local_path, status=200) @docs( @@ -280,7 +296,7 @@ async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: summary="Publish a given revocation registry", ) @match_info_schema(RevRegIdMatchInfoSchema()) -@response_schema(RevRegCreateResultSchema(), 200) +@response_schema(RevRegResultSchema(), 200) async def publish_registry(request: web.BaseRequest): """ Request handler to publish a revocation registry based on the registry id. @@ -293,23 +309,23 @@ async def publish_registry(request: web.BaseRequest): """ context = request.app["request_context"] - registry_id = request.match_info["rev_reg_id"] + rev_reg_id = request.match_info["rev_reg_id"] try: revoc = IndyRevocation(context) - revoc_registry = await revoc.get_issuer_rev_reg_record(registry_id) + rev_reg = await revoc.get_issuer_rev_reg_record(rev_reg_id) - await revoc_registry.publish_registry_definition(context) - LOGGER.debug("published registry definition: %s", registry_id) + await rev_reg.publish_registry_definition(context) + LOGGER.debug("published registry definition: %s", rev_reg_id) - await revoc_registry.publish_registry_entry(context) - LOGGER.debug("published registry entry: %s", registry_id) + await rev_reg.publish_registry_entry(context) + LOGGER.debug("published registry entry: %s", rev_reg_id) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except RevocationError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - return web.json_response({"result": revoc_registry.serialize()}) + return web.json_response({"result": rev_reg.serialize()}) @docs( @@ -318,7 +334,7 @@ async def publish_registry(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @request_schema(RevRegUpdateTailsFileUriSchema()) -@response_schema(RevRegCreateResultSchema(), 200) +@response_schema(RevRegResultSchema(), 200) async def update_registry(request: web.BaseRequest): """ Request handler to update a revocation registry based on the registry id. @@ -335,18 +351,50 @@ async def update_registry(request: web.BaseRequest): body = await request.json() tails_public_uri = body.get("tails_public_uri") - registry_id = request.match_info["rev_reg_id"] + rev_reg_id = request.match_info["rev_reg_id"] try: revoc = IndyRevocation(context) - revoc_registry = await revoc.get_issuer_rev_reg_record(registry_id) - await revoc_registry.set_tails_file_public_uri(context, tails_public_uri) + rev_reg = await revoc.get_issuer_rev_reg_record(rev_reg_id) + await rev_reg.set_tails_file_public_uri(context, tails_public_uri) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except RevocationError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - return web.json_response({"result": revoc_registry.serialize()}) + return web.json_response({"result": rev_reg.serialize()}) + + +@docs(tags=["revocation"], summary="Set revocation registry state manually.") +@match_info_schema(RevRegIdMatchInfoSchema()) +@querystring_schema(SetRevRegStateQueryStringSchema()) +@response_schema(RevRegResultSchema(), 200) +async def set_registry_state(request: web.BaseRequest): + """ + Request handler to set a revocation registry state manually. + + Args: + request: aiohttp request object + + Returns: + The revocation registry record, updated + + """ + context = request.app["request_context"] + rev_reg_id = request.match_info["rev_reg_id"] + state = request.query.get("state") + + try: + revoc = IndyRevocation(context) + rev_reg = await revoc.get_issuer_rev_reg_record(rev_reg_id) + await rev_reg.set_state(context, state) + + LOGGER.debug("set registry %s state: %s", rev_reg_id, state) + + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + + return web.json_response({"result": rev_reg.serialize()}) async def register(app: web.Application): @@ -374,6 +422,10 @@ async def register(app: web.Application): ), web.patch("/revocation/registry/{rev_reg_id}", update_registry), web.post("/revocation/registry/{rev_reg_id}/publish", publish_registry), + web.patch( + "/revocation/registry/{rev_reg_id}/set-state", + set_registry_state, + ), ] ) diff --git a/aries_cloudagent/revocation/tests/test_routes.py b/aries_cloudagent/revocation/tests/test_routes.py index 34254dc0ea..e6a15a9d91 100644 --- a/aries_cloudagent/revocation/tests/test_routes.py +++ b/aries_cloudagent/revocation/tests/test_routes.py @@ -436,6 +436,72 @@ async def test_update_registry_x(self): with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.update_registry(request) + async def test_set_registry_state(self): + REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( + self.test_did, self.test_did + ) + request = async_mock.MagicMock() + request.app = self.app + request.match_info = {"rev_reg_id": REV_REG_ID} + request.json = async_mock.CoroutineMock( + return_value={ + "max_cred_num": "1000", + } + ) + request.query = { + "state": test_module.IssuerRevRegRecord.STATE_ACTIVE, + } + + with async_mock.patch.object( + test_module, "IndyRevocation", autospec=True + ) as mock_indy_revoc, async_mock.patch.object( + test_module.web, "json_response", async_mock.Mock() + ) as mock_json_response: + mock_indy_revoc.return_value = async_mock.MagicMock( + get_issuer_rev_reg_record=async_mock.CoroutineMock( + return_value=async_mock.MagicMock( + set_state=async_mock.CoroutineMock(), + save=async_mock.CoroutineMock(), + serialize=async_mock.MagicMock(return_value="dummy"), + ) + ) + ) + + result = await test_module.set_registry_state(request) + mock_json_response.assert_called_once_with({"result": "dummy"}) + assert result is mock_json_response.return_value + + async def test_set_registry_state_not_found(self): + REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( + self.test_did, self.test_did + ) + request = async_mock.MagicMock() + request.app = self.app + request.match_info = {"rev_reg_id": REV_REG_ID} + request.json = async_mock.CoroutineMock( + return_value={ + "max_cred_num": "1000", + } + ) + request.query = { + "state": test_module.IssuerRevRegRecord.STATE_ACTIVE, + } + + with async_mock.patch.object( + test_module, "IndyRevocation", autospec=True + ) as mock_indy_revoc, async_mock.patch.object( + test_module.web, "FileResponse", async_mock.Mock() + ) as mock_json_response: + mock_indy_revoc.return_value = async_mock.MagicMock( + get_issuer_rev_reg_record=async_mock.CoroutineMock( + side_effect=test_module.StorageNotFoundError(error_code="dummy") + ) + ) + + with self.assertRaises(HTTPNotFound): + result = await test_module.set_registry_state(request) + mock_json_response.assert_not_called() + async def test_register(self): mock_app = async_mock.MagicMock() mock_app.add_routes = async_mock.MagicMock()