diff --git a/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py b/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py index 867e2999fb..58631cfadb 100644 --- a/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py +++ b/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py @@ -86,7 +86,7 @@ class TestLegacyIndyRegistry(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar-anoncreds"}, + settings={"wallet.type": "askar-anoncreds"}, profile_class=AskarAnoncredsProfile, ) self.registry = test_module.LegacyIndyRegistry() diff --git a/aries_cloudagent/anoncreds/error_messages.py b/aries_cloudagent/anoncreds/error_messages.py new file mode 100644 index 0000000000..8ec6fc5477 --- /dev/null +++ b/aries_cloudagent/anoncreds/error_messages.py @@ -0,0 +1,3 @@ +"""Error messages for anoncreds.""" + +ANONCREDS_PROFILE_REQUIRED_MSG = "AnonCreds interface requires AskarAnoncreds profile" diff --git a/aries_cloudagent/anoncreds/holder.py b/aries_cloudagent/anoncreds/holder.py index 9f175c0aa6..32ee6e86b1 100644 --- a/aries_cloudagent/anoncreds/holder.py +++ b/aries_cloudagent/anoncreds/holder.py @@ -24,6 +24,7 @@ from ..core.profile import Profile from ..ledger.base import BaseLedger from ..wallet.error import WalletNotFoundError +from .error_messages import ANONCREDS_PROFILE_REQUIRED_MSG from .models.anoncreds_cred_def import CredDef LOGGER = logging.getLogger(__name__) @@ -72,7 +73,7 @@ def __init__(self, profile: Profile): def profile(self) -> AskarAnoncredsProfile: """Accessor for the profile instance.""" if not isinstance(self._profile, AskarAnoncredsProfile): - raise ValueError("AnonCreds interface requires AskarAnoncreds") + raise ValueError(ANONCREDS_PROFILE_REQUIRED_MSG) return self._profile diff --git a/aries_cloudagent/anoncreds/issuer.py b/aries_cloudagent/anoncreds/issuer.py index 312bd8400f..0b6b1e0e42 100644 --- a/aries_cloudagent/anoncreds/issuer.py +++ b/aries_cloudagent/anoncreds/issuer.py @@ -27,6 +27,7 @@ AnonCredsSchemaAlreadyExists, BaseAnonCredsError, ) +from .error_messages import ANONCREDS_PROFILE_REQUIRED_MSG from .events import CredDefFinishedEvent from .models.anoncreds_cred_def import CredDef, CredDefResult from .models.anoncreds_schema import AnonCredsSchema, SchemaResult, SchemaState @@ -97,7 +98,7 @@ def __init__(self, profile: Profile): def profile(self) -> AskarAnoncredsProfile: """Accessor for the profile instance.""" if not isinstance(self._profile, AskarAnoncredsProfile): - raise ValueError("AnonCreds interface requires AskarAnoncreds") + raise ValueError(ANONCREDS_PROFILE_REQUIRED_MSG) return self._profile diff --git a/aries_cloudagent/anoncreds/models/anoncreds_cred_def.py b/aries_cloudagent/anoncreds/models/anoncreds_cred_def.py index 0630114bcc..02488a98c2 100644 --- a/aries_cloudagent/anoncreds/models/anoncreds_cred_def.py +++ b/aries_cloudagent/anoncreds/models/anoncreds_cred_def.py @@ -28,7 +28,7 @@ class CredDefValuePrimary(BaseModel): class Meta: """PrimarySchema metadata.""" - schema_class = "CredDefValuePrimarySchema" + schema_class = "CredDefValuePrimarySchemaAnoncreds" def __init__(self, n: str, s: str, r: dict, rctxt: str, z: str, **kwargs): """Initialize an instance. @@ -51,7 +51,7 @@ def __init__(self, n: str, s: str, r: dict, rctxt: str, z: str, **kwargs): self.z = z -class CredDefValuePrimarySchema(BaseModelSchema): +class CredDefValuePrimarySchemaAnoncreds(BaseModelSchema): """Cred def value primary schema.""" class Meta: @@ -73,7 +73,7 @@ class CredDefValueRevocation(BaseModel): class Meta: """CredDefValueRevocation metadata.""" - schema_class = "CredDefValueRevocationSchema" + schema_class = "CredDefValueRevocationSchemaAnoncreds" def __init__( self, @@ -120,7 +120,7 @@ def __init__( self.y = y -class CredDefValueRevocationSchema(BaseModelSchema): +class CredDefValueRevocationSchemaAnoncreds(BaseModelSchema): """Cred def value revocation schema.""" class Meta: @@ -158,7 +158,7 @@ class CredDefValue(BaseModel): class Meta: """CredDefValue metadata.""" - schema_class = "CredDefValueSchema" + schema_class = "CredDefValueSchemaAnoncreds" def __init__( self, @@ -180,7 +180,7 @@ def __init__( self.revocation = revocation -class CredDefValueSchema(BaseModelSchema): +class CredDefValueSchemaAnoncreds(BaseModelSchema): """Cred def value schema.""" class Meta: @@ -190,11 +190,11 @@ class Meta: unknown = EXCLUDE primary = fields.Nested( - CredDefValuePrimarySchema(), + CredDefValuePrimarySchemaAnoncreds(), metadata={"description": "Primary value for credential definition"}, ) revocation = fields.Nested( - CredDefValueRevocationSchema(), + CredDefValueRevocationSchemaAnoncreds(), metadata={"description": "Revocation value for credential definition"}, required=False, ) @@ -277,7 +277,7 @@ class Meta: "example": "default", } ) - value = fields.Nested(CredDefValueSchema()) + value = fields.Nested(CredDefValueSchemaAnoncreds()) class CredDefState(BaseModel): diff --git a/aries_cloudagent/anoncreds/revocation.py b/aries_cloudagent/anoncreds/revocation.py index 142731baf1..4ba4ca6b08 100644 --- a/aries_cloudagent/anoncreds/revocation.py +++ b/aries_cloudagent/anoncreds/revocation.py @@ -34,6 +34,7 @@ from ..core.event_bus import Event, EventBus from ..core.profile import Profile, ProfileSession from ..tails.base import BaseTailsServer +from .error_messages import ANONCREDS_PROFILE_REQUIRED_MSG from .events import RevListFinishedEvent, RevRegDefFinishedEvent from .issuer import ( CATEGORY_CRED_DEF, @@ -95,7 +96,7 @@ def __init__(self, profile: Profile): def profile(self) -> AskarAnoncredsProfile: """Accessor for the profile instance.""" if not isinstance(self._profile, AskarAnoncredsProfile): - raise ValueError("AnonCreds interface requires AskarAnoncreds") + raise ValueError(ANONCREDS_PROFILE_REQUIRED_MSG) return self._profile diff --git a/aries_cloudagent/anoncreds/routes.py b/aries_cloudagent/anoncreds/routes.py index a0e9bf47fb..062c2026f8 100644 --- a/aries_cloudagent/anoncreds/routes.py +++ b/aries_cloudagent/anoncreds/routes.py @@ -14,7 +14,6 @@ from marshmallow import fields from ..admin.request_context import AdminRequestContext -from ..askar.profile import AskarProfile from ..core.event_bus import EventBus from ..ledger.error import LedgerError from ..messaging.models.openapi import OpenAPISchema @@ -31,6 +30,7 @@ RevRegIdMatchInfoSchema, ) from ..storage.error import StorageNotFoundError +from ..utils.profiles import is_not_anoncreds_profile_raise_web_exception from .base import ( AnonCredsObjectNotFound, AnonCredsRegistrationError, @@ -50,6 +50,7 @@ from .registry import AnonCredsRegistry from .revocation import AnonCredsRevocation, AnonCredsRevocationError from .revocation_setup import DefaultRevocationSetup +from .util import handle_value_error LOGGER = logging.getLogger(__name__) @@ -139,7 +140,7 @@ class SchemaPostRequestSchema(OpenAPISchema): options = fields.Nested(SchemaPostOptionSchema()) -@docs(tags=["anoncreds"], summary="Create a schema on the connected ledger") +@docs(tags=["anoncreds - schemas"], summary="Create a schema on the connected ledger") @request_schema(SchemaPostRequestSchema()) @response_schema(SchemaResultSchema(), 200, description="") async def schemas_post(request: web.BaseRequest): @@ -178,6 +179,9 @@ async def schemas_post(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) body = await request.json() options = body.get("options", {}) @@ -191,8 +195,8 @@ async def schemas_post(request: web.BaseRequest): name = schema_data.get("name") version = schema_data.get("version") - issuer = AnonCredsIssuer(context.profile) try: + issuer = AnonCredsIssuer(profile) result = await issuer.create_and_register_schema( issuer_id, name, @@ -201,11 +205,13 @@ async def schemas_post(request: web.BaseRequest): options, ) return web.json_response(result.serialize()) + except ValueError as e: + handle_value_error(e) except (AnonCredsIssuerError, AnonCredsRegistrationError) as e: raise web.HTTPBadRequest(reason=e.roll_up) from e -@docs(tags=["anoncreds"], summary="Retrieve an individual schemas details") +@docs(tags=["anoncreds - schemas"], summary="Retrieve an individual schemas details") @match_info_schema(SchemaIdMatchInfo()) @response_schema(GetSchemaResultSchema(), 200, description="") async def schema_get(request: web.BaseRequest): @@ -219,10 +225,14 @@ async def schema_get(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + anoncreds_registry = context.inject(AnonCredsRegistry) schema_id = request.match_info["schema_id"] try: - schema = await anoncreds_registry.get_schema(context.profile, schema_id) + schema = await anoncreds_registry.get_schema(profile, schema_id) return web.json_response(schema.serialize()) except AnonCredsObjectNotFound as e: raise web.HTTPNotFound(reason=f"Schema not found: {schema_id}") from e @@ -230,7 +240,7 @@ async def schema_get(request: web.BaseRequest): raise web.HTTPBadRequest(reason=e.roll_up) from e -@docs(tags=["anoncreds"], summary="Retrieve all schema ids") +@docs(tags=["anoncreds - schemas"], summary="Retrieve all schema ids") @querystring_schema(SchemasQueryStringSchema()) @response_schema(GetSchemasResponseSchema(), 200, description="") async def schemas_get(request: web.BaseRequest): @@ -244,15 +254,21 @@ async def schemas_get(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) schema_issuer_id = request.query.get("schema_issuer_id") schema_name = request.query.get("schema_name") schema_version = request.query.get("schema_version") - issuer = AnonCredsIssuer(context.profile) - schema_ids = await issuer.get_created_schemas( - schema_name, schema_version, schema_issuer_id - ) + try: + issuer = AnonCredsIssuer(profile) + schema_ids = await issuer.get_created_schemas( + schema_name, schema_version, schema_issuer_id + ) + except ValueError as e: + handle_value_error(e) return web.json_response({"schema_ids": schema_ids}) @@ -364,7 +380,8 @@ class CredDefsQueryStringSchema(OpenAPISchema): @docs( - tags=["anoncreds"], summary="Create a credential definition on the connected ledger" + tags=["anoncreds - credential definitions"], + summary="Create a credential definition on the connected ledger", ) @request_schema(CredDefPostRequestSchema()) @response_schema(CredDefResultSchema(), 200, description="") @@ -379,6 +396,10 @@ async def cred_def_post(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + body = await request.json() options = body.get("options", {}) cred_def = body.get("credential_definition") @@ -390,8 +411,8 @@ async def cred_def_post(request: web.BaseRequest): schema_id = cred_def.get("schemaId") tag = cred_def.get("tag") - issuer = AnonCredsIssuer(context.profile) try: + issuer = AnonCredsIssuer(profile) result = await issuer.create_and_register_credential_definition( issuer_id, schema_id, @@ -399,17 +420,19 @@ async def cred_def_post(request: web.BaseRequest): options=options, ) return web.json_response(result.serialize()) + except ValueError as e: + handle_value_error(e) except ( AnonCredsIssuerError, AnonCredsObjectNotFound, AnonCredsResolutionError, - ValueError, ) as e: raise web.HTTPBadRequest(reason=e.roll_up) from e @docs( - tags=["anoncreds"], summary="Retrieve an individual credential definition details" + tags=["anoncreds - credential definitions"], + summary="Retrieve an individual credential definition details", ) @match_info_schema(CredIdMatchInfo()) @response_schema(GetCredDefResultSchema(), 200, description="") @@ -424,11 +447,15 @@ async def cred_def_get(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + anon_creds_registry = context.inject(AnonCredsRegistry) credential_id = request.match_info["cred_def_id"] try: result = await anon_creds_registry.get_credential_definition( - context.profile, credential_id + profile, credential_id ) return web.json_response(result.serialize()) except AnonCredsObjectNotFound as e: @@ -450,7 +477,10 @@ class GetCredDefsResponseSchema(OpenAPISchema): ) -@docs(tags=["anoncreds"], summary="Retrieve all credential definition ids") +@docs( + tags=["anoncreds - credential definitions"], + summary="Retrieve all credential definition ids", +) @querystring_schema(CredDefsQueryStringSchema()) @response_schema(GetCredDefsResponseSchema(), 200, description="") async def cred_defs_get(request: web.BaseRequest): @@ -464,15 +494,22 @@ async def cred_defs_get(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] - issuer = AnonCredsIssuer(context.profile) + profile = context.profile - cred_def_ids = await issuer.get_created_credential_definitions( - issuer_id=request.query.get("issuer_id"), - schema_id=request.query.get("schema_id"), - schema_name=request.query.get("schema_name"), - schema_version=request.query.get("schema_version"), - ) - return web.json_response({"credential_definition_ids": cred_def_ids}) + is_not_anoncreds_profile_raise_web_exception(profile) + + try: + issuer = AnonCredsIssuer(profile) + + cred_def_ids = await issuer.get_created_credential_definitions( + issuer_id=request.query.get("issuer_id"), + schema_id=request.query.get("schema_id"), + schema_name=request.query.get("schema_name"), + schema_version=request.query.get("schema_version"), + ) + return web.json_response({"credential_definition_ids": cred_def_ids}) + except ValueError as e: + handle_value_error(e) class InnerRevRegDefSchema(OpenAPISchema): @@ -523,7 +560,7 @@ class RevRegDefOptionsSchema(OpenAPISchema): ) -class RevRegCreateRequestSchema(OpenAPISchema): +class RevRegCreateRequestSchemaAnoncreds(OpenAPISchema): """Wrapper for revocation registry creation request.""" revocation_registry_definition = fields.Nested(InnerRevRegDefSchema()) @@ -531,16 +568,19 @@ class RevRegCreateRequestSchema(OpenAPISchema): @docs( - tags=["anoncreds"], + tags=["anoncreds - revocation"], summary="Create and publish a registration revocation on the connected ledger", ) -@request_schema(RevRegCreateRequestSchema()) +@request_schema(RevRegCreateRequestSchemaAnoncreds()) @response_schema(RevRegDefResultSchema(), 200, description="") async def rev_reg_def_post(request: web.BaseRequest): """Request handler for creating revocation registry definition.""" context: AdminRequestContext = request["context"] - body = await request.json() + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + body = await request.json() revocation_registry_definition = body.get("revocation_registry_definition") options = body.get("options", {}) @@ -554,8 +594,8 @@ async def rev_reg_def_post(request: web.BaseRequest): max_cred_num = revocation_registry_definition.get("maxCredNum") tag = revocation_registry_definition.get("tag") - issuer = AnonCredsIssuer(context.profile) - revocation = AnonCredsRevocation(context.profile) + issuer = AnonCredsIssuer(profile) + revocation = AnonCredsRevocation(profile) # check we published this cred def found = await issuer.match_created_credential_definitions(cred_def_id) if not found: @@ -611,7 +651,7 @@ class RevListCreateRequestSchema(OpenAPISchema): @docs( - tags=["anoncreds"], + tags=["anoncreds - revocation"], summary="Create and publish a revocation status list on the connected ledger", ) @request_schema(RevListCreateRequestSchema()) @@ -619,12 +659,16 @@ class RevListCreateRequestSchema(OpenAPISchema): async def rev_list_post(request: web.BaseRequest): """Request handler for creating registering a revocation list.""" context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + body = await request.json() rev_reg_def_id = body.get("rev_reg_def_id") options = body.get("options", {}) - revocation = AnonCredsRevocation(context.profile) try: + revocation = AnonCredsRevocation(profile) result = await shield( revocation.create_and_register_revocation_list( rev_reg_def_id, @@ -633,6 +677,8 @@ async def rev_list_post(request: web.BaseRequest): ) LOGGER.debug("published revocation list for: %s", rev_reg_def_id) return web.json_response(result.serialize()) + except ValueError as e: + handle_value_error(e) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except (AnonCredsRevocationError, LedgerError) as err: @@ -640,7 +686,7 @@ async def rev_list_post(request: web.BaseRequest): @docs( - tags=["anoncreds"], + tags=["anoncreds - revocation"], summary="Upload local tails file to server", ) @match_info_schema(RevRegIdMatchInfoSchema()) @@ -653,7 +699,10 @@ async def upload_tails_file(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] - profile: AskarProfile = context.profile + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + rev_reg_id = request.match_info["rev_reg_id"] try: revocation = AnonCredsRevocation(profile) @@ -665,12 +714,14 @@ async def upload_tails_file(request: web.BaseRequest): await revocation.upload_tails_file(rev_reg_def) return web.json_response({}) + except ValueError as e: + handle_value_error(e) except AnonCredsIssuerError as e: raise web.HTTPInternalServerError(reason=str(e)) from e @docs( - tags=["anoncreds"], + tags=["anoncreds - revocation"], summary="Update the active registry", ) @match_info_schema(RevRegIdMatchInfoSchema()) @@ -683,11 +734,17 @@ async def set_active_registry(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + rev_reg_id = request.match_info["rev_reg_id"] try: - revocation = AnonCredsRevocation(context.profile) + revocation = AnonCredsRevocation(profile) await revocation.set_active_registry(rev_reg_id) return web.json_response({}) + except ValueError as e: + handle_value_error(e) except AnonCredsRevocationError as e: raise web.HTTPInternalServerError(reason=str(e)) from e @@ -734,8 +791,15 @@ def post_process_routes(app: web.Application): app._state["swagger_dict"]["tags"] = [] app._state["swagger_dict"]["tags"].append( { - "name": "anoncreds", - "description": "Anoncreds management", + "name": "anoncreds - schemas", + "description": "Anoncreds schema management", + "externalDocs": {"description": "Specification", "url": SPEC_URI}, + } + ) + app._state["swagger_dict"]["tags"].append( + { + "name": "anoncreds - credential definitions", + "description": "Anoncreds credential definition management", "externalDocs": {"description": "Specification", "url": SPEC_URI}, } ) diff --git a/aries_cloudagent/anoncreds/tests/test_holder.py b/aries_cloudagent/anoncreds/tests/test_holder.py index c5ed3c6abd..6a286b050a 100644 --- a/aries_cloudagent/anoncreds/tests/test_holder.py +++ b/aries_cloudagent/anoncreds/tests/test_holder.py @@ -110,7 +110,7 @@ class MockMimeTypeRecord: class TestAnonCredsHolder(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar-anoncreds"}, + settings={"wallet.type": "askar-anoncreds"}, profile_class=AskarAnoncredsProfile, ) self.holder = test_module.AnonCredsHolder(self.profile) diff --git a/aries_cloudagent/anoncreds/tests/test_issuer.py b/aries_cloudagent/anoncreds/tests/test_issuer.py index 224d4916c3..49e00cd4e6 100644 --- a/aries_cloudagent/anoncreds/tests/test_issuer.py +++ b/aries_cloudagent/anoncreds/tests/test_issuer.py @@ -129,7 +129,7 @@ def get_mock_schema_result( class TestAnonCredsIssuer(IsolatedAsyncioTestCase): async def asyncSetUp(self) -> None: self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar-anoncreds"}, + settings={"wallet.type": "askar-anoncreds"}, profile_class=AskarAnoncredsProfile, ) self.issuer = test_module.AnonCredsIssuer(self.profile) diff --git a/aries_cloudagent/anoncreds/tests/test_routes.py b/aries_cloudagent/anoncreds/tests/test_routes.py index b2ecda8c5d..5e90463b54 100644 --- a/aries_cloudagent/anoncreds/tests/test_routes.py +++ b/aries_cloudagent/anoncreds/tests/test_routes.py @@ -21,6 +21,7 @@ ) from aries_cloudagent.tests import mock +from ...askar.profile import AskarProfile from .. import routes as test_module @@ -53,10 +54,12 @@ class TestAnoncredsRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self) -> None: self.session_inject = {} self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar-anoncreds"}, + settings={"wallet.type": "askar-anoncreds"}, profile_class=AskarAnoncredsProfile, ) - self.context = AdminRequestContext.test_context(self.session_inject) + self.context = AdminRequestContext.test_context( + self.session_inject, self.profile + ) self.request_dict = { "context": self.context, } @@ -337,6 +340,188 @@ async def test_set_active_registry(self, mock_set): with self.assertRaises(KeyError): await test_module.set_active_registry(self.request) + async def test_schema_endpoints_wrong_profile_403(self): + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar"}, + profile_class=AskarProfile, + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + ) + + # POST schema + self.request.json = mock.CoroutineMock( + return_value={ + "schema": { + "issuerId": "Q4TmbeGPoWeWob4Xf6KetA", + "attrNames": ["score"], + "name": "Example Schema", + "version": "0.0.1", + } + } + ) + with self.assertRaises(web.HTTPForbidden): + await test_module.schemas_post(self.request) + + # GET schema + self.request.match_info = {"schema_id": "schema_id"} + with self.assertRaises(web.HTTPForbidden): + await test_module.schema_get(self.request) + + # GET schemas + with self.assertRaises(web.HTTPForbidden): + await test_module.schemas_get(self.request) + + async def test_cred_def_endpoints_wrong_profile_403(self): + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar"}, + profile_class=AskarProfile, + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + ) + + # POST cred def + self.request.json = mock.CoroutineMock( + return_value={ + "credential_definition": { + "issuerId": "issuerId", + "schemaId": "schemaId", + "tag": "tag", + }, + "options": { + "revocation_registry_size": 0, + "support_revocation": True, + }, + } + ) + with self.assertRaises(web.HTTPForbidden): + await test_module.cred_def_post(self.request) + + # GET cred def + self.request.match_info = {"cred_def_id": "cred_def_id"} + with self.assertRaises(web.HTTPForbidden): + await test_module.cred_def_get(self.request) + + # GET cred defs + with self.assertRaises(web.HTTPForbidden): + await test_module.cred_defs_get(self.request) + + async def test_rev_reg_wrong_profile_403(self): + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar"}, + profile_class=AskarProfile, + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + ) + + self.request.json = mock.CoroutineMock( + return_value={ + "revocation_registry_definition": { + "credDefId": "cred_def_id", + "issuerId": "issuer_id", + "maxCredNum": 100, + }, + "options": { + "tails_public_uri": "http://tails_public_uri", + "tails_local_uri": "http://tails_local_uri", + }, + } + ) + with self.assertRaises(web.HTTPForbidden): + await test_module.rev_reg_def_post(self.request) + + async def test_rev_list_wrong_profile_403(self): + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar"}, + profile_class=AskarProfile, + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + ) + + self.request.json = mock.CoroutineMock( + return_value={"revRegDefId": "rev_reg_def_id", "options": {}} + ) + with self.assertRaises(web.HTTPForbidden): + await test_module.rev_list_post(self.request) + + async def test_uploads_tails_wrong_profile_403(self): + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar"}, + profile_class=AskarProfile, + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + ) + + self.request.match_info = {"rev_reg_id": "rev_reg_id"} + with self.assertRaises(web.HTTPForbidden): + await test_module.upload_tails_file(self.request) + + async def test_active_registry_wrong_profile_403(self): + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar"}, + profile_class=AskarProfile, + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + ) + + self.request.match_info = {"rev_reg_id": "rev_reg_id"} + + with self.assertRaises(web.HTTPForbidden): + await test_module.set_active_registry(self.request) + @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/anoncreds/tests/test_verifier.py b/aries_cloudagent/anoncreds/tests/test_verifier.py index 7aaa8e342f..4501f1351a 100644 --- a/aries_cloudagent/anoncreds/tests/test_verifier.py +++ b/aries_cloudagent/anoncreds/tests/test_verifier.py @@ -41,7 +41,7 @@ class TestAnonCredsVerifier(IsolatedAsyncioTestCase): async def asyncSetUp(self) -> None: self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar-anoncreds"}, + settings={"wallet.type": "askar-anoncreds"}, profile_class=AskarAnoncredsProfile, ) self.verifier = test_module.AnonCredsVerifier(self.profile) diff --git a/aries_cloudagent/anoncreds/util.py b/aries_cloudagent/anoncreds/util.py index d565f9d407..e558e33ad7 100644 --- a/aries_cloudagent/anoncreds/util.py +++ b/aries_cloudagent/anoncreds/util.py @@ -5,6 +5,10 @@ from pathlib import Path from platform import system +from aiohttp import web + +from .error_messages import ANONCREDS_PROFILE_REQUIRED_MSG + async def generate_pr_nonce() -> str: """Generate a nonce for a proof request.""" @@ -36,3 +40,10 @@ def indy_client_dir(subpath: str = None, create: bool = False) -> str: makedirs(target_dir, exist_ok=True) return target_dir + + +def handle_value_error(e: ValueError): + """Handle ValueError message as web response type.""" + if ANONCREDS_PROFILE_REQUIRED_MSG in str(e): + raise web.HTTPForbidden(reason=str(e)) from e + raise web.HTTPInternalServerError() from e diff --git a/aries_cloudagent/config/default_context.py b/aries_cloudagent/config/default_context.py index 18e5c4a8ca..2f8c0181fe 100644 --- a/aries_cloudagent/config/default_context.py +++ b/aries_cloudagent/config/default_context.py @@ -19,8 +19,8 @@ from ..utils.dependencies import is_indy_sdk_module_installed from ..utils.stats import Collector from ..wallet.default_verification_key_strategy import ( - DefaultVerificationKeyStrategy, BaseVerificationKeyStrategy, + DefaultVerificationKeyStrategy, ) from ..wallet.did_method import DIDMethods from ..wallet.key_type import KeyTypes @@ -143,27 +143,38 @@ async def load_plugins(self, context: InjectionContext): plugin_registry.register_plugin("aries_cloudagent.settings") plugin_registry.register_plugin("aries_cloudagent.vc") plugin_registry.register_plugin("aries_cloudagent.wallet") + + anoncreds_plugins = [ + "aries_cloudagent.anoncreds", + "aries_cloudagent.anoncreds.default.did_indy", + "aries_cloudagent.anoncreds.default.did_web", + "aries_cloudagent.anoncreds.default.legacy_indy", + "aries_cloudagent.revocation_anoncreds", + ] + + askar_plugins = [ + "aries_cloudagent.messaging.credential_definitions", + "aries_cloudagent.messaging.schemas", + "aries_cloudagent.revocation", + ] + + def register_askar_plugins(): + for plugin in askar_plugins: + plugin_registry.register_plugin(plugin) + + def register_anoncreds_plugins(): + for plugin in anoncreds_plugins: + plugin_registry.register_plugin(plugin) + if wallet_type == "askar-anoncreds": - plugin_registry.register_plugin("aries_cloudagent.anoncreds") - plugin_registry.register_plugin( - "aries_cloudagent.anoncreds.default.did_indy" - ) - plugin_registry.register_plugin( - "aries_cloudagent.anoncreds.default.did_web" - ) - plugin_registry.register_plugin( - "aries_cloudagent.anoncreds.default.legacy_indy" - ) - plugin_registry.register_plugin("aries_cloudagent.revocation_anoncreds") + register_anoncreds_plugins() else: - plugin_registry.register_plugin( - "aries_cloudagent.messaging.credential_definitions" - ) - plugin_registry.register_plugin("aries_cloudagent.messaging.schemas") - plugin_registry.register_plugin("aries_cloudagent.revocation") + register_askar_plugins() if context.settings.get("multitenant.admin_enabled"): plugin_registry.register_plugin("aries_cloudagent.multitenant.admin") + register_askar_plugins() + register_anoncreds_plugins() # Register external plugins for plugin_path in self.settings.get("external_plugins", []): diff --git a/aries_cloudagent/core/conductor.py b/aries_cloudagent/core/conductor.py index 9e2ce40be8..18d2447571 100644 --- a/aries_cloudagent/core/conductor.py +++ b/aries_cloudagent/core/conductor.py @@ -17,6 +17,11 @@ from ..admin.base_server import BaseAdminServer from ..admin.server import AdminResponder, AdminServer +from ..commands.upgrade import ( + add_version_record, + get_upgrade_version_list, + upgrade, +) from ..config.default_context import ContextBuilder from ..config.injection_context import InjectionContext from ..config.ledger import ( @@ -27,11 +32,6 @@ from ..config.logging import LoggingConfigurator from ..config.provider import ClassProvider from ..config.wallet import wallet_config -from ..commands.upgrade import ( - get_upgrade_version_list, - add_version_record, - upgrade, -) from ..core.profile import Profile from ..indy.verifier import IndyVerifier from ..ledger.base import BaseLedger @@ -62,6 +62,8 @@ from ..protocols.out_of_band.v1_0.messages.invitation import HSProto, InvitationMessage from ..storage.base import BaseStorage from ..storage.error import StorageNotFoundError +from ..storage.record import StorageRecord +from ..storage.type import RECORD_TYPE_ACAPY_STORAGE_TYPE from ..transport.inbound.manager import InboundTransportManager from ..transport.inbound.message import InboundMessage from ..transport.outbound.base import OutboundDeliveryError @@ -75,6 +77,7 @@ from ..version import RECORD_TYPE_ACAPY_VERSION, __version__ from ..wallet.did_info import DIDInfo from .dispatcher import Dispatcher +from .error import StartupError from .oob_processor import OobMessageProcessor from .util import SHUTDOWN_EVENT_TOPIC, STARTUP_EVENT_TOPIC @@ -284,6 +287,7 @@ async def start(self) -> None: """Start the agent.""" context = self.root_profile.context + await self.check_for_valid_wallet_type(self.root_profile) # Start up transports try: @@ -770,3 +774,52 @@ def webhook_router( LOGGER.warning( "Cannot queue message webhook for delivery, no supported transport" ) + + async def check_for_valid_wallet_type(self, profile): + """Check wallet type and set it if not set. Raise an error if wallet type config doesn't match existing storage type.""" # noqa: E501 + async with profile.session() as session: + storage_type_from_config = profile.settings.get("wallet.type") + storage = session.inject(BaseStorage) + try: + storage_type_record = await storage.find_record( + type_filter=RECORD_TYPE_ACAPY_STORAGE_TYPE, tag_query={} + ) + storage_type_from_storage = storage_type_record.value + except StorageNotFoundError: + storage_type_record = None + + if not storage_type_record: + LOGGER.warning("Wallet type record not found.") + try: + acapy_version = await storage.find_record( + type_filter=RECORD_TYPE_ACAPY_VERSION, tag_query={} + ) + except StorageNotFoundError: + acapy_version = None + if acapy_version: + storage_type_from_storage = "askar" + LOGGER.info( + f"Existing agent found. Setting wallet type to {storage_type_from_storage}." # noqa: E501 + ) + await storage.add_record( + StorageRecord( + RECORD_TYPE_ACAPY_STORAGE_TYPE, + storage_type_from_storage, + ) + ) + else: + storage_type_from_storage = storage_type_from_config + LOGGER.info( + f"New agent. Setting wallet type to {storage_type_from_config}." + ) + await storage.add_record( + StorageRecord( + RECORD_TYPE_ACAPY_STORAGE_TYPE, + storage_type_from_config, + ) + ) + + if storage_type_from_storage != storage_type_from_config: + raise StartupError( + f"Wallet type config [{storage_type_from_config}] doesn't match with the wallet type in storage [{storage_type_record.value}]" # noqa: E501 + ) diff --git a/aries_cloudagent/core/tests/test_conductor.py b/aries_cloudagent/core/tests/test_conductor.py index 8e6360b4b2..5414c07bea 100644 --- a/aries_cloudagent/core/tests/test_conductor.py +++ b/aries_cloudagent/core/tests/test_conductor.py @@ -1,7 +1,7 @@ from io import StringIO +from unittest import IsolatedAsyncioTestCase from aries_cloudagent.tests import mock -from unittest import IsolatedAsyncioTestCase from ...admin.base_server import BaseAdminServer from ...config.base_context import ContextBuilder @@ -25,6 +25,7 @@ from ...resolver.did_resolver import DIDResolver from ...storage.base import BaseStorage from ...storage.error import StorageNotFoundError +from ...storage.in_memory import InMemoryStorage from ...transport.inbound.message import InboundMessage from ...transport.inbound.receipt import MessageReceipt from ...transport.outbound.base import OutboundDeliveryError @@ -42,10 +43,14 @@ class Config: - test_settings = {"admin.webhook_urls": ["http://sample.webhook.ca"]} + test_settings = { + "admin.webhook_urls": ["http://sample.webhook.ca"], + "wallet.type": "askar", + } test_settings_admin = { "admin.webhook_urls": ["http://sample.webhook.ca"], "admin.enabled": True, + "wallet.type": "askar", } test_settings_with_queue = {"queue.enable_undelivered_queue": True} @@ -114,7 +119,12 @@ async def test_startup_version_record_exists(self): ) as mock_logger, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value="v0.7.3")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value="v0.7.3"), + ] + ), ), mock.patch.object( test_module, "get_upgrade_version_list", @@ -168,7 +178,12 @@ async def test_startup_version_no_upgrade_add_record(self): ) as mock_outbound_mgr, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value="v0.8.1")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value="v0.8.1"), + ] + ), ), mock.patch.object( test_module, "get_upgrade_version_list", @@ -200,7 +215,12 @@ async def test_startup_version_no_upgrade_add_record(self): ) as mock_outbound_mgr, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ), mock.patch.object( test_module, "get_upgrade_version_list", @@ -226,6 +246,7 @@ async def test_startup_version_force_upgrade(self): "admin.webhook_urls": ["http://sample.webhook.ca"], "upgrade.from_version": "v0.7.5", "upgrade.force_upgrade": True, + "wallet.type": "askar", } builder: ContextBuilder = StubContextBuilder(test_settings) conductor = test_module.Conductor(builder) @@ -238,7 +259,12 @@ async def test_startup_version_force_upgrade(self): ) as mock_logger, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value="v0.8.0")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value="v0.8.0"), + ] + ), ), mock.patch.object( test_module, "get_upgrade_version_list", @@ -272,7 +298,12 @@ async def test_startup_version_force_upgrade(self): ) as mock_logger, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value="v0.7.0")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value="v0.7.0"), + ] + ), ), mock.patch.object( test_module, "get_upgrade_version_list", @@ -306,7 +337,9 @@ async def test_startup_version_force_upgrade(self): ) as mock_logger, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(side_effect=StorageNotFoundError()), + mock.CoroutineMock( + side_effect=[mock.MagicMock(value="askar"), StorageNotFoundError()] + ), ), mock.patch.object( test_module, "get_upgrade_version_list", @@ -342,7 +375,9 @@ async def test_startup_version_record_not_exists(self): ) as mock_logger, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(side_effect=StorageNotFoundError()), + mock.CoroutineMock( + side_effect=[mock.MagicMock(value="askar"), StorageNotFoundError()] + ), ), mock.patch.object( test_module, "get_upgrade_version_list", @@ -416,7 +451,12 @@ async def test_startup_no_public_did(self): ) as mock_logger, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ): mock_outbound_mgr.return_value.registered_transports = {} mock_outbound_mgr.return_value.enqueue_message = mock.CoroutineMock() @@ -846,7 +886,12 @@ async def test_admin(self): ) as admin_stop, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ): await conductor.start() admin_start.assert_awaited_once_with() @@ -861,6 +906,7 @@ async def test_admin_startx(self): "admin.enabled": "1", "debug.print_invitation": True, "debug.print_connections_invitation": True, + "wallet.type": "askar", } ) conductor = test_module.Conductor(builder) @@ -892,7 +938,12 @@ async def test_admin_startx(self): ) as conn_mgr, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ): admin_start.side_effect = KeyError("trouble") oob_mgr.return_value.create_invitation = mock.CoroutineMock( @@ -933,7 +984,12 @@ async def test_start_static(self): ) as mock_mgr, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ), mock.patch.object( test_module, "OutboundTransportManager", autospec=True ) as mock_outbound_mgr: @@ -1092,6 +1148,7 @@ async def test_print_invite_connection(self): "debug.print_invitation": True, "debug.print_connections_invitation": True, "invite_base_url": "http://localhost", + "wallet.type": "askar", } ) conductor = test_module.Conductor(builder) @@ -1099,7 +1156,12 @@ async def test_print_invite_connection(self): with mock.patch("sys.stdout", new=StringIO()) as captured, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ), mock.patch.object( test_module, "OutboundTransportManager", autospec=True ) as mock_outbound_mgr: @@ -1141,7 +1203,12 @@ async def test_clear_default_mediator(self): ) as mock_mgr, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ): await conductor.start() await conductor.stop() @@ -1179,7 +1246,12 @@ async def test_set_default_mediator(self): ), mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ): await conductor.start() await conductor.stop() @@ -1205,7 +1277,12 @@ async def test_set_default_mediator_x(self): ), mock.patch.object(test_module, "LOGGER") as mock_logger, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ): await conductor.start() await conductor.stop() @@ -1348,7 +1425,12 @@ async def test_mediator_invitation_0160(self, mock_from_url, _): ), mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ): await conductor.start() await conductor.stop() @@ -1402,7 +1484,12 @@ async def test_mediator_invitation_0434(self, mock_from_url, _): ) as mock_mgr, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ): assert not conductor.root_profile.settings["mediation.connections_invite"] await conductor.start() @@ -1455,7 +1542,12 @@ async def test_mediation_invitation_should_use_stored_invitation( ), mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ): await conductor.start() await conductor.stop() @@ -1496,7 +1588,12 @@ async def test_mediation_invitation_should_not_create_connection_for_old_invitat with mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ): # when await conductor.start() @@ -1534,7 +1631,12 @@ async def test_mediator_invitation_x(self, _): ) as mock_logger, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(return_value=mock.MagicMock(value=f"v{__version__}")), + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value=f"v{__version__}"), + ] + ), ): await conductor.start() await conductor.stop() @@ -1592,7 +1694,9 @@ async def test_startup_x_no_storage_version(self): ) as mock_logger, mock.patch.object( BaseStorage, "find_record", - mock.CoroutineMock(side_effect=StorageNotFoundError()), + mock.CoroutineMock( + side_effect=[mock.MagicMock(value="askar"), StorageNotFoundError()] + ), ), mock.patch.object( test_module, "upgrade", @@ -1617,3 +1721,228 @@ async def test_startup_x_no_storage_version(self): mock_inbound_mgr.return_value.registered_transports = {} mock_outbound_mgr.return_value.registered_transports = {} await conductor.start() + + async def test_startup_storage_type_exists_and_matches(self): + builder: ContextBuilder = StubContextBuilder(self.test_settings) + conductor = test_module.Conductor(builder) + + with mock.patch.object( + test_module, "InboundTransportManager", autospec=True + ) as mock_inbound_mgr, mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr, mock.patch.object( + test_module, "LoggingConfigurator", autospec=True + ) as mock_logger, mock.patch.object( + BaseStorage, + "find_record", + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar"), + mock.MagicMock(value="v0.7.3"), + ] + ), + ), mock.patch.object( + test_module, + "get_upgrade_version_list", + mock.MagicMock( + return_value=["v0.7.4", "0.7.5", "v0.8.0-rc1", "v8.0.0", "v0.8.1-rc2"] + ), + ), mock.patch.object( + test_module, + "upgrade", + mock.CoroutineMock(), + ): + mock_outbound_mgr.return_value.registered_transports = { + "test": mock.MagicMock(schemes=["http"]) + } + await conductor.setup() + + session = await conductor.root_profile.session() + + wallet = session.inject(BaseWallet) + await wallet.create_public_did( + SOV, + ED25519, + ) + + mock_inbound_mgr.return_value.registered_transports = {} + mock_outbound_mgr.return_value.registered_transports = {} + + await conductor.start() + + await conductor.stop() + + async def test_startup_storage_type_exists_and_does_not_match(self): + builder: ContextBuilder = StubContextBuilder(self.test_settings) + conductor = test_module.Conductor(builder) + + with mock.patch.object( + test_module, "InboundTransportManager", autospec=True + ) as mock_inbound_mgr, mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr, mock.patch.object( + test_module, "LoggingConfigurator", autospec=True + ) as mock_logger, mock.patch.object( + BaseStorage, + "find_record", + mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value="askar-anoncreds"), + mock.MagicMock(value="v0.7.3"), + ] + ), + ), mock.patch.object( + test_module, + "get_upgrade_version_list", + mock.MagicMock( + return_value=["v0.7.4", "0.7.5", "v0.8.0-rc1", "v8.0.0", "v0.8.1-rc2"] + ), + ), mock.patch.object( + test_module, + "upgrade", + mock.CoroutineMock(), + ): + mock_outbound_mgr.return_value.registered_transports = { + "test": mock.MagicMock(schemes=["http"]) + } + await conductor.setup() + + session = await conductor.root_profile.session() + + wallet = session.inject(BaseWallet) + await wallet.create_public_did( + SOV, + ED25519, + ) + + mock_inbound_mgr.return_value.registered_transports = {} + mock_outbound_mgr.return_value.registered_transports = {} + + with self.assertRaises(test_module.StartupError): + await conductor.start() + + await conductor.stop() + + async def test_startup_storage_type_does_not_exist_and_existing_agent_then_set_to_askar( + self, + ): + builder: ContextBuilder = StubContextBuilder(self.test_settings) + conductor = test_module.Conductor(builder) + + with mock.patch.object( + test_module, "InboundTransportManager", autospec=True + ) as mock_inbound_mgr, mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr, mock.patch.object( + test_module, "LoggingConfigurator", autospec=True + ) as mock_logger, mock.patch.object( + BaseStorage, + "find_record", + mock.CoroutineMock( + side_effect=[ + StorageNotFoundError(), + mock.MagicMock(value="v0.7.3"), + mock.MagicMock(value="v0.7.3"), + ] + ), + ), mock.patch.object( + InMemoryStorage, + "add_record", + mock.CoroutineMock(return_value=None), + ) as mock_add_record, mock.patch.object( + test_module, + "get_upgrade_version_list", + mock.MagicMock( + return_value=["v0.7.4", "0.7.5", "v0.8.0-rc1", "v8.0.0", "v0.8.1-rc2"] + ), + ), mock.patch.object( + test_module, + "upgrade", + mock.CoroutineMock(), + ): + mock_outbound_mgr.return_value.registered_transports = { + "test": mock.MagicMock(schemes=["http"]) + } + await conductor.setup() + + session = await conductor.root_profile.session() + + wallet = session.inject(BaseWallet) + await wallet.create_public_did( + SOV, + ED25519, + ) + + mock_inbound_mgr.return_value.registered_transports = {} + mock_outbound_mgr.return_value.registered_transports = {} + + await conductor.start() + + await conductor.stop() + + storage_record = mock_add_record.call_args_list[0].args[0] + assert storage_record.value == "askar" + + async def test_startup_storage_type_does_not_exist_and_new_anoncreds_agent( + self, + ): + test_settings = { + "admin.webhook_urls": ["http://sample.webhook.ca"], + "wallet.type": "askar-anoncreds", + } + builder: ContextBuilder = StubContextBuilder(test_settings) + conductor = test_module.Conductor(builder) + + with mock.patch.object( + test_module, "InboundTransportManager", autospec=True + ) as mock_inbound_mgr, mock.patch.object( + test_module, "OutboundTransportManager", autospec=True + ) as mock_outbound_mgr, mock.patch.object( + test_module, "LoggingConfigurator", autospec=True + ) as mock_logger, mock.patch.object( + BaseStorage, + "find_record", + mock.CoroutineMock( + side_effect=[ + StorageNotFoundError(), + StorageNotFoundError(), + mock.MagicMock(value="v0.7.3"), + ] + ), + ), mock.patch.object( + InMemoryStorage, + "add_record", + mock.CoroutineMock(return_value=None), + ) as mock_add_record, mock.patch.object( + test_module, + "get_upgrade_version_list", + mock.MagicMock( + return_value=["v0.7.4", "0.7.5", "v0.8.0-rc1", "v8.0.0", "v0.8.1-rc2"] + ), + ), mock.patch.object( + test_module, + "upgrade", + mock.CoroutineMock(), + ): + mock_outbound_mgr.return_value.registered_transports = { + "test": mock.MagicMock(schemes=["http"]) + } + await conductor.setup() + + session = await conductor.root_profile.session() + + wallet = session.inject(BaseWallet) + await wallet.create_public_did( + SOV, + ED25519, + ) + + mock_inbound_mgr.return_value.registered_transports = {} + mock_outbound_mgr.return_value.registered_transports = {} + + await conductor.start() + + await conductor.stop() + + storage_record = mock_add_record.call_args_list[0].args[0] + assert storage_record.value == "askar-anoncreds" diff --git a/aries_cloudagent/messaging/credential_definitions/routes.py b/aries_cloudagent/messaging/credential_definitions/routes.py index d0068d916a..31a07b9ae1 100644 --- a/aries_cloudagent/messaging/credential_definitions/routes.py +++ b/aries_cloudagent/messaging/credential_definitions/routes.py @@ -14,7 +14,6 @@ request_schema, response_schema, ) - from marshmallow import fields from ...admin.request_context import AdminRequestContext @@ -44,6 +43,7 @@ from ...revocation.indy import IndyRevocation from ...storage.base import BaseStorage, StorageRecord from ...storage.error import StorageError, StorageNotFoundError +from ...utils.profiles import is_anoncreds_profile_raise_web_exception from ..models.base import BaseModelError from ..models.openapi import OpenAPISchema from ..valid import ( @@ -195,6 +195,9 @@ async def credential_definitions_send_credential_definition(request: web.BaseReq """ context: AdminRequestContext = request["context"] profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + outbound_handler = request["outbound_message_router"] create_transaction_for_endorser = json.loads( @@ -229,13 +232,13 @@ async def credential_definitions_send_credential_definition(request: web.BaseReq ) # check if we need to endorse - if is_author_role(context.profile): + if is_author_role(profile): # authors cannot write to the ledger write_ledger = False create_transaction_for_endorser = True if not connection_id: # author has not provided a connection id, so determine which to use - connection_id = await get_endorser_connection_id(context.profile) + connection_id = await get_endorser_connection_id(profile) if not connection_id: raise web.HTTPBadRequest(reason="No endorser connection found") @@ -314,7 +317,7 @@ async def credential_definitions_send_credential_definition(request: web.BaseReq if not create_transaction_for_endorser: # Notify event meta_data["processing"]["auto_create_rev_reg"] = True - await notify_cred_def_event(context.profile, cred_def_id, meta_data) + await notify_cred_def_event(profile, cred_def_id, meta_data) return web.json_response( { @@ -332,7 +335,7 @@ async def credential_definitions_send_credential_definition(request: web.BaseReq "endorser.auto_create_rev_reg" ) - transaction_mgr = TransactionManager(context.profile) + transaction_mgr = TransactionManager(profile) try: transaction = await transaction_mgr.create_record( messages_attach=cred_def["signed_txn"], @@ -381,6 +384,8 @@ async def credential_definitions_created(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + is_anoncreds_profile_raise_web_exception(context.profile) + session = await context.session() storage = session.inject(BaseStorage) found = await storage.find_all_records( @@ -412,12 +417,16 @@ async def credential_definitions_get_credential_definition(request: web.BaseRequ """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + cred_def_id = request.match_info["cred_def_id"] - async with context.profile.session() as session: + async with profile.session() as session: multitenant_mgr = session.inject_or(BaseMultitenantManager) if multitenant_mgr: - ledger_exec_inst = IndyLedgerRequestsExecutor(context.profile) + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) else: ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) ledger_id, ledger = await ledger_exec_inst.get_ledger_for_identifier( @@ -458,14 +467,17 @@ async def credential_definitions_fix_cred_def_wallet_record(request: web.BaseReq """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) cred_def_id = request.match_info["cred_def_id"] - async with context.profile.session() as session: + async with profile.session() as session: storage = session.inject(BaseStorage) multitenant_mgr = session.inject_or(BaseMultitenantManager) if multitenant_mgr: - ledger_exec_inst = IndyLedgerRequestsExecutor(context.profile) + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) else: ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) ledger_id, ledger = await ledger_exec_inst.get_ledger_for_identifier( diff --git a/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py b/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py index 4b941f3795..b88e7bf2fa 100644 --- a/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py +++ b/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py @@ -1,7 +1,10 @@ from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from ....admin.request_context import AdminRequestContext +from ....askar.profile_anon import AskarAnoncredsProfile +from ....connections.models.conn_record import ConnRecord from ....core.in_memory import InMemoryProfile from ....indy.issuer import IndyIssuer from ....ledger.base import BaseLedger @@ -11,10 +14,7 @@ from ....multitenant.base import BaseMultitenantManager from ....multitenant.manager import MultitenantManager from ....storage.base import BaseStorage - from .. import routes as test_module -from ....connections.models.conn_record import ConnRecord - SCHEMA_ID = "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0" CRED_DEF_ID = "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag" @@ -389,6 +389,46 @@ async def test_get_credential_definition_no_ledger(self): self.request ) + async def test_credential_definition_endpoints_wrong_profile_403(self): + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar"}, + profile_class=AskarAnoncredsProfile, + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + ) + self.request.json = mock.CoroutineMock( + return_value={ + "schema_id": "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0", + "support_revocation": False, + "tag": "tag", + } + ) + + self.request.query = {"create_transaction_for_endorser": "false"} + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.credential_definitions_send_credential_definition( + self.request + ) + + self.request.match_info = {"cred_def_id": CRED_DEF_ID} + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.credential_definitions_created(self.request) + + self.request.match_info = {"cred_def_id": CRED_DEF_ID} + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.credential_definitions_get_credential_definition( + self.request + ) + async def test_register(self): mock_app = mock.MagicMock() mock_app.add_routes = mock.MagicMock() diff --git a/aries_cloudagent/messaging/schemas/routes.py b/aries_cloudagent/messaging/schemas/routes.py index c5a9952601..5ef8e0a0e8 100644 --- a/aries_cloudagent/messaging/schemas/routes.py +++ b/aries_cloudagent/messaging/schemas/routes.py @@ -41,6 +41,7 @@ ) from ...storage.base import BaseStorage, StorageRecord from ...storage.error import StorageError, StorageNotFoundError +from ...utils.profiles import is_anoncreds_profile_raise_web_exception from ..models.base import BaseModelError from ..models.openapi import OpenAPISchema from ..valid import ( @@ -177,6 +178,9 @@ async def schemas_send_schema(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + outbound_handler = request["outbound_message_router"] create_transaction_for_endorser = json.loads( @@ -348,6 +352,8 @@ async def schemas_created(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + is_anoncreds_profile_raise_web_exception(context.profile) + session = await context.session() storage = session.inject(BaseStorage) found = await storage.find_all_records( @@ -374,12 +380,16 @@ async def schemas_get_schema(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + schema_id = request.match_info["schema_id"] - async with context.profile.session() as session: + async with profile.session() as session: multitenant_mgr = session.inject_or(BaseMultitenantManager) if multitenant_mgr: - ledger_exec_inst = IndyLedgerRequestsExecutor(context.profile) + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) else: ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) ledger_id, ledger = await ledger_exec_inst.get_ledger_for_identifier( @@ -420,16 +430,17 @@ async def schemas_fix_schema_wallet_record(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] - profile = context.profile + is_anoncreds_profile_raise_web_exception(profile) + schema_id = request.match_info["schema_id"] async with profile.session() as session: storage = session.inject(BaseStorage) multitenant_mgr = session.inject_or(BaseMultitenantManager) if multitenant_mgr: - ledger_exec_inst = IndyLedgerRequestsExecutor(context.profile) + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) else: ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) ledger_id, ledger = await ledger_exec_inst.get_ledger_for_identifier( diff --git a/aries_cloudagent/messaging/schemas/tests/test_routes.py b/aries_cloudagent/messaging/schemas/tests/test_routes.py index 8e783eea22..6e42b4c190 100644 --- a/aries_cloudagent/messaging/schemas/tests/test_routes.py +++ b/aries_cloudagent/messaging/schemas/tests/test_routes.py @@ -3,6 +3,7 @@ from aries_cloudagent.tests import mock from ....admin.request_context import AdminRequestContext +from ....askar.profile_anon import AskarAnoncredsProfile from ....connections.models.conn_record import ConnRecord from ....core.in_memory import InMemoryProfile from ....indy.issuer import IndyIssuer @@ -399,6 +400,43 @@ async def test_get_schema_x_ledger(self): with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.schemas_get_schema(self.request) + async def test_schema_endpoints_wrong_profile_403(self): + self.profile = InMemoryProfile.test_profile( + settings={"wallet-type": "askar"}, + profile_class=AskarAnoncredsProfile, + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + ) + + self.request.json = mock.CoroutineMock( + return_value={ + "schema_name": "schema_name", + "schema_version": "1.0", + "attributes": ["table", "drink", "colour"], + } + ) + + self.request.query = {"create_transaction_for_endorser": "false"} + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.schemas_send_schema(self.request) + + self.request.match_info = {"schema_id": SCHEMA_ID} + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.schemas_created(self.request) + + self.request.match_info = {"schema_id": SCHEMA_ID} + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.schemas_get_schema(self.request) + async def test_register(self): mock_app = mock.MagicMock() mock_app.add_routes = mock.MagicMock() diff --git a/aries_cloudagent/multitenant/admin/routes.py b/aries_cloudagent/multitenant/admin/routes.py index d4c0546779..889caefaf2 100644 --- a/aries_cloudagent/multitenant/admin/routes.py +++ b/aries_cloudagent/multitenant/admin/routes.py @@ -19,6 +19,7 @@ from ...multitenant.base import BaseMultitenantManager from ...storage.error import StorageError, StorageNotFoundError from ...utils.endorsement_setup import attempt_auto_author_with_endorser_setup +from ...utils.profiles import subwallet_type_not_same_as_base_wallet_raise_web_exception from ...wallet.error import WalletSettingsError from ...wallet.models.wallet_record import WalletRecord, WalletRecordSchema from ..error import WalletKeyMissingError @@ -167,8 +168,12 @@ class CreateWalletRequestSchema(OpenAPISchema): wallet_type = fields.Str( dump_default="in_memory", + required=False, validate=validate.OneOf(list(ProfileManagerProvider.MANAGER_TYPES)), - metadata={"description": "Type of the wallet to create", "example": "indy"}, + metadata={ + "description": "Type of the wallet to create. Must be same as base wallet.", + "example": "askar", + }, ) wallet_dispatch_type = fields.Str( @@ -431,6 +436,13 @@ async def wallet_create(request: web.BaseRequest): context: AdminRequestContext = request["context"] body = await request.json() + base_wallet_type = context.profile.settings.get("wallet.type") + sub_wallet_type = body.get("wallet_type", base_wallet_type) + + subwallet_type_not_same_as_base_wallet_raise_web_exception( + base_wallet_type, sub_wallet_type + ) + key_management_mode = body.get("key_management_mode") or WalletRecord.MODE_MANAGED wallet_key = body.get("wallet_key") wallet_webhook_urls = body.get("wallet_webhook_urls") or [] @@ -441,7 +453,7 @@ async def wallet_create(request: web.BaseRequest): wallet_dispatch_type = "base" settings = { - "wallet.type": body.get("wallet_type") or "in_memory", + "wallet.type": sub_wallet_type, "wallet.name": body.get("wallet_name"), "wallet.key": wallet_key, "wallet.webhook_urls": wallet_webhook_urls, diff --git a/aries_cloudagent/multitenant/admin/tests/test_routes.py b/aries_cloudagent/multitenant/admin/tests/test_routes.py index cc549ade09..9f4a6d32ba 100644 --- a/aries_cloudagent/multitenant/admin/tests/test_routes.py +++ b/aries_cloudagent/multitenant/admin/tests/test_routes.py @@ -6,6 +6,8 @@ from aries_cloudagent.tests import mock from ....admin.request_context import AdminRequestContext +from ....askar.profile import AskarProfile +from ....core.in_memory.profile import InMemoryProfile from ....messaging.models.base import BaseModelError from ....storage.error import StorageError, StorageNotFoundError from ....wallet.models.wallet_record import WalletRecord @@ -21,8 +23,15 @@ async def asyncSetUp(self): self.mock_multitenant_mgr.__aenter__ = mock.CoroutineMock( return_value=self.mock_multitenant_mgr ) + self.profile = InMemoryProfile.test_profile( + settings={"wallet.type": "askar"}, + profile_class=AskarProfile, + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } - self.context = AdminRequestContext.test_context() self.context.profile.context.injector.bind_instance( BaseMultitenantManager, self.mock_multitenant_mgr ) @@ -146,7 +155,7 @@ async def test_wallet_create_tenant_settings(self): body = { "wallet_name": "test", "default_label": "test_label", - "wallet_type": "indy", + "wallet_type": "askar", "wallet_key": "test", "key_management_mode": "managed", "wallet_webhook_urls": [], @@ -206,11 +215,59 @@ async def test_wallet_create_tenant_settings(self): assert self.mock_multitenant_mgr.get_wallet_profile.called assert test_module.attempt_auto_author_with_endorser_setup.called + async def test_wallet_create_wallet_type_different_from_base_wallet_raises_403( + self, + ): + body = { + "wallet_name": "test", + "default_label": "test_label", + "wallet_type": "askar", + "wallet_key": "test", + "key_management_mode": "managed", + "wallet_webhook_urls": [], + "wallet_dispatch_type": "base", + } + wallet_mock = mock.MagicMock( + serialize=mock.MagicMock( + return_value={ + "wallet_id": "test", + "settings": {}, + "key_management_mode": body["key_management_mode"], + } + ) + ) + # wallet_record + self.mock_multitenant_mgr.create_wallet = mock.CoroutineMock( + return_value=wallet_mock + ) + + self.mock_multitenant_mgr.create_auth_token = mock.CoroutineMock( + return_value="test_token" + ) + self.mock_multitenant_mgr.get_wallet_profile = mock.CoroutineMock( + return_value=mock.MagicMock() + ) + self.request.json = mock.CoroutineMock(return_value=body) + + await test_module.wallet_create(self.request) + + body["wallet_type"] = "askar-anoncreds" + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.wallet_create(self.request) + + body["wallet_type"] = "indy" + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.wallet_create(self.request) + + body["wallet_type"] = "in_memory" + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.wallet_create(self.request) + async def test_wallet_create(self): body = { "wallet_name": "test", "default_label": "test_label", - "wallet_type": "indy", + "wallet_type": "askar", "wallet_key": "test", "key_management_mode": "managed", "wallet_webhook_urls": [], @@ -304,7 +361,7 @@ async def test_wallet_create_optional_default_fields(self): self.mock_multitenant_mgr.create_wallet.assert_called_once_with( { "wallet.name": body["wallet_name"], - "wallet.type": "in_memory", + "wallet.type": "askar", "wallet.key": body["wallet_key"], "default_label": body["label"], "image_url": body["image_url"], @@ -336,7 +393,7 @@ async def test_wallet_create_raw_key_derivation(self): await test_module.wallet_create(self.request) self.mock_multitenant_mgr.create_wallet.assert_called_once_with( { - "wallet.type": "in_memory", + "wallet.type": "askar", "wallet.name": body["wallet_name"], "wallet.key": body["wallet_key"], "wallet.key_derivation_method": body["wallet_key_derivation"], diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index 8a4dd0b007..c1f72768b5 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -15,7 +15,6 @@ request_schema, response_schema, ) - from marshmallow import fields, validate, validates_schema from marshmallow.exceptions import ValidationError @@ -58,6 +57,7 @@ ) from ..storage.base import BaseStorage from ..storage.error import StorageError, StorageNotFoundError +from ..utils.profiles import is_anoncreds_profile_raise_web_exception from .error import RevocationError, RevocationNotSupportedError from .indy import IndyRevocation from .manager import RevocationManager, RevocationManagerError @@ -270,7 +270,7 @@ def validate_fields(self, data, **kwargs): ) -class PublishRevocationsSchema(OpenAPISchema): +class PublishRevocationsSchemaAnoncreds(OpenAPISchema): """Request and result schema for revocation publication API call.""" rrid2crid = fields.Dict( @@ -293,7 +293,7 @@ class TxnOrPublishRevocationsResultSchema(OpenAPISchema): """Result schema for credential definition send request.""" sent = fields.Nested( - PublishRevocationsSchema(), + PublishRevocationsSchemaAnoncreds(), required=False, metadata={"definition": "Content sent"}, ) @@ -330,7 +330,7 @@ class ClearPendingRevocationsRequestSchema(OpenAPISchema): ) -class CredRevRecordResultSchema(OpenAPISchema): +class CredRevRecordResultSchemaAnoncreds(OpenAPISchema): """Result schema for credential revocation record request.""" result = fields.Nested(IssuerCredRevRecordSchema()) @@ -518,6 +518,10 @@ async def revoke(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + body = await request.json() cred_ex_id = body.get("cred_ex_id") body["notify"] = body.get("notify", context.settings.get("revocation.notify")) @@ -529,8 +533,7 @@ async def revoke(request: web.BaseRequest): request.query.get("create_transaction_for_endorser", "false") ) endorser_conn_id = request.query.get("conn_id") - rev_manager = RevocationManager(context.profile) - profile = context.profile + rev_manager = RevocationManager(profile) outbound_handler = request["outbound_message_router"] write_ledger = not create_transaction_for_endorser @@ -610,7 +613,7 @@ async def revoke(request: web.BaseRequest): @docs(tags=["revocation"], summary="Publish pending revocations to ledger") -@request_schema(PublishRevocationsSchema()) +@request_schema(PublishRevocationsSchemaAnoncreds()) @querystring_schema(CreateRevRegTxnForEndorserOptionSchema()) @querystring_schema(RevRegConnIdMatchInfoSchema()) @response_schema(TxnOrPublishRevocationsResultSchema(), 200, description="") @@ -625,6 +628,10 @@ async def publish_revocations(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + body = await request.json() rrid2crid = body.get("rrid2crid") create_transaction_for_endorser = json.loads( @@ -632,8 +639,7 @@ async def publish_revocations(request: web.BaseRequest): ) write_ledger = not create_transaction_for_endorser endorser_conn_id = request.query.get("conn_id") - rev_manager = RevocationManager(context.profile) - profile = context.profile + rev_manager = RevocationManager(profile) outbound_handler = request["outbound_message_router"] if is_author_role(profile): @@ -680,7 +686,7 @@ async def publish_revocations(request: web.BaseRequest): @docs(tags=["revocation"], summary="Clear pending revocations") @request_schema(ClearPendingRevocationsRequestSchema()) -@response_schema(PublishRevocationsSchema(), 200, description="") +@response_schema(PublishRevocationsSchemaAnoncreds(), 200, description="") async def clear_pending_revocations(request: web.BaseRequest): """Request handler for clearing pending revocations. @@ -692,10 +698,14 @@ async def clear_pending_revocations(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + body = await request.json() purge = body.get("purge") - rev_manager = RevocationManager(context.profile) + rev_manager = RevocationManager(profile) try: results = await rev_manager.clear_pending_revocations(purge) @@ -719,6 +729,9 @@ async def rotate_rev_reg(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + cred_def_id = request.match_info["cred_def_id"] try: @@ -748,6 +761,9 @@ async def create_rev_reg(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + body = await request.json() credential_definition_id = body.get("credential_definition_id") @@ -798,6 +814,8 @@ async def rev_regs_created(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + is_anoncreds_profile_raise_web_exception(context.profile) + search_tags = list(vars(RevRegsCreatedQueryStringSchema)["_declared_fields"]) tag_filter = { tag: request.query[tag] for tag in search_tags if tag in request.query @@ -835,11 +853,14 @@ async def get_rev_reg(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) rev_reg_id = request.match_info["rev_reg_id"] try: - revoc = IndyRevocation(context.profile) + revoc = IndyRevocation(profile) 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 @@ -864,10 +885,13 @@ async def get_rev_reg_issued_count(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) rev_reg_id = request.match_info["rev_reg_id"] - async with context.profile.session() as session: + async with profile.session() as session: try: await IssuerRevRegRecord.retrieve_by_revoc_reg_id(session, rev_reg_id) except StorageNotFoundError as err: @@ -896,11 +920,14 @@ async def get_rev_reg_issued(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) rev_reg_id = request.match_info["rev_reg_id"] recs = [] - async with context.profile.session() as session: + async with profile.session() as session: try: await IssuerRevRegRecord.retrieve_by_revoc_reg_id(session, rev_reg_id) except StorageNotFoundError as err: @@ -930,10 +957,13 @@ async def get_rev_reg_indy_recs(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) rev_reg_id = request.match_info["rev_reg_id"] - revoc = IndyRevocation(context.profile) + revoc = IndyRevocation(profile) rev_reg_delta = await revoc.get_issuer_rev_reg_delta(rev_reg_id) return web.json_response( @@ -961,6 +991,9 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) rev_reg_id = request.match_info["rev_reg_id"] @@ -970,7 +1003,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): rev_reg_record = None genesis_transactions = None - async with context.profile.session() as session: + async with profile.session() as session: try: rev_reg_record = await IssuerRevRegRecord.retrieve_by_revoc_reg_id( session, rev_reg_id @@ -1003,7 +1036,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): reason += ": missing wallet-type?" raise web.HTTPInternalServerError(reason=reason) - rev_manager = RevocationManager(context.profile) + rev_manager = RevocationManager(profile) try: ( rev_reg_delta, @@ -1037,7 +1070,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): summary="Get credential revocation status", ) @querystring_schema(CredRevRecordQueryStringSchema()) -@response_schema(CredRevRecordResultSchema(), 200, description="") +@response_schema(CredRevRecordResultSchemaAnoncreds(), 200, description="") async def get_cred_rev_record(request: web.BaseRequest): """Request handler to get credential revocation record. @@ -1049,13 +1082,16 @@ async def get_cred_rev_record(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) rev_reg_id = request.query.get("rev_reg_id") cred_rev_id = request.query.get("cred_rev_id") # numeric string cred_ex_id = request.query.get("cred_ex_id") try: - async with context.profile.session() as session: + async with profile.session() as session: if rev_reg_id and cred_rev_id: rec = await IssuerCredRevRecord.retrieve_by_ids( session, rev_reg_id, cred_rev_id @@ -1087,11 +1123,14 @@ async def get_active_rev_reg(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) cred_def_id = request.match_info["cred_def_id"] try: - revoc = IndyRevocation(context.profile) + revoc = IndyRevocation(profile) 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 @@ -1117,11 +1156,14 @@ async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) rev_reg_id = request.match_info["rev_reg_id"] try: - revoc = IndyRevocation(context.profile) + revoc = IndyRevocation(profile) 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 @@ -1143,10 +1185,13 @@ async def upload_tails_file(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) rev_reg_id = request.match_info["rev_reg_id"] try: - revoc = IndyRevocation(context.profile) + revoc = IndyRevocation(profile) 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 @@ -1155,7 +1200,7 @@ async def upload_tails_file(request: web.BaseRequest): raise web.HTTPNotFound(reason=f"No local tails file for rev reg {rev_reg_id}") try: - await rev_reg.upload_tails_file(context.profile) + await rev_reg.upload_tails_file(profile) except RevocationError as e: raise web.HTTPInternalServerError(reason=str(e)) @@ -1182,6 +1227,9 @@ async def send_rev_reg_def(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + outbound_handler = request["outbound_message_router"] rev_reg_id = request.match_info["rev_reg_id"] create_transaction_for_endorser = json.loads( @@ -1299,6 +1347,9 @@ async def send_rev_reg_entry(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + outbound_handler = request["outbound_message_router"] create_transaction_for_endorser = json.loads( request.query.get("create_transaction_for_endorser", "false") @@ -1415,6 +1466,9 @@ async def update_rev_reg(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + body = await request.json() tails_public_uri = body.get("tails_public_uri") @@ -1449,6 +1503,9 @@ async def set_rev_reg_state(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + rev_reg_id = request.match_info["rev_reg_id"] state = request.query.get("state") @@ -1690,9 +1747,13 @@ class TailsDeleteResponseSchema(OpenAPISchema): async def delete_tails(request: web.BaseRequest) -> json: """Delete Tails Files.""" context: AdminRequestContext = request["context"] + profile = context.profile + + is_anoncreds_profile_raise_web_exception(profile) + rev_reg_id = request.query.get("rev_reg_id") cred_def_id = request.query.get("cred_def_id") - revoc = IndyRevocation(context.profile) + revoc = IndyRevocation(profile) session = revoc._profile.session() if rev_reg_id: rev_reg = await revoc.get_issuer_rev_reg_record(rev_reg_id) diff --git a/aries_cloudagent/revocation/tests/test_routes.py b/aries_cloudagent/revocation/tests/test_routes.py index 0a384bc2b3..71a9841d16 100644 --- a/aries_cloudagent/revocation/tests/test_routes.py +++ b/aries_cloudagent/revocation/tests/test_routes.py @@ -1,14 +1,16 @@ import os -import pytest import shutil +from unittest import IsolatedAsyncioTestCase +import pytest from aiohttp.web import HTTPBadRequest, HTTPNotFound -from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock from aries_cloudagent.core.in_memory import InMemoryProfile from aries_cloudagent.revocation.error import RevocationError +from aries_cloudagent.tests import mock +from ...admin.request_context import AdminRequestContext +from ...askar.profile_anon import AskarAnoncredsProfile from ...storage.in_memory import InMemoryStorage from .. import routes as test_module @@ -1050,6 +1052,85 @@ async def test_set_rev_reg_state_not_found(self): result = await test_module.set_rev_reg_state(self.request) mock_json_response.assert_not_called() + async def test_wrong_profile_403(self): + self.profile = InMemoryProfile.test_profile( + settings={"wallet.type": "askar"}, + profile_class=AskarAnoncredsProfile, + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + ) + + self.request.json = mock.CoroutineMock( + return_value={ + "rev_reg_id": "rr_id", + "cred_rev_id": "23", + "publish": "false", + } + ) + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.revoke(self.request) + + self.request.json = mock.CoroutineMock() + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.publish_revocations(self.request) + + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.clear_pending_revocations(self.request) + + CRED_DEF_ID = f"{self.test_did}:3:CL:1234:default" + self.request.json = mock.CoroutineMock( + return_value={ + "max_cred_num": "1000", + "credential_definition_id": CRED_DEF_ID, + } + ) + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.create_rev_reg(self.request) + + REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( + self.test_did, self.test_did + ) + self.request.match_info = {"rev_reg_id": REV_REG_ID} + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.get_rev_reg(self.request) + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.get_rev_reg_issued(self.request) + + CRED_REV_ID = "1" + self.request.query = { + "rev_reg_id": REV_REG_ID, + "cred_rev_id": CRED_REV_ID, + } + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.get_cred_rev_record(self.request) + + self.request.match_info = {"cred_def_id": CRED_DEF_ID} + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.get_active_rev_reg(self.request) + + self.request.match_info = {"rev_reg_id": REV_REG_ID} + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.get_tails_file(self.request) + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.upload_tails_file(self.request) + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.send_rev_reg_def(self.request) + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.send_rev_reg_entry(self.request) + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.update_rev_reg(self.request) + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.set_rev_reg_state(self.request) + async def test_register(self): mock_app = mock.MagicMock() mock_app.add_routes = mock.MagicMock() diff --git a/aries_cloudagent/revocation_anoncreds/models/issuer_cred_rev_record.py b/aries_cloudagent/revocation_anoncreds/models/issuer_cred_rev_record.py index bab3909b83..8a6aa3475c 100644 --- a/aries_cloudagent/revocation_anoncreds/models/issuer_cred_rev_record.py +++ b/aries_cloudagent/revocation_anoncreds/models/issuer_cred_rev_record.py @@ -17,7 +17,7 @@ class IssuerCredRevRecord(BaseRecord): class Meta: """IssuerCredRevRecord metadata.""" - schema_class = "IssuerCredRevRecordSchema" + schema_class = "IssuerCredRevRecordSchemaAnoncreds" RECORD_TYPE = "issuer_cred_rev" RECORD_ID_NAME = "record_id" @@ -123,7 +123,7 @@ def __eq__(self, other: Any) -> bool: return super().__eq__(other) -class IssuerCredRevRecordSchema(BaseRecordSchema): +class IssuerCredRevRecordSchemaAnoncreds(BaseRecordSchema): """Schema to allow de/serialization of credential revocation records.""" class Meta: diff --git a/aries_cloudagent/revocation_anoncreds/routes.py b/aries_cloudagent/revocation_anoncreds/routes.py index cac93d0104..cec9aa164a 100644 --- a/aries_cloudagent/revocation_anoncreds/routes.py +++ b/aries_cloudagent/revocation_anoncreds/routes.py @@ -29,7 +29,7 @@ create_transaction_for_endorser_description, endorser_connection_id_description, ) -from ..askar.profile import AskarProfile +from ..askar.profile_anon import AskarAnoncredsProfile from ..indy.issuer import IndyIssuerError from ..indy.models.revocation import IndyRevRegDef from ..ledger.base import BaseLedger @@ -43,8 +43,6 @@ INDY_CRED_REV_ID_VALIDATE, INDY_REV_REG_ID_EXAMPLE, INDY_REV_REG_ID_VALIDATE, - INDY_REV_REG_SIZE_EXAMPLE, - INDY_REV_REG_SIZE_VALIDATE, UUID4_EXAMPLE, UUID4_VALIDATE, WHOLE_NUM_EXAMPLE, @@ -60,41 +58,23 @@ IssuerRevRegRecordSchema, ) from ..storage.error import StorageError, StorageNotFoundError +from ..utils.profiles import is_not_anoncreds_profile_raise_web_exception from .manager import RevocationManager, RevocationManagerError from .models.issuer_cred_rev_record import ( IssuerCredRevRecord, - IssuerCredRevRecordSchema, + IssuerCredRevRecordSchemaAnoncreds, ) LOGGER = logging.getLogger(__name__) +TAG_TITLE = "anoncreds - revocation" + class RevocationAnoncredsModuleResponseSchema(OpenAPISchema): """Response schema for Revocation Module.""" -class RevRegCreateRequestSchema(OpenAPISchema): - """Request schema for revocation registry creation request.""" - - credential_definition_id = fields.Str( - validate=INDY_CRED_DEF_ID_VALIDATE, - metadata={ - "description": "Credential definition identifier", - "example": INDY_CRED_DEF_ID_EXAMPLE, - }, - ) - max_cred_num = fields.Int( - required=False, - validate=INDY_REV_REG_SIZE_VALIDATE, - metadata={ - "description": "Revocation registry size", - "strict": True, - "example": INDY_REV_REG_SIZE_EXAMPLE, - }, - ) - - -class RevRegResultSchema(OpenAPISchema): +class RevRegResultSchemaAnoncreds(OpenAPISchema): """Result schema for revocation registry creation request.""" result = fields.Nested(IssuerRevRegRecordSchema()) @@ -104,7 +84,9 @@ class TxnOrRevRegResultSchema(OpenAPISchema): """Result schema for credential definition send request.""" sent = fields.Nested( - RevRegResultSchema(), required=False, metadata={"definition": "Content sent"} + RevRegResultSchemaAnoncreds(), + required=False, + metadata={"definition": "Content sent"}, ) txn = fields.Nested( TransactionRecordSchema(), @@ -218,16 +200,16 @@ class ClearPendingRevocationsRequestSchema(OpenAPISchema): class CredRevRecordResultSchema(OpenAPISchema): """Result schema for credential revocation record request.""" - result = fields.Nested(IssuerCredRevRecordSchema()) + result = fields.Nested(IssuerCredRevRecordSchemaAnoncreds()) -class CredRevRecordDetailsResultSchema(OpenAPISchema): +class CredRevRecordDetailsResultSchemaAnoncreds(OpenAPISchema): """Result schema for credential revocation record request.""" - results = fields.List(fields.Nested(IssuerCredRevRecordSchema())) + results = fields.List(fields.Nested(IssuerCredRevRecordSchemaAnoncreds())) -class CredRevIndyRecordsResultSchema(OpenAPISchema): +class CredRevIndyRecordsResultSchemaAnoncreds(OpenAPISchema): """Result schema for revoc reg delta.""" rev_reg_delta = fields.Dict( @@ -235,7 +217,7 @@ class CredRevIndyRecordsResultSchema(OpenAPISchema): ) -class RevRegIssuedResultSchema(OpenAPISchema): +class RevRegIssuedResultSchemaAnoncreds(OpenAPISchema): """Result schema for revocation registry credentials issued request.""" result = fields.Int( @@ -257,7 +239,7 @@ class RevRegUpdateRequestMatchInfoSchema(OpenAPISchema): ) -class RevRegWalletUpdatedResultSchema(OpenAPISchema): +class RevRegWalletUpdatedResultSchemaAnoncreds(OpenAPISchema): """Number of wallet revocation entries status updated.""" rev_reg_delta = fields.Dict( @@ -271,7 +253,7 @@ class RevRegWalletUpdatedResultSchema(OpenAPISchema): ) -class RevRegsCreatedSchema(OpenAPISchema): +class RevRegsCreatedSchemaAnoncreds(OpenAPISchema): """Result schema for request for revocation registries created.""" rev_reg_ids = fields.List( @@ -443,7 +425,7 @@ class PublishRevocationsResultSchema(OpenAPISchema): ) -class RevokeRequestSchema(CredRevRecordQueryStringSchema): +class RevokeRequestSchemaAnoncreds(CredRevRecordQueryStringSchema): """Parameters and validators for revocation request.""" @validates_schema @@ -516,10 +498,10 @@ def validate_fields(self, data, **kwargs): @docs( - tags=["revocation"], + tags=[TAG_TITLE], summary="Revoke an issued credential", ) -@request_schema(RevokeRequestSchema()) +@request_schema(RevokeRequestSchemaAnoncreds()) @response_schema(RevocationAnoncredsModuleResponseSchema(), description="") async def revoke(request: web.BaseRequest): """Request handler for storing a credential revocation. @@ -532,6 +514,10 @@ async def revoke(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + body = await request.json() cred_ex_id = body.get("cred_ex_id") body["notify"] = body.get("notify", context.settings.get("revocation.notify")) @@ -547,7 +533,7 @@ async def revoke(request: web.BaseRequest): reason="Request must specify notify_version if notify is true" ) - rev_manager = RevocationManager(context.profile) + rev_manager = RevocationManager(profile) try: if cred_ex_id: # rev_reg_id and cred_rev_id should not be present so we can @@ -567,7 +553,7 @@ async def revoke(request: web.BaseRequest): raise web.HTTPBadRequest(reason=err.roll_up) from err -@docs(tags=["revocation"], summary="Publish pending revocations to ledger") +@docs(tags=[TAG_TITLE], summary="Publish pending revocations to ledger") @request_schema(PublishRevocationsSchema()) @response_schema(PublishRevocationsResultSchema(), 200, description="") async def publish_revocations(request: web.BaseRequest): @@ -581,11 +567,15 @@ async def publish_revocations(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + body = await request.json() options = body.get("options", {}) rrid2crid = body.get("rrid2crid") - rev_manager = RevocationManager(context.profile) + rev_manager = RevocationManager(profile) try: rev_reg_resp = await rev_manager.publish_pending_revocations(rrid2crid, options) @@ -600,12 +590,12 @@ async def publish_revocations(request: web.BaseRequest): @docs( - tags=["revocation"], + tags=[TAG_TITLE], summary="Search for matching revocation registries that current agent created", ) @querystring_schema(RevRegsCreatedQueryStringSchema()) -@response_schema(RevRegsCreatedSchema(), 200, description="") -async def rev_regs_created(request: web.BaseRequest): +@response_schema(RevRegsCreatedSchemaAnoncreds(), 200, description="") +async def get_rev_regs(request: web.BaseRequest): """Request handler to get revocation registries that current agent created. Args: @@ -616,7 +606,10 @@ async def rev_regs_created(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] - profile: AskarProfile = context.profile + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + search_tags = list(vars(RevRegsCreatedQueryStringSchema)["_declared_fields"]) tag_filter = { tag: request.query[tag] for tag in search_tags if tag in request.query @@ -628,7 +621,6 @@ async def rev_regs_created(request: web.BaseRequest): found = await revocation.get_created_revocation_registry_definitions( cred_def_id, state ) - except AnonCredsIssuerError as e: raise web.HTTPInternalServerError(reason=str(e)) from e # TODO remove state == init @@ -636,11 +628,11 @@ async def rev_regs_created(request: web.BaseRequest): @docs( - tags=["revocation"], + tags=[TAG_TITLE], summary="Get revocation registry by revocation registry id", ) @match_info_schema(RevRegIdMatchInfoSchema()) -@response_schema(RevRegResultSchema(), 200, description="") +@response_schema(RevRegResultSchemaAnoncreds(), 200, description="") async def get_rev_reg(request: web.BaseRequest): """Request handler to get a revocation registry by rev reg id. @@ -652,7 +644,10 @@ async def get_rev_reg(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] - profile: AskarProfile = context.profile + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + rev_reg_id = request.match_info["rev_reg_id"] rev_reg = await _get_issuer_rev_reg_record(profile, rev_reg_id) @@ -660,7 +655,7 @@ async def get_rev_reg(request: web.BaseRequest): async def _get_issuer_rev_reg_record( - profile: AskarProfile, rev_reg_id + profile: AskarAnoncredsProfile, rev_reg_id ) -> IssuerRevRegRecord: # fetch rev reg def from anoncreds try: @@ -707,11 +702,11 @@ async def _get_issuer_rev_reg_record( @docs( - tags=["revocation"], + tags=[TAG_TITLE], summary="Get current active revocation registry by credential definition id", ) @match_info_schema(RevocationCredDefIdMatchInfoSchema()) -@response_schema(RevRegResultSchema(), 200, description="") +@response_schema(RevRegResultSchemaAnoncreds(), 200, description="") async def get_active_rev_reg(request: web.BaseRequest): """Request handler to get current active revocation registry by cred def id. @@ -723,7 +718,10 @@ async def get_active_rev_reg(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] - profile: AskarProfile = context.profile + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + cred_def_id = request.match_info["cred_def_id"] try: revocation = AnonCredsRevocation(profile) @@ -735,9 +733,9 @@ async def get_active_rev_reg(request: web.BaseRequest): return web.json_response({"result": rev_reg.serialize()}) -@docs(tags=["revocation"], summary="Rotate revocation registry") +@docs(tags=[TAG_TITLE], summary="Rotate revocation registry") @match_info_schema(RevocationCredDefIdMatchInfoSchema()) -@response_schema(RevRegsCreatedSchema(), 200, description="") +@response_schema(RevRegsCreatedSchemaAnoncreds(), 200, description="") async def rotate_rev_reg(request: web.BaseRequest): """Request handler to rotate the active revocation registries for cred. def. @@ -749,7 +747,10 @@ async def rotate_rev_reg(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] - profile: AskarProfile = context.profile + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + cred_def_id = request.match_info["cred_def_id"] try: @@ -762,11 +763,11 @@ async def rotate_rev_reg(request: web.BaseRequest): @docs( - tags=["revocation"], + tags=[TAG_TITLE], summary="Get number of credentials issued against revocation registry", ) @match_info_schema(RevRegIdMatchInfoSchema()) -@response_schema(RevRegIssuedResultSchema(), 200, description="") +@response_schema(RevRegIssuedResultSchemaAnoncreds(), 200, description="") async def get_rev_reg_issued_count(request: web.BaseRequest): """Request handler to get number of credentials issued against revocation registry. @@ -778,7 +779,10 @@ async def get_rev_reg_issued_count(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] - profile: AskarProfile = context.profile + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + rev_reg_id = request.match_info["rev_reg_id"] try: revocation = AnonCredsRevocation(profile) @@ -790,7 +794,7 @@ async def get_rev_reg_issued_count(request: web.BaseRequest): except AnonCredsIssuerError as e: raise web.HTTPInternalServerError(reason=str(e)) from e - async with context.profile.session() as session: + async with profile.session() as session: count = len( await IssuerCredRevRecord.query_by_ids(session, rev_reg_id=rev_reg_id) ) @@ -799,11 +803,11 @@ async def get_rev_reg_issued_count(request: web.BaseRequest): @docs( - tags=["revocation"], + tags=[TAG_TITLE], summary="Get details of credentials issued against revocation registry", ) @match_info_schema(RevRegIdMatchInfoSchema()) -@response_schema(CredRevRecordDetailsResultSchema(), 200, description="") +@response_schema(CredRevRecordDetailsResultSchemaAnoncreds(), 200, description="") async def get_rev_reg_issued(request: web.BaseRequest): """Request handler to get credentials issued against revocation registry. @@ -815,7 +819,10 @@ async def get_rev_reg_issued(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] - profile: AskarProfile = context.profile + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + rev_reg_id = request.match_info["rev_reg_id"] try: revocation = AnonCredsRevocation(profile) @@ -827,7 +834,7 @@ async def get_rev_reg_issued(request: web.BaseRequest): except AnonCredsIssuerError as e: raise web.HTTPInternalServerError(reason=str(e)) from e - async with context.profile.session() as session: + async with profile.session() as session: recs = await IssuerCredRevRecord.query_by_ids(session, rev_reg_id=rev_reg_id) results = [] for rec in recs: @@ -837,11 +844,11 @@ async def get_rev_reg_issued(request: web.BaseRequest): @docs( - tags=["revocation"], + tags=[TAG_TITLE], summary="Get details of revoked credentials from ledger", ) @match_info_schema(RevRegIdMatchInfoSchema()) -@response_schema(CredRevIndyRecordsResultSchema(), 200, description="") +@response_schema(CredRevIndyRecordsResultSchemaAnoncreds(), 200, description="") async def get_rev_reg_indy_recs(request: web.BaseRequest): """Request handler to get details of revoked credentials from ledger. @@ -853,13 +860,17 @@ async def get_rev_reg_indy_recs(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + rev_reg_id = request.match_info["rev_reg_id"] indy_registry = LegacyIndyRegistry() if await indy_registry.supports(rev_reg_id): try: rev_reg_delta, _ts = await indy_registry.get_revocation_registry_delta( - context.profile, rev_reg_id, None + profile, rev_reg_id, None ) except (AnonCredsObjectNotFound, AnonCredsResolutionError) as e: raise web.HTTPInternalServerError(reason=str(e)) from e @@ -877,12 +888,12 @@ async def get_rev_reg_indy_recs(request: web.BaseRequest): @docs( - tags=["revocation"], + tags=[TAG_TITLE], summary="Fix revocation state in wallet and return number of updated entries", ) @match_info_schema(RevRegIdMatchInfoSchema()) @querystring_schema(RevRegUpdateRequestMatchInfoSchema()) -@response_schema(RevRegWalletUpdatedResultSchema(), 200, description="") +@response_schema(RevRegWalletUpdatedResultSchemaAnoncreds(), 200, description="") async def update_rev_reg_revoked_state(request: web.BaseRequest): """Request handler to fix ledger entry of credentials revoked against registry. @@ -894,6 +905,9 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) rev_reg_id = request.match_info["rev_reg_id"] @@ -903,7 +917,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): genesis_transactions = None try: - revocation = AnonCredsRevocation(context.profile) + revocation = AnonCredsRevocation(profile) rev_reg_def = await revocation.get_created_revocation_registry_definition( rev_reg_id ) @@ -912,7 +926,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): except AnonCredsIssuerError as e: raise web.HTTPInternalServerError(reason=str(e)) from e - async with context.profile.session() as session: + async with profile.session() as session: genesis_transactions = context.settings.get("ledger.genesis_transactions") if not genesis_transactions: ledger_manager = context.injector.inject(BaseMultipleLedgerManager) @@ -938,7 +952,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): reason += ": missing wallet-type?" raise web.HTTPInternalServerError(reason=reason) - rev_manager = RevocationManager(context.profile) + rev_manager = RevocationManager(profile) try: ( rev_reg_delta, @@ -970,7 +984,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): @docs( - tags=["revocation"], + tags=[TAG_TITLE], summary="Get credential revocation status", ) @querystring_schema(CredRevRecordQueryStringSchema()) @@ -986,13 +1000,16 @@ async def get_cred_rev_record(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) rev_reg_id = request.query.get("rev_reg_id") cred_rev_id = request.query.get("cred_rev_id") # numeric string cred_ex_id = request.query.get("cred_ex_id") try: - async with context.profile.session() as session: + async with profile.session() as session: if rev_reg_id and cred_rev_id: rec = await IssuerCredRevRecord.retrieve_by_ids( session, rev_reg_id, cred_rev_id @@ -1008,7 +1025,7 @@ async def get_cred_rev_record(request: web.BaseRequest): @docs( - tags=["revocation"], + tags=[TAG_TITLE], summary="Download tails file", produces=["application/octet-stream"], ) @@ -1029,7 +1046,10 @@ async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: # do we need it there or is this only for tranisition. # context: AdminRequestContext = request["context"] - profile: AskarProfile = context.profile + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + rev_reg_id = request.match_info["rev_reg_id"] try: revocation = AnonCredsRevocation(profile) @@ -1045,10 +1065,10 @@ async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: return web.FileResponse(path=tails_local_path, status=200) -@docs(tags=["revocation"], summary="Set revocation registry state manually") +@docs(tags=[TAG_TITLE], summary="Set revocation registry state manually") @match_info_schema(RevRegIdMatchInfoSchema()) @querystring_schema(SetRevRegStateQueryStringSchema()) -@response_schema(RevRegResultSchema(), 200, description="") +@response_schema(RevRegResultSchemaAnoncreds(), 200, description="") async def set_rev_reg_state(request: web.BaseRequest): """Request handler to set a revocation registry state manually. @@ -1060,7 +1080,10 @@ async def set_rev_reg_state(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] - profile: AskarProfile = context.profile + profile = context.profile + + is_not_anoncreds_profile_raise_web_exception(profile) + rev_reg_id = request.match_info["rev_reg_id"] state = request.query.get("state") @@ -1087,52 +1110,58 @@ async def register(app: web.Application): """Register routes.""" app.add_routes( [ - web.post("/revocation/revoke", revoke), - web.post("/revocation/publish-revocations", publish_revocations), + web.post("/anoncreds/revocation/revoke", revoke), + web.post("/anoncreds/revocation/publish-revocations", publish_revocations), web.get( - "/revocation/credential-record", get_cred_rev_record, allow_head=False + "/anoncreds/revocation/credential-record", + get_cred_rev_record, + allow_head=False, + ), + web.get( + "/anoncreds/revocation/registries", + get_rev_regs, + allow_head=False, ), web.get( - "/revocation/registries/created", - rev_regs_created, + "/anoncreds/revocation/registry/{rev_reg_id}", + get_rev_reg, allow_head=False, ), - web.get("/revocation/registry/{rev_reg_id}", get_rev_reg, allow_head=False), web.get( - "/revocation/active-registry/{cred_def_id}", + "/anoncreds/revocation/active-registry/{cred_def_id}", get_active_rev_reg, allow_head=False, ), web.post( - "/revocation/active-registry/{cred_def_id}/rotate", + "/anoncreds/revocation/active-registry/{cred_def_id}/rotate", rotate_rev_reg, ), web.get( - "/revocation/registry/{rev_reg_id}/issued", + "/anoncreds/revocation/registry/{rev_reg_id}/issued", get_rev_reg_issued_count, allow_head=False, ), web.get( - "/revocation/registry/{rev_reg_id}/issued/details", + "/anoncreds/revocation/registry/{rev_reg_id}/issued/details", get_rev_reg_issued, allow_head=False, ), web.get( - "/revocation/registry/{rev_reg_id}/issued/indy_recs", + "/anoncreds/revocation/registry/{rev_reg_id}/issued/indy_recs", get_rev_reg_indy_recs, allow_head=False, ), web.get( - "/revocation/registry/{rev_reg_id}/tails-file", + "/anoncreds/revocation/registry/{rev_reg_id}/tails-file", get_tails_file, allow_head=False, ), web.patch( - "/revocation/registry/{rev_reg_id}/set-state", + "/anoncreds/revocation/registry/{rev_reg_id}/set-state", set_rev_reg_state, ), web.put( - "/revocation/registry/{rev_reg_id}/fix-revocation-entry-state", + "/anoncreds/revocation/registry/{rev_reg_id}/fix-revocation-entry-state", update_rev_reg_revoked_state, ), ] @@ -1147,7 +1176,7 @@ def post_process_routes(app: web.Application): app._state["swagger_dict"]["tags"] = [] app._state["swagger_dict"]["tags"].append( { - "name": "revocation", + "name": TAG_TITLE, "description": "Revocation registry management", "externalDocs": { "description": "Overview", diff --git a/aries_cloudagent/revocation_anoncreds/tests/test_routes.py b/aries_cloudagent/revocation_anoncreds/tests/test_routes.py index 624313b919..2198e7668b 100644 --- a/aries_cloudagent/revocation_anoncreds/tests/test_routes.py +++ b/aries_cloudagent/revocation_anoncreds/tests/test_routes.py @@ -7,18 +7,20 @@ from aries_cloudagent.tests import mock +from ...admin.request_context import AdminRequestContext from ...anoncreds.models.anoncreds_revocation import ( RevRegDef, RevRegDefValue, ) from ...askar.profile import AskarProfile +from ...askar.profile_anon import AskarAnoncredsProfile from ...core.in_memory import InMemoryProfile from .. import routes as test_module class TestRevocationRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.profile = InMemoryProfile.test_profile(profile_class=AskarProfile) + self.profile = InMemoryProfile.test_profile(profile_class=AskarAnoncredsProfile) self.context = self.profile.context setattr(self.context, "profile", self.profile) self.request_dict = { @@ -37,7 +39,7 @@ def setUp(self): async def test_validate_cred_rev_rec_qs_and_revoke_req(self): for req in ( test_module.CredRevRecordQueryStringSchema(), - test_module.RevokeRequestSchema(), + test_module.RevokeRequestSchemaAnoncreds(), ): req.validate_fields( { @@ -182,7 +184,7 @@ async def test_rev_regs_created(self): ) as mock_json_response: mock_query.return_value = ["dummy"] - result = await test_module.rev_regs_created(self.request) + result = await test_module.get_rev_regs(self.request) mock_json_response.assert_called_once_with({"rev_reg_ids": ["dummy"]}) assert result is mock_json_response.return_value @@ -520,6 +522,65 @@ async def test_set_rev_reg_state_not_found(self): result = await test_module.set_rev_reg_state(self.request) mock_json_response.assert_not_called() + async def test_wrong_profile_403(self): + self.profile = InMemoryProfile.test_profile( + settings={"wallet.type": "askar"}, + profile_class=AskarProfile, + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + ) + + self.request.json = mock.CoroutineMock( + return_value={ + "rev_reg_id": "rr_id", + "cred_rev_id": "23", + "publish": "false", + } + ) + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.revoke(self.request) + + self.request.json = mock.CoroutineMock() + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.publish_revocations(self.request) + + REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( + self.test_did, self.test_did + ) + self.request.match_info = {"rev_reg_id": REV_REG_ID} + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.get_rev_reg(self.request) + + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.get_rev_reg_issued_count(self.request) + + CRED_REV_ID = "1" + self.request.query = { + "rev_reg_id": REV_REG_ID, + "cred_rev_id": CRED_REV_ID, + } + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.get_cred_rev_record(self.request) + + self.request.match_info = {"rev_reg_id": REV_REG_ID} + with self.assertRaises(test_module.web.HTTPForbidden): + result = await test_module.get_tails_file(self.request) + + self.request.query = { + "state": test_module.RevRegDefState.STATE_FINISHED, + } + with self.assertRaises(test_module.web.HTTPForbidden): + await test_module.set_rev_reg_state(self.request) + async def test_register(self): mock_app = mock.MagicMock() mock_app.add_routes = mock.MagicMock() diff --git a/aries_cloudagent/storage/type.py b/aries_cloudagent/storage/type.py new file mode 100644 index 0000000000..7a0cc9aab7 --- /dev/null +++ b/aries_cloudagent/storage/type.py @@ -0,0 +1,3 @@ +"""Library version information.""" + +RECORD_TYPE_ACAPY_STORAGE_TYPE = "acapy_storage_type" diff --git a/aries_cloudagent/utils/profiles.py b/aries_cloudagent/utils/profiles.py new file mode 100644 index 0000000000..45a440ed79 --- /dev/null +++ b/aries_cloudagent/utils/profiles.py @@ -0,0 +1,31 @@ +"""Profile utilities.""" + +from aiohttp import web + +from ..anoncreds.error_messages import ANONCREDS_PROFILE_REQUIRED_MSG +from ..askar.profile_anon import AskarAnoncredsProfile +from ..core.profile import Profile + + +def is_anoncreds_profile_raise_web_exception(profile: Profile) -> None: + """Raise a web exception when the supplied profile is anoncreds.""" + if isinstance(profile, AskarAnoncredsProfile): + raise web.HTTPForbidden( + reason="Interface not supported for an anoncreds profile" + ) + + +def is_not_anoncreds_profile_raise_web_exception(profile: Profile) -> None: + """Raise a web exception when the supplied profile is anoncreds.""" + if not isinstance(profile, AskarAnoncredsProfile): + raise web.HTTPForbidden(reason=ANONCREDS_PROFILE_REQUIRED_MSG) + + +def subwallet_type_not_same_as_base_wallet_raise_web_exception( + base_wallet_type: str, sub_wallet_type: str +) -> None: + """Raise a web exception when the subwallet type is not the same as the base wallet type.""" # noqa: E501 + if base_wallet_type != sub_wallet_type: + raise web.HTTPForbidden( + reason="Subwallet type must be the same as the base wallet type" + ) diff --git a/demo/features/0454-present-proof.feature b/demo/features/0454-present-proof.feature index 76e4816da4..7a76ec771a 100644 --- a/demo/features/0454-present-proof.feature +++ b/demo/features/0454-present-proof.feature @@ -74,7 +74,7 @@ Feature: RFC 0454 Aries agent present proof | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | | Faber | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | | Faber | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | - | Acme | --public-did --mediation --multitenant | --mediation --multitenant | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Acme | --public-did --mediation --multitenant --wallet-type askar-anoncreds | --mediation --multitenant --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @T001.2-RFC0454 diff --git a/demo/features/revocation-api.feature b/demo/features/revocation-api.feature index 0049c42da3..7145e2f71b 100644 --- a/demo/features/revocation-api.feature +++ b/demo/features/revocation-api.feature @@ -41,6 +41,8 @@ Feature: ACA-Py Revocation API | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | | Acme | --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | | Acme | --revocation --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Acme | --revocation --public-did --multitenant | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Acme | --revocation --public-did --multitenant --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @Revoc-api.x @GHA-Anoncreds-break Scenario Outline: Without endorser: issue, revoke credentials, manually create revocation registries @@ -69,8 +71,10 @@ Feature: ACA-Py Revocation API Then "Bob" can verify the credential from "" was revoked Examples: | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | - #| Acme | --revocation --public-did --did-exchange | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Acme | --revocation --public-did --did-exchange | --wallet-type askar | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | | Acme | --revocation --public-did --did-exchange --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Acme | --revocation --public-did --did-exchange --multitenant --wallet-type askar | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Acme | --revocation --public-did --did-exchange --multitenant --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @Revoc-api @GHA Scenario Outline: Using revocation api, rotate revocation diff --git a/demo/features/steps/0453-issue-credential.py b/demo/features/steps/0453-issue-credential.py index 5ccee12d5b..977a72e209 100644 --- a/demo/features/steps/0453-issue-credential.py +++ b/demo/features/steps/0453-issue-credential.py @@ -1,32 +1,29 @@ -from behave import given, when, then import json -from time import sleep -import time from bdd_support.agent_backchannel_client import ( - aries_container_create_schema_cred_def, - aries_container_check_exists_cred_def, - aries_container_issue_credential, - aries_container_receive_credential, - read_schema_data, - read_credential_data, agent_container_DELETE, agent_container_GET, agent_container_POST, + aries_container_check_exists_cred_def, + aries_container_create_schema_cred_def, + aries_container_issue_credential, + aries_container_receive_credential, async_sleep, + read_credential_data, + read_schema_data, ) -from runners.agent_container import AgentContainer +from behave import given, then, when from runners.support.agent import ( - CRED_FORMAT_INDY, - CRED_FORMAT_JSON_LD, - DID_METHOD_SOV, DID_METHOD_KEY, - KEY_TYPE_ED255, KEY_TYPE_BLS, SIG_TYPE_BLS, ) +def is_anoncreds(agent): + return agent["agent"].wallet_type == "askar-anoncreds" + + # This step is defined in another feature file # Given "Acme" and "Bob" have an existing connection @@ -174,9 +171,14 @@ def step_impl(context, holder): print("connection_id:", cred_exchange["cred_ex_record"]["connection_id"]) # revoke the credential - revoke_status = agent_container_POST( + if is_anoncreds(agent): + endpoint = "/anoncreds/revocation/revoke" + else: + endpoint = "/revocation/revoke" + + agent_container_POST( agent["agent"], - "/revocation/revoke", + endpoint, data={ "rev_reg_id": cred_exchange["indy"]["rev_reg_id"], "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], diff --git a/demo/features/steps/0586-sign-transaction.py b/demo/features/steps/0586-sign-transaction.py index 25a63e46c9..491e82744c 100644 --- a/demo/features/steps/0586-sign-transaction.py +++ b/demo/features/steps/0586-sign-transaction.py @@ -405,13 +405,18 @@ def step_impl(context, agent_name, schema_name): def step_impl(context, agent_name): agent = context.active_agents[agent_name] + if not is_anoncreds(agent): + endpoint = "/revocation/registries/created" + else: + endpoint = "/anoncreds/revocation/registries" + rev_regs = {"rev_reg_ids": []} i = 5 while 0 == len(rev_regs["rev_reg_ids"]) and i > 0: async_sleep(1.0) rev_regs = agent_container_GET( agent["agent"], - "/revocation/registries/created", + endpoint, params={ "cred_def_id": context.cred_def_id, }, @@ -483,6 +488,11 @@ def step_impl(context, agent_name): def step_impl(context, agent_name): agent = context.active_agents[agent_name] + if not is_anoncreds(agent): + endpoint = "/revocation/registry/" + else: + endpoint = "/anoncreds/revocation/registry/" + # a registry is promoted to active when its initial entry is sent i = 5 @@ -493,7 +503,7 @@ def step_impl(context, agent_name): if context.rev_reg_id is not None: reg_info = agent_container_GET( agent["agent"], - f"/revocation/registry/{context.rev_reg_id}", + f"{endpoint}{context.rev_reg_id}", ) state = reg_info["result"]["state"] if state in ["active", "finished"]: @@ -574,6 +584,11 @@ def step_impl(context, holder, schema_name, credential_data, issuer): def step_impl(context, agent_name): agent = context.active_agents[agent_name] + if not is_anoncreds(agent): + endpoint = "/revocation/revoke" + else: + endpoint = "/anoncreds/revocation/revoke" + # get the required revocation info from the last credential exchange cred_exchange = context.cred_exchange @@ -584,7 +599,7 @@ def step_impl(context, agent_name): agent_container_POST( agent["agent"], - "/revocation/revoke", + endpoint, data={ "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], "publish": False, @@ -627,7 +642,7 @@ def step_impl(context, agent_name): "conn_id": connection_id, "create_transaction_for_endorser": "true", } - + endpoint = "/revocation/revoke" else: data = { "cred_rev_id": cred_exchange["indy"]["cred_rev_id"], @@ -640,10 +655,11 @@ def step_impl(context, agent_name): }, } params = {} + endpoint = "/anoncreds/revocation/revoke" agent_container_POST( agent["agent"], - "/revocation/revoke", + endpoint, data=data, params=params, ) @@ -658,10 +674,15 @@ def step_impl(context, agent_name): def step_impl(context, agent_name): agent = context.active_agents[agent_name] + if not is_anoncreds(agent): + endpoint = "/revocation/publish-revocations" + else: + endpoint = "/anoncreds/revocation/publish-revocations" + # create rev_reg entry transaction created_rev_reg = agent_container_POST( agent["agent"], - "/revocation/publish-revocations", + endpoint, data={ "rrid2crid": { context.cred_exchange["indy"]["rev_reg_id"]: [ @@ -699,6 +720,7 @@ def step_impl(context, agent_name): "conn_id": connection_id, "create_transaction_for_endorser": "true", } + endpoint = "/revocation/publish-revocations" else: data = { "rrid2crid": { @@ -712,10 +734,11 @@ def step_impl(context, agent_name): }, } params = {} + endpoint = "/anoncreds/revocation/publish-revocations" agent_container_POST( agent["agent"], - "/revocation/publish-revocations", + endpoint, data=data, params=params, ) @@ -764,16 +787,34 @@ def step_impl(context, agent_name, schema_name): schema_info = read_schema_data(schema_name) connection_id = agent["agent"].agent.connection_id - created_txn = agent_container_POST( - agent["agent"], - "/schemas", - data=schema_info["schema"], - params={"conn_id": connection_id, "create_transaction_for_endorser": "false"}, - ) + if not is_anoncreds(agent): + schema_id = agent_container_POST( + agent["agent"], + "/schemas", + data=schema_info["schema"], + params={ + "conn_id": connection_id, + "create_transaction_for_endorser": "false", + }, + )["schema_id"] + else: + schema_id = agent_container_POST( + agent["agent"], + "/anoncreds/schema", + data={ + "schema": { + "name": schema_info["schema"]["schema_name"], + "version": schema_info["schema"]["schema_version"], + "attrNames": schema_info["schema"]["attributes"], + "issuerId": agent["agent"].agent.did, + }, + "options": {}, + }, + )["schema_state"]["schema_id"] # assert goodness - assert created_txn["schema_id"] - context.schema_id = created_txn["schema_id"] + assert schema_id + context.schema_id = schema_id @given( @@ -784,26 +825,48 @@ def step_impl(context, agent_name, schema_name): connection_id = agent["agent"].agent.connection_id - # TODO for now assume there is a single schema; should find the schema based on the supplied name - schemas = agent_container_GET(agent["agent"], "/schemas/created") - assert len(schemas["schema_ids"]) == 1 + if not is_anoncreds(agent): + # TODO for now assume there is a single schema; should find the schema based on the supplied name + schemas = agent_container_GET(agent["agent"], "/schemas/created") + assert len(schemas["schema_ids"]) == 1 - schema_id = schemas["schema_ids"][0] - created_txn = agent_container_POST( - agent["agent"], - "/credential-definitions", - data={ - "schema_id": schema_id, - "tag": "test_cred_def_with_endorsement", - "support_revocation": True, - "revocation_registry_size": 1000, - }, - params={"conn_id": connection_id, "create_transaction_for_endorser": "false"}, - ) + credential_definition_id = agent_container_POST( + agent["agent"], + "/credential-definitions", + data={ + "schema_id": schemas["schema_ids"][0], + "tag": "test_cred_def_with_endorsement", + "support_revocation": True, + "revocation_registry_size": 1000, + }, + params={ + "conn_id": connection_id, + "create_transaction_for_endorser": "false", + }, + ) + else: + schemas = agent_container_GET(agent["agent"], "/anoncreds/schemas") + assert len(schemas["schema_ids"]) == 1 + + credential_definition_id = agent_container_POST( + agent["agent"], + "/anoncreds/credential-definition", + data={ + "credential_definition": { + "schemaId": schemas["schema_ids"][0], + "issuerId": agent["agent"].agent.did, + "tag": "test_cred_def_with_endorsement", + }, + "options": { + "support_revocation": True, + "revocation_registry_size": 1000, + }, + }, + )["credential_definition_state"]["credential_definition_id"] # assert goodness - assert created_txn["credential_definition_id"] - context.cred_def_id = created_txn["credential_definition_id"] + assert credential_definition_id + context.cred_def_id = credential_definition_id @given( @@ -814,37 +877,60 @@ def step_impl(context, agent_name, schema_name): connection_id = agent["agent"].agent.connection_id - # generate revocation registry transaction - rev_reg = agent_container_POST( - agent["agent"], - "/revocation/create-registry", - data={"credential_definition_id": context.cred_def_id, "max_cred_num": 1000}, - params={}, - ) - rev_reg_id = rev_reg["result"]["revoc_reg_id"] - assert rev_reg_id is not None + if not is_anoncreds(agent): + # generate revocation registry transaction + rev_reg = agent_container_POST( + agent["agent"], + "/revocation/create-registry", + data={ + "credential_definition_id": context.cred_def_id, + "max_cred_num": 1000, + }, + params={}, + ) + rev_reg_id = rev_reg["result"]["revoc_reg_id"] + assert rev_reg_id is not None - # update revocation registry - agent_container_PATCH( - agent["agent"], - f"/revocation/registry/{rev_reg_id}", - data={ - "tails_public_uri": f"http://host.docker.internal:6543/revocation/registry/{rev_reg_id}/tails-file" - }, - params={}, - ) + # update revocation registry + agent_container_PATCH( + agent["agent"], + f"/revocation/registry/{rev_reg_id}", + data={ + "tails_public_uri": f"http://host.docker.internal:6543/revocation/registry/{rev_reg_id}/tails-file" + }, + params={}, + ) + + # create rev_reg def + created_txn = agent_container_POST( + agent["agent"], + f"/revocation/registry/{rev_reg_id}/definition", + data={}, + params={ + "conn_id": connection_id, + "create_transaction_for_endorser": "false", + }, + ) + assert created_txn + + else: + # generate revocation registry transaction + rev_reg_id = agent_container_POST( + agent["agent"], + "/anoncreds/revocation-registry-definition", + data={ + "revocation_registry_definition": { + "credDefId": context.cred_def_id, + "issuerId": agent["agent"].agent.did, + "maxCredNum": 1000, + "tag": "default", + }, + "options": {}, + }, + params={}, + )["revocation_registry_definition_state"]["revocation_registry_definition_id"] + assert rev_reg_id is not None - # create rev_reg def - created_txn = agent_container_POST( - agent["agent"], - f"/revocation/registry/{rev_reg_id}/definition", - data={}, - params={ - "conn_id": connection_id, - "create_transaction_for_endorser": "false", - }, - ) - assert created_txn context.rev_reg_id = rev_reg_id @@ -854,13 +940,18 @@ def step_impl(context, agent_name, schema_name): def step_impl(context, agent_name): agent = context.active_agents[agent_name] + if not is_anoncreds(agent): + endpoint = "/revocation/registries/created" + else: + endpoint = "/anoncreds/revocation/registries" + rev_regs = {"rev_reg_ids": []} i = 5 while 0 == len(rev_regs["rev_reg_ids"]) and i > 0: async_sleep(1.0) rev_regs = agent_container_GET( agent["agent"], - "/revocation/registries/created", + endpoint, params={ "cred_def_id": context.cred_def_id, }, diff --git a/demo/features/steps/revocation-api.py b/demo/features/steps/revocation-api.py index d7cfc271a8..b1af0a2811 100644 --- a/demo/features/steps/revocation-api.py +++ b/demo/features/steps/revocation-api.py @@ -1,4 +1,3 @@ -from behave import given, when, then import json import os @@ -7,7 +6,11 @@ agent_container_POST, async_sleep, ) -from runners.agent_container import AgentContainer +from behave import given, then + + +def is_anoncreds(agent): + return agent["agent"].wallet_type == "askar-anoncreds" BDD_EXTRA_AGENT_ARGS = os.getenv("BDD_EXTRA_AGENT_ARGS") @@ -28,20 +31,24 @@ def step_impl(context, count=None): @then('"{issuer}" lists revocation registries {count}') def step_impl(context, issuer, count=None): agent = context.active_agents[issuer] + + if not is_anoncreds(agent): + endpoint = "/revocation/registries/created" + else: + endpoint = "/anoncreds/revocation/registries" + async_sleep(5.0) - created_response = agent_container_GET( - agent["agent"], f"/revocation/registries/created" - ) + created_response = agent_container_GET(agent["agent"], endpoint) full_response = agent_container_GET( - agent["agent"], f"/revocation/registries/created", params={"state": "full"} + agent["agent"], endpoint, params={"state": "full"} ) decommissioned_response = agent_container_GET( agent["agent"], - f"/revocation/registries/created", + endpoint, params={"state": "decommissioned"}, ) finished_response = agent_container_GET( - agent["agent"], f"/revocation/registries/created", params={"state": "finished"} + agent["agent"], endpoint, params={"state": "finished"} ) async_sleep(4.0) if count: @@ -63,22 +70,26 @@ def step_impl(context, issuer, count=None): @then('"{issuer}" rotates revocation registries') def step_impl(context, issuer): agent = context.active_agents[issuer] + + if not is_anoncreds(agent): + endpoint = "/revocation/active-registry/" + else: + endpoint = "/anoncreds/revocation/active-registry/" + cred_def_id = context.cred_def_id original_active_response = agent_container_GET( - agent["agent"], f"/revocation/active-registry/{cred_def_id}" + agent["agent"], f"{endpoint}{cred_def_id}" ) print("original_active_response:", json.dumps(original_active_response)) rotate_response = agent_container_POST( agent["agent"], - f"/revocation/active-registry/{cred_def_id}/rotate", + f"{endpoint}{cred_def_id}/rotate", data={}, ) print("rotate_response:", json.dumps(rotate_response)) async_sleep(10.0) - active_response = agent_container_GET( - agent["agent"], f"/revocation/active-registry/{cred_def_id}" - ) + active_response = agent_container_GET(agent["agent"], f"{endpoint}{cred_def_id}") print("active_response:", json.dumps(active_response)) diff --git a/demo/runners/faber.py b/demo/runners/faber.py index 50b42694dd..8a8d83e687 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -1,10 +1,10 @@ import asyncio +import datetime import json import logging import os import sys import time -import datetime from aiohttp import ClientError from qrcode import QRCode @@ -12,9 +12,9 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from runners.agent_container import ( # noqa:E402 + AriesAgent, arg_parser, create_agent_with_args, - AriesAgent, ) from runners.support.agent import ( # noqa:E402 CRED_FORMAT_INDY, @@ -28,7 +28,6 @@ prompt_loop, ) - CRED_PREVIEW_TYPE = "https://didcomm.org/issue-credential/2.0/credential-preview" SELF_ATTESTED = os.getenv("SELF_ATTESTED") TAILS_FILE_COUNT = int(os.getenv("TAILS_FILE_COUNT", 100)) @@ -739,9 +738,20 @@ async def main(args): publish = ( await prompt("Publish now? [Y/N]: ", default="N") ).strip() in "yY" + + # Anoncreds has different endpoints for revocation + is_anoncreds = False + if faber_agent.agent.__dict__["wallet_type"] == "askar-anoncreds": + is_anoncreds = True + try: + endpoint = ( + "/anoncreds/revocation/revoke" + if is_anoncreds + else "/revocation/revoke" + ) await faber_agent.agent.admin_POST( - "/revocation/revoke", + endpoint, { "rev_reg_id": rev_reg_id, "cred_rev_id": cred_rev_id, @@ -757,58 +767,80 @@ async def main(args): elif option == "6" and faber_agent.revocation: try: - resp = await faber_agent.agent.admin_POST( - "/revocation/publish-revocations", {} + endpoint = ( + "/anoncreds/revocation/publish-revocations" + if is_anoncreds + else "/revocation/publish-revocations" ) + resp = await faber_agent.agent.admin_POST(endpoint, {}) faber_agent.agent.log( "Published revocations for {} revocation registr{} {}".format( len(resp["rrid2crid"]), "y" if len(resp["rrid2crid"]) == 1 else "ies", - json.dumps([k for k in resp["rrid2crid"]], indent=4), + json.dumps(list(resp["rrid2crid"]), indent=4), ) ) except ClientError: pass elif option == "7" and faber_agent.revocation: try: + endpoint = ( + f"/anoncreds/revocation/active-registry/{faber_agent.cred_def_id}/rotate" + if is_anoncreds + else f"/revocation/active-registry/{faber_agent.cred_def_id}/rotate" + ) resp = await faber_agent.agent.admin_POST( - f"/revocation/active-registry/{faber_agent.cred_def_id}/rotate", + endpoint, {}, ) faber_agent.agent.log( "Rotated registries for {}. Decommissioned Registries: {}".format( faber_agent.cred_def_id, - json.dumps([r for r in resp["rev_reg_ids"]], indent=4), + json.dumps(list(resp["rev_reg_ids"]), indent=4), ) ) except ClientError: pass elif option == "8" and faber_agent.revocation: - states = [ - "init", - "generated", - "posted", - "active", - "full", - "decommissioned", - ] + if is_anoncreds: + endpoint = "/anoncreds/revocation/registries" + states = [ + "finished", + "failed", + "action", + "wait", + "decommissioned", + "full", + ] + default_state = "finished" + else: + endpoint = "/revocation/registries/created" + states = [ + "init", + "generated", + "posted", + "active", + "full", + "decommissioned", + ] + default_state = "active" state = ( await prompt( f"Filter by state: {states}: ", - default="active", + default=default_state, ) ).strip() if state not in states: state = "active" try: resp = await faber_agent.agent.admin_GET( - "/revocation/registries/created", + endpoint, params={"state": state}, ) faber_agent.agent.log( "Registries (state = '{}'): {}".format( state, - json.dumps([r for r in resp["rev_reg_ids"]], indent=4), + json.dumps(list(resp["rev_reg_ids"]), indent=4), ) ) except ClientError: