From 31f14c9a201fb52b2602a409b334fe30fdf6ed5c Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 5 Feb 2021 11:29:07 -0500 Subject: [PATCH 01/59] feat: begin to define new notification messages Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/connections.py | 1 + 1 file changed, 1 insertion(+) diff --git a/acapy_plugin_toolbox/connections.py b/acapy_plugin_toolbox/connections.py index 6a2e550c..43addaba 100644 --- a/acapy_plugin_toolbox/connections.py +++ b/acapy_plugin_toolbox/connections.py @@ -35,6 +35,7 @@ DELETE = '{}/delete'.format(PROTOCOL) DELETED = '{}/deleted'.format(PROTOCOL) RECEIVE_INVITATION = '{}/receive-invitation'.format(PROTOCOL) +CONNECTED = '{}/connected'.format(PROTOCOL) # Message Type string to Message Class map MESSAGE_TYPES = { From de13d9d1bdfeaec70514b46f0a21d574e15c260b Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 11 Feb 2021 17:12:52 -0500 Subject: [PATCH 02/59] feat: notify on connection completed Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/basicmessage.py | 44 ++++---------------- acapy_plugin_toolbox/connections.py | 61 ++++++++++++++++++++++------ acapy_plugin_toolbox/util.py | 50 +++++++++++++++++++++++ 3 files changed, 106 insertions(+), 49 deletions(-) diff --git a/acapy_plugin_toolbox/basicmessage.py b/acapy_plugin_toolbox/basicmessage.py index c0e7f310..5f330c01 100644 --- a/acapy_plugin_toolbox/basicmessage.py +++ b/acapy_plugin_toolbox/basicmessage.py @@ -29,7 +29,8 @@ from marshmallow import fields from .util import ( - admin_only, datetime_from_iso, generate_model_schema, timestamp_utc_iso + admin_only, datetime_from_iso, generate_model_schema, send_to_admins, + timestamp_utc_iso ) PROTOCOL_URI = "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/basicmessage/1.0" @@ -240,46 +241,17 @@ async def handle(self, context: RequestContext, responder: BaseResponder): }, ) - session = await context.session() - connection_mgr = ConnectionManager(session) - storage = session.inject(BaseStorage) - admin_ids = map( - lambda record: record.tags['connection_id'], - filter( - lambda record: json.loads(record.value) == 'admin', - await storage.find_all_records( - ConnRecord.RECORD_TYPE_METADATA, {'key': 'group'} - ) - ) - ) - admins = [ - await ConnRecord.retrieve_by_id(session, id) - for id in admin_ids - ] - - if not admins: - return - - admins = filter(lambda admin: admin.state == 'active', admins) - admin_verkeys = [ - target.recipient_keys[0] - for admin in admins - for target in await connection_mgr.get_connection_targets( - connection=admin - ) - ] - notification = New( connection_id=context.connection_record.connection_id, message=msg ) - for verkey in admin_verkeys: - await responder.send( - notification, - reply_to_verkey=verkey, - to_session_only=True - ) + await send_to_admins( + session, + notification, + responder, + to_session_only=True + ) Get, GetSchema = generate_model_schema( diff --git a/acapy_plugin_toolbox/connections.py b/acapy_plugin_toolbox/connections.py index 43addaba..a584e175 100644 --- a/acapy_plugin_toolbox/connections.py +++ b/acapy_plugin_toolbox/connections.py @@ -3,24 +3,29 @@ # pylint: disable=invalid-name # pylint: disable=too-few-public-methods -from typing import Dict, Any +import re +from typing import Any, Dict -from marshmallow import Schema, fields, validate - -from aries_cloudagent.core.profile import ProfileSession +from aries_cloudagent.connections.models.conn_record import ConnRecord +from aries_cloudagent.core.profile import InjectionContext from aries_cloudagent.core.protocol_registry import ProtocolRegistry -from aries_cloudagent.messaging.base_handler import BaseHandler, BaseResponder, RequestContext -from aries_cloudagent.protocols.connections.v1_0.manager import ConnectionManager -from aries_cloudagent.connections.models.conn_record import ( - ConnRecord +from aries_cloudagent.core.event_bus import Event, EventBus, EventContext +from aries_cloudagent.messaging.base_handler import ( + BaseHandler, BaseResponder, RequestContext +) +from aries_cloudagent.protocols.connections.v1_0.manager import ( + ConnectionManager ) from aries_cloudagent.protocols.connections.v1_0.messages.connection_invitation import ( - ConnectionInvitation, + ConnectionInvitation +) +from aries_cloudagent.protocols.problem_report.v1_0.message import ( + ProblemReport ) -from aries_cloudagent.protocols.problem_report.v1_0.message import ProblemReport from aries_cloudagent.storage.error import StorageNotFoundError +from marshmallow import Schema, fields, validate -from .util import generate_model_schema, admin_only +from .util import admin_only, generate_model_schema, send_to_admins PROTOCOL = ( 'https://github.com/hyperledger/aries-toolbox/' @@ -47,20 +52,41 @@ DELETED: 'acapy_plugin_toolbox.connections.Deleted', RECEIVE_INVITATION: 'acapy_plugin_toolbox.connections.' 'ReceiveInvitation', + CONNECTED: 'acapy_plugin_toolbox.connections.Connected', } +EVENT_PATTERN = re.compile(ConnRecord.WEBHOOK_TOPIC + ".*") + async def setup( - session: ProfileSession, + context: InjectionContext, protocol_registry: ProtocolRegistry = None ): """Setup the connections plugin.""" if not protocol_registry: - protocol_registry = session.inject(ProtocolRegistry) + protocol_registry = context.inject(ProtocolRegistry) protocol_registry.register_message_types( MESSAGE_TYPES ) + event_bus = context.inject(EventBus) + event_bus.subscribe(EVENT_PATTERN, connections_event_handler) + + +async def connections_event_handler(context: EventContext, event: Event): + """Handle connection events. + + Send connected message to admins when connections reach active state. + """ + record: ConnRecord = ConnRecord.deserialize(event.payload) + if record.state == ConnRecord.State.RESPONSE: + responder = context.inject(BaseResponder) + async with context.session() as session: + await send_to_admins( + session, + Connected(**conn_record_to_message_repr(record)), + responder, + ) BaseConnectionSchema = Schema.from_dict({ @@ -79,6 +105,7 @@ async def setup( 'raw_repr': fields.Dict(required=False) }) + Connection, ConnectionSchema = generate_model_schema( name='Connection', handler='acapy_plugin_toolbox.util.PassHandler', @@ -87,6 +114,14 @@ async def setup( ) +Connected, ConnectedSchema = generate_model_schema( + name='Connected', + handler='acapy_plugin_toolbox.util.PassHandler', + msg_type=CONNECTED, + schema=BaseConnectionSchema +) + + def conn_record_to_message_repr(conn: ConnRecord) -> Dict[str, Any]: """Map ConnRecord onto Connection.""" def _state_map(state: str) -> str: diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index 3f07e134..c843eeec 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -5,9 +5,14 @@ import sys import logging import functools +import json from datetime import datetime, timezone from dateutil.parser import isoparse +from aries_cloudagent.connections.models.conn_record import ConnRecord +from aries_cloudagent.protocols.connections.v1_0.manager import ConnectionManager +from aries_cloudagent.storage.base import BaseStorage +from aries_cloudagent.core.profile import ProfileSession from aries_cloudagent.messaging.agent_message import ( AgentMessage, AgentMessageSchema ) @@ -174,3 +179,48 @@ async def handle(self, context: RequestContext, _responder): "Pass: Not handling message of type %s", context.message._type ) + + +async def admin_connections(session: ProfileSession): + """Return admin connections.""" + storage = session.inject(BaseStorage) + admin_ids = map( + lambda record: record.tags['connection_id'], + filter( + lambda record: json.loads(record.value) == 'admin', + await storage.find_all_records( + ConnRecord.RECORD_TYPE_METADATA, {'key': 'group'} + ) + ) + ) + admins = [ + await ConnRecord.retrieve_by_id(session, id) + for id in admin_ids + ] + return admins + + +async def send_to_admins( + session: ProfileSession, + message: AgentMessage, + responder: BaseResponder, + to_session_only: bool = False +): + """Send a message to all admin connections.""" + admins = await admin_connections(session) + admins = list(filter(lambda admin: admin.state == 'active', admins)) + connection_mgr = ConnectionManager(session) + admin_verkeys = [ + target.recipient_keys[0] + for admin in admins + for target in await connection_mgr.get_connection_targets( + connection=admin + ) + ] + + for verkey in admin_verkeys: + await responder.send( + message, + reply_to_verkey=verkey, + to_session_only=to_session_only + ) From c7a34b471dd31a467fc15b50e86cb49ff9ae027e Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 16 Feb 2021 09:01:28 -0500 Subject: [PATCH 03/59] feat: add decorator for creating message classes Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/util.py | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index c843eeec..7de9dad9 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -83,6 +83,54 @@ def admin_only(func): return require_role('admin')(func) +def expand_message_class(cls): + """Class decorator for removing boilerplate of AgentMessages.""" + if not hasattr(cls, "message_type"): + raise ValueError( + "Expected value message_type not found on class {}" + .format(cls.__name__) + ) + if not hasattr(cls, "handler"): + raise ValueError( + "Expected value handler value not found on class {}" + .format(cls.__name__) + ) + if not hasattr(cls, "Fields") and not hasattr(cls, "fields_from"): + raise ValueError( + "Class {} must have nested class Fields or schema defining expected fields" + .format(cls.__name__) + ) + + cls.Meta = type(cls.__name__ + ".Meta", (), { + "__module__": cls.__module__, + "handler_class": cls.handler, + "message_type": cls.message_type, + "schema_class": cls.__name__ + ".Schema" + }) + + fields = {} + if hasattr(cls, "Fields"): + fields.update({var: getattr(cls.Fields, var) for var in vars(cls.Fields)}) + if hasattr(cls, "fields_from"): + fields.update(cls.fields_from._declared_fields) + + cls.Schema = type(cls.__name__ + ".Schema", (AgentMessageSchema,), { + "__module__": cls.__module__, + **fields + }) + cls.Schema.Meta = type(cls.Schema.__name__ + ".Meta", (), { + "__module__": cls.__module__, + "model_class": cls + }) + cls._get_schema_class = lambda: cls.Schema + + if hasattr(cls, "protocol") and cls.protocol: + cls.Meta.message_type = "{}/{}".format(cls.protocol, cls.message_type) + cls._type = property(fget=lambda self: self.Meta.message_type) + + return cls + + def generic_init(instance, **kwargs): """Initialize from kwargs into slots.""" for slot in instance.__slots__: From 9d85c191db535c9ce69921f609620e0d4138f42b Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 16 Feb 2021 09:15:28 -0500 Subject: [PATCH 04/59] chore: prepare for new holder protocol Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/group/all.py | 2 +- acapy_plugin_toolbox/group/holder.py | 2 +- acapy_plugin_toolbox/holder/__init__.py | 1 + .../{holder.py => holder/v0_1.py} | 67 ++++++++++--------- 4 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 acapy_plugin_toolbox/holder/__init__.py rename acapy_plugin_toolbox/{holder.py => holder/v0_1.py} (91%) diff --git a/acapy_plugin_toolbox/group/all.py b/acapy_plugin_toolbox/group/all.py index cece6344..3de02761 100644 --- a/acapy_plugin_toolbox/group/all.py +++ b/acapy_plugin_toolbox/group/all.py @@ -8,7 +8,7 @@ from ..schemas import setup as schema_setup from ..dids import setup as did_setup from ..static_connections import setup as static_conn_setup -from ..holder import setup as holder_setup +from ..holder.v0_1 import setup as holder_setup from ..issuer import setup as issuer_setup from ..basicmessage import setup as basic_message_setup from ..taa import setup as taa_setup diff --git a/acapy_plugin_toolbox/group/holder.py b/acapy_plugin_toolbox/group/holder.py index 506b82ec..d91dfd21 100644 --- a/acapy_plugin_toolbox/group/holder.py +++ b/acapy_plugin_toolbox/group/holder.py @@ -4,7 +4,7 @@ from aries_cloudagent.core.protocol_registry import ProtocolRegistry from ..credential_definitions import setup as cred_def_setup -from ..holder import setup as holder_setup +from ..holder.v0_1 import setup as holder_setup async def setup(session: ProfileSession): diff --git a/acapy_plugin_toolbox/holder/__init__.py b/acapy_plugin_toolbox/holder/__init__.py new file mode 100644 index 00000000..265113d9 --- /dev/null +++ b/acapy_plugin_toolbox/holder/__init__.py @@ -0,0 +1 @@ +"""Holder admin protocol.""" diff --git a/acapy_plugin_toolbox/holder.py b/acapy_plugin_toolbox/holder/v0_1.py similarity index 91% rename from acapy_plugin_toolbox/holder.py rename to acapy_plugin_toolbox/holder/v0_1.py index 1c03928d..69a331bd 100644 --- a/acapy_plugin_toolbox/holder.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -3,44 +3,47 @@ # pylint: disable=invalid-name # pylint: disable=too-few-public-methods -from marshmallow import fields import json +from aries_cloudagent.connections.models.conn_record import ConnRecord from aries_cloudagent.core.profile import ProfileSession from aries_cloudagent.core.protocol_registry import ProtocolRegistry from aries_cloudagent.indy.holder import IndyHolder -from aries_cloudagent.messaging.base_handler import BaseHandler, BaseResponder, RequestContext -from aries_cloudagent.protocols.issue_credential.v1_0.routes import ( - V10CredentialExchangeListResultSchema, - V10CredentialProposalRequestMandSchema +from aries_cloudagent.messaging.base_handler import ( + BaseHandler, BaseResponder, RequestContext +) +from aries_cloudagent.protocols.issue_credential.v1_0.manager import ( + CredentialManager +) +from aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_proposal import ( + CredentialProposal ) from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( - V10CredentialExchange, V10CredentialExchangeSchema ) -from aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_proposal import ( - CredentialProposal, +from aries_cloudagent.protocols.issue_credential.v1_0.routes import ( + V10CredentialProposalRequestMandSchema ) -from aries_cloudagent.protocols.issue_credential.v1_0.manager import CredentialManager - -from aries_cloudagent.protocols.present_proof.v1_0.routes import ( - V10PresentationExchangeListSchema, - V10PresentationProposalRequestSchema +from aries_cloudagent.protocols.present_proof.v1_0.manager import ( + PresentationManager +) +from aries_cloudagent.protocols.present_proof.v1_0.messages.presentation_proposal import ( + PresentationProposal ) from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( - V10PresentationExchange, - V10PresentationExchangeSchema + V10PresentationExchange, V10PresentationExchangeSchema ) -from aries_cloudagent.protocols.present_proof.v1_0.messages.presentation_proposal import ( - PresentationProposal, +from aries_cloudagent.protocols.present_proof.v1_0.routes import ( + V10PresentationExchangeListSchema, V10PresentationProposalRequestSchema +) +from aries_cloudagent.protocols.problem_report.v1_0.message import ( + ProblemReport ) -from aries_cloudagent.protocols.present_proof.v1_0.manager import PresentationManager - -from aries_cloudagent.connections.models.conn_record import ConnRecord from aries_cloudagent.storage.error import StorageNotFoundError -from aries_cloudagent.protocols.problem_report.v1_0.message import ProblemReport +from marshmallow import fields + +from ..util import admin_only, generate_model_schema -from .util import generate_model_schema, admin_only PROTOCOL = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/admin-holder/0.1' SEND_CRED_PROPOSAL = '{}/send-credential-proposal'.format(PROTOCOL) @@ -54,17 +57,17 @@ MESSAGE_TYPES = { SEND_CRED_PROPOSAL: - 'acapy_plugin_toolbox.holder.SendCredProposal', + 'acapy_plugin_toolbox.holder.v0_1.SendCredProposal', SEND_PRES_PROPOSAL: - 'acapy_plugin_toolbox.holder.SendPresProposal', + 'acapy_plugin_toolbox.holder.v0_1.SendPresProposal', CREDENTIALS_GET_LIST: - 'acapy_plugin_toolbox.holder.CredGetList', + 'acapy_plugin_toolbox.holder.v0_1.CredGetList', CREDENTIALS_LIST: - 'acapy_plugin_toolbox.holder.CredList', + 'acapy_plugin_toolbox.holder.v0_1.CredList', PRESENTATIONS_GET_LIST: - 'acapy_plugin_toolbox.holder.PresGetList', + 'acapy_plugin_toolbox.holder.v0_1.PresGetList', PRESENTATIONS_LIST: - 'acapy_plugin_toolbox.holder.PresList', + 'acapy_plugin_toolbox.holder.v0_1.PresList', } @@ -82,7 +85,7 @@ async def setup( SendCredProposal, SendCredProposalSchema = generate_model_schema( name='SendCredProposal', - handler='acapy_plugin_toolbox.holder.SendCredProposalHandler', + handler='acapy_plugin_toolbox.holder.v0_1.SendCredProposalHandler', msg_type=SEND_CRED_PROPOSAL, schema=V10CredentialProposalRequestMandSchema ) @@ -153,7 +156,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): SendPresProposal, SendPresProposalSchema = generate_model_schema( name='SendPresProposal', - handler='acapy_plugin_toolbox.holder.SendPresProposalHandler', + handler='acapy_plugin_toolbox.holder.v0_1.SendPresProposalHandler', msg_type=SEND_PRES_PROPOSAL, schema=V10PresentationProposalRequestSchema ) @@ -227,7 +230,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): CredGetList, CredGetListSchema = generate_model_schema( name='CredGetList', - handler='acapy_plugin_toolbox.holder.CredGetListHandler', + handler='acapy_plugin_toolbox.holder.v0_1.CredGetListHandler', msg_type=CREDENTIALS_GET_LIST, schema={ 'connection_id': fields.Str(required=False), @@ -290,7 +293,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): PresGetList, PresGetListSchema = generate_model_schema( name='PresGetList', - handler='acapy_plugin_toolbox.holder.PresGetListHandler', + handler='acapy_plugin_toolbox.holder.v0_1.PresGetListHandler', msg_type=PRESENTATIONS_GET_LIST, schema={ 'connection_id': fields.Str(required=False), From 31963be7ddd6685b2956f8a05281c779c77177ab Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 16 Feb 2021 16:25:17 -0500 Subject: [PATCH 05/59] feat: add pagination decorators Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/decorators/__init__.py | 0 acapy_plugin_toolbox/decorators/pagination.py | 72 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 acapy_plugin_toolbox/decorators/__init__.py create mode 100644 acapy_plugin_toolbox/decorators/pagination.py diff --git a/acapy_plugin_toolbox/decorators/__init__.py b/acapy_plugin_toolbox/decorators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/acapy_plugin_toolbox/decorators/pagination.py b/acapy_plugin_toolbox/decorators/pagination.py new file mode 100644 index 00000000..b4493b14 --- /dev/null +++ b/acapy_plugin_toolbox/decorators/pagination.py @@ -0,0 +1,72 @@ +"""Decorators for Pagination.""" + +from typing import Any, Sequence, Tuple, Iterator +from functools import singledispatchmethod + +from aries_cloudagent.messaging.models.base import BaseModel +from marshmallow import fields + +from ..util import expand_model_class + + +@expand_model_class +class Page(BaseModel): + """Page decorator for messages containing a paginated object.""" + + class Fields: + """Fields of page decorator.""" + count = fields.Int(required=True) + offset = fields.Int(required=True) + remaining = fields.Int(required=False) + + def __init__( + self, count: int = 0, offset: int = 0, remaining: int = None, **kwargs + ): + super().__init__(**kwargs) + self.count = count + self.offset = offset + self.remaining = remaining + + +@expand_model_class +class Paginate(BaseModel): + """Paginate decorator for messages querying for a paginated object.""" + + class Fields: + """Fields of paginate decorator.""" + limit = fields.Int(required=True) + offset = fields.Int(required=True, missing=0) + + def __init__(self, limit: int = 0, offset: int = 0, **kwargs): + super().__init__(**kwargs) + self.limit = limit + self.offset = offset + + @singledispatchmethod + def apply(self, items) -> Tuple[Sequence[Any], Page]: + """Apply pagination, returning paginated results and Page decorator.""" + raise NotImplementedError( + "Paginate.apply is not implemented for type {}".format(type(items)) + ) + + @apply.register + def apply(self, items: list) -> Tuple[Sequence[Any], Page]: + """Apply pagination to list.""" + end = self.offset + self.limit + result = items[self.offset:end] + remaining = len(items[end:]) + page = Page(len(result), self.offset, remaining) + return result, page + + @apply.register + def apply(self, items: Iterator) -> Tuple[Sequence[Any], Page]: + """Apply pagination to iterator.""" + for _, _ in zip(range(self.offset), items): + pass + + result = [] + for _idx, item in zip(range(self.offset, self.offset + self.limit), items): + result.append(item) + + page = Page(len(result), self.offset) + return result, page From 4263a539e43e76ad66c23521a2ce73f07934294d Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 16 Feb 2021 16:26:05 -0500 Subject: [PATCH 06/59] feat: add expand_model_class decorator Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/util.py | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index 7de9dad9..4641bd9b 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -19,6 +19,7 @@ from aries_cloudagent.messaging.base_handler import ( BaseHandler, BaseResponder, RequestContext ) +from aries_cloudagent.messaging.models.base import BaseModel, BaseModelSchema from aries_cloudagent.protocols.problem_report.v1_0.message import ( ProblemReport ) @@ -131,6 +132,46 @@ def expand_message_class(cls): return cls +def expand_model_class(cls): + """Class decorator for removing boilerplate from BaseModels.""" + if not hasattr(cls, "Fields") and not hasattr(cls, "fields_from"): + raise ValueError( + "Class {} must have nested class Fields or schema defining expected fields" + .format(cls.__name__) + ) + + if hasattr(cls, "Meta") and cls.Meta != BaseModel.Meta: + cls.Meta.schema_class = cls.__name__ + ".Schema" + else: + cls.Meta = type(cls.__name__ + ".Meta", (), { + "__module__": cls.__module__, + "schema_class": cls.__name__ + ".Schema" + }) + + fields = {} + if hasattr(cls, "Fields"): + fields.update({var: getattr(cls.Fields, var) for var in vars(cls.Fields)}) + if hasattr(cls, "fields_from"): + fields.update(cls.fields_from._declared_fields) + + cls.Schema = type(cls.__name__ + ".Schema", (BaseModelSchema,), { + "__module__": cls.__module__, + **fields + }) + cls.Schema.Meta = type(cls.Schema.__name__ + ".Meta", (), { + "__module__": cls.__module__, + "model_class": cls + }) + + if hasattr(cls, "unknown"): + cls.Schema.Meta.unknown = cls.unknown + + cls._get_schema_class = lambda: cls.Schema + + return cls + + + def generic_init(instance, **kwargs): """Initialize from kwargs into slots.""" for slot in instance.__slots__: From c3c412699b6389aa252c77dddf390de6fd0911a3 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 16 Feb 2021 16:26:27 -0500 Subject: [PATCH 07/59] feat: add helpers for common connection checks Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/util.py | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index 4641bd9b..e8eb4824 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -3,6 +3,7 @@ # pylint: disable=too-few-public-methods import sys +from typing import Type import logging import functools import json @@ -12,6 +13,7 @@ from aries_cloudagent.connections.models.conn_record import ConnRecord from aries_cloudagent.protocols.connections.v1_0.manager import ConnectionManager from aries_cloudagent.storage.base import BaseStorage +from aries_cloudagent.storage.error import StorageNotFoundError from aries_cloudagent.core.profile import ProfileSession from aries_cloudagent.messaging.agent_message import ( AgentMessage, AgentMessageSchema @@ -313,3 +315,45 @@ async def send_to_admins( reply_to_verkey=verkey, to_session_only=to_session_only ) + + +class InvalidConnection(Exception): + """Raised if no connection or connection is not ready.""" + + +async def get_connection(session: ProfileSession, connection_id: str): + """Get connection record or raise error if not found or conn is not ready.""" + try: + conn_record = await ConnRecord.retrieve_by_id( + session, + connection_id + ) + if not conn_record.is_ready: + raise InvalidConnection("Connection is not ready.") + + return conn_record + except StorageNotFoundError as err: + raise InvalidConnection("Connection not found.") from err + + +class ExceptionReporter: + def __init__( + self, + responder: BaseResponder, + exception: Type[Exception], + original_message: AgentMessage = None + ): + self.responder = responder + self.exception = exception + self.original_message = original_message + + async def __aenter__(self): + return self + + async def __aexit__(self, err_type, err_value, err_traceback): + """Exit the context manager.""" + if isinstance(err_value, self.exception): + report = ProblemReport(explain_ltxt=str(err_value)) + if self.original_message: + report.assign_thread_from(self.original_message) + await self.responder.send_reply(report) From 027901454d300d78043e08ed1ae353e0d8425c74 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 16 Feb 2021 16:27:53 -0500 Subject: [PATCH 08/59] test: add tests for utilities Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/tests/__init__.py | 0 acapy_plugin_toolbox/tests/test_util.py | 152 ++++++++++++++++++++++++ requirements.dev.txt | 2 + 3 files changed, 154 insertions(+) create mode 100644 acapy_plugin_toolbox/tests/__init__.py create mode 100644 acapy_plugin_toolbox/tests/test_util.py create mode 100644 requirements.dev.txt diff --git a/acapy_plugin_toolbox/tests/__init__.py b/acapy_plugin_toolbox/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/acapy_plugin_toolbox/tests/test_util.py b/acapy_plugin_toolbox/tests/test_util.py new file mode 100644 index 00000000..1749f187 --- /dev/null +++ b/acapy_plugin_toolbox/tests/test_util.py @@ -0,0 +1,152 @@ +"""Test utilities.""" +import pytest + +from marshmallow import fields +from aries_cloudagent.messaging.agent_message import AgentMessage, AgentMessageSchema +from aries_cloudagent.messaging.models.base import BaseModel, BaseModelSchema + +from ..util import expand_message_class, expand_model_class + + +def test_expand_message_class(): + """Test that expand_message_class is correctly expanding.""" + @expand_message_class + class TestMessage(AgentMessage): + message_type = "test_type" + handler = "handler" + + class Fields: + test = fields.Str(required=True) + + def __init__(self, test: str = None, **kwargs): + super().__init__(**kwargs) + self.test = test + + assert TestMessage.Schema.Meta.model_class == TestMessage + assert TestMessage.Meta.message_type == "test_type" + assert TestMessage.Meta.handler_class == "handler" + assert TestMessage.Meta.schema_class == "TestMessage.Schema" + + test = TestMessage(test="test") + test2 = TestMessage.deserialize(test.serialize()) + assert test.test == test2.test + assert test._type == test2._type + + +def test_expand_message_class_with_protocol(): + """Test protocol is prepended to message_type.""" + @expand_message_class + class TestMessage(AgentMessage): + protocol = "protocol" + message_type = "type" + handler = "handler" + + class Fields: + test = fields.Str(required=True) + + def __init__(self, test: str = None): + super().__init__() + self.test = test + + test = TestMessage("test") + assert test._type == "protocol/type" + + +def test_expand_message_class_x_missing_message_type(): + """Test that missing message type raises error.""" + with pytest.raises(ValueError): + @expand_message_class + class TestMessage(AgentMessage): + handler = "handler" + + class Fields: + test = fields.Str(required=True) + + +def test_expand_message_class_x_missing_handler(): + """Test that missing handler raises error.""" + with pytest.raises(ValueError): + @expand_message_class + class TestMessage(AgentMessage): + message_type = "test_type" + + class Fields: + test = fields.Str(required=True) + + +def test_expand_message_class_x_missing_fields(): + """Test that missing Fields and no schema raises error.""" + with pytest.raises(ValueError): + @expand_message_class + class TestMessage(AgentMessage): + message_type = "test_type" + handler = "handler" + + +def test_expand_message_class_fields_from(): + """Test that expand message class can reuse another schema.""" + class OtherTestMessage(AgentMessage): + def __init__(self, one: str = None, **kwargs): + super().__init__(**kwargs) + self.one = one + + class TestSchema(AgentMessageSchema): + one = fields.Str(required=True) + + @expand_message_class + class TestMessage(OtherTestMessage): + message_type = "type" + handler = "handler" + fields_from = TestSchema + + test = TestMessage("test") + assert test.one + assert TestMessage.deserialize(test.serialize()) + + +def test_expand_model_class(): + """Test that models are expanded as expected.""" + @expand_model_class + class TestModel(BaseModel): + """Test Model.""" + class Fields: + test = fields.Str(required=True) + + def __init__(self, test: str = None, **kwargs): + super().__init__(**kwargs) + self.test = test + + test = TestModel("test") + assert test.test == "test" + assert isinstance(test.Schema(), BaseModelSchema) + assert test.Meta != BaseModel.Meta + assert test.test == TestModel.deserialize(test.serialize()).test + + +def test_expand_model_class_x_missing_fields(): + """Test that missing Fields and no schema raises error.""" + with pytest.raises(ValueError): + @expand_model_class + class TestModel(BaseModel): + pass + + +def test_expand_model_class_fields_from(): + """Test that expand model class can reuse another schema.""" + class OtherTestModel(BaseModel): + def __init__(self, one: str = None, **kwargs): + super().__init__(**kwargs) + self.one = one + + class TestSchema(BaseModelSchema): + one = fields.Str(required=True) + + @expand_model_class + class TestModel(OtherTestModel): + model_type = "type" + handler = "handler" + fields_from = TestSchema + + test = TestModel("test") + assert test.one + assert TestModel.deserialize(test.serialize()) diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 00000000..ee4ba018 --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,2 @@ +pytest +pytest-asyncio From e6fb89687cc27c6da7576f892ccf49f32b3335e3 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 16 Feb 2021 16:28:21 -0500 Subject: [PATCH 09/59] feat: add setup with config for flake8 Signed-off-by: Daniel Bluhm --- setup.cfg | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..8fdac056 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[flake8] +# https://github.com/ambv/black#line-length +max-line-length = 90 +exclude = + */tests/** +extend_ignore = D202, W503 +per_file_ignores = */__init__.py:D104 From 169d5f44102467003bd415ad0dcdbec992dbe3be Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 17 Feb 2021 09:25:42 -0500 Subject: [PATCH 10/59] refactor: use improved boilerplate removal for holder Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/decorators/pagination.py | 26 +- acapy_plugin_toolbox/holder/v0_1.py | 420 +++++++++++------- 2 files changed, 256 insertions(+), 190 deletions(-) diff --git a/acapy_plugin_toolbox/decorators/pagination.py b/acapy_plugin_toolbox/decorators/pagination.py index b4493b14..3de3c6d8 100644 --- a/acapy_plugin_toolbox/decorators/pagination.py +++ b/acapy_plugin_toolbox/decorators/pagination.py @@ -1,7 +1,6 @@ """Decorators for Pagination.""" -from typing import Any, Sequence, Tuple, Iterator -from functools import singledispatchmethod +from typing import Any, Sequence, Tuple from aries_cloudagent.messaging.models.base import BaseModel from marshmallow import fields @@ -35,21 +34,13 @@ class Paginate(BaseModel): class Fields: """Fields of paginate decorator.""" limit = fields.Int(required=True) - offset = fields.Int(required=True, missing=0) + offset = fields.Int(required=False, missing=0) def __init__(self, limit: int = 0, offset: int = 0, **kwargs): super().__init__(**kwargs) self.limit = limit self.offset = offset - @singledispatchmethod - def apply(self, items) -> Tuple[Sequence[Any], Page]: - """Apply pagination, returning paginated results and Page decorator.""" - raise NotImplementedError( - "Paginate.apply is not implemented for type {}".format(type(items)) - ) - - @apply.register def apply(self, items: list) -> Tuple[Sequence[Any], Page]: """Apply pagination to list.""" end = self.offset + self.limit @@ -57,16 +48,3 @@ def apply(self, items: list) -> Tuple[Sequence[Any], Page]: remaining = len(items[end:]) page = Page(len(result), self.offset, remaining) return result, page - - @apply.register - def apply(self, items: Iterator) -> Tuple[Sequence[Any], Page]: - """Apply pagination to iterator.""" - for _, _ in zip(range(self.offset), items): - pass - - result = [] - for _idx, item in zip(range(self.offset, self.offset + self.limit), items): - result.append(item) - - page = Page(len(result), self.offset) - return result, page diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 69a331bd..d129ed8f 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -3,38 +3,40 @@ # pylint: disable=invalid-name # pylint: disable=too-few-public-methods -import json +import re +from typing import Sequence +from aries_cloudagent.config.injection_context import InjectionContext from aries_cloudagent.connections.models.conn_record import ConnRecord -from aries_cloudagent.core.profile import ProfileSession +from aries_cloudagent.core.event_bus import Event, EventBus, EventContext from aries_cloudagent.core.protocol_registry import ProtocolRegistry from aries_cloudagent.indy.holder import IndyHolder +from aries_cloudagent.messaging.agent_message import AgentMessage from aries_cloudagent.messaging.base_handler import ( BaseHandler, BaseResponder, RequestContext ) -from aries_cloudagent.protocols.issue_credential.v1_0.manager import ( - CredentialManager -) -from aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_proposal import ( - CredentialProposal -) -from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( - V10CredentialExchangeSchema +from aries_cloudagent.messaging.models.base import BaseModel +from aries_cloudagent.protocols.issue_credential.v1_0.messages.inner.credential_preview import ( + CredAttrSpec ) +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import \ + V10CredentialExchange as CredExRecord +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import \ + V10CredentialExchangeSchema as CredExRecordSchema from aries_cloudagent.protocols.issue_credential.v1_0.routes import ( V10CredentialProposalRequestMandSchema ) -from aries_cloudagent.protocols.present_proof.v1_0.manager import ( - PresentationManager -) -from aries_cloudagent.protocols.present_proof.v1_0.messages.presentation_proposal import ( - PresentationProposal +from aries_cloudagent.protocols.issue_credential.v1_0.manager import CredentialManager +from aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_proposal import CredentialProposal +from aries_cloudagent.protocols.present_proof import v1_0 as proof +from aries_cloudagent.protocols.present_proof.v1_0.messages.inner.presentation_preview import ( + PresentationPreview ) from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( V10PresentationExchange, V10PresentationExchangeSchema ) from aries_cloudagent.protocols.present_proof.v1_0.routes import ( - V10PresentationExchangeListSchema, V10PresentationProposalRequestSchema + V10PresentationProposalRequestSchema ) from aries_cloudagent.protocols.problem_report.v1_0.message import ( ProblemReport @@ -42,60 +44,247 @@ from aries_cloudagent.storage.error import StorageNotFoundError from marshmallow import fields -from ..util import admin_only, generate_model_schema +from ..decorators.pagination import Page, Paginate +from ..util import ( + ExceptionReporter, InvalidConnection, admin_only, expand_message_class, + expand_model_class, get_connection, send_to_admins +) +PASS = 'acapy_plugin_toolbox.util.PassHandler' PROTOCOL = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/admin-holder/0.1' +PACKAGE = 'acapy_plugin_toolbox.holder.v0_1' + + +@expand_model_class +class CredentialRepresentation(BaseModel): + """Representation of Credentials in messages.""" + class Fields: + """Fields for Credential Representation.""" + issuer_did = fields.Str() + isser_connection_id = fields.Str() + name = fields.Str() + comment = fields.Str() + received_at = fields.DateTime(format="iso") + attributes = fields.List(fields.Nested(CredAttrSpec)) + metadata = fields.Dict() + raw_repr = fields.Dict() + + def __init__( + self, + *, + issuer_did: str = None, + issuer_connection_id: str = None, + name: str = None, + comment: str = None, + received_at: str = None, + attributes: Sequence[CredAttrSpec] = None, + metadata: dict = None, + raw_repr: dict = None + ): + """Initialize model.""" + self.issuer_did = issuer_did + self.issuer_connection_id = issuer_connection_id + self.name = name + self.comment = comment + self.received_at = received_at + self.attributes = attributes + self.metadata = metadata + self.raw_repr = raw_repr + + +@expand_message_class +class CredGetList(AgentMessage): + """Credential list retrieval message.""" + protocol = PROTOCOL + message_type = "credentials-get-list" + handler = f"{PACKAGE}.CredGetListHandler" + + class Fields: + """Credential get list fields.""" + paginate = fields.Nested( + Paginate.Schema, + required=False, + data_key="~paginate", + missing=Paginate(limit=10, offset=0) + ) + + def __init__(self, connection_id: str = None, paginate: Paginate = None, **kwargs): + super().__init__(**kwargs) + self.connection_id = connection_id + self.paginate = paginate + + +@expand_message_class +class CredList(AgentMessage): + """Credential list message.""" + protocol = PROTOCOL + message_type = "credentials-list" + handler = PASS + + class Fields: + """Fields of credential list message.""" + results = fields.List(fields.Dict()) + page = fields.Nested(Page.Schema, required=False, data_key="~page") + + def __init__( + self, + results: Sequence[dict], + page: Page = None, + **kwargs + ): + super().__init__(**kwargs) + self.results = results + self.page = page + + +@expand_message_class +class SendCredProposal(AgentMessage): + """Send Credential Proposal Message.""" + protocol = PROTOCOL + message_type = "send-credential-proposal" + handler = f"{PACKAGE}.SendCredProposalHandler" + fields_from = V10CredentialProposalRequestMandSchema + + +@expand_message_class +class CredExchange(AgentMessage): + """Credential exchange message.""" + protocol = PROTOCOL + message_type = "credential-exchange" + handler = PASS + fields_from = CredExRecordSchema + + +@expand_message_class +class CredOfferRecv(AgentMessage): + """Credential offer received message.""" + protocol = PROTOCOL + message_type = "credential-offer-received" + handler = PASS + fields_from = CredExRecordSchema + + +@expand_message_class +class PresGetList(AgentMessage): + """Presentation get list message.""" + protocol = PROTOCOL + message_type = 'presentations-get-list' + handler = f"{PACKAGE}.PresGetListHandler" + + class Fields: + """Message fields.""" + connection_id = fields.Str(required=False) + verified = fields.Str(required=False) + paginate = fields.Nested( + Paginate.Schema, + required=False, + data_key="~paginate", + missing=Paginate(limit=10, offset=0) + ) + + def __init__( + self, + connection_id: str = None, + verified: str = None, + paginate: Paginate = None, + **kwargs + ): + super().__init__(**kwargs) + self.connection_id = connection_id + self.verified = verified + self.paginate = paginate + + +@expand_message_class +class PresList(AgentMessage): + """Presentation get list response message.""" + protocol = PROTOCOL + message_type = 'presentations-list' + handler = PASS + + class Fields: + """Fields for presentation list message.""" + results = fields.List(fields.Dict()) + page = fields.Nested(Page.Schema, required=False, data_key="~page") + + def __init__(self, results, page: Page = None, **kwargs): + super().__init__(**kwargs) + self.results = results + self.page = page + + +@expand_message_class +class SendPresProposal(AgentMessage): + """Presentation proposal message.""" + protocol = PROTOCOL + message_type = 'send-presentation-proposal' + handler = f"{PACKAGE}.SendPresProposalHandler" + fields_from = V10PresentationProposalRequestSchema + + def __init__( + self, + *, + connection_id: str = None, + comment: str = None, + presentation_proposal: PresentationPreview = None, + auto_present: bool = None, + trace: bool = None + ): + self.connection_id = connection_id + self.comment = comment + self.presentation_proposal = presentation_proposal + self.auto_present = auto_present + self.trace = trace + + +@expand_message_class +class PresExchange(AgentMessage): + """Presentation Exchange message.""" + protocol = PROTOCOL + message_type = "presentation-exchange" + handler = PASS + fields_from = V10PresentationExchangeSchema -SEND_CRED_PROPOSAL = '{}/send-credential-proposal'.format(PROTOCOL) -CRED_EXCHANGE = '{}/credential-exchange'.format(PROTOCOL) -SEND_PRES_PROPOSAL = '{}/send-presentation-proposal'.format(PROTOCOL) -PRES_EXCHANGE = '{}/presentation-exchange'.format(PROTOCOL) -CREDENTIALS_GET_LIST = '{}/credentials-get-list'.format(PROTOCOL) -CREDENTIALS_LIST = '{}/credentials-list'.format(PROTOCOL) -PRESENTATIONS_GET_LIST = '{}/presentations-get-list'.format(PROTOCOL) -PRESENTATIONS_LIST = '{}/presentations-list'.format(PROTOCOL) MESSAGE_TYPES = { - SEND_CRED_PROPOSAL: - 'acapy_plugin_toolbox.holder.v0_1.SendCredProposal', - SEND_PRES_PROPOSAL: - 'acapy_plugin_toolbox.holder.v0_1.SendPresProposal', - CREDENTIALS_GET_LIST: - 'acapy_plugin_toolbox.holder.v0_1.CredGetList', - CREDENTIALS_LIST: - 'acapy_plugin_toolbox.holder.v0_1.CredList', - PRESENTATIONS_GET_LIST: - 'acapy_plugin_toolbox.holder.v0_1.PresGetList', - PRESENTATIONS_LIST: - 'acapy_plugin_toolbox.holder.v0_1.PresList', + msg_class.Meta.message_type: '{}.{}'.format(PACKAGE, msg_class.__name__) + for msg_class in [ + PresGetList, PresList, SendPresProposal, PresExchange, + CredGetList, CredList, SendCredProposal, CredExchange, + CredOfferRecv + ] } async def setup( - session: ProfileSession, + context: InjectionContext, protocol_registry: ProblemReport = None ): """Setup the holder plugin.""" if not protocol_registry: - protocol_registry = session.inject(ProtocolRegistry) + protocol_registry = context.inject(ProtocolRegistry) protocol_registry.register_message_types( MESSAGE_TYPES ) + bus: EventBus = context.inject(EventBus) + bus.subscribe( + re.compile(CredExRecord.WEBHOOK_TOPIC + ".*"), + issue_credential_event_handler + ) -SendCredProposal, SendCredProposalSchema = generate_model_schema( - name='SendCredProposal', - handler='acapy_plugin_toolbox.holder.v0_1.SendCredProposalHandler', - msg_type=SEND_CRED_PROPOSAL, - schema=V10CredentialProposalRequestMandSchema -) - -CredExchange, CredExchangeSchema = generate_model_schema( - name='CredExchange', - handler='acapy_plugin_toolbox.util.PassHandler', - msg_type=CRED_EXCHANGE, - schema=V10CredentialExchangeSchema -) +async def issue_credential_event_handler(context: EventContext, event: Event): + """Handle issue credential events.""" + record: CredExRecord = CredExRecord.deserialize(event.payload) + if record.state == CredExRecord.STATE_OFFER_RECEIVED: + offer_recv = CredOfferRecv(**record.serialize()) + responder = context.inject(BaseResponder) + async with context.session() as session: + await send_to_admins( + session, + offer_recv, + responder + ) class SendCredProposalHandler(BaseHandler): @@ -154,56 +343,20 @@ async def handle(self, context: RequestContext, responder: BaseResponder): await responder.send_reply(cred_exchange) -SendPresProposal, SendPresProposalSchema = generate_model_schema( - name='SendPresProposal', - handler='acapy_plugin_toolbox.holder.v0_1.SendPresProposalHandler', - msg_type=SEND_PRES_PROPOSAL, - schema=V10PresentationProposalRequestSchema -) - -PresExchange, PresExchangeSchema = generate_model_schema( - name='PresExchange', - handler='acapy_plugin_toolbox.util.PassHandler', - msg_type=PRES_EXCHANGE, - schema=V10PresentationExchangeSchema -) - - class SendPresProposalHandler(BaseHandler): """Handler for received send presentation proposal request.""" @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received send presentation proposal request.""" - - connection_id = str(context.message.connection_id) session = await context.session() - try: - conn_record = await ConnRecord.retrieve_by_id( - session, - connection_id - ) - except StorageNotFoundError: - report = ProblemReport( - explain_ltxt='Connection not found.', - who_retries='none' - ) - report.assign_thread_from(context.message) - await responder.send_reply(report) - return - - if not conn_record.is_ready: - report = ProblemReport( - explain_ltxt='Connection invalid.', - who_retries='none' - ) - report.assign_thread_from(context.message) - await responder.send_reply(report) - return + connection_id = str(context.message.connection_id) + async with ExceptionReporter(responder, InvalidConnection, context.message): + await get_connection(session, connection_id) comment = context.message.comment # Aries#0037 calls it a proposal in the proposal struct but it's of type preview - presentation_proposal = PresentationProposal( + presentation_proposal = proof.messages.presentation_proposal.PresentationProposal( comment=comment, presentation_proposal=context.message.presentation_proposal ) @@ -212,7 +365,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.settings.get("debug.auto_respond_presentation_request") ) - presentation_manager = PresentationManager(session) + presentation_manager = proof.manager.PresentationManager(session) presentation_exchange_record = ( await presentation_manager.create_exchange_for_proposal( @@ -228,88 +381,21 @@ async def handle(self, context: RequestContext, responder: BaseResponder): await responder.send_reply(pres_exchange) -CredGetList, CredGetListSchema = generate_model_schema( - name='CredGetList', - handler='acapy_plugin_toolbox.holder.v0_1.CredGetListHandler', - msg_type=CREDENTIALS_GET_LIST, - schema={ - 'connection_id': fields.Str(required=False), - 'credential_definition_id': fields.Str(required=False), - 'schema_id': fields.Str(required=False) - } -) - -CredList, CredListSchema = generate_model_schema( - name='CredList', - handler='acapy_plugin_toolbox.util.PassHandler', - msg_type=CREDENTIALS_LIST, - #schema=V10CredentialExchangeListResultSchema - schema={ - 'results': fields.List(fields.Dict()) - } -) - - class CredGetListHandler(BaseHandler): """Handler for received get cred list request.""" @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received get cred list request.""" - - # holder: BaseHolder = await context.inject(BaseHolder) - # credentials = await holder.get_credentials(0, 100, {}) - # cred_list = CredList(results=credentials) - # await responder.send_reply(cred_list) - - start = 0 #request.query.get("start") - count = 10 #request.query.get("count") - - # url encoded json wql - encoded_wql = "{}" #request.query.get("wql") or "{}" - wql = json.loads(encoded_wql) - - # defaults - #start = int(start) if isinstance(start, str) else 0 - #count = int(count) if isinstance(count, str) else 10 - session = await context.session() holder: IndyHolder = session.inject(IndyHolder) - credentials = await holder.get_credentials(start, count, wql) - - # post_filter_positive = dict( - # filter(lambda item: item[1] is not None, { - # # 'state': V10CredentialExchange.STATE_CREDENTIAL_RECEIVED, - # #'role': V10CredentialExchange.ROLE_HOLDER, - # 'connection_id': context.message.connection_id, - # 'credential_definition_id': context.message.credential_definition_id, - # 'schema_id': context.message.schema_id - # }.items()) - # ) - # records = await V10CredentialExchange.query(context, {}, post_filter_positive) - cred_list = CredList(results=credentials) - await responder.send_reply(cred_list) + paginate: Paginate = context.message.paginate + credentials = await holder.get_credentials(paginate.offset, paginate.limit, {}) + page = Page(len(credentials), paginate.offset) -PresGetList, PresGetListSchema = generate_model_schema( - name='PresGetList', - handler='acapy_plugin_toolbox.holder.v0_1.PresGetListHandler', - msg_type=PRESENTATIONS_GET_LIST, - schema={ - 'connection_id': fields.Str(required=False), - 'verified': fields.Str(required=False), - } -) - -PresList, PresListSchema = generate_model_schema( - name='PresList', - handler='acapy_plugin_toolbox.util.PassHandler', - msg_type=PRESENTATIONS_LIST, - schema=V10PresentationExchangeListSchema - # schema={ - # 'results': fields.List(fields.Dict()) - # } -) + cred_list = CredList(results=credentials, page=page) + await responder.send_reply(cred_list) class PresGetListHandler(BaseHandler): @@ -320,6 +406,8 @@ async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received get cred list request.""" session = await context.session() + paginate: Paginate = context.message.paginate + post_filter_positive = dict( filter(lambda item: item[1] is not None, { # 'state': V10PresentialExchange.STATE_CREDENTIAL_RECEIVED, @@ -331,5 +419,5 @@ async def handle(self, context: RequestContext, responder: BaseResponder): records = await V10PresentationExchange.query( session, {}, post_filter_positive=post_filter_positive ) - cred_list = PresList(results=records) + cred_list = PresList(*paginate.apply(records)) await responder.send_reply(cred_list) From d4903b62a3629d32962f9229fe2b97b64a2764a5 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 19 Feb 2021 12:54:17 -0500 Subject: [PATCH 11/59] feat: reduce handler boilerplate Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/util.py | 68 +++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index e8eb4824..27a943ee 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -56,18 +56,16 @@ def require_role(role): """ def _require_role(func): @functools.wraps(func) - async def _wrapped( - handler, - context: RequestContext, - responder: BaseResponder - ): + async def _wrapped(*args): + context, *_ = [arg for arg in args if isinstance(arg, RequestContext)] + responder, *_ = [arg for arg in args if isinstance(arg, BaseResponder)] if context.connection_record: session = await context.session() group = await context.connection_record.metadata_get( session, 'group' ) if group == role: - return await func(handler, context, responder) + return await func(*args) report = ProblemReport( explain_ltxt='This connection is not authorized to perform' @@ -88,16 +86,13 @@ def admin_only(func): def expand_message_class(cls): """Class decorator for removing boilerplate of AgentMessages.""" + # pylint: disable=protected-access + if not hasattr(cls, "message_type"): raise ValueError( "Expected value message_type not found on class {}" .format(cls.__name__) ) - if not hasattr(cls, "handler"): - raise ValueError( - "Expected value handler value not found on class {}" - .format(cls.__name__) - ) if not hasattr(cls, "Fields") and not hasattr(cls, "fields_from"): raise ValueError( "Class {} must have nested class Fields or schema defining expected fields" @@ -106,14 +101,17 @@ def expand_message_class(cls): cls.Meta = type(cls.__name__ + ".Meta", (), { "__module__": cls.__module__, - "handler_class": cls.handler, "message_type": cls.message_type, "schema_class": cls.__name__ + ".Schema" }) fields = {} if hasattr(cls, "Fields"): - fields.update({var: getattr(cls.Fields, var) for var in vars(cls.Fields)}) + fields.update({ + var: getattr(cls.Fields, var) + for var in vars(cls.Fields) + if not var.startswith("__") + }) if hasattr(cls, "fields_from"): fields.update(cls.fields_from._declared_fields) @@ -121,6 +119,7 @@ def expand_message_class(cls): "__module__": cls.__module__, **fields }) + cls.__slots__ = list(fields.keys()) cls.Schema.Meta = type(cls.Schema.__name__ + ".Meta", (), { "__module__": cls.__module__, "model_class": cls @@ -131,6 +130,16 @@ def expand_message_class(cls): cls.Meta.message_type = "{}/{}".format(cls.protocol, cls.message_type) cls._type = property(fget=lambda self: self.Meta.message_type) + if hasattr(cls, "handle"): + cls.Handler = handler(cls.handle) + cls._get_handler_class = lambda: cls.Handler + elif hasattr(cls, "handler"): + cls.Meta.handler_class = cls.handler + else: + cls.Handler = PassHandler + cls._get_handler_class = lambda: cls.Handler + cls.Meta.handler_class = PassHandler.load_path + return cls @@ -160,6 +169,7 @@ def expand_model_class(cls): "__module__": cls.__module__, **fields }) + cls.__slots__ = list(fields.keys()) cls.Schema.Meta = type(cls.Schema.__name__ + ".Meta", (), { "__module__": cls.__module__, "model_class": cls @@ -173,6 +183,26 @@ def expand_model_class(cls): return cls +def handler(func): + """Function decorator for creating Python handler classes.""" + + class Handler(BaseHandler): + __doc__ = func.__doc__ + __name__ = func.__name__ + __module__ = func.__module__ + + @property + @classmethod + def load_path(cls): + """Return load path for this handler.""" + return f"{cls.__module__}.{cls.__name__}" + + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle message.""" + return await func(context.message, context, responder) + + return Handler + def generic_init(instance, **kwargs): """Initialize from kwargs into slots.""" @@ -183,6 +213,12 @@ def generic_init(instance, **kwargs): super(type(instance), instance).__init__(**kwargs) +def with_generic_init(cls): + """Class decorator for adding generic init method.""" + cls.__init__ = generic_init + return cls + + def generate_model_schema( # pylint: disable=protected-access name: str, handler: str, @@ -262,6 +298,12 @@ class Meta: class PassHandler(BaseHandler): """Handler for messages requiring no handling.""" + @property + @classmethod + def load_path(cls): + """Return the load path of this handler.""" + return f"{cls.__module__}.{cls.__name__}" + async def handle(self, context: RequestContext, _responder): """Handle messages require no handling.""" # pylint: disable=protected-access From e03665cebddca828d0361ca8348d47360666b35c Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 19 Feb 2021 12:56:18 -0500 Subject: [PATCH 12/59] fix: inconsistent messaging from events Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/connections.py | 10 +++++----- acapy_plugin_toolbox/holder/v0_1.py | 9 +++++---- acapy_plugin_toolbox/util.py | 9 +++++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/acapy_plugin_toolbox/connections.py b/acapy_plugin_toolbox/connections.py index a584e175..55664ded 100644 --- a/acapy_plugin_toolbox/connections.py +++ b/acapy_plugin_toolbox/connections.py @@ -7,9 +7,9 @@ from typing import Any, Dict from aries_cloudagent.connections.models.conn_record import ConnRecord -from aries_cloudagent.core.profile import InjectionContext +from aries_cloudagent.core.profile import InjectionContext, Profile from aries_cloudagent.core.protocol_registry import ProtocolRegistry -from aries_cloudagent.core.event_bus import Event, EventBus, EventContext +from aries_cloudagent.core.event_bus import Event, EventBus from aries_cloudagent.messaging.base_handler import ( BaseHandler, BaseResponder, RequestContext ) @@ -73,15 +73,15 @@ async def setup( event_bus.subscribe(EVENT_PATTERN, connections_event_handler) -async def connections_event_handler(context: EventContext, event: Event): +async def connections_event_handler(profile: Profile, event: Event): """Handle connection events. Send connected message to admins when connections reach active state. """ record: ConnRecord = ConnRecord.deserialize(event.payload) if record.state == ConnRecord.State.RESPONSE: - responder = context.inject(BaseResponder) - async with context.session() as session: + responder = profile.inject(BaseResponder) + async with profile.session() as session: await send_to_admins( session, Connected(**conn_record_to_message_repr(record)), diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index d129ed8f..7412e307 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -8,7 +8,8 @@ from aries_cloudagent.config.injection_context import InjectionContext from aries_cloudagent.connections.models.conn_record import ConnRecord -from aries_cloudagent.core.event_bus import Event, EventBus, EventContext +from aries_cloudagent.core.event_bus import Event, EventBus +from aries_cloudagent.core.profile import Profile from aries_cloudagent.core.protocol_registry import ProtocolRegistry from aries_cloudagent.indy.holder import IndyHolder from aries_cloudagent.messaging.agent_message import AgentMessage @@ -273,13 +274,13 @@ async def setup( ) -async def issue_credential_event_handler(context: EventContext, event: Event): +async def issue_credential_event_handler(profile: Profile, event: Event): """Handle issue credential events.""" record: CredExRecord = CredExRecord.deserialize(event.payload) if record.state == CredExRecord.STATE_OFFER_RECEIVED: offer_recv = CredOfferRecv(**record.serialize()) - responder = context.inject(BaseResponder) - async with context.session() as session: + responder = profile.inject(BaseResponder) + async with profile.session() as session: await send_to_admins( session, offer_recv, diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index 27a943ee..5c66caba 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -343,18 +343,19 @@ async def send_to_admins( admins = await admin_connections(session) admins = list(filter(lambda admin: admin.state == 'active', admins)) connection_mgr = ConnectionManager(session) - admin_verkeys = [ - target.recipient_keys[0] + admin_targets = [ + target for admin in admins for target in await connection_mgr.get_connection_targets( connection=admin ) ] - for verkey in admin_verkeys: + for target in admin_targets: await responder.send( message, - reply_to_verkey=verkey, + reply_to_verkey=target.recipient_keys[0], + reply_from_verkey=target.sender_key, to_session_only=to_session_only ) From f8722b0edf04716105bcb809bfcbca6fb1dc6456 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 19 Feb 2021 12:56:54 -0500 Subject: [PATCH 13/59] refactor: reduce boilerplate in holder v0_1 Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/holder/v0_1.py | 321 +++++++++++------------- acapy_plugin_toolbox/tests/test_util.py | 18 +- 2 files changed, 160 insertions(+), 179 deletions(-) diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 7412e307..d12adca3 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -17,6 +17,12 @@ BaseHandler, BaseResponder, RequestContext ) from aries_cloudagent.messaging.models.base import BaseModel +from aries_cloudagent.protocols.issue_credential.v1_0.manager import ( + CredentialManager +) +from aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_proposal import ( + CredentialProposal +) from aries_cloudagent.protocols.issue_credential.v1_0.messages.inner.credential_preview import ( CredAttrSpec ) @@ -27,8 +33,6 @@ from aries_cloudagent.protocols.issue_credential.v1_0.routes import ( V10CredentialProposalRequestMandSchema ) -from aries_cloudagent.protocols.issue_credential.v1_0.manager import CredentialManager -from aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_proposal import CredentialProposal from aries_cloudagent.protocols.present_proof import v1_0 as proof from aries_cloudagent.protocols.present_proof.v1_0.messages.inner.presentation_preview import ( PresentationPreview @@ -48,11 +52,9 @@ from ..decorators.pagination import Page, Paginate from ..util import ( ExceptionReporter, InvalidConnection, admin_only, expand_message_class, - expand_model_class, get_connection, send_to_admins + expand_model_class, get_connection, send_to_admins, with_generic_init ) -PASS = 'acapy_plugin_toolbox.util.PassHandler' -PROTOCOL = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/admin-holder/0.1' PACKAGE = 'acapy_plugin_toolbox.holder.v0_1' @@ -93,12 +95,15 @@ def __init__( self.raw_repr = raw_repr +class AdminHolderMessage(AgentMessage): + """Admin Holder Protocol Message Base class.""" + protocol = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/admin-holder/0.1' + + @expand_message_class -class CredGetList(AgentMessage): +class CredGetList(AdminHolderMessage): """Credential list retrieval message.""" - protocol = PROTOCOL message_type = "credentials-get-list" - handler = f"{PACKAGE}.CredGetListHandler" class Fields: """Credential get list fields.""" @@ -109,18 +114,29 @@ class Fields: missing=Paginate(limit=10, offset=0) ) - def __init__(self, connection_id: str = None, paginate: Paginate = None, **kwargs): + def __init__(self, paginate: Paginate = None, **kwargs): super().__init__(**kwargs) - self.connection_id = connection_id self.paginate = paginate + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle received get cred list request.""" + session = await context.session() + holder: IndyHolder = session.inject(IndyHolder) + + credentials = await holder.get_credentials( + self.paginate.offset, self.paginate.limit, {} + ) + page = Page(len(credentials), self.paginate.offset) + + cred_list = CredList(results=credentials, page=page) + await responder.send_reply(cred_list) + @expand_message_class -class CredList(AgentMessage): +class CredList(AdminHolderMessage): """Credential list message.""" - protocol = PROTOCOL message_type = "credentials-list" - handler = PASS class Fields: """Fields of credential list message.""" @@ -138,39 +154,86 @@ def __init__( self.page = page +@with_generic_init @expand_message_class -class SendCredProposal(AgentMessage): +class SendCredProposal(AdminHolderMessage): """Send Credential Proposal Message.""" - protocol = PROTOCOL message_type = "send-credential-proposal" - handler = f"{PACKAGE}.SendCredProposalHandler" fields_from = V10CredentialProposalRequestMandSchema + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle received send proposal request.""" + connection_id = str(context.message.connection_id) + credential_definition_id = context.message.cred_def_id + comment = context.message.comment + + credential_manager = CredentialManager(context.profile) + + session = await context.session() + try: + conn_record = await ConnRecord.retrieve_by_id( + session, + connection_id + ) + except StorageNotFoundError: + report = ProblemReport( + explain_ltxt='Connection not found.', + who_retries='none' + ) + report.assign_thread_from(context.message) + await responder.send_reply(report) + return + + if not conn_record.is_ready: + report = ProblemReport( + explain_ltxt='Connection invalid.', + who_retries='none' + ) + report.assign_thread_from(context.message) + await responder.send_reply(report) + return + credential_exchange_record = await credential_manager.create_proposal( + connection_id, + comment=comment, + credential_preview=context.message.credential_proposal, + cred_def_id=credential_definition_id + ) + + await responder.send( + CredentialProposal( + comment=context.message.comment, + credential_proposal=context.message.credential_proposal, + cred_def_id=credential_definition_id + ), + connection_id=connection_id + ) + cred_exchange = CredExchange(**credential_exchange_record.serialize()) + cred_exchange.assign_thread_from(context.message) + await responder.send_reply(cred_exchange) + + +@with_generic_init @expand_message_class -class CredExchange(AgentMessage): +class CredExchange(AdminHolderMessage): """Credential exchange message.""" - protocol = PROTOCOL message_type = "credential-exchange" - handler = PASS fields_from = CredExRecordSchema +@with_generic_init @expand_message_class -class CredOfferRecv(AgentMessage): +class CredOfferRecv(AdminHolderMessage): """Credential offer received message.""" - protocol = PROTOCOL message_type = "credential-offer-received" - handler = PASS fields_from = CredExRecordSchema @expand_message_class -class PresGetList(AgentMessage): +class PresGetList(AdminHolderMessage): """Presentation get list message.""" - protocol = PROTOCOL message_type = 'presentations-get-list' - handler = f"{PACKAGE}.PresGetListHandler" class Fields: """Message fields.""" @@ -195,13 +258,32 @@ def __init__( self.verified = verified self.paginate = paginate + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle received get cred list request.""" + + session = await context.session() + paginate: Paginate = context.message.paginate + + post_filter_positive = dict( + filter(lambda item: item[1] is not None, { + # 'state': V10PresentialExchange.STATE_CREDENTIAL_RECEIVED, + 'role': V10PresentationExchange.ROLE_PROVER, + 'connection_id': context.message.connection_id, + 'verified': context.message.verified, + }.items()) + ) + records = await V10PresentationExchange.query( + session, {}, post_filter_positive=post_filter_positive + ) + cred_list = PresList(*paginate.apply(records)) + await responder.send_reply(cred_list) + @expand_message_class -class PresList(AgentMessage): +class PresList(AdminHolderMessage): """Presentation get list response message.""" - protocol = PROTOCOL message_type = 'presentations-list' - handler = PASS class Fields: """Fields for presentation list message.""" @@ -215,11 +297,9 @@ def __init__(self, results, page: Page = None, **kwargs): @expand_message_class -class SendPresProposal(AgentMessage): +class SendPresProposal(AdminHolderMessage): """Presentation proposal message.""" - protocol = PROTOCOL message_type = 'send-presentation-proposal' - handler = f"{PACKAGE}.SendPresProposalHandler" fields_from = V10PresentationProposalRequestSchema def __init__( @@ -229,21 +309,56 @@ def __init__( comment: str = None, presentation_proposal: PresentationPreview = None, auto_present: bool = None, - trace: bool = None + trace: bool = None, + **kwargs ): + super().__init__(**kwargs) self.connection_id = connection_id self.comment = comment self.presentation_proposal = presentation_proposal self.auto_present = auto_present self.trace = trace + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle received send presentation proposal request.""" + session = await context.session() + connection_id = str(context.message.connection_id) + async with ExceptionReporter(responder, InvalidConnection, context.message): + await get_connection(session, connection_id) + + comment = context.message.comment + # Aries#0037 calls it a proposal in the proposal struct but it's of type preview + presentation_proposal = proof.messages.presentation_proposal.PresentationProposal( + comment=comment, + presentation_proposal=context.message.presentation_proposal + ) + auto_present = ( + context.message.auto_present or + context.settings.get("debug.auto_respond_presentation_request") + ) + + presentation_manager = proof.manager.PresentationManager(context.profile) + presentation_exchange_record = ( + await presentation_manager.create_exchange_for_proposal( + connection_id=connection_id, + presentation_proposal_message=presentation_proposal, + auto_present=auto_present + ) + ) + await responder.send(presentation_proposal, connection_id=connection_id) + + pres_exchange = PresExchange(**presentation_exchange_record.serialize()) + pres_exchange.assign_thread_from(context.message) + await responder.send_reply(pres_exchange) + + +@with_generic_init @expand_message_class -class PresExchange(AgentMessage): +class PresExchange(AdminHolderMessage): """Presentation Exchange message.""" - protocol = PROTOCOL message_type = "presentation-exchange" - handler = PASS fields_from = V10PresentationExchangeSchema @@ -286,139 +401,3 @@ async def issue_credential_event_handler(profile: Profile, event: Event): offer_recv, responder ) - - -class SendCredProposalHandler(BaseHandler): - """Handler for received send proposal request.""" - - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle received send proposal request.""" - connection_id = str(context.message.connection_id) - credential_definition_id = context.message.credential_definition_id - comment = context.message.comment - - credential_manager = CredentialManager(context.profile) - - session = await context.session() - try: - conn_record = await ConnRecord.retrieve_by_id( - session, - connection_id - ) - except StorageNotFoundError: - report = ProblemReport( - explain_ltxt='Connection not found.', - who_retries='none' - ) - report.assign_thread_from(context.message) - await responder.send_reply(report) - return - - if not conn_record.is_ready: - report = ProblemReport( - explain_ltxt='Connection invalid.', - who_retries='none' - ) - report.assign_thread_from(context.message) - await responder.send_reply(report) - return - - credential_exchange_record = await credential_manager.create_proposal( - connection_id, - comment=comment, - credential_preview=context.message.credential_proposal, - cred_def_id=credential_definition_id - ) - - await responder.send( - CredentialProposal( - comment=context.message.comment, - credential_proposal=context.message.credential_proposal, - cred_def_id=context.message.credential_definition_id - ), - connection_id=connection_id - ) - cred_exchange = CredExchange(**credential_exchange_record.serialize()) - cred_exchange.assign_thread_from(context.message) - await responder.send_reply(cred_exchange) - - -class SendPresProposalHandler(BaseHandler): - """Handler for received send presentation proposal request.""" - - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle received send presentation proposal request.""" - session = await context.session() - connection_id = str(context.message.connection_id) - async with ExceptionReporter(responder, InvalidConnection, context.message): - await get_connection(session, connection_id) - - comment = context.message.comment - # Aries#0037 calls it a proposal in the proposal struct but it's of type preview - presentation_proposal = proof.messages.presentation_proposal.PresentationProposal( - comment=comment, - presentation_proposal=context.message.presentation_proposal - ) - auto_present = ( - context.message.auto_present or - context.settings.get("debug.auto_respond_presentation_request") - ) - - presentation_manager = proof.manager.PresentationManager(session) - - presentation_exchange_record = ( - await presentation_manager.create_exchange_for_proposal( - connection_id=connection_id, - presentation_proposal_message=presentation_proposal, - auto_present=auto_present - ) - ) - await responder.send(presentation_proposal, connection_id=connection_id) - - pres_exchange = PresExchange(**presentation_exchange_record.serialize()) - pres_exchange.assign_thread_from(context.message) - await responder.send_reply(pres_exchange) - - -class CredGetListHandler(BaseHandler): - """Handler for received get cred list request.""" - - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle received get cred list request.""" - session = await context.session() - holder: IndyHolder = session.inject(IndyHolder) - - paginate: Paginate = context.message.paginate - credentials = await holder.get_credentials(paginate.offset, paginate.limit, {}) - page = Page(len(credentials), paginate.offset) - - cred_list = CredList(results=credentials, page=page) - await responder.send_reply(cred_list) - - -class PresGetListHandler(BaseHandler): - """Handler for received get cred list request.""" - - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle received get cred list request.""" - - session = await context.session() - paginate: Paginate = context.message.paginate - - post_filter_positive = dict( - filter(lambda item: item[1] is not None, { - # 'state': V10PresentialExchange.STATE_CREDENTIAL_RECEIVED, - 'role': V10PresentationExchange.ROLE_PROVER, - 'connection_id': context.message.connection_id, - 'verified': context.message.verified, - }.items()) - ) - records = await V10PresentationExchange.query( - session, {}, post_filter_positive=post_filter_positive - ) - cred_list = PresList(*paginate.apply(records)) - await responder.send_reply(cred_list) diff --git a/acapy_plugin_toolbox/tests/test_util.py b/acapy_plugin_toolbox/tests/test_util.py index 1749f187..8ed26e63 100644 --- a/acapy_plugin_toolbox/tests/test_util.py +++ b/acapy_plugin_toolbox/tests/test_util.py @@ -5,7 +5,7 @@ from aries_cloudagent.messaging.agent_message import AgentMessage, AgentMessageSchema from aries_cloudagent.messaging.models.base import BaseModel, BaseModelSchema -from ..util import expand_message_class, expand_model_class +from ..util import expand_message_class, expand_model_class, PassHandler def test_expand_message_class(): @@ -31,6 +31,7 @@ def __init__(self, test: str = None, **kwargs): test2 = TestMessage.deserialize(test.serialize()) assert test.test == test2.test assert test._type == test2._type + assert test.__slots__ == ["test"] def test_expand_message_class_with_protocol(): @@ -63,15 +64,16 @@ class Fields: test = fields.Str(required=True) -def test_expand_message_class_x_missing_handler(): +def test_expand_message_class_missing_handler_uses_pass(): """Test that missing handler raises error.""" - with pytest.raises(ValueError): - @expand_message_class - class TestMessage(AgentMessage): - message_type = "test_type" + @expand_message_class + class TestMessage(AgentMessage): + message_type = "test_type" - class Fields: - test = fields.Str(required=True) + class Fields: + test = fields.Str(required=True) + + assert TestMessage.Handler == PassHandler def test_expand_message_class_x_missing_fields(): From 9e935f45b6431ea3c223ba1088e0942bd8b2c6a7 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 19 Feb 2021 14:59:55 -0500 Subject: [PATCH 14/59] feat: add accept and received messages to holder Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/holder/v0_1.py | 101 ++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index d12adca3..a95dacfa 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -14,11 +14,11 @@ from aries_cloudagent.indy.holder import IndyHolder from aries_cloudagent.messaging.agent_message import AgentMessage from aries_cloudagent.messaging.base_handler import ( - BaseHandler, BaseResponder, RequestContext + BaseResponder, RequestContext ) -from aries_cloudagent.messaging.models.base import BaseModel +from aries_cloudagent.messaging.models.base import BaseModel, BaseModelError from aries_cloudagent.protocols.issue_credential.v1_0.manager import ( - CredentialManager + CredentialManager, CredentialManagerError ) from aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_proposal import ( CredentialProposal @@ -46,7 +46,7 @@ from aries_cloudagent.protocols.problem_report.v1_0.message import ( ProblemReport ) -from aries_cloudagent.storage.error import StorageNotFoundError +from aries_cloudagent.storage.error import StorageError, StorageNotFoundError from marshmallow import fields from ..decorators.pagination import Page, Paginate @@ -230,6 +230,67 @@ class CredOfferRecv(AdminHolderMessage): fields_from = CredExRecordSchema +@expand_message_class +class CredOfferAccept(AdminHolderMessage): + """Credential offer accept message.""" + message_type = "credential-offer-accept" + + class Fields: + """Fields of cred offer accept message.""" + credential_exchange_id = fields.Str(required=True) + + def __init__(self, credential_exchange_id: str = None, **kwargs): + super().__init__(**kwargs) + self.credential_exchange_id = credential_exchange_id + + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle credential offer accept message.""" + + cred_ex_record = None + connection_record = None + async with context.session() as session: + async with ExceptionReporter( + responder, + (StorageError, CredentialManagerError, BaseModelError), + self + ): + cred_ex_record = await CredExRecord.retrieve_by_id( + session, self.credential_exchange_id + ) + connection_id = cred_ex_record.connection_id + connection_record = await get_connection(session, connection_id) + + credential_manager = CredentialManager(context.profile) + ( + cred_ex_record, + credential_request_message, + ) = await credential_manager.create_request( + cred_ex_record, connection_record.my_did + ) + + sent = CredRequestSent(**cred_ex_record.serialize()) + + await responder.send(credential_request_message, connection_id=connection_id) + await responder.send_reply(sent) + + +@with_generic_init +@expand_message_class +class CredRequestSent(AdminHolderMessage): + """Credential offer acceptance received and credential request sent.""" + message_type = "credential-request-sent" + fields_from = CredExRecordSchema + + +@with_generic_init +@expand_message_class +class CredReceived(AdminHolderMessage): + """Credential received notification message.""" + message_type = "credential-received" + fields_from = CredExRecordSchema + + @expand_message_class class PresGetList(AdminHolderMessage): """Presentation get list message.""" @@ -363,11 +424,11 @@ class PresExchange(AdminHolderMessage): MESSAGE_TYPES = { - msg_class.Meta.message_type: '{}.{}'.format(PACKAGE, msg_class.__name__) + msg_class.Meta.message_type: '{}.{}'.format(msg_class.__module__, msg_class.__name__) for msg_class in [ PresGetList, PresList, SendPresProposal, PresExchange, CredGetList, CredList, SendCredProposal, CredExchange, - CredOfferRecv + CredOfferRecv, CredOfferAccept, CredRequestSent ] } @@ -392,12 +453,24 @@ async def setup( async def issue_credential_event_handler(profile: Profile, event: Event): """Handle issue credential events.""" record: CredExRecord = CredExRecord.deserialize(event.payload) + + if record.state not in ( + CredExRecord.STATE_OFFER_RECEIVED, + CredExRecord.STATE_CREDENTIAL_RECEIVED + ): + return + + responder = profile.inject(BaseResponder) + message = None if record.state == CredExRecord.STATE_OFFER_RECEIVED: - offer_recv = CredOfferRecv(**record.serialize()) - responder = profile.inject(BaseResponder) - async with profile.session() as session: - await send_to_admins( - session, - offer_recv, - responder - ) + message = CredOfferRecv(**record.serialize()) + + if record.state == CredExRecord.STATE_CREDENTIAL_RECEIVED: + message = CredReceived(**record.serialize()) + + async with profile.session() as session: + await send_to_admins( + session, + message, + responder + ) From d549542ae1b9aacfb953071c158fd54cde8540e5 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 24 Feb 2021 19:11:56 -0500 Subject: [PATCH 15/59] feat: add admin-holder presentation notification messages Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/holder/v0_1.py | 79 +++++++++++++++++++++++++++-- acapy_plugin_toolbox/issuer.py | 2 +- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index a95dacfa..549285c1 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -41,7 +41,7 @@ V10PresentationExchange, V10PresentationExchangeSchema ) from aries_cloudagent.protocols.present_proof.v1_0.routes import ( - V10PresentationProposalRequestSchema + V10PresentationProposalRequestSchema, IndyCredPrecisSchema ) from aries_cloudagent.protocols.problem_report.v1_0.message import ( ProblemReport @@ -328,7 +328,6 @@ async def handle(self, context: RequestContext, responder: BaseResponder): post_filter_positive = dict( filter(lambda item: item[1] is not None, { - # 'state': V10PresentialExchange.STATE_CREDENTIAL_RECEIVED, 'role': V10PresentationExchange.ROLE_PROVER, 'connection_id': context.message.connection_id, 'verified': context.message.verified, @@ -423,12 +422,66 @@ class PresExchange(AdminHolderMessage): fields_from = V10PresentationExchangeSchema +@expand_message_class +class PresRequestReceived(AdminHolderMessage): + """Presentation Request Received.""" + message_type = "presentation-request-received" + + DEFAULT_COUNT = 10 + + class Fields: + """Fields of Presentation request received message.""" + record = fields.Nested(V10PresentationExchangeSchema) + matching_credentials = fields.Nested(IndyCredPrecisSchema, many=True) + page = fields.Nested(Page.Schema, required=False) + + def __init__(self, record: V10PresentationExchange, **kwargs): + super().__init__(**kwargs) + self.record = record + self.matching_credentials = [] + self.page = None + + async def retrieve_matching_credentials(self, profile: Profile): + holder = profile.inject(IndyHolder) + self.matching_credentials = await holder.get_credentials_for_presentation_request_by_referent( + self.record.presentation_request, + (), + 0, + self.DEFAULT_COUNT, + extra_query={}, + ) + self.page = Page(count=self.DEFAULT_COUNT, offset=self.DEFAULT_COUNT) + + +@expand_message_class +class PresRequestApprove(AdminHolderMessage): + """Approve presentation request.""" + message_type = "presentation-request-approve" + + class Fields: + """Fields on pres request approve message.""" + presentation_exchange_id = fields.Str(required=True) + + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle presentation request approved message.""" + + MESSAGE_TYPES = { msg_class.Meta.message_type: '{}.{}'.format(msg_class.__module__, msg_class.__name__) for msg_class in [ - PresGetList, PresList, SendPresProposal, PresExchange, - CredGetList, CredList, SendCredProposal, CredExchange, - CredOfferRecv, CredOfferAccept, CredRequestSent + CredExchange, + CredGetList, + CredList, + CredOfferAccept, + CredOfferRecv, + CredRequestSent, + PresExchange, + PresGetList, + PresList, + PresRequestApprove, + SendCredProposal, + SendPresProposal, ] } @@ -448,6 +501,10 @@ async def setup( re.compile(CredExRecord.WEBHOOK_TOPIC + ".*"), issue_credential_event_handler ) + bus.subscribe( + re.compile(V10PresentationExchange.WEBHOOK_TOPIC + ".*"), + present_proof_event_handler + ) async def issue_credential_event_handler(profile: Profile, event: Event): @@ -474,3 +531,15 @@ async def issue_credential_event_handler(profile: Profile, event: Event): message, responder ) + + +async def present_proof_event_handler(profile: Profile, event: Event): + """Handle present proof events.""" + record: V10PresentationExchange = V10PresentationExchange.deserialize(event.payload) + + if record.state == V10PresentationExchange.STATE_REQUEST_RECEIVED: + responder = profile.inject(BaseResponder) + message = PresRequestReceived(record) + await message.retrieve_matching_credentials(profile) + async with profile.session() as session: + await send_to_admins(session, message, responder) diff --git a/acapy_plugin_toolbox/issuer.py b/acapy_plugin_toolbox/issuer.py index c8dbdaa9..183be616 100644 --- a/acapy_plugin_toolbox/issuer.py +++ b/acapy_plugin_toolbox/issuer.py @@ -226,7 +226,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ] ) - presentation_manager = PresentationManager(session) + presentation_manager = PresentationManager(context.profile) presentation_exchange_record = ( await presentation_manager.create_exchange_for_request( From d7ad60cbf8af20d1906b634d7b7b9a250e7aa553 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 24 Feb 2021 20:15:50 -0500 Subject: [PATCH 16/59] test: test holder event handlers Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/holder/tests/__init__.py | 0 .../holder/tests/test_events.py | 109 ++++++++++++++++++ requirements.dev.txt | 1 + setup.cfg | 3 + 4 files changed, 113 insertions(+) create mode 100644 acapy_plugin_toolbox/holder/tests/__init__.py create mode 100644 acapy_plugin_toolbox/holder/tests/test_events.py diff --git a/acapy_plugin_toolbox/holder/tests/__init__.py b/acapy_plugin_toolbox/holder/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/acapy_plugin_toolbox/holder/tests/test_events.py b/acapy_plugin_toolbox/holder/tests/test_events.py new file mode 100644 index 00000000..a5f8fc46 --- /dev/null +++ b/acapy_plugin_toolbox/holder/tests/test_events.py @@ -0,0 +1,109 @@ +"""Test holder event handlers.""" + +# pylint: disable=redefined-outer-name + +import pytest +from aries_cloudagent.core.event_bus import EventBus, Event +from aries_cloudagent.core.in_memory import InMemoryProfile +from aries_cloudagent.core.profile import Profile +from aries_cloudagent.core.protocol_registry import ProtocolRegistry +from aries_cloudagent.messaging.responder import BaseResponder, MockResponder +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import V10CredentialExchange +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import V10PresentationExchange +from asynctest import mock + +from .. import v0_1 as test_module + + +@pytest.fixture +def event_bus(): + """Event bus fixture.""" + yield EventBus() + + +@pytest.fixture +def profile(event_bus): + """Profile fixture.""" + holder = mock.MagicMock() + holder.get_credentials_for_presentation_request_by_referent = mock.CoroutineMock() + yield InMemoryProfile.test_profile(bind={ + EventBus: event_bus, + BaseResponder: MockResponder(), + ProtocolRegistry: ProtocolRegistry(), + test_module.IndyHolder: holder + }) + + +@pytest.fixture +def context(profile): + """Context fixture.""" + yield profile.context + + +class MockSendToAdmins: + """Mock send_to_admins method.""" + def __init__(self): + self.message = None + + async def __call__(self, session, message, responder): + self.message = message + + +@pytest.fixture +def mock_send_to_admins(): + temp = test_module.send_to_admins + test_module.send_to_admins = MockSendToAdmins() + yield test_module.send_to_admins + test_module.send_to_admins = temp + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "handler, topic", + [ + ("issue_credential_event_handler", V10CredentialExchange.WEBHOOK_TOPIC), + ("present_proof_event_handler", V10PresentationExchange.WEBHOOK_TOPIC) + ] +) +async def test_events_subscribed_and_triggered( + profile, context, event_bus, handler, topic +): + """Test events are correctly registered and triggered.""" + with mock.patch.object( + test_module, + handler, + mock.CoroutineMock() + ) as mock_event_handler: + await test_module.setup(context) + await event_bus.notify(profile, Event(topic, {"test": "payload"})) + mock_event_handler.assert_called_once() + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "handler, state, message", + [ + ( + test_module.issue_credential_event_handler, + V10CredentialExchange.STATE_OFFER_RECEIVED, + test_module.CredOfferRecv + ), + ( + test_module.issue_credential_event_handler, + V10CredentialExchange.STATE_CREDENTIAL_RECEIVED, + test_module.CredReceived + ), + ( + test_module.present_proof_event_handler, + V10PresentationExchange.STATE_REQUEST_RECEIVED, + test_module.PresRequestReceived + ) + ] +) +async def test_message_sent_on_correct_state( + profile, mock_send_to_admins, handler, state, message +): + """Test message sent on handle given correct state.""" + event = Event("anything", {"state": state}) + await handler(profile, event) + assert isinstance(mock_send_to_admins.message, message) diff --git a/requirements.dev.txt b/requirements.dev.txt index ee4ba018..ba6cfc66 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,2 +1,3 @@ pytest pytest-asyncio +asynctest==0.13.0 diff --git a/setup.cfg b/setup.cfg index 8fdac056..3a07e0e8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,3 +5,6 @@ exclude = */tests/** extend_ignore = D202, W503 per_file_ignores = */__init__.py:D104 + +[tool:pytest] +addopts = -p no:warnings From 45cbf55a615cec7b4f2bcdbcb54135a79fc2a629 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 24 Feb 2021 20:23:12 -0500 Subject: [PATCH 17/59] test: negative test cases for holder events Signed-off-by: Daniel Bluhm --- .../holder/tests/test_events.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/acapy_plugin_toolbox/holder/tests/test_events.py b/acapy_plugin_toolbox/holder/tests/test_events.py index a5f8fc46..718b0df1 100644 --- a/acapy_plugin_toolbox/holder/tests/test_events.py +++ b/acapy_plugin_toolbox/holder/tests/test_events.py @@ -107,3 +107,42 @@ async def test_message_sent_on_correct_state( event = Event("anything", {"state": state}) await handler(profile, event) assert isinstance(mock_send_to_admins.message, message) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "handler, state", + [ + *[ + (test_module.issue_credential_event_handler, state) + for state in [ + V10CredentialExchange.STATE_ACKED, + V10CredentialExchange.STATE_ISSUED, + V10CredentialExchange.STATE_OFFER_SENT, + V10CredentialExchange.STATE_PROPOSAL_RECEIVED, + V10CredentialExchange.STATE_PROPOSAL_SENT, + V10CredentialExchange.STATE_REQUEST_RECEIVED, + V10CredentialExchange.STATE_REQUEST_SENT, + ] + ], + *[ + (test_module.present_proof_event_handler, state) + for state in [ + V10PresentationExchange.STATE_PRESENTATION_ACKED, + V10PresentationExchange.STATE_PRESENTATION_RECEIVED, + V10PresentationExchange.STATE_PRESENTATION_SENT, + V10PresentationExchange.STATE_PROPOSAL_RECEIVED, + V10PresentationExchange.STATE_PROPOSAL_SENT, + V10PresentationExchange.STATE_REQUEST_SENT, + V10PresentationExchange.STATE_VERIFIED, + ] + ] + ] +) +async def test_message_not_sent_on_incorrect_state( + profile, mock_send_to_admins, handler, state +): + """Test message sent on handle given correct state.""" + event = Event("anything", {"state": state}) + await handler(profile, event) + assert mock_send_to_admins.message is None From 4e67fc7db7b52f82e9d5ab7661a47d5ffcfddb6e Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 24 Feb 2021 20:55:40 -0500 Subject: [PATCH 18/59] chore: cleanup importing deeply nested classes Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/__init__.py | 4 +- acapy_plugin_toolbox/holder/__init__.py | 31 ++++++++++++ acapy_plugin_toolbox/holder/v0_1.py | 63 +++++++++---------------- 3 files changed, 55 insertions(+), 43 deletions(-) diff --git a/acapy_plugin_toolbox/__init__.py b/acapy_plugin_toolbox/__init__.py index 04d48bed..d8295772 100644 --- a/acapy_plugin_toolbox/__init__.py +++ b/acapy_plugin_toolbox/__init__.py @@ -1,3 +1,5 @@ -"""Shortcut to group all.""" +"""Shortcut to group all and rexports.""" + +from aries_cloudagent.protocols.problem_report.v1_0.message import ProblemReport from .group.all import setup diff --git a/acapy_plugin_toolbox/holder/__init__.py b/acapy_plugin_toolbox/holder/__init__.py index 265113d9..8dfa4377 100644 --- a/acapy_plugin_toolbox/holder/__init__.py +++ b/acapy_plugin_toolbox/holder/__init__.py @@ -1 +1,32 @@ """Holder admin protocol.""" + +# Shortcuts to deeply nested classes +from aries_cloudagent.protocols.issue_credential import \ + v1_0 as issue_credential +from aries_cloudagent.protocols.issue_credential.v1_0.manager import ( + CredentialManager, CredentialManagerError +) +from aries_cloudagent.protocols.issue_credential.v1_0.messages.inner.credential_preview import \ + CredAttrSpec as CredentialAttributeSpec +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import \ + V10CredentialExchange as CredExRecord +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import \ + V10CredentialExchangeSchema as CredExRecordSchema +from aries_cloudagent.protocols.issue_credential.v1_0.routes import \ + V10CredentialProposalRequestMandSchema as CredentialProposalRequestSchema +from aries_cloudagent.protocols.present_proof import v1_0 as present_proof +from aries_cloudagent.protocols.present_proof.v1_0.manager import ( + PresentationManager, PresentationManagerError +) +from aries_cloudagent.protocols.present_proof.v1_0.messages.inner.presentation_preview import ( + PresentationPreview +) +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import \ + V10PresentationExchange as PresExRecord +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import \ + V10PresentationExchangeSchema as PresExRecordSchema +from aries_cloudagent.protocols.present_proof.v1_0.routes import ( + IndyCredPrecisSchema +) +from aries_cloudagent.protocols.present_proof.v1_0.routes import \ + V10PresentationProposalRequestSchema as PresentationProposalRequestSchema diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 549285c1..3b09abe3 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -17,43 +17,22 @@ BaseResponder, RequestContext ) from aries_cloudagent.messaging.models.base import BaseModel, BaseModelError -from aries_cloudagent.protocols.issue_credential.v1_0.manager import ( - CredentialManager, CredentialManagerError -) -from aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_proposal import ( - CredentialProposal -) -from aries_cloudagent.protocols.issue_credential.v1_0.messages.inner.credential_preview import ( - CredAttrSpec -) -from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import \ - V10CredentialExchange as CredExRecord -from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import \ - V10CredentialExchangeSchema as CredExRecordSchema -from aries_cloudagent.protocols.issue_credential.v1_0.routes import ( - V10CredentialProposalRequestMandSchema -) -from aries_cloudagent.protocols.present_proof import v1_0 as proof -from aries_cloudagent.protocols.present_proof.v1_0.messages.inner.presentation_preview import ( - PresentationPreview -) -from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( - V10PresentationExchange, V10PresentationExchangeSchema -) -from aries_cloudagent.protocols.present_proof.v1_0.routes import ( - V10PresentationProposalRequestSchema, IndyCredPrecisSchema -) -from aries_cloudagent.protocols.problem_report.v1_0.message import ( - ProblemReport -) from aries_cloudagent.storage.error import StorageError, StorageNotFoundError from marshmallow import fields +from .. import ProblemReport from ..decorators.pagination import Page, Paginate from ..util import ( ExceptionReporter, InvalidConnection, admin_only, expand_message_class, expand_model_class, get_connection, send_to_admins, with_generic_init ) +from . import ( + CredentialAttributeSpec, CredentialManager, CredentialManagerError, + CredentialProposalRequestSchema, CredExRecord, CredExRecordSchema, + IndyCredPrecisSchema, PresentationPreview, + PresentationProposalRequestSchema, PresExRecord, PresExRecordSchema, + issue_credential, present_proof +) PACKAGE = 'acapy_plugin_toolbox.holder.v0_1' @@ -68,7 +47,7 @@ class Fields: name = fields.Str() comment = fields.Str() received_at = fields.DateTime(format="iso") - attributes = fields.List(fields.Nested(CredAttrSpec)) + attributes = fields.List(fields.Nested(CredentialAttributeSpec)) metadata = fields.Dict() raw_repr = fields.Dict() @@ -80,7 +59,7 @@ def __init__( name: str = None, comment: str = None, received_at: str = None, - attributes: Sequence[CredAttrSpec] = None, + attributes: Sequence[CredentialAttributeSpec] = None, metadata: dict = None, raw_repr: dict = None ): @@ -159,7 +138,7 @@ def __init__( class SendCredProposal(AdminHolderMessage): """Send Credential Proposal Message.""" message_type = "send-credential-proposal" - fields_from = V10CredentialProposalRequestMandSchema + fields_from = CredentialProposalRequestSchema @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): @@ -202,7 +181,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) await responder.send( - CredentialProposal( + issue_credential.messages.credential_proposal.CredentialProposal( comment=context.message.comment, credential_proposal=context.message.credential_proposal, cred_def_id=credential_definition_id @@ -328,12 +307,12 @@ async def handle(self, context: RequestContext, responder: BaseResponder): post_filter_positive = dict( filter(lambda item: item[1] is not None, { - 'role': V10PresentationExchange.ROLE_PROVER, + 'role': PresExRecord.ROLE_PROVER, 'connection_id': context.message.connection_id, 'verified': context.message.verified, }.items()) ) - records = await V10PresentationExchange.query( + records = await PresExRecord.query( session, {}, post_filter_positive=post_filter_positive ) cred_list = PresList(*paginate.apply(records)) @@ -360,7 +339,7 @@ def __init__(self, results, page: Page = None, **kwargs): class SendPresProposal(AdminHolderMessage): """Presentation proposal message.""" message_type = 'send-presentation-proposal' - fields_from = V10PresentationProposalRequestSchema + fields_from = PresentationProposalRequestSchema def __init__( self, @@ -419,7 +398,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): class PresExchange(AdminHolderMessage): """Presentation Exchange message.""" message_type = "presentation-exchange" - fields_from = V10PresentationExchangeSchema + fields_from = PresExRecordSchema @expand_message_class @@ -431,11 +410,11 @@ class PresRequestReceived(AdminHolderMessage): class Fields: """Fields of Presentation request received message.""" - record = fields.Nested(V10PresentationExchangeSchema) + record = fields.Nested(PresExRecordSchema) matching_credentials = fields.Nested(IndyCredPrecisSchema, many=True) page = fields.Nested(Page.Schema, required=False) - def __init__(self, record: V10PresentationExchange, **kwargs): + def __init__(self, record: PresExRecord, **kwargs): super().__init__(**kwargs) self.record = record self.matching_credentials = [] @@ -502,7 +481,7 @@ async def setup( issue_credential_event_handler ) bus.subscribe( - re.compile(V10PresentationExchange.WEBHOOK_TOPIC + ".*"), + re.compile(PresExRecord.WEBHOOK_TOPIC + ".*"), present_proof_event_handler ) @@ -535,9 +514,9 @@ async def issue_credential_event_handler(profile: Profile, event: Event): async def present_proof_event_handler(profile: Profile, event: Event): """Handle present proof events.""" - record: V10PresentationExchange = V10PresentationExchange.deserialize(event.payload) + record: PresExRecord = PresExRecord.deserialize(event.payload) - if record.state == V10PresentationExchange.STATE_REQUEST_RECEIVED: + if record.state == PresExRecord.STATE_REQUEST_RECEIVED: responder = profile.inject(BaseResponder) message = PresRequestReceived(record) await message.retrieve_matching_credentials(profile) From d2389089751b1e67c496fe12a6c71aba8fab4ea4 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 24 Feb 2021 21:18:21 -0500 Subject: [PATCH 19/59] feat: allow specifying acapy to install during image build Signed-off-by: Daniel Bluhm --- docker/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index f70cc3a6..88ce47a4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,5 +14,8 @@ RUN ln -s /home/indy/.pyenv/versions/3.6.9/lib/python3.6/site-packages site-pack RUN pip3 install -e ./aries-acapy-plugin-toolbox +ARG ACAPY="" +RUN if [ -n "${ACAPY}" ]; then pip3 install ${ACAPY}; fi + ENTRYPOINT ["/bin/bash", "-c", "aca-py \"$@\"", "--"] CMD ["start", "--plugin", "acapy_plugin_toolbox", "--arg-file", "default.yml"] From 44d257724f440906a9f7682476341b09d36b71c4 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 24 Feb 2021 22:29:44 -0500 Subject: [PATCH 20/59] fix: rearrange dockerfile for better cache invalidation Signed-off-by: Daniel Bluhm --- docker/Dockerfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 88ce47a4..f8fd6c7a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,11 +1,14 @@ FROM bcgovimages/aries-cloudagent:py36-1.15-0_0.6.0rc0 -USER root +ARG ACAPY="" +RUN if [ -n "${ACAPY}" ]; then pip3 install ${ACAPY}; fi + ADD acapy_plugin_toolbox aries-acapy-plugin-toolbox/acapy_plugin_toolbox ADD requirements.txt aries-acapy-plugin-toolbox ADD setup.py aries-acapy-plugin-toolbox ADD README.md aries-acapy-plugin-toolbox -ADD docker/default.yml . + +USER root RUN chown -R indy:indy aries-acapy-plugin-toolbox USER $user @@ -14,8 +17,7 @@ RUN ln -s /home/indy/.pyenv/versions/3.6.9/lib/python3.6/site-packages site-pack RUN pip3 install -e ./aries-acapy-plugin-toolbox -ARG ACAPY="" -RUN if [ -n "${ACAPY}" ]; then pip3 install ${ACAPY}; fi +ADD docker/default.yml . ENTRYPOINT ["/bin/bash", "-c", "aca-py \"$@\"", "--"] CMD ["start", "--plugin", "acapy_plugin_toolbox", "--arg-file", "default.yml"] From 2892579d94a5c08be8dcfcfe77721e3dfe684634 Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Wed, 24 Feb 2021 02:29:20 +0000 Subject: [PATCH 21/59] Using event bus rather than re-implementing BasicMessageHandler Signed-off-by: Matthew Wright --- acapy_plugin_toolbox/basicmessage.py | 133 +++++++-------------------- acapy_plugin_toolbox/routing.py | 1 - 2 files changed, 34 insertions(+), 100 deletions(-) diff --git a/acapy_plugin_toolbox/basicmessage.py b/acapy_plugin_toolbox/basicmessage.py index 5f330c01..8a306159 100644 --- a/acapy_plugin_toolbox/basicmessage.py +++ b/acapy_plugin_toolbox/basicmessage.py @@ -1,13 +1,12 @@ """BasicMessage Plugin.""" # pylint: disable=invalid-name, too-few-public-methods -import json -from datetime import datetime -from typing import Union +import re from aries_cloudagent.connections.models.conn_record import ConnRecord from aries_cloudagent.core.profile import ProfileSession from aries_cloudagent.core.protocol_registry import ProtocolRegistry +from aries_cloudagent.core.event_bus import Event, EventBus, EventContext from aries_cloudagent.messaging.base_handler import ( BaseHandler, BaseResponder, RequestContext ) @@ -18,24 +17,17 @@ BaseRecord, BaseRecordSchema ) from aries_cloudagent.messaging.valid import INDY_ISO8601_DATETIME -from aries_cloudagent.protocols.connections.v1_0.manager import ( - ConnectionManager -) +from aries_cloudagent.protocols.basicmessage.v1_0.messages.basicmessage import BasicMessage from aries_cloudagent.protocols.problem_report.v1_0.message import ( ProblemReport ) -from aries_cloudagent.storage.base import BaseStorage from aries_cloudagent.storage.error import StorageNotFoundError from marshmallow import fields from .util import ( - admin_only, datetime_from_iso, generate_model_schema, send_to_admins, - timestamp_utc_iso + admin_only, datetime_from_iso, generate_model_schema, send_to_admins ) -PROTOCOL_URI = "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/basicmessage/1.0" -BASIC_MESSAGE = f"{PROTOCOL_URI}/message" - ADMIN_PROTOCOL_URI = "https://github.com/hyperledger/" \ "aries-toolbox/tree/master/docs/admin-basicmessage/0.1" GET = f"{ADMIN_PROTOCOL_URI}/get" @@ -44,12 +36,13 @@ NEW = f"{ADMIN_PROTOCOL_URI}/new" MESSAGE_TYPES = { - BASIC_MESSAGE: 'acapy_plugin_toolbox.basicmessage.BasicMessage', GET: 'acapy_plugin_toolbox.basicmessage.Get', SEND: 'acapy_plugin_toolbox.basicmessage.Send', DELETE: 'acapy_plugin_toolbox.basicmessage.Delete' } +BASIC_MESSAGE_EVENT_PATTERN = re.compile("^basicmessages$") + async def setup( session: ProfileSession, @@ -62,6 +55,34 @@ async def setup( MESSAGE_TYPES ) + event_bus = session.inject(EventBus) + event_bus.subscribe(BASIC_MESSAGE_EVENT_PATTERN, basic_message_event_handler) + + +async def basic_message_event_handler(context: EventContext, event: Event): + """ + Handle basic message events. + + Send a notification to admins when messages are received. + """ + + msg: BasicMessageRecord = BasicMessageRecord.deserialize(event.payload) + msg.sent_time = context.message.sent_time + + notification = New( + connection_id=event.payload["connection_id"], + message=msg + ) + + responder = context.inject(BaseResponder) + async with context.session() as session: + await send_to_admins( + session, + notification, + responder, + to_session_only=True + ) + class BasicMessageRecord(BaseRecord): """BasicMessage Record.""" @@ -153,53 +174,6 @@ class Meta: content = fields.Str(required=False) -def basic_message_init( - self, - *, - sent_time: Union[str, datetime] = None, - content: str = None, - localization: str = None, - **kwargs, -): - """ - Initialize basic message object. - - Args: - sent_time: Time message was sent - content: message content - localization: localization - - """ - # pylint: disable=protected-access - super(BasicMessage, self).__init__(**kwargs) - if not sent_time: - sent_time = timestamp_utc_iso() - if localization: - self._decorators["l10n"] = localization - self.sent_time = sent_time - self.content = content - - -BasicMessage, BasicMessageSchema = generate_model_schema( - name='BasicMessage', - handler='acapy_plugin_toolbox.basicmessage.BasicMessageHandler', - msg_type=BASIC_MESSAGE, - schema={ - 'sent_time': fields.Str( - required=False, - description="Time message was sent, ISO8601", - **INDY_ISO8601_DATETIME, - ), - 'content': fields.Str( - required=True, - description="Message content", - example="Hello", - ) - }, - init=basic_message_init -) - - New, NewSchema = generate_model_schema( name='New', handler='acapy_plugin_toolbox.util.PassHandler', @@ -215,45 +189,6 @@ def basic_message_init( ) -class BasicMessageHandler(BaseHandler): - """Handler for received Basic Messages.""" - # pylint: disable=protected-access - - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle received basic message.""" - session = await context.session() - msg = BasicMessageRecord( - connection_id=context.connection_record.connection_id, - message_id=context.message._id, - sent_time=context.message.sent_time, - content=context.message.content, - state=BasicMessageRecord.STATE_RECV - ) - await msg.save(session, reason='New message received.') - - await responder.send_webhook( - "basicmessages", - { - "connection_id": context.connection_record.connection_id, - "message_id": context.message._id, - "content": context.message.content, - "state": "received", - }, - ) - - notification = New( - connection_id=context.connection_record.connection_id, - message=msg - ) - - await send_to_admins( - session, - notification, - responder, - to_session_only=True - ) - - Get, GetSchema = generate_model_schema( name='Get', handler='acapy_plugin_toolbox.basicmessage.GetHandler', diff --git a/acapy_plugin_toolbox/routing.py b/acapy_plugin_toolbox/routing.py index c54c4fb7..4ccd2eba 100644 --- a/acapy_plugin_toolbox/routing.py +++ b/acapy_plugin_toolbox/routing.py @@ -1,4 +1,3 @@ -"""BasicMessage Plugin.""" # pylint: disable=invalid-name, too-few-public-methods From dcc478e28affd7d1f727407b531cef6dd11372ca Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Thu, 25 Feb 2021 17:12:36 +0000 Subject: [PATCH 22/59] add tests for basic_message_event_handler Signed-off-by: Matthew Wright --- .../tests/test_basicmessage.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 acapy_plugin_toolbox/tests/test_basicmessage.py diff --git a/acapy_plugin_toolbox/tests/test_basicmessage.py b/acapy_plugin_toolbox/tests/test_basicmessage.py new file mode 100644 index 00000000..d3509f58 --- /dev/null +++ b/acapy_plugin_toolbox/tests/test_basicmessage.py @@ -0,0 +1,75 @@ +"""Test BasicMessage""" +from typing import Mapping + +import pytest +from aries_cloudagent.admin.request_context import AdminRequestContext +from aries_cloudagent.core.event_bus import Event, EventBus +from aries_cloudagent.core.protocol_registry import ProtocolRegistry +from aries_cloudagent.messaging.responder import BaseResponder, MockResponder +from aries_cloudagent.protocols.basicmessage.v1_0.messages.basicmessage import ( + BasicMessage, +) +from asynctest import mock + +from .. import basicmessage as basicmessage_module + + +@pytest.fixture +def event_bus(): + """Event bus fixture.""" + yield EventBus() + + +@pytest.fixture +def context(event_bus): + """Context fixture.""" + context = AdminRequestContext.test_context() + context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) + context.injector.bind_instance(EventBus, event_bus) + context.injector.bind_instance(BaseResponder, MockResponder()) + yield context + + +class MockSendToAdmins: + """Mock send_to_admins method.""" + + def __init__(self): + self.message = None + + async def __call__( + self, session, message, responder, to_session_only: bool = False + ): + self.message = message + + +@pytest.fixture +def mock_send_to_admins(): + temp = basicmessage_module.send_to_admins + basicmessage_module.send_to_admins = MockSendToAdmins() + yield basicmessage_module.send_to_admins + basicmessage_module.send_to_admins = temp + + +@pytest.mark.asyncio +async def test_basic_message_event_handler_notify_admins( + mock_send_to_admins, event_bus, context +): + await basicmessage_module.setup(context) + context.message = BasicMessage(content="Hello world") + + assert mock_send_to_admins.message == None + + await event_bus.notify( + context, + Event( + "basicmessages", + { + "connection_id": "connection-1", + "message_id": context.message._id, + "content": context.message.content, + "state": "received", + }, + ), + ) + + assert mock_send_to_admins.message.message.content == "Hello world" From 852889c43422415067291be850303b62d5571bad Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 25 Feb 2021 22:36:50 -0500 Subject: [PATCH 23/59] fix: attributes missing from event payload, save basic message Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/basicmessage.py | 20 +++++++------ .../tests/test_basicmessage.py | 29 +++++++++++-------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/acapy_plugin_toolbox/basicmessage.py b/acapy_plugin_toolbox/basicmessage.py index 8a306159..ccc01853 100644 --- a/acapy_plugin_toolbox/basicmessage.py +++ b/acapy_plugin_toolbox/basicmessage.py @@ -4,9 +4,10 @@ import re from aries_cloudagent.connections.models.conn_record import ConnRecord -from aries_cloudagent.core.profile import ProfileSession +from aries_cloudagent.core.profile import ProfileSession, Profile +from aries_cloudagent.config.injection_context import InjectionContext from aries_cloudagent.core.protocol_registry import ProtocolRegistry -from aries_cloudagent.core.event_bus import Event, EventBus, EventContext +from aries_cloudagent.core.event_bus import Event, EventBus from aries_cloudagent.messaging.base_handler import ( BaseHandler, BaseResponder, RequestContext ) @@ -45,21 +46,21 @@ async def setup( - session: ProfileSession, + context: InjectionContext, protocol_registry: ProblemReport = None ): """Setup the basicmessage plugin.""" if not protocol_registry: - protocol_registry = session.inject(ProtocolRegistry) + protocol_registry = context.inject(ProtocolRegistry) protocol_registry.register_message_types( MESSAGE_TYPES ) - event_bus = session.inject(EventBus) + event_bus = context.inject(EventBus) event_bus.subscribe(BASIC_MESSAGE_EVENT_PATTERN, basic_message_event_handler) -async def basic_message_event_handler(context: EventContext, event: Event): +async def basic_message_event_handler(profile: Profile, event: Event): """ Handle basic message events. @@ -67,15 +68,16 @@ async def basic_message_event_handler(context: EventContext, event: Event): """ msg: BasicMessageRecord = BasicMessageRecord.deserialize(event.payload) - msg.sent_time = context.message.sent_time + msg.state = BasicMessageRecord.STATE_RECV notification = New( connection_id=event.payload["connection_id"], message=msg ) - responder = context.inject(BaseResponder) - async with context.session() as session: + responder = profile.inject(BaseResponder) + async with profile.session() as session: + await msg.save(session, reason="New message") await send_to_admins( session, notification, diff --git a/acapy_plugin_toolbox/tests/test_basicmessage.py b/acapy_plugin_toolbox/tests/test_basicmessage.py index d3509f58..8bbbb52e 100644 --- a/acapy_plugin_toolbox/tests/test_basicmessage.py +++ b/acapy_plugin_toolbox/tests/test_basicmessage.py @@ -4,10 +4,11 @@ import pytest from aries_cloudagent.admin.request_context import AdminRequestContext from aries_cloudagent.core.event_bus import Event, EventBus +from aries_cloudagent.core.in_memory import InMemoryProfile from aries_cloudagent.core.protocol_registry import ProtocolRegistry from aries_cloudagent.messaging.responder import BaseResponder, MockResponder from aries_cloudagent.protocols.basicmessage.v1_0.messages.basicmessage import ( - BasicMessage, + BasicMessage ) from asynctest import mock @@ -21,13 +22,18 @@ def event_bus(): @pytest.fixture -def context(event_bus): +def profile(event_bus): """Context fixture.""" - context = AdminRequestContext.test_context() - context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) - context.injector.bind_instance(EventBus, event_bus) - context.injector.bind_instance(BaseResponder, MockResponder()) - yield context + yield InMemoryProfile.test_profile(bind={ + EventBus: event_bus, + BaseResponder: MockResponder(), + ProtocolRegistry: ProtocolRegistry(), + }) + + +@pytest.fixture +def context(profile): + yield profile.context class MockSendToAdmins: @@ -52,21 +58,20 @@ def mock_send_to_admins(): @pytest.mark.asyncio async def test_basic_message_event_handler_notify_admins( - mock_send_to_admins, event_bus, context + mock_send_to_admins, event_bus, profile, context ): await basicmessage_module.setup(context) - context.message = BasicMessage(content="Hello world") assert mock_send_to_admins.message == None await event_bus.notify( - context, + profile, Event( "basicmessages", { "connection_id": "connection-1", - "message_id": context.message._id, - "content": context.message.content, + "message_id": "test id", + "content": "Hello world", "state": "received", }, ), From 14376e055cb293f053b75a7f62afae873405cb77 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Mon, 1 Mar 2021 11:26:12 -0500 Subject: [PATCH 24/59] feat: allow demo docker configs to use alternate acapy commit Signed-off-by: Daniel Bluhm --- demo/Dockerfile.demo | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/demo/Dockerfile.demo b/demo/Dockerfile.demo index 44108253..b9c14c78 100644 --- a/demo/Dockerfile.demo +++ b/demo/Dockerfile.demo @@ -1,6 +1,8 @@ FROM bcgovimages/aries-cloudagent:py36-1.15-0_0.6.0rc0 -USER root +ARG ACAPY="" +RUN if [ -n "${ACAPY}" ]; then pip3 install ${ACAPY}; fi + ADD acapy_plugin_toolbox aries-acapy-plugin-toolbox/acapy_plugin_toolbox ADD requirements.txt aries-acapy-plugin-toolbox ADD setup.py aries-acapy-plugin-toolbox @@ -8,8 +10,9 @@ ADD README.md aries-acapy-plugin-toolbox ADD demo/configs ./configs/ ADD docker/default.yml ./configs/default.yml ADD demo/ngrok-wait.sh ./ngrok-wait.sh -RUN chown -R indy:indy aries-acapy-plugin-toolbox +USER root +RUN chown -R indy:indy aries-acapy-plugin-toolbox RUN chmod +x ngrok-wait.sh ADD https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 ./jq From ca5eb771eec66deb3559ddd26834269b8c93c372 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Mon, 1 Mar 2021 14:10:02 -0500 Subject: [PATCH 25/59] fix: credential labels Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/decorators/pagination.py | 6 +++--- acapy_plugin_toolbox/holder/v0_1.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/acapy_plugin_toolbox/decorators/pagination.py b/acapy_plugin_toolbox/decorators/pagination.py index 3de3c6d8..683975e6 100644 --- a/acapy_plugin_toolbox/decorators/pagination.py +++ b/acapy_plugin_toolbox/decorators/pagination.py @@ -14,15 +14,15 @@ class Page(BaseModel): class Fields: """Fields of page decorator.""" - count = fields.Int(required=True) + count_ = fields.Int(required=True, data_key="count") offset = fields.Int(required=True) remaining = fields.Int(required=False) def __init__( - self, count: int = 0, offset: int = 0, remaining: int = None, **kwargs + self, count_: int = 0, offset: int = 0, remaining: int = None, **kwargs ): super().__init__(**kwargs) - self.count = count + self.count = count_ self.offset = offset self.remaining = remaining diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 3b09abe3..8a8555af 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -31,7 +31,7 @@ CredentialProposalRequestSchema, CredExRecord, CredExRecordSchema, IndyCredPrecisSchema, PresentationPreview, PresentationProposalRequestSchema, PresExRecord, PresExRecordSchema, - issue_credential, present_proof + issue_credential ) PACKAGE = 'acapy_plugin_toolbox.holder.v0_1' @@ -101,14 +101,14 @@ def __init__(self, paginate: Paginate = None, **kwargs): async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received get cred list request.""" session = await context.session() - holder: IndyHolder = session.inject(IndyHolder) - credentials = await holder.get_credentials( - self.paginate.offset, self.paginate.limit, {} - ) - page = Page(len(credentials), self.paginate.offset) + credentials = await CredExRecord.query(session) + page = self.paginate.apply(credentials) - cred_list = CredList(results=credentials, page=page) + cred_list = CredList( + results=[credential.serialize() for credential in credentials], + page=page + ) await responder.send_reply(cred_list) @@ -429,7 +429,7 @@ async def retrieve_matching_credentials(self, profile: Profile): self.DEFAULT_COUNT, extra_query={}, ) - self.page = Page(count=self.DEFAULT_COUNT, offset=self.DEFAULT_COUNT) + self.page = Page(count_=self.DEFAULT_COUNT, offset=self.DEFAULT_COUNT) @expand_message_class From e199ae843062c6a984af377334fc89c21dc196c5 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 2 Mar 2021 11:51:11 -0500 Subject: [PATCH 26/59] fix: send pres proposal And regression test Signed-off-by: Daniel Bluhm --- .../holder/tests/test_send_pres_proposal.py | 72 +++++++++++++++++++ acapy_plugin_toolbox/holder/v0_1.py | 6 +- 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 acapy_plugin_toolbox/holder/tests/test_send_pres_proposal.py diff --git a/acapy_plugin_toolbox/holder/tests/test_send_pres_proposal.py b/acapy_plugin_toolbox/holder/tests/test_send_pres_proposal.py new file mode 100644 index 00000000..6335ac6f --- /dev/null +++ b/acapy_plugin_toolbox/holder/tests/test_send_pres_proposal.py @@ -0,0 +1,72 @@ +"""Test SendPresProposal message and handler.""" + +import pytest +from aries_cloudagent.connections.models.conn_record import ConnRecord +from aries_cloudagent.core.in_memory import InMemoryProfile +from aries_cloudagent.messaging.request_context import RequestContext +from aries_cloudagent.messaging.responder import MockResponder +from asynctest import mock + +from .. import v0_1 as test_module +from ..v0_1 import SendPresProposal + +TEST_CONN_ID = "test-connection-id" +TEST_PROPOSAL = "test-proposal" +TEST_COMMENT = "test-comment" + +@pytest.fixture +def mock_admin_connection(): + """Mock connection fixture.""" + connection = mock.MagicMock(spec=ConnRecord) + connection.metadata_get = mock.CoroutineMock(return_value="admin") + yield connection + + +@pytest.fixture +def message(): + """Message fixture.""" + yield SendPresProposal( + connection_id=TEST_CONN_ID, + presentation_proposal=TEST_PROPOSAL, + comment=TEST_COMMENT + ) + + +@pytest.fixture +def profile(): + """Profile fixture.""" + yield InMemoryProfile.test_profile() + + +@pytest.fixture +def context(profile, message, mock_admin_connection): + """RequestContext fixture.""" + context = RequestContext(profile) + context.message = message + context.connection_record = mock_admin_connection + context.connection_ready = True + yield context + + +@pytest.fixture +def mock_responder(): + """Mock responder fixture.""" + yield MockResponder() + + +@pytest.mark.asyncio +@mock.patch.object( + test_module, + "get_connection", + mock.CoroutineMock(return_value=mock.MagicMock(spec=ConnRecord)) +) +async def test_handler(context, mock_responder, message): + """Test SendPresProposal handler.""" + await message.handle(context, mock_responder) + assert len(mock_responder.messages) == 2 + (prop, prop_recipient), (response, _) = mock_responder.messages + assert prop.presentation_proposal == TEST_PROPOSAL + assert prop.comment == TEST_COMMENT + assert prop_recipient["connection_id"] == TEST_CONN_ID + assert isinstance(response, test_module.PresExchange) + assert response.connection_id == TEST_CONN_ID diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 8a8555af..57b37821 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -31,7 +31,7 @@ CredentialProposalRequestSchema, CredExRecord, CredExRecordSchema, IndyCredPrecisSchema, PresentationPreview, PresentationProposalRequestSchema, PresExRecord, PresExRecordSchema, - issue_credential + issue_credential, present_proof ) PACKAGE = 'acapy_plugin_toolbox.holder.v0_1' @@ -368,7 +368,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): comment = context.message.comment # Aries#0037 calls it a proposal in the proposal struct but it's of type preview - presentation_proposal = proof.messages.presentation_proposal.PresentationProposal( + presentation_proposal = present_proof.messages.presentation_proposal.PresentationProposal( comment=comment, presentation_proposal=context.message.presentation_proposal ) @@ -377,7 +377,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.settings.get("debug.auto_respond_presentation_request") ) - presentation_manager = proof.manager.PresentationManager(context.profile) + presentation_manager = present_proof.manager.PresentationManager(context.profile) presentation_exchange_record = ( await presentation_manager.create_exchange_for_proposal( From 81ff630951bdfa1fd2a91349573132c8a6696a32 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 2 Mar 2021 13:45:54 -0500 Subject: [PATCH 27/59] chore: rearrange test structure to allow for better fixture usage Signed-off-by: Daniel Bluhm --- .../holder/tests/test_send_pres_proposal.py | 72 ------------------- .../holder/tests => tests}/__init__.py | 0 tests/conftest.py | 48 +++++++++++++ .../tests => tests/holder}/__init__.py | 0 .../tests => tests/holder}/test_events.py | 13 ++-- tests/holder/test_send_pres_proposal.py | 42 +++++++++++ .../tests => tests}/test_basicmessage.py | 10 +-- .../tests => tests}/test_util.py | 12 ++-- 8 files changed, 108 insertions(+), 89 deletions(-) delete mode 100644 acapy_plugin_toolbox/holder/tests/test_send_pres_proposal.py rename {acapy_plugin_toolbox/holder/tests => tests}/__init__.py (100%) create mode 100644 tests/conftest.py rename {acapy_plugin_toolbox/tests => tests/holder}/__init__.py (100%) rename {acapy_plugin_toolbox/holder/tests => tests/holder}/test_events.py (95%) create mode 100644 tests/holder/test_send_pres_proposal.py rename {acapy_plugin_toolbox/tests => tests}/test_basicmessage.py (84%) rename {acapy_plugin_toolbox/tests => tests}/test_util.py (95%) diff --git a/acapy_plugin_toolbox/holder/tests/test_send_pres_proposal.py b/acapy_plugin_toolbox/holder/tests/test_send_pres_proposal.py deleted file mode 100644 index 6335ac6f..00000000 --- a/acapy_plugin_toolbox/holder/tests/test_send_pres_proposal.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Test SendPresProposal message and handler.""" - -import pytest -from aries_cloudagent.connections.models.conn_record import ConnRecord -from aries_cloudagent.core.in_memory import InMemoryProfile -from aries_cloudagent.messaging.request_context import RequestContext -from aries_cloudagent.messaging.responder import MockResponder -from asynctest import mock - -from .. import v0_1 as test_module -from ..v0_1 import SendPresProposal - -TEST_CONN_ID = "test-connection-id" -TEST_PROPOSAL = "test-proposal" -TEST_COMMENT = "test-comment" - -@pytest.fixture -def mock_admin_connection(): - """Mock connection fixture.""" - connection = mock.MagicMock(spec=ConnRecord) - connection.metadata_get = mock.CoroutineMock(return_value="admin") - yield connection - - -@pytest.fixture -def message(): - """Message fixture.""" - yield SendPresProposal( - connection_id=TEST_CONN_ID, - presentation_proposal=TEST_PROPOSAL, - comment=TEST_COMMENT - ) - - -@pytest.fixture -def profile(): - """Profile fixture.""" - yield InMemoryProfile.test_profile() - - -@pytest.fixture -def context(profile, message, mock_admin_connection): - """RequestContext fixture.""" - context = RequestContext(profile) - context.message = message - context.connection_record = mock_admin_connection - context.connection_ready = True - yield context - - -@pytest.fixture -def mock_responder(): - """Mock responder fixture.""" - yield MockResponder() - - -@pytest.mark.asyncio -@mock.patch.object( - test_module, - "get_connection", - mock.CoroutineMock(return_value=mock.MagicMock(spec=ConnRecord)) -) -async def test_handler(context, mock_responder, message): - """Test SendPresProposal handler.""" - await message.handle(context, mock_responder) - assert len(mock_responder.messages) == 2 - (prop, prop_recipient), (response, _) = mock_responder.messages - assert prop.presentation_proposal == TEST_PROPOSAL - assert prop.comment == TEST_COMMENT - assert prop_recipient["connection_id"] == TEST_CONN_ID - assert isinstance(response, test_module.PresExchange) - assert response.connection_id == TEST_CONN_ID diff --git a/acapy_plugin_toolbox/holder/tests/__init__.py b/tests/__init__.py similarity index 100% rename from acapy_plugin_toolbox/holder/tests/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..99ce38d8 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,48 @@ +"""Common testing fixtures.""" +from contextlib import contextmanager +import pytest +from aries_cloudagent.connections.models.conn_record import ConnRecord +from aries_cloudagent.core.in_memory import InMemoryProfile +from aries_cloudagent.messaging.request_context import RequestContext +from aries_cloudagent.messaging.responder import MockResponder +from asynctest import mock + + +@pytest.fixture +def mock_admin_connection(): + """Mock connection fixture.""" + connection = mock.MagicMock(spec=ConnRecord) + connection.metadata_get = mock.CoroutineMock(return_value="admin") + yield connection + + +@pytest.fixture +def profile(): + """Profile fixture.""" + yield InMemoryProfile.test_profile() + + +@pytest.fixture +def context(profile, mock_admin_connection): + """RequestContext fixture.""" + context = RequestContext(profile) + context.connection_record = mock_admin_connection + context.connection_ready = True + yield context + + +@pytest.fixture +def mock_responder(): + """Mock responder fixture.""" + yield MockResponder() + + +@contextmanager +def mock_get_connection(module, conn: ConnRecord = None): + """Mock get_connection on a module""" + with mock.patch.object( + module, + "get_connection", + mock.CoroutineMock(return_value=conn or mock.MagicMock(spec=ConnRecord)) + ) as get_connection: + yield get_connection diff --git a/acapy_plugin_toolbox/tests/__init__.py b/tests/holder/__init__.py similarity index 100% rename from acapy_plugin_toolbox/tests/__init__.py rename to tests/holder/__init__.py diff --git a/acapy_plugin_toolbox/holder/tests/test_events.py b/tests/holder/test_events.py similarity index 95% rename from acapy_plugin_toolbox/holder/tests/test_events.py rename to tests/holder/test_events.py index 718b0df1..7e33d8bf 100644 --- a/acapy_plugin_toolbox/holder/tests/test_events.py +++ b/tests/holder/test_events.py @@ -3,16 +3,19 @@ # pylint: disable=redefined-outer-name import pytest -from aries_cloudagent.core.event_bus import EventBus, Event +from aries_cloudagent.core.event_bus import Event, EventBus from aries_cloudagent.core.in_memory import InMemoryProfile -from aries_cloudagent.core.profile import Profile from aries_cloudagent.core.protocol_registry import ProtocolRegistry from aries_cloudagent.messaging.responder import BaseResponder, MockResponder -from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import V10CredentialExchange -from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import V10PresentationExchange +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchange +) +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchange +) from asynctest import mock -from .. import v0_1 as test_module +from acapy_plugin_toolbox.holder import v0_1 as test_module @pytest.fixture diff --git a/tests/holder/test_send_pres_proposal.py b/tests/holder/test_send_pres_proposal.py new file mode 100644 index 00000000..dd2986a7 --- /dev/null +++ b/tests/holder/test_send_pres_proposal.py @@ -0,0 +1,42 @@ +"""Test SendPresProposal message and handler.""" + +import pytest + +from acapy_plugin_toolbox.holder import v0_1 as test_module +from acapy_plugin_toolbox.holder.v0_1 import SendPresProposal +from ..conftest import mock_get_connection + +TEST_CONN_ID = "test-connection-id" +TEST_PROPOSAL = "test-proposal" +TEST_COMMENT = "test-comment" + + +@pytest.fixture +def message(): + """Message fixture.""" + yield SendPresProposal( + connection_id=TEST_CONN_ID, + presentation_proposal=TEST_PROPOSAL, + comment=TEST_COMMENT + ) + + +@pytest.fixture +def context(context, message): + """Context fixture""" + context.message = message + yield context + + +@pytest.mark.asyncio +async def test_handler(context, mock_responder, message): + """Test SendPresProposal handler.""" + with mock_get_connection(test_module): + await message.handle(context, mock_responder) + assert len(mock_responder.messages) == 2 + (prop, prop_recipient), (response, _) = mock_responder.messages + assert prop.presentation_proposal == TEST_PROPOSAL + assert prop.comment == TEST_COMMENT + assert prop_recipient["connection_id"] == TEST_CONN_ID + assert isinstance(response, test_module.PresExchange) + assert response.connection_id == TEST_CONN_ID diff --git a/acapy_plugin_toolbox/tests/test_basicmessage.py b/tests/test_basicmessage.py similarity index 84% rename from acapy_plugin_toolbox/tests/test_basicmessage.py rename to tests/test_basicmessage.py index 8bbbb52e..7d3235e6 100644 --- a/acapy_plugin_toolbox/tests/test_basicmessage.py +++ b/tests/test_basicmessage.py @@ -1,18 +1,12 @@ """Test BasicMessage""" -from typing import Mapping import pytest -from aries_cloudagent.admin.request_context import AdminRequestContext from aries_cloudagent.core.event_bus import Event, EventBus from aries_cloudagent.core.in_memory import InMemoryProfile from aries_cloudagent.core.protocol_registry import ProtocolRegistry from aries_cloudagent.messaging.responder import BaseResponder, MockResponder -from aries_cloudagent.protocols.basicmessage.v1_0.messages.basicmessage import ( - BasicMessage -) -from asynctest import mock -from .. import basicmessage as basicmessage_module +from acapy_plugin_toolbox import basicmessage as basicmessage_module @pytest.fixture @@ -62,7 +56,7 @@ async def test_basic_message_event_handler_notify_admins( ): await basicmessage_module.setup(context) - assert mock_send_to_admins.message == None + assert mock_send_to_admins.message is None await event_bus.notify( profile, diff --git a/acapy_plugin_toolbox/tests/test_util.py b/tests/test_util.py similarity index 95% rename from acapy_plugin_toolbox/tests/test_util.py rename to tests/test_util.py index 8ed26e63..ce5f9991 100644 --- a/acapy_plugin_toolbox/tests/test_util.py +++ b/tests/test_util.py @@ -1,11 +1,15 @@ """Test utilities.""" -import pytest -from marshmallow import fields -from aries_cloudagent.messaging.agent_message import AgentMessage, AgentMessageSchema +import pytest +from aries_cloudagent.messaging.agent_message import ( + AgentMessage, AgentMessageSchema +) from aries_cloudagent.messaging.models.base import BaseModel, BaseModelSchema +from marshmallow import fields -from ..util import expand_message_class, expand_model_class, PassHandler +from acapy_plugin_toolbox.util import ( + PassHandler, expand_message_class, expand_model_class +) def test_expand_message_class(): From d979cb20c7ef22725cc2bc25a9bb7dd76e553044 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 2 Mar 2021 14:13:25 -0500 Subject: [PATCH 28/59] test: add more fixtures to conftest and use where appropriate Signed-off-by: Daniel Bluhm --- tests/conftest.py | 72 ++++++++++++++++----- tests/holder/test_send_pres_proposal.py | 3 +- tests/test_basicmessage.py | 83 +++++++------------------ 3 files changed, 81 insertions(+), 77 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 99ce38d8..afe1bfbf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,9 @@ from aries_cloudagent.connections.models.conn_record import ConnRecord from aries_cloudagent.core.in_memory import InMemoryProfile from aries_cloudagent.messaging.request_context import RequestContext -from aries_cloudagent.messaging.responder import MockResponder +from aries_cloudagent.messaging.responder import BaseResponder, MockResponder +from aries_cloudagent.core.event_bus import EventBus +from aries_cloudagent.core.protocol_registry import ProtocolRegistry from asynctest import mock @@ -17,9 +19,25 @@ def mock_admin_connection(): @pytest.fixture -def profile(): +def event_bus(): + """Event bus fixture.""" + yield EventBus() + + +@pytest.fixture +def mock_responder(): + """Mock responder fixture.""" + yield MockResponder() + + +@pytest.fixture +def profile(event_bus, mock_responder): """Profile fixture.""" - yield InMemoryProfile.test_profile() + yield InMemoryProfile.test_profile(bind={ + EventBus: event_bus, + BaseResponder: mock_responder, + ProtocolRegistry: ProtocolRegistry(), + }) @pytest.fixture @@ -32,17 +50,41 @@ def context(profile, mock_admin_connection): @pytest.fixture -def mock_responder(): - """Mock responder fixture.""" - yield MockResponder() +def mock_get_connection(): + """Mock get_connection on a module""" + @contextmanager + def _mock_get_connection(module, conn: ConnRecord = None): + with mock.patch.object( + module, + "get_connection", + mock.CoroutineMock(return_value=conn or mock.MagicMock(spec=ConnRecord)) + ) as get_connection: + yield get_connection + yield _mock_get_connection -@contextmanager -def mock_get_connection(module, conn: ConnRecord = None): - """Mock get_connection on a module""" - with mock.patch.object( - module, - "get_connection", - mock.CoroutineMock(return_value=conn or mock.MagicMock(spec=ConnRecord)) - ) as get_connection: - yield get_connection +class MockSendToAdmins: + """Mock send_to_admins method.""" + + def __init__(self): + self.message = None + + async def __call__( + self, session, message, responder, to_session_only: bool = False + ): + self.message = message + + +@pytest.fixture +def mock_send_to_admins(): + """Mock send to admins factory fixture. + + Benefit of making this a fixture is primarily for ease of use. + """ + @contextmanager + def _mock_send_to_admins(module): + temp = module.send_to_admins + module.send_to_admins = MockSendToAdmins() + yield module.send_to_admins + module.send_to_admins = temp + yield _mock_send_to_admins diff --git a/tests/holder/test_send_pres_proposal.py b/tests/holder/test_send_pres_proposal.py index dd2986a7..df059fc9 100644 --- a/tests/holder/test_send_pres_proposal.py +++ b/tests/holder/test_send_pres_proposal.py @@ -4,7 +4,6 @@ from acapy_plugin_toolbox.holder import v0_1 as test_module from acapy_plugin_toolbox.holder.v0_1 import SendPresProposal -from ..conftest import mock_get_connection TEST_CONN_ID = "test-connection-id" TEST_PROPOSAL = "test-proposal" @@ -29,7 +28,7 @@ def context(context, message): @pytest.mark.asyncio -async def test_handler(context, mock_responder, message): +async def test_handler(context, mock_responder, message, mock_get_connection): """Test SendPresProposal handler.""" with mock_get_connection(test_module): await message.handle(context, mock_responder) diff --git a/tests/test_basicmessage.py b/tests/test_basicmessage.py index 7d3235e6..fb7138a7 100644 --- a/tests/test_basicmessage.py +++ b/tests/test_basicmessage.py @@ -1,74 +1,37 @@ """Test BasicMessage""" import pytest -from aries_cloudagent.core.event_bus import Event, EventBus -from aries_cloudagent.core.in_memory import InMemoryProfile -from aries_cloudagent.core.protocol_registry import ProtocolRegistry -from aries_cloudagent.messaging.responder import BaseResponder, MockResponder +from aries_cloudagent.core.event_bus import Event from acapy_plugin_toolbox import basicmessage as basicmessage_module @pytest.fixture -def event_bus(): - """Event bus fixture.""" - yield EventBus() - - -@pytest.fixture -def profile(event_bus): - """Context fixture.""" - yield InMemoryProfile.test_profile(bind={ - EventBus: event_bus, - BaseResponder: MockResponder(), - ProtocolRegistry: ProtocolRegistry(), - }) - - -@pytest.fixture -def context(profile): +def injection_context(profile): + """Injection context fixture.""" yield profile.context -class MockSendToAdmins: - """Mock send_to_admins method.""" - - def __init__(self): - self.message = None - - async def __call__( - self, session, message, responder, to_session_only: bool = False - ): - self.message = message - - -@pytest.fixture -def mock_send_to_admins(): - temp = basicmessage_module.send_to_admins - basicmessage_module.send_to_admins = MockSendToAdmins() - yield basicmessage_module.send_to_admins - basicmessage_module.send_to_admins = temp - - @pytest.mark.asyncio async def test_basic_message_event_handler_notify_admins( - mock_send_to_admins, event_bus, profile, context + event_bus, profile, injection_context, mock_send_to_admins ): - await basicmessage_module.setup(context) - - assert mock_send_to_admins.message is None - - await event_bus.notify( - profile, - Event( - "basicmessages", - { - "connection_id": "connection-1", - "message_id": "test id", - "content": "Hello world", - "state": "received", - }, - ), - ) - - assert mock_send_to_admins.message.message.content == "Hello world" + with mock_send_to_admins(basicmessage_module) as send_to_admins: + await basicmessage_module.setup(injection_context) + + assert send_to_admins.message is None + + await event_bus.notify( + profile, + Event( + "basicmessages", + { + "connection_id": "connection-1", + "message_id": "test id", + "content": "Hello world", + "state": "received", + }, + ), + ) + + assert send_to_admins.message.message.content == "Hello world" From 85161abbaac364bad7b0c2d32714d17a4647412d Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 4 Mar 2021 21:35:36 -0500 Subject: [PATCH 29/59] fix: add descriptions and examples to paginate Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/decorators/pagination.py | 15 ++++++++++++--- acapy_plugin_toolbox/holder/v0_1.py | 13 ++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/acapy_plugin_toolbox/decorators/pagination.py b/acapy_plugin_toolbox/decorators/pagination.py index 683975e6..f58f0cf7 100644 --- a/acapy_plugin_toolbox/decorators/pagination.py +++ b/acapy_plugin_toolbox/decorators/pagination.py @@ -32,9 +32,18 @@ class Paginate(BaseModel): """Paginate decorator for messages querying for a paginated object.""" class Fields: - """Fields of paginate decorator.""" - limit = fields.Int(required=True) - offset = fields.Int(required=False, missing=0) + """Pagination decorator.""" + limit = fields.Int( + required=True, + description="return at most n items in paginated results", + example=10 + ) + offset = fields.Int( + required=False, + missing=0, + description="Offset returned results by n items", + example=20 + ) def __init__(self, limit: int = 0, offset: int = 0, **kwargs): super().__init__(**kwargs) diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 57b37821..7ed22f25 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -34,8 +34,6 @@ issue_credential, present_proof ) -PACKAGE = 'acapy_plugin_toolbox.holder.v0_1' - @expand_model_class class CredentialRepresentation(BaseModel): @@ -446,21 +444,26 @@ async def handle(self, context: RequestContext, responder: BaseResponder): """Handle presentation request approved message.""" +PROTOCOL = AdminHolderMessage.protocol +TITLE = "Holder Admin Protocol" +NAME = "admin-holder" +VERSION = "0.1" MESSAGE_TYPES = { msg_class.Meta.message_type: '{}.{}'.format(msg_class.__module__, msg_class.__name__) for msg_class in [ - CredExchange, CredGetList, CredList, CredOfferAccept, CredOfferRecv, CredRequestSent, - PresExchange, + CredReceived, + SendCredProposal, + CredExchange, PresGetList, PresList, PresRequestApprove, - SendCredProposal, SendPresProposal, + PresExchange, ] } From 0631d1438edaea006ae7d68fdb10df1dabf12f86 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 5 Mar 2021 14:51:05 -0500 Subject: [PATCH 30/59] test: add tests for PresGetList Signed-off-by: Daniel Bluhm --- tests/holder/conftest.py | 22 +++++++++++++++ tests/holder/test_pres_get_list.py | 45 ++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/holder/conftest.py create mode 100644 tests/holder/test_pres_get_list.py diff --git a/tests/holder/conftest.py b/tests/holder/conftest.py new file mode 100644 index 00000000..efca20c2 --- /dev/null +++ b/tests/holder/conftest.py @@ -0,0 +1,22 @@ +"""Common fixtures for holder protocol.""" + +from contextlib import contextmanager + +import pytest +from asynctest import mock + + +@pytest.fixture +def mock_record_query(): + """Mock PresExRecord.query on a module.""" + @contextmanager + def _mock_record_query(obj, result=None, spec=None): + with mock.patch.object( + obj, "query", + mock.CoroutineMock( + return_value=result or + mock.MagicMock(spec=spec) + ) + ) as record_query: + yield record_query + yield _mock_record_query diff --git a/tests/holder/test_pres_get_list.py b/tests/holder/test_pres_get_list.py new file mode 100644 index 00000000..a7b249b1 --- /dev/null +++ b/tests/holder/test_pres_get_list.py @@ -0,0 +1,45 @@ +"""Test PresGetList message and handler.""" +import pytest +from acapy_plugin_toolbox.holder import v0_1 as test_module +from acapy_plugin_toolbox.holder.v0_1 import PresGetList, PresList + +TEST_CONN_ID = "test-connection-id" + + +@pytest.fixture +def pres_record(): + """Factory for test presentation records.""" + def _pres_record(): + return test_module.PresExRecord() + yield _pres_record + + +@pytest.fixture +def message(): + """Message fixture.""" + yield PresGetList( + connection_id=TEST_CONN_ID + ) + + +@pytest.fixture +def context(context, message): + """Context fixture.""" + context.message = message + yield context + + +@pytest.mark.asyncio +async def test_handler(context, mock_responder, message, mock_record_query, pres_record): + """Test PresGetList handler.""" + rec1 = pres_record() + with mock_record_query( + test_module.PresExRecord, [rec1], spec=test_module.PresExRecord + ) as record_query: + await message.handle(context, mock_responder) + record_query.assert_called_once() + assert len(mock_responder.messages) == 1 + pres_list, _ = mock_responder.messages[0] + assert isinstance(pres_list, PresList) + assert pres_list.serialize() + assert pres_list.results == [rec1.serialize()] From a43c0ec74f1b93765e4349840ddc12f38195477b Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 5 Mar 2021 14:51:34 -0500 Subject: [PATCH 31/59] fix: pagination limit < 1 means get all Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/decorators/pagination.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/acapy_plugin_toolbox/decorators/pagination.py b/acapy_plugin_toolbox/decorators/pagination.py index f58f0cf7..b0e2d83a 100644 --- a/acapy_plugin_toolbox/decorators/pagination.py +++ b/acapy_plugin_toolbox/decorators/pagination.py @@ -52,7 +52,8 @@ def __init__(self, limit: int = 0, offset: int = 0, **kwargs): def apply(self, items: list) -> Tuple[Sequence[Any], Page]: """Apply pagination to list.""" - end = self.offset + self.limit + limit = self.limit if self.limit >= 1 else len(items[self.offset:]) + end = self.offset + limit result = items[self.offset:end] remaining = len(items[end:]) page = Page(len(result), self.offset, remaining) From 6d32e89d2679412342998bd9512cc7d251ad8873 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 5 Mar 2021 14:52:24 -0500 Subject: [PATCH 32/59] feat: add logging to holder handlers Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/holder/v0_1.py | 22 +++++++++++++++++----- acapy_plugin_toolbox/util.py | 16 ++++++++++++++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 7ed22f25..f0196985 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -5,6 +5,7 @@ import re from typing import Sequence +import logging from aries_cloudagent.config.injection_context import InjectionContext from aries_cloudagent.connections.models.conn_record import ConnRecord @@ -24,7 +25,8 @@ from ..decorators.pagination import Page, Paginate from ..util import ( ExceptionReporter, InvalidConnection, admin_only, expand_message_class, - expand_model_class, get_connection, send_to_admins, with_generic_init + expand_model_class, get_connection, send_to_admins, with_generic_init, + log_handling ) from . import ( CredentialAttributeSpec, CredentialManager, CredentialManagerError, @@ -35,6 +37,9 @@ ) +LOGGER = logging.getLogger(__name__) + + @expand_model_class class CredentialRepresentation(BaseModel): """Representation of Credentials in messages.""" @@ -95,13 +100,14 @@ def __init__(self, paginate: Paginate = None, **kwargs): super().__init__(**kwargs) self.paginate = paginate + @log_handling @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received get cred list request.""" session = await context.session() credentials = await CredExRecord.query(session) - page = self.paginate.apply(credentials) + credentials, page = self.paginate.apply(credentials) cred_list = CredList( results=[credential.serialize() for credential in credentials], @@ -138,6 +144,7 @@ class SendCredProposal(AdminHolderMessage): message_type = "send-credential-proposal" fields_from = CredentialProposalRequestSchema + @log_handling @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received send proposal request.""" @@ -220,6 +227,7 @@ def __init__(self, credential_exchange_id: str = None, **kwargs): super().__init__(**kwargs) self.credential_exchange_id = credential_exchange_id + @log_handling @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): """Handle credential offer accept message.""" @@ -294,8 +302,9 @@ def __init__( super().__init__(**kwargs) self.connection_id = connection_id self.verified = verified - self.paginate = paginate + self.paginate = paginate or Paginate() + @log_handling @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received get cred list request.""" @@ -313,8 +322,9 @@ async def handle(self, context: RequestContext, responder: BaseResponder): records = await PresExRecord.query( session, {}, post_filter_positive=post_filter_positive ) - cred_list = PresList(*paginate.apply(records)) - await responder.send_reply(cred_list) + records, page = paginate.apply(records) + pres_list = PresList([record.serialize() for record in records], page=page) + await responder.send_reply(pres_list) @expand_message_class @@ -356,6 +366,7 @@ def __init__( self.auto_present = auto_present self.trace = trace + @log_handling @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received send presentation proposal request.""" @@ -439,6 +450,7 @@ class Fields: """Fields on pres request approve message.""" presentation_exchange_id = fields.Str(required=True) + @log_handling @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): """Handle presentation request approved message.""" diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index 5c66caba..dcfc7abb 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -26,6 +26,8 @@ ProblemReport ) +LOGGER = logging.getLogger(__name__) + def timestamp_utc_iso(timespec: str = 'seconds') -> str: """Timestamp in UTC in ISO 8601 format. @@ -84,6 +86,15 @@ def admin_only(func): return require_role('admin')(func) +def log_handling(func): + @functools.wraps(func) + async def _logged(*args): + context, *_ = [arg for arg in args if isinstance(arg, RequestContext)] + LOGGER.debug("%s called with message: %s", func, context.message) + return await func(*args) + return _logged + + def expand_message_class(cls): """Class decorator for removing boilerplate of AgentMessages.""" # pylint: disable=protected-access @@ -307,8 +318,7 @@ def load_path(cls): async def handle(self, context: RequestContext, _responder): """Handle messages require no handling.""" # pylint: disable=protected-access - logger = logging.getLogger(__name__) - logger.debug( + LOGGER.info( "Pass: Not handling message of type %s", context.message._type ) @@ -330,6 +340,7 @@ async def admin_connections(session: ProfileSession): await ConnRecord.retrieve_by_id(session, id) for id in admin_ids ] + LOGGER.info("Discovered admins: %s", admin_connections) return admins @@ -340,6 +351,7 @@ async def send_to_admins( to_session_only: bool = False ): """Send a message to all admin connections.""" + LOGGER.info("Sending message to admins: %s", message) admins = await admin_connections(session) admins = list(filter(lambda admin: admin.state == 'active', admins)) connection_mgr = ConnectionManager(session) From b67827e8e009e016ac5ac83bf1b5ddeb321367a8 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 5 Mar 2021 15:15:52 -0500 Subject: [PATCH 33/59] fix: use decorated handler's module logger Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/util.py | 5 ++++- tests/holder/test_pres_get_list.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index dcfc7abb..4c8002e5 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -87,10 +87,13 @@ def admin_only(func): def log_handling(func): + """Logging decorator for handlers.""" + logger = logging.getLogger(func.__module__) + @functools.wraps(func) async def _logged(*args): context, *_ = [arg for arg in args if isinstance(arg, RequestContext)] - LOGGER.debug("%s called with message: %s", func, context.message) + logger.debug("%s called with message: %s", func.__qualname__, context.message) return await func(*args) return _logged diff --git a/tests/holder/test_pres_get_list.py b/tests/holder/test_pres_get_list.py index a7b249b1..6f78fbeb 100644 --- a/tests/holder/test_pres_get_list.py +++ b/tests/holder/test_pres_get_list.py @@ -30,7 +30,9 @@ def context(context, message): @pytest.mark.asyncio -async def test_handler(context, mock_responder, message, mock_record_query, pres_record): +async def test_handler( + context, mock_responder, message, mock_record_query, pres_record +): """Test PresGetList handler.""" rec1 = pres_record() with mock_record_query( @@ -43,3 +45,5 @@ async def test_handler(context, mock_responder, message, mock_record_query, pres assert isinstance(pres_list, PresList) assert pres_list.serialize() assert pres_list.results == [rec1.serialize()] + assert pres_list.page is not None + assert pres_list.page.count == 1 From 0794ebc603bbaf7520cfa0d84d28a06bebf9d76a Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 5 Mar 2021 18:59:46 -0500 Subject: [PATCH 34/59] refactor: remove groups for now Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/__init__.py | 27 +++++++++++++++-- acapy_plugin_toolbox/group/__init__.py | 0 acapy_plugin_toolbox/group/all.py | 35 ----------------------- acapy_plugin_toolbox/group/connections.py | 15 ---------- acapy_plugin_toolbox/group/holder.py | 14 --------- acapy_plugin_toolbox/group/issuance.py | 18 ------------ acapy_plugin_toolbox/util.py | 2 +- 7 files changed, 26 insertions(+), 85 deletions(-) delete mode 100644 acapy_plugin_toolbox/group/__init__.py delete mode 100644 acapy_plugin_toolbox/group/all.py delete mode 100644 acapy_plugin_toolbox/group/connections.py delete mode 100644 acapy_plugin_toolbox/group/holder.py delete mode 100644 acapy_plugin_toolbox/group/issuance.py diff --git a/acapy_plugin_toolbox/__init__.py b/acapy_plugin_toolbox/__init__.py index d8295772..4324e5a8 100644 --- a/acapy_plugin_toolbox/__init__.py +++ b/acapy_plugin_toolbox/__init__.py @@ -1,5 +1,28 @@ """Shortcut to group all and rexports.""" -from aries_cloudagent.protocols.problem_report.v1_0.message import ProblemReport +import os +import logging -from .group.all import setup +from aries_cloudagent.protocols.problem_report.v1_0.message import ( + ProblemReport +) +from aries_cloudagent.config.injection_context import InjectionContext + +from . import ( + basicmessage, connections, credential_definitions, dids, invitations, + issuer, mediator, routing, schemas, static_connections, taa +) + +MODULES = [ + basicmessage, connections, credential_definitions, dids, invitations, + issuer, mediator, routing, schemas, static_connections, taa +] + + +async def setup(context: InjectionContext): + """Load Toolbox Plugin.""" + log_level = os.environ.get("ACAPY_TOOLBOX_LOG_LEVEL", logging.WARNING) + logging.getLogger("acapy_plugin_toolbox").setLevel(log_level) + print("Setting logging level of acapy_plugin_toolbox to", log_level) + for mod in MODULES: + await mod.setup(context) diff --git a/acapy_plugin_toolbox/group/__init__.py b/acapy_plugin_toolbox/group/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/acapy_plugin_toolbox/group/all.py b/acapy_plugin_toolbox/group/all.py deleted file mode 100644 index 3de02761..00000000 --- a/acapy_plugin_toolbox/group/all.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Load all plugins.""" - -from aries_cloudagent.core.profile import ProfileSession -from aries_cloudagent.core.protocol_registry import ProtocolRegistry - -from ..connections import setup as connection_setup -from ..credential_definitions import setup as cred_def_setup -from ..schemas import setup as schema_setup -from ..dids import setup as did_setup -from ..static_connections import setup as static_conn_setup -from ..holder.v0_1 import setup as holder_setup -from ..issuer import setup as issuer_setup -from ..basicmessage import setup as basic_message_setup -from ..taa import setup as taa_setup -#from ..payments import setup as payment_setup -from ..invitations import setup as invitations_setup -from ..mediator import setup as mediator_setup -from ..routing import setup as routing_setup - -async def setup(session: ProfileSession): - """Setup Toolbox Plugin.""" - protocol_registry = session.inject(ProtocolRegistry) - await connection_setup(session, protocol_registry) - await cred_def_setup(session, protocol_registry) - await schema_setup(session, protocol_registry) - await did_setup(session, protocol_registry) - await static_conn_setup(session, protocol_registry) - await holder_setup(session, protocol_registry) - await issuer_setup(session, protocol_registry) - await basic_message_setup(session, protocol_registry) - await taa_setup(session, protocol_registry) -# await payment_setup(session, protocol_registry) - await invitations_setup(session, protocol_registry) - await mediator_setup(session, protocol_registry) - await routing_setup(session, protocol_registry) diff --git a/acapy_plugin_toolbox/group/connections.py b/acapy_plugin_toolbox/group/connections.py deleted file mode 100644 index a7377d8a..00000000 --- a/acapy_plugin_toolbox/group/connections.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Load connections plugins.""" - -from aries_cloudagent.core.profile import ProfileSession -from aries_cloudagent.core.protocol_registry import ProtocolRegistry - -from ..connections import setup as connection_setup -from ..static_connections import setup as static_conn_setup -from ..invitations import setup as invitations_setup - -async def setup(session: ProfileSession): - """Setup Toolbox Plugin.""" - protocol_registry = session.inject(ProtocolRegistry) - await connection_setup(session, protocol_registry) - await static_conn_setup(session, protocol_registry) - await invitations_setup(session, protocol_registry) diff --git a/acapy_plugin_toolbox/group/holder.py b/acapy_plugin_toolbox/group/holder.py deleted file mode 100644 index d91dfd21..00000000 --- a/acapy_plugin_toolbox/group/holder.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Load holder plugins.""" - -from aries_cloudagent.core.profile import ProfileSession -from aries_cloudagent.core.protocol_registry import ProtocolRegistry - -from ..credential_definitions import setup as cred_def_setup -from ..holder.v0_1 import setup as holder_setup - - -async def setup(session: ProfileSession): - """Setup Toolbox Plugin.""" - protocol_registry = session.inject(ProtocolRegistry) - await cred_def_setup(session, protocol_registry) - await holder_setup(session, protocol_registry) diff --git a/acapy_plugin_toolbox/group/issuance.py b/acapy_plugin_toolbox/group/issuance.py deleted file mode 100644 index 6cbb76d5..00000000 --- a/acapy_plugin_toolbox/group/issuance.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Load issuance plugins.""" - -from aries_cloudagent.core.profile import ProfileSession -from aries_cloudagent.core.protocol_registry import ProtocolRegistry - -from ..credential_definitions import setup as cred_def_setup -from ..schemas import setup as schema_setup -from ..dids import setup as did_setup -from ..issuer import setup as issuer_setup - - -async def setup(session: ProfileSession): - """Setup Toolbox Plugin.""" - protocol_registry = session.inject(ProtocolRegistry) - await cred_def_setup(session, protocol_registry) - await schema_setup(session, protocol_registry) - await did_setup(session, protocol_registry) - await issuer_setup(session, protocol_registry) diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index 4c8002e5..4473ca0c 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -354,7 +354,7 @@ async def send_to_admins( to_session_only: bool = False ): """Send a message to all admin connections.""" - LOGGER.info("Sending message to admins: %s", message) + LOGGER.info("Sending message to admins: %s", message.serialize()) admins = await admin_connections(session) admins = list(filter(lambda admin: admin.state == 'active', admins)) connection_mgr = ConnectionManager(session) From bf33668a6e15b062519464e3177b1b57b39de3ed Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Mon, 8 Mar 2021 17:53:22 -0500 Subject: [PATCH 35/59] fix: logging output Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/__init__.py | 3 ++- acapy_plugin_toolbox/holder/v0_1.py | 5 +++++ acapy_plugin_toolbox/util.py | 2 +- demo/configs/alice.yml | 1 + demo/configs/bob.yml | 1 + demo/configs/default.yml | 1 + demo/configs/mediator.yml | 1 + demo/docker-compose.alice-bob.yml | 4 ++++ docker/default.yml | 1 + 9 files changed, 17 insertions(+), 2 deletions(-) diff --git a/acapy_plugin_toolbox/__init__.py b/acapy_plugin_toolbox/__init__.py index 4324e5a8..a7455cb0 100644 --- a/acapy_plugin_toolbox/__init__.py +++ b/acapy_plugin_toolbox/__init__.py @@ -12,10 +12,11 @@ basicmessage, connections, credential_definitions, dids, invitations, issuer, mediator, routing, schemas, static_connections, taa ) +from .holder import v0_1 as holder MODULES = [ basicmessage, connections, credential_definitions, dids, invitations, - issuer, mediator, routing, schemas, static_connections, taa + issuer, mediator, routing, schemas, static_connections, taa, holder ] diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index f0196985..af726322 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -504,6 +504,7 @@ async def setup( async def issue_credential_event_handler(profile: Profile, event: Event): """Handle issue credential events.""" record: CredExRecord = CredExRecord.deserialize(event.payload) + LOGGER.debug("IssueCredential Event; %s: %s", event.topic, event.payload) if record.state not in ( CredExRecord.STATE_OFFER_RECEIVED, @@ -515,9 +516,11 @@ async def issue_credential_event_handler(profile: Profile, event: Event): message = None if record.state == CredExRecord.STATE_OFFER_RECEIVED: message = CredOfferRecv(**record.serialize()) + LOGGER.debug("Prepared Message: %s", message.serialize()) if record.state == CredExRecord.STATE_CREDENTIAL_RECEIVED: message = CredReceived(**record.serialize()) + LOGGER.debug("Prepared Message: %s", message.serialize()) async with profile.session() as session: await send_to_admins( @@ -530,10 +533,12 @@ async def issue_credential_event_handler(profile: Profile, event: Event): async def present_proof_event_handler(profile: Profile, event: Event): """Handle present proof events.""" record: PresExRecord = PresExRecord.deserialize(event.payload) + LOGGER.debug("PresentProof Event; %s: %s", event.topic, event.payload) if record.state == PresExRecord.STATE_REQUEST_RECEIVED: responder = profile.inject(BaseResponder) message = PresRequestReceived(record) + LOGGER.debug("Prepared Message: %s", message.serialize()) await message.retrieve_matching_credentials(profile) async with profile.session() as session: await send_to_admins(session, message, responder) diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index 4473ca0c..be8a4b79 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -343,7 +343,7 @@ async def admin_connections(session: ProfileSession): await ConnRecord.retrieve_by_id(session, id) for id in admin_ids ] - LOGGER.info("Discovered admins: %s", admin_connections) + LOGGER.info("Discovered admins: %s", admins) return admins diff --git a/demo/configs/alice.yml b/demo/configs/alice.yml index 96e6edd0..ccc2d363 100644 --- a/demo/configs/alice.yml +++ b/demo/configs/alice.yml @@ -28,6 +28,7 @@ debug-credentials: true debug-presentations: true auto-accept-invites: true auto-accept-requests: true +auto-ping-connection: true connections-invite: true invite-metadata-json: '{"group": "admin"}' diff --git a/demo/configs/bob.yml b/demo/configs/bob.yml index 4b758ca7..67eef5c0 100644 --- a/demo/configs/bob.yml +++ b/demo/configs/bob.yml @@ -28,6 +28,7 @@ debug-credentials: true debug-presentations: true auto-accept-invites: true auto-accept-requests: true +auto-ping-connection: true connections-invite: true invite-metadata-json: '{"group": "admin"}' diff --git a/demo/configs/default.yml b/demo/configs/default.yml index eb8615c9..962aca54 100644 --- a/demo/configs/default.yml +++ b/demo/configs/default.yml @@ -26,6 +26,7 @@ debug-credentials: true debug-presentations: true auto-accept-invites: true auto-accept-requests: true +auto-ping-connection: true # Generate Admin Invitation connections-invite: true diff --git a/demo/configs/mediator.yml b/demo/configs/mediator.yml index 75ee6731..d3904e4e 100644 --- a/demo/configs/mediator.yml +++ b/demo/configs/mediator.yml @@ -25,6 +25,7 @@ connections-invite: true invite-multi-use: true auto-accept-invites: true auto-accept-requests: true +auto-ping-connection: true # Mediation open-mediation: true diff --git a/demo/docker-compose.alice-bob.yml b/demo/docker-compose.alice-bob.yml index 11e89954..f5dccb95 100644 --- a/demo/docker-compose.alice-bob.yml +++ b/demo/docker-compose.alice-bob.yml @@ -10,6 +10,8 @@ services: dockerfile: ./demo/Dockerfile.demo environment: NGROK_NAME: ngrok-alice + # Modify for more log output + ACAPY_TOOLBOX_LOG_LEVEL: WARNING ports: - "3001:3001" command: aca-py start --arg-file ./configs/alice.yml @@ -21,6 +23,8 @@ services: image: acapy-toolbox-ngrok environment: NGROK_NAME: ngrok-bob + # Modify for more log output + ACAPY_TOOLBOX_LOG_LEVEL: WARNING ports: - "3003:3003" command: aca-py start --arg-file ./configs/bob.yml diff --git a/docker/default.yml b/docker/default.yml index 7a640697..ca0a2b95 100644 --- a/docker/default.yml +++ b/docker/default.yml @@ -9,6 +9,7 @@ admin-insecure-mode: true debug-connections: true auto-accept-invites: true auto-accept-requests: true +auto-ping-connection: true connections-invite: true label: Aries Cloud Agent + Toolbox Plugin invite-label: ACA-Py (Admin) From dec90f9e8457f9ad7b9f4ac0da908ecb977ca680 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Sat, 13 Mar 2021 21:53:03 -0500 Subject: [PATCH 36/59] chore: update requirements.txt to point at event bus branch Signed-off-by: Daniel Bluhm --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a9b649a4..0fe6e670 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aries-cloudagent[indy]@git+https://github.com/hyperledger/aries-cloudagent-python@1a276b521301021f7faacdf231851983b0b4d862 +aries-cloudagent[indy]@git+https://github.com/indicio-tech/aries-cloudagent-python@10b8134ddb9bc6a26058c929c35ec7b230787174 marshmallow==3.5.1 flake8 python-dateutil From 2a529d778bbc392fed294120d895491944f8ae78 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Mon, 15 Mar 2021 22:07:00 -0400 Subject: [PATCH 37/59] fix: invitations returned in connections retrieval fixes KABN-77 Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/connections.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/acapy_plugin_toolbox/connections.py b/acapy_plugin_toolbox/connections.py index 55664ded..7830b021 100644 --- a/acapy_plugin_toolbox/connections.py +++ b/acapy_plugin_toolbox/connections.py @@ -187,9 +187,13 @@ async def handle(self, context: RequestContext, responder: BaseResponder): 'their_did': context.message.their_did, }.items()) ) + # Filter out invitations, admin-invitations will handle those + post_filter_negative = { + "state": ConnRecord.State.INVITATION.rfc160 + } # TODO: Filter on state (needs mapping back to ACA-Py connection states) records = await ConnRecord.query( - session, tag_filter + session, tag_filter, post_filter_negative=post_filter_negative ) results = [ Connection(**conn_record_to_message_repr(record)) From 2fe61c58477c4cf66625ba9da2f91da5ac609157 Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Fri, 12 Mar 2021 16:52:04 -0700 Subject: [PATCH 38/59] Ability to filter credentials-get-list states Signed-off-by: Matthew Wright --- acapy_plugin_toolbox/holder/v0_1.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index af726322..566df579 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -4,7 +4,7 @@ # pylint: disable=too-few-public-methods import re -from typing import Sequence +from typing import Sequence, List, Optional import logging from aries_cloudagent.config.injection_context import InjectionContext @@ -95,10 +95,17 @@ class Fields: data_key="~paginate", missing=Paginate(limit=10, offset=0) ) + states = fields.List(fields.Str(required=True), required=False) - def __init__(self, paginate: Paginate = None, **kwargs): + def __init__( + self, + paginate: Paginate = None, + states: Optional[List[str]] = None, + **kwargs + ): super().__init__(**kwargs) self.paginate = paginate + self.states = states @log_handling @admin_only @@ -107,6 +114,10 @@ async def handle(self, context: RequestContext, responder: BaseResponder): session = await context.session() credentials = await CredExRecord.query(session) + + if self.states: + credentials = [c for c in credentials if c.state in self.states] + credentials, page = self.paginate.apply(credentials) cred_list = CredList( From 5cbfc39ed6646bfdba52277ab7c1de45f3cc10ef Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 30 Mar 2021 13:42:59 -0400 Subject: [PATCH 39/59] feat: add presentation approve message and test Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/holder/v0_1.py | 133 +++++++++++++++++++++++++++- acapy_plugin_toolbox/util.py | 7 +- tests/holder/conftest.py | 18 ++++ tests/holder/test_pres_approve.py | 83 +++++++++++++++++ 4 files changed, 234 insertions(+), 7 deletions(-) create mode 100644 tests/holder/test_pres_approve.py diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 566df579..f3d05586 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -4,15 +4,17 @@ # pylint: disable=too-few-public-methods import re -from typing import Sequence, List, Optional +from typing import Sequence, List, Optional, cast import logging from aries_cloudagent.config.injection_context import InjectionContext from aries_cloudagent.connections.models.conn_record import ConnRecord from aries_cloudagent.core.event_bus import Event, EventBus -from aries_cloudagent.core.profile import Profile +from aries_cloudagent.core.profile import Profile, ProfileSession from aries_cloudagent.core.protocol_registry import ProtocolRegistry -from aries_cloudagent.indy.holder import IndyHolder +from aries_cloudagent.indy.holder import IndyHolder, IndyHolderError +from aries_cloudagent.ledger.error import LedgerError +from aries_cloudagent.wallet.error import WalletNotFoundError from aries_cloudagent.messaging.agent_message import AgentMessage from aries_cloudagent.messaging.base_handler import ( BaseResponder, RequestContext @@ -33,7 +35,7 @@ CredentialProposalRequestSchema, CredExRecord, CredExRecordSchema, IndyCredPrecisSchema, PresentationPreview, PresentationProposalRequestSchema, PresExRecord, PresExRecordSchema, - issue_credential, present_proof + issue_credential, present_proof, PresentationManager ) @@ -452,6 +454,10 @@ async def retrieve_matching_credentials(self, profile: Profile): self.page = Page(count_=self.DEFAULT_COUNT, offset=self.DEFAULT_COUNT) +class InvalidPresentationExchange(Exception): + """Raised when given Presentation Exchange ID or record is not valid.""" + + @expand_message_class class PresRequestApprove(AdminHolderMessage): """Approve presentation request.""" @@ -460,11 +466,130 @@ class PresRequestApprove(AdminHolderMessage): class Fields: """Fields on pres request approve message.""" presentation_exchange_id = fields.Str(required=True) + self_attested_attributes = fields.Dict( + description="Self-attested attributes to build into proof", + required=True, + keys=fields.Str(example="attr_name"), # marshmallow/apispec v3.0 ignores + values=fields.Str( + example="self_attested_value", + description=( + "Self-attested attribute values to use in requested-credentials " + "structure for proof construction" + ), + ), + ) + requested_attributes = fields.Dict( + description=( + "Nested object mapping proof request attribute referents to " + "requested-attribute specifiers" + ), + required=True, + keys=fields.Str(example="attr_referent"), # marshmallow/apispec v3.0 ignores + values=fields.Nested(present_proof.routes.IndyRequestedCredsRequestedAttrSchema()), + ) + requested_predicates = fields.Dict( + description=( + "Nested object mapping proof request predicate referents to " + "requested-predicate specifiers" + ), + required=True, + keys=fields.Str(example="pred_referent"), # marshmallow/apispec v3.0 ignores + values=fields.Nested(present_proof.routes.IndyRequestedCredsRequestedPredSchema()), + ) + comment = fields.Str(required=False) + + def __init__( + self, + presentation_exchange_id: str, + self_attested_attributes: dict, + requested_attributes: dict, + requested_predicates: dict, + comment: str = None, + **kwargs + ): + super().__init__(**kwargs) + self.presentation_exchange_id = presentation_exchange_id + self.self_attested_attributes = self_attested_attributes + self.requested_attributes = requested_attributes + self.requested_predicates = requested_predicates + self.comment = comment + + @staticmethod + async def get_pres_ex_record( + session: ProfileSession, pres_ex_id: str + ) -> PresExRecord: + """Retrieve a presentation exchange record and validate its state.""" + try: + pres_ex_record = await PresExRecord.retrieve_by_id( + session, pres_ex_id + ) + pres_ex_record = cast(PresExRecord, pres_ex_record) + except StorageNotFoundError as err: + raise InvalidPresentationExchange( + "Presentation exchange ID not found" + ) from err + + if pres_ex_record.state != (PresExRecord.STATE_REQUEST_RECEIVED): + raise InvalidPresentationExchange( + "Presentation must be in request received state" + ) + + return pres_ex_record @log_handling @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): """Handle presentation request approved message.""" + async with context.session() as session: + async with ExceptionReporter( + responder, InvalidPresentationExchange, context.message + ): + pres_ex_record = await self.get_pres_ex_record( + session, self.presentation_exchange_id + ) + + async with ExceptionReporter( + responder, InvalidConnection, context.message + ): + conn_record = await get_connection( + session, pres_ex_record.connection_id + ) + + presentation_manager = PresentationManager(context.profile) + async with ExceptionReporter( + responder, + ( + BaseModelError, + IndyHolderError, + LedgerError, + StorageError, + WalletNotFoundError + ), + context.message + ): + pres_ex_record, message = await presentation_manager.create_presentation( + pres_ex_record, + { + "self_attested_attributes": self.self_attested_attributes, + "requested_attributes": self.requested_attributes, + "requested_predicates": self.requested_predicates + }, + comment=self.comment + ) + + await responder.send(message, connection_id=conn_record.connection_id) + + presentation_sent = PresSent(**pres_ex_record.serialize()) + presentation_sent.assign_thread_from(self) + await responder.send_reply(presentation_sent) + + +@with_generic_init +@expand_message_class +class PresSent(AdminHolderMessage): + """Presentation Exchange message.""" + message_type = "presentation-sent" + fields_from = PresExRecordSchema PROTOCOL = AdminHolderMessage.protocol diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index be8a4b79..a5c38ab5 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -3,7 +3,7 @@ # pylint: disable=too-few-public-methods import sys -from typing import Type +from typing import Type, Union, Tuple, cast import logging import functools import json @@ -379,13 +379,14 @@ class InvalidConnection(Exception): """Raised if no connection or connection is not ready.""" -async def get_connection(session: ProfileSession, connection_id: str): +async def get_connection(session: ProfileSession, connection_id: str) -> ConnRecord: """Get connection record or raise error if not found or conn is not ready.""" try: conn_record = await ConnRecord.retrieve_by_id( session, connection_id ) + conn_record = cast(ConnRecord, conn_record) if not conn_record.is_ready: raise InvalidConnection("Connection is not ready.") @@ -398,7 +399,7 @@ class ExceptionReporter: def __init__( self, responder: BaseResponder, - exception: Type[Exception], + exception: Union[Type[Exception], Tuple[Type[Exception], ...]], original_message: AgentMessage = None ): self.responder = responder diff --git a/tests/holder/conftest.py b/tests/holder/conftest.py index efca20c2..804b13cf 100644 --- a/tests/holder/conftest.py +++ b/tests/holder/conftest.py @@ -2,6 +2,8 @@ from contextlib import contextmanager +from acapy_plugin_toolbox.holder import PresExRecord + import pytest from asynctest import mock @@ -20,3 +22,19 @@ def _mock_record_query(obj, result=None, spec=None): ) as record_query: yield record_query yield _mock_record_query + + +@pytest.fixture +def mock_get_pres_ex_record(): + """Mock get_pres_ex_record.""" + @contextmanager + def _mock_get_pres_ex_record(obj, pres_ex_record: PresExRecord = None): + with mock.patch.object( + obj, + "get_pres_ex_record", + mock.CoroutineMock( + return_value=pres_ex_record or mock.MagicMock(autospec=True) + ) + ) as get_pres_ex_record: + yield get_pres_ex_record + yield _mock_get_pres_ex_record diff --git a/tests/holder/test_pres_approve.py b/tests/holder/test_pres_approve.py new file mode 100644 index 00000000..455a59a0 --- /dev/null +++ b/tests/holder/test_pres_approve.py @@ -0,0 +1,83 @@ +"""Test PresRequestApprove message and handler.""" + +import pytest + +from asynctest import mock +from acapy_plugin_toolbox.holder import v0_1 as test_module +from acapy_plugin_toolbox.holder.v0_1 import PresRequestApprove +from acapy_plugin_toolbox.holder import PresentationManager, PresExRecord +from aries_cloudagent.connections.models.conn_record import ConnRecord + +TEST_PRES_EX_ID = "test-presentation_exchange_id" +TEST_CONN_ID = "test-connection-id" +TEST_SELF_ATTESTED_ATTRS = {} +TEST_REQUESTED_ATTRS = {} +TEST_REQUESTED_PREDS = {} +TEST_COMMENT = "test-comment" + + +@pytest.fixture +def message(): + """Message fixture.""" + yield PresRequestApprove( + presentation_exchange_id=TEST_PRES_EX_ID, + self_attested_attributes=TEST_SELF_ATTESTED_ATTRS, + requested_attributes=TEST_REQUESTED_ATTRS, + requested_predicates=TEST_REQUESTED_PREDS, + comment=TEST_COMMENT + ) + + +@pytest.fixture +def context(context, message): + context.message = message + yield context + + +@pytest.fixture +def record(): + yield PresExRecord( + presentation_exchange_id=TEST_PRES_EX_ID, + connection_id=TEST_CONN_ID + ) + + +@pytest.fixture +def conn_record(): + yield ConnRecord(connection_id=TEST_CONN_ID) + + +@pytest.mark.asyncio +async def test_handler( + context, + mock_responder, + message, + mock_get_connection, + mock_get_pres_ex_record, + record, + conn_record +): + """Test PresRequestApprove handler.""" + mock_presentation_manager = mock.MagicMock(spec=PresentationManager) + mock_presentation_manager.create_presentation = mock.CoroutineMock( + return_value=(record, mock.MagicMock()) + ) + with mock_get_connection( + test_module, conn_record + ), mock_get_pres_ex_record( + PresRequestApprove, record + ), mock.patch.object( + test_module, + "PresentationManager", + mock.MagicMock(return_value=mock_presentation_manager) + ): + await message.handle(context, mock_responder) + + assert len(mock_responder.messages) == 2 + + reply, _reply_args = mock_responder.messages.pop() + assert reply.presentation_exchange_id == TEST_PRES_EX_ID + + _pres, pres_args = mock_responder.messages.pop() + assert "connection_id" in pres_args + assert pres_args["connection_id"] == TEST_CONN_ID From 61b1f8a2fa2c1ae74845f27ed3f9e67a3449e695 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 7 Apr 2021 01:37:27 -0400 Subject: [PATCH 40/59] fix: basic messages not sent through sessions if available Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/basicmessage.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/acapy_plugin_toolbox/basicmessage.py b/acapy_plugin_toolbox/basicmessage.py index ccc01853..0d027ec8 100644 --- a/acapy_plugin_toolbox/basicmessage.py +++ b/acapy_plugin_toolbox/basicmessage.py @@ -7,6 +7,7 @@ from aries_cloudagent.core.profile import ProfileSession, Profile from aries_cloudagent.config.injection_context import InjectionContext from aries_cloudagent.core.protocol_registry import ProtocolRegistry +from aries_cloudagent.protocols.connections.v1_0.manager import ConnectionManager from aries_cloudagent.core.event_bus import Event, EventBus from aries_cloudagent.messaging.base_handler import ( BaseHandler, BaseResponder, RequestContext @@ -320,7 +321,20 @@ async def handle(self, context: RequestContext, responder: BaseResponder): localization=LocalizationDecorator(locale='en') ) - await responder.send(msg, connection_id=connection.connection_id) + connection_mgr = ConnectionManager(session) + targets = [ + target + for target in await connection_mgr.get_connection_targets( + connection=connection + ) + ] + + for target in targets: + await responder.send( + msg, + reply_to_verkey=target.recipient_keys[0], + reply_from_verkey=target.sender_key + ) record = BasicMessageRecord( connection_id=context.message.connection_id, From 58f26bf71c7c9cb15a51ca42c10968cfab0c685f Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Tue, 13 Apr 2021 12:21:12 -0400 Subject: [PATCH 41/59] docs: fix schema info for docs Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/decorators/pagination.py | 6 ++-- acapy_plugin_toolbox/holder/v0_1.py | 35 ++++++++++++++++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/acapy_plugin_toolbox/decorators/pagination.py b/acapy_plugin_toolbox/decorators/pagination.py index b0e2d83a..4e93e704 100644 --- a/acapy_plugin_toolbox/decorators/pagination.py +++ b/acapy_plugin_toolbox/decorators/pagination.py @@ -14,9 +14,9 @@ class Page(BaseModel): class Fields: """Fields of page decorator.""" - count_ = fields.Int(required=True, data_key="count") - offset = fields.Int(required=True) - remaining = fields.Int(required=False) + count_ = fields.Int(required=True, data_key="count", example=10) + offset = fields.Int(required=True, example=20) + remaining = fields.Int(required=False, example=15) def __init__( self, count_: int = 0, offset: int = 0, remaining: int = None, **kwargs diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index f3d05586..f834018e 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -95,9 +95,27 @@ class Fields: Paginate.Schema, required=False, data_key="~paginate", - missing=Paginate(limit=10, offset=0) + missing=Paginate(limit=10, offset=0), + ) + states = fields.List( + fields.Str(required=True), + required=False, + example=["offer_received"], + description="Filter listed credentials by state.", + validate=validate.OneOf( + [ + CredExRecord.STATE_ACKED, + CredExRecord.STATE_CREDENTIAL_RECEIVED, + CredExRecord.STATE_ISSUED, + CredExRecord.STATE_OFFER_RECEIVED, + CredExRecord.STATE_OFFER_SENT, + CredExRecord.STATE_PROPOSAL_RECEIVED, + CredExRecord.STATE_PROPOSAL_SENT, + CredExRecord.STATE_REQUEST_RECEIVED, + CredExRecord.STATE_REQUEST_SENT, + ] + ), ) - states = fields.List(fields.Str(required=True), required=False) def __init__( self, @@ -136,7 +154,12 @@ class CredList(AdminHolderMessage): class Fields: """Fields of credential list message.""" - results = fields.List(fields.Dict()) + results = fields.List( + fields.Dict(), + required=True, + description="List of requested credentials", + example=[], + ) page = fields.Nested(Page.Schema, required=False, data_key="~page") def __init__( @@ -234,7 +257,11 @@ class CredOfferAccept(AdminHolderMessage): class Fields: """Fields of cred offer accept message.""" - credential_exchange_id = fields.Str(required=True) + credential_exchange_id = fields.Str( + required=True, + description="ID of the credential exchange to accept", + example=UUIDFour.EXAMPLE + ) def __init__(self, credential_exchange_id: str = None, **kwargs): super().__init__(**kwargs) From 4480d013e203f42e9ddfb4b216299862d0ffb664 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 15 Apr 2021 12:25:25 -0400 Subject: [PATCH 42/59] feat: add get matching credentials message Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/holder/__init__.py | 9 ++ acapy_plugin_toolbox/holder/v0_1.py | 99 ++++++++++++++++++- .../test_pres_get_matching_credentials.py | 66 +++++++++++++ 3 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 tests/holder/test_pres_get_matching_credentials.py diff --git a/acapy_plugin_toolbox/holder/__init__.py b/acapy_plugin_toolbox/holder/__init__.py index 8dfa4377..72bdc4de 100644 --- a/acapy_plugin_toolbox/holder/__init__.py +++ b/acapy_plugin_toolbox/holder/__init__.py @@ -30,3 +30,12 @@ ) from aries_cloudagent.protocols.present_proof.v1_0.routes import \ V10PresentationProposalRequestSchema as PresentationProposalRequestSchema + +__all__ = [ + "issue_credential", "CredentialManager", "CredentialManagerError", + "CredentialAttributeSpec", "CredExRecord", "CredExRecordSchema", + "CredentialProposalRequestSchema", "present_proof", + "PresentationManager", "PresentationManagerError", + "PresentationPreview", "PresExRecord", "PresExRecordSchema", "IndyCredPrecisSchema", + "PresentationProposalRequestSchema" +] diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index f834018e..0600246d 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -4,7 +4,7 @@ # pylint: disable=too-few-public-methods import re -from typing import Sequence, List, Optional, cast +from typing import Sequence, List, Optional, Tuple, cast, Any import logging from aries_cloudagent.config.injection_context import InjectionContext @@ -13,6 +13,7 @@ from aries_cloudagent.core.profile import Profile, ProfileSession from aries_cloudagent.core.protocol_registry import ProtocolRegistry from aries_cloudagent.indy.holder import IndyHolder, IndyHolderError +from aries_cloudagent.indy.sdk.holder import IndySdkHolder from aries_cloudagent.ledger.error import LedgerError from aries_cloudagent.wallet.error import WalletNotFoundError from aries_cloudagent.messaging.agent_message import AgentMessage @@ -21,7 +22,8 @@ ) from aries_cloudagent.messaging.models.base import BaseModel, BaseModelError from aries_cloudagent.storage.error import StorageError, StorageNotFoundError -from marshmallow import fields +from aries_cloudagent.messaging.valid import UUIDFour +from marshmallow import fields, validate from .. import ProblemReport from ..decorators.pagination import Page, Paginate @@ -619,6 +621,93 @@ class PresSent(AdminHolderMessage): fields_from = PresExRecordSchema +@expand_message_class +class PresGetMatchingCredentials(AdminHolderMessage): + """Retrieve matching credentials for a presentation request.""" + message_type = "presentation-get-matching-credentials" + + class Fields: + presentation_exchange_id = fields.Str( + required=True, description="Presentation to match credentials to." + ) + paginate = fields.Nested( + Paginate.Schema, + required=False, + data_key="~paginate", + missing=Paginate(limit=10, offset=0), + ) + + def __init__( + self, presentation_exchange_id: str, paginate: Paginate = None, **kwargs + ): + super().__init__(**kwargs) + self.presentation_exchange_id = presentation_exchange_id + self.paginate = paginate + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + holder = cast(IndySdkHolder, context.inject(IndyHolder)) + async with context.session() as session: + async with ExceptionReporter( + responder, InvalidPresentationExchange, context.message + ): + pres_ex_record = await PresRequestApprove.get_pres_ex_record( + session, self.presentation_exchange_id + ) + + matches = PresMatchingCredentials( + presentation_exchange_id=self.presentation_exchange_id, + matching_credentials=await holder.get_credentials_for_presentation_request_by_referent( + pres_ex_record.presentation_request, + (), + self.paginate.offset, + self.paginate.limit, + extra_query={}, + ), + page=Page(count_=self.paginate.limit, offset=self.paginate.offset) + ) + matches.assign_thread_from(self) + await responder.send_reply(matches) + + +@with_generic_init +@expand_message_class +class PresMatchingCredentials(AdminHolderMessage): + """Presentation Matching Credentials""" + message_type = "presentation-matching-credentials" + + class Fields: + """Fields for MatchingCredentials.""" + presentation_exchange_id = fields.Str( + required=True, + description="Exchange ID for matched credentials." + ) + matching_credentials = fields.Nested( + IndyCredPrecisSchema, + many=True, + description="Matched credentials." + ) + page = fields.Nested( + Page.Schema, + required=False, + description="Pagination info for matched credentials." + ) + + def __init__( + self, + presentation_exchange_id: str, + matching_credentials: Tuple[Any, ...], + page: Page = None, + **kwargs + ): + """Initialize PresMatchingCredentials""" + super().__init__(**kwargs) + self.presentation_exchange_id = presentation_exchange_id + self.matching_credentials = matching_credentials + self.page = page + + PROTOCOL = AdminHolderMessage.protocol TITLE = "Holder Admin Protocol" NAME = "admin-holder" @@ -637,6 +726,8 @@ class PresSent(AdminHolderMessage): PresGetList, PresList, PresRequestApprove, + PresGetMatchingCredentials, + PresMatchingCredentials, SendPresProposal, PresExchange, ] @@ -645,7 +736,7 @@ class PresSent(AdminHolderMessage): async def setup( context: InjectionContext, - protocol_registry: ProblemReport = None + protocol_registry: Optional[ProtocolRegistry] = None ): """Setup the holder plugin.""" if not protocol_registry: @@ -700,7 +791,7 @@ async def present_proof_event_handler(profile: Profile, event: Event): if record.state == PresExRecord.STATE_REQUEST_RECEIVED: responder = profile.inject(BaseResponder) - message = PresRequestReceived(record) + message: PresRequestReceived = PresRequestReceived(record) LOGGER.debug("Prepared Message: %s", message.serialize()) await message.retrieve_matching_credentials(profile) async with profile.session() as session: diff --git a/tests/holder/test_pres_get_matching_credentials.py b/tests/holder/test_pres_get_matching_credentials.py new file mode 100644 index 00000000..a0be3198 --- /dev/null +++ b/tests/holder/test_pres_get_matching_credentials.py @@ -0,0 +1,66 @@ +"""Test PresGetMatchingCredentials message and handler.""" + +# pylint: disable=redefined-outer-name + +import pytest +from aries_cloudagent.indy.holder import IndyHolder +from aries_cloudagent.indy.sdk.holder import IndySdkHolder +from asynctest import mock + +from acapy_plugin_toolbox.decorators.pagination import Paginate +from acapy_plugin_toolbox.holder import PresExRecord +from acapy_plugin_toolbox.holder.v0_1 import ( + PresGetMatchingCredentials, PresMatchingCredentials, PresRequestApprove +) + +TEST_PRES_EX_ID = "test-pres-ex-id" +TEST_CONN_ID = "test-connection-id" + + +@pytest.fixture +def message(): + """Message fixture.""" + yield PresGetMatchingCredentials( + presentation_exchange_id=TEST_PRES_EX_ID, + paginate=Paginate(10) + ) + + +@pytest.fixture +def holder(): + yield mock.MagicMock(IndySdkHolder) + + +@pytest.fixture +def context(context, message, holder): + """Context fixture""" + context.message = message + context.injector.bind_instance(IndyHolder, holder) + yield context + + +@pytest.fixture +def record(): + yield PresExRecord( + presentation_exchange_id=TEST_PRES_EX_ID, + connection_id=TEST_CONN_ID + ) + + +@pytest.mark.asyncio +async def test_handler( + context, mock_responder, message, mock_get_pres_ex_record, record, holder +): + + holder.get_credentials_for_presentation_request_by_referent.return_value = () + with mock_get_pres_ex_record( + PresRequestApprove, record + ): + await message.handle(context, mock_responder) + + assert len(mock_responder.messages) == 1 + reply, _reply_args = mock_responder.messages.pop() + assert isinstance(reply, PresMatchingCredentials) + assert reply.presentation_exchange_id == TEST_PRES_EX_ID + assert reply.matching_credentials == () + assert reply.page.count == message.paginate.limit From 121e1e8b938d6f199a702c3f54481e2bd1034fda Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 15 Apr 2021 16:16:42 -0400 Subject: [PATCH 43/59] docs: update field info with descriptions in holder Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/holder/v0_1.py | 61 ++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 0600246d..6c09e33b 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -98,6 +98,7 @@ class Fields: required=False, data_key="~paginate", missing=Paginate(limit=10, offset=0), + description="Pagination decorator." ) states = fields.List( fields.Str(required=True), @@ -162,7 +163,12 @@ class Fields: description="List of requested credentials", example=[], ) - page = fields.Nested(Page.Schema, required=False, data_key="~page") + page = fields.Nested( + Page.Schema, + required=False, + data_key="~page", + description="Pagination decorator." + ) def __init__( self, @@ -325,25 +331,26 @@ class PresGetList(AdminHolderMessage): class Fields: """Message fields.""" - connection_id = fields.Str(required=False) - verified = fields.Str(required=False) + connection_id = fields.Str( + required=False, + description="Filter presentations by connection_id" + ) paginate = fields.Nested( Paginate.Schema, required=False, data_key="~paginate", - missing=Paginate(limit=10, offset=0) + missing=Paginate(limit=10, offset=0), + description="Pagination decorator." ) def __init__( self, connection_id: str = None, - verified: str = None, paginate: Paginate = None, **kwargs ): super().__init__(**kwargs) self.connection_id = connection_id - self.verified = verified self.paginate = paginate or Paginate() @log_handling @@ -358,7 +365,6 @@ async def handle(self, context: RequestContext, responder: BaseResponder): filter(lambda item: item[1] is not None, { 'role': PresExRecord.ROLE_PROVER, 'connection_id': context.message.connection_id, - 'verified': context.message.verified, }.items()) ) records = await PresExRecord.query( @@ -376,8 +382,16 @@ class PresList(AdminHolderMessage): class Fields: """Fields for presentation list message.""" - results = fields.List(fields.Dict()) - page = fields.Nested(Page.Schema, required=False, data_key="~page") + results = fields.List( + fields.Dict(), + description="Retrieved presentations." + ) + page = fields.Nested( + Page.Schema, + required=False, + data_key="~page", + description="Pagination decorator." + ) def __init__(self, results, page: Page = None, **kwargs): super().__init__(**kwargs) @@ -461,9 +475,21 @@ class PresRequestReceived(AdminHolderMessage): class Fields: """Fields of Presentation request received message.""" - record = fields.Nested(PresExRecordSchema) - matching_credentials = fields.Nested(IndyCredPrecisSchema, many=True) - page = fields.Nested(Page.Schema, required=False) + record = fields.Nested( + PresExRecordSchema, + required=True, + description="Presentation details." + ) + matching_credentials = fields.Nested( + IndyCredPrecisSchema, + many=True, + description="Credentials matching the requested attributes." + ) + page = fields.Nested( + Page.Schema, + required=False, + description="Pagination decorator." + ) def __init__(self, record: PresExRecord, **kwargs): super().__init__(**kwargs) @@ -525,7 +551,11 @@ class Fields: keys=fields.Str(example="pred_referent"), # marshmallow/apispec v3.0 ignores values=fields.Nested(present_proof.routes.IndyRequestedCredsRequestedPredSchema()), ) - comment = fields.Str(required=False) + comment = fields.Str( + required=False, + description="Optional comment.", + example="Nothing to see here." + ) def __init__( self, @@ -628,13 +658,16 @@ class PresGetMatchingCredentials(AdminHolderMessage): class Fields: presentation_exchange_id = fields.Str( - required=True, description="Presentation to match credentials to." + required=True, + description="Presentation to match credentials to.", + example=UUIDFour.EXAMPLE ) paginate = fields.Nested( Paginate.Schema, required=False, data_key="~paginate", missing=Paginate(limit=10, offset=0), + description="Pagination decorator." ) def __init__( From 6675eacaa5e052e7e7adaed212d1ab0998d713dd Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Thu, 15 Apr 2021 17:05:51 -0400 Subject: [PATCH 44/59] fix: changes from recent acapy and event-bus updates Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/basicmessage.py | 2 +- acapy_plugin_toolbox/connections.py | 2 +- acapy_plugin_toolbox/holder/__init__.py | 11 ++++++++--- acapy_plugin_toolbox/holder/v0_1.py | 11 ++++++----- tests/holder/test_events.py | 4 ++-- tests/test_basicmessage.py | 3 ++- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/acapy_plugin_toolbox/basicmessage.py b/acapy_plugin_toolbox/basicmessage.py index 0d027ec8..bf66f51d 100644 --- a/acapy_plugin_toolbox/basicmessage.py +++ b/acapy_plugin_toolbox/basicmessage.py @@ -43,7 +43,7 @@ DELETE: 'acapy_plugin_toolbox.basicmessage.Delete' } -BASIC_MESSAGE_EVENT_PATTERN = re.compile("^basicmessages$") +BASIC_MESSAGE_EVENT_PATTERN = re.compile("^acapy::basicmessage::received$") async def setup( diff --git a/acapy_plugin_toolbox/connections.py b/acapy_plugin_toolbox/connections.py index 7830b021..e16804ca 100644 --- a/acapy_plugin_toolbox/connections.py +++ b/acapy_plugin_toolbox/connections.py @@ -55,7 +55,7 @@ CONNECTED: 'acapy_plugin_toolbox.connections.Connected', } -EVENT_PATTERN = re.compile(ConnRecord.WEBHOOK_TOPIC + ".*") +EVENT_PATTERN = re.compile(f"acapy::record::{ConnRecord.RECORD_TOPIC}::.*") async def setup( diff --git a/acapy_plugin_toolbox/holder/__init__.py b/acapy_plugin_toolbox/holder/__init__.py index 72bdc4de..96df6a2d 100644 --- a/acapy_plugin_toolbox/holder/__init__.py +++ b/acapy_plugin_toolbox/holder/__init__.py @@ -18,8 +18,8 @@ from aries_cloudagent.protocols.present_proof.v1_0.manager import ( PresentationManager, PresentationManagerError ) -from aries_cloudagent.protocols.present_proof.v1_0.messages.inner.presentation_preview import ( - PresentationPreview +from aries_cloudagent.protocols.present_proof.indy.pres_preview import ( + IndyPresPreview as PresentationPreview ) from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import \ V10PresentationExchange as PresExRecord @@ -28,6 +28,10 @@ from aries_cloudagent.protocols.present_proof.v1_0.routes import ( IndyCredPrecisSchema ) +from aries_cloudagent.protocols.present_proof.indy.requested_creds import ( + IndyRequestedCredsRequestedAttrSchema, + IndyRequestedCredsRequestedPredSchema, +) from aries_cloudagent.protocols.present_proof.v1_0.routes import \ V10PresentationProposalRequestSchema as PresentationProposalRequestSchema @@ -37,5 +41,6 @@ "CredentialProposalRequestSchema", "present_proof", "PresentationManager", "PresentationManagerError", "PresentationPreview", "PresExRecord", "PresExRecordSchema", "IndyCredPrecisSchema", - "PresentationProposalRequestSchema" + "PresentationProposalRequestSchema", "IndyRequestedCredsRequestedAttrSchema", + "IndyRequestedCredsRequestedPredSchema" ] diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 6c09e33b..44a791e7 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -37,7 +37,8 @@ CredentialProposalRequestSchema, CredExRecord, CredExRecordSchema, IndyCredPrecisSchema, PresentationPreview, PresentationProposalRequestSchema, PresExRecord, PresExRecordSchema, - issue_credential, present_proof, PresentationManager + issue_credential, present_proof, PresentationManager, + IndyRequestedCredsRequestedPredSchema, IndyRequestedCredsRequestedAttrSchema ) @@ -540,7 +541,7 @@ class Fields: ), required=True, keys=fields.Str(example="attr_referent"), # marshmallow/apispec v3.0 ignores - values=fields.Nested(present_proof.routes.IndyRequestedCredsRequestedAttrSchema()), + values=fields.Nested(IndyRequestedCredsRequestedAttrSchema()), ) requested_predicates = fields.Dict( description=( @@ -549,7 +550,7 @@ class Fields: ), required=True, keys=fields.Str(example="pred_referent"), # marshmallow/apispec v3.0 ignores - values=fields.Nested(present_proof.routes.IndyRequestedCredsRequestedPredSchema()), + values=fields.Nested(IndyRequestedCredsRequestedPredSchema()), ) comment = fields.Str( required=False, @@ -779,11 +780,11 @@ async def setup( ) bus: EventBus = context.inject(EventBus) bus.subscribe( - re.compile(CredExRecord.WEBHOOK_TOPIC + ".*"), + re.compile(f"acapy::record::{CredExRecord.RECORD_TOPIC}::.*"), issue_credential_event_handler ) bus.subscribe( - re.compile(PresExRecord.WEBHOOK_TOPIC + ".*"), + re.compile(f"acapy::record::{PresExRecord.RECORD_TOPIC}::.*"), present_proof_event_handler ) diff --git a/tests/holder/test_events.py b/tests/holder/test_events.py index 7e33d8bf..4c2d59ce 100644 --- a/tests/holder/test_events.py +++ b/tests/holder/test_events.py @@ -64,8 +64,8 @@ def mock_send_to_admins(): @pytest.mark.parametrize( "handler, topic", [ - ("issue_credential_event_handler", V10CredentialExchange.WEBHOOK_TOPIC), - ("present_proof_event_handler", V10PresentationExchange.WEBHOOK_TOPIC) + ("issue_credential_event_handler", f"acapy::record::{V10CredentialExchange.RECORD_TOPIC}::test"), + ("present_proof_event_handler", f"acapy::record::{V10PresentationExchange.RECORD_TOPIC}::test") ] ) async def test_events_subscribed_and_triggered( diff --git a/tests/test_basicmessage.py b/tests/test_basicmessage.py index fb7138a7..a1d4c38e 100644 --- a/tests/test_basicmessage.py +++ b/tests/test_basicmessage.py @@ -24,7 +24,7 @@ async def test_basic_message_event_handler_notify_admins( await event_bus.notify( profile, Event( - "basicmessages", + "acapy::basicmessage::received", { "connection_id": "connection-1", "message_id": "test id", @@ -34,4 +34,5 @@ async def test_basic_message_event_handler_notify_admins( ), ) + assert send_to_admins.message assert send_to_admins.message.message.content == "Hello world" From 4ee738b199f50e48c2d242110957f93997d48ad9 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 23 Apr 2021 11:39:43 -0400 Subject: [PATCH 45/59] fix: use base von-image to avoid ACA-Py version conflicts Signed-off-by: Daniel Bluhm --- demo/Dockerfile.demo | 4 ++-- docker/Dockerfile | 4 ++-- requirements.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/Dockerfile.demo b/demo/Dockerfile.demo index 84f9005e..11cc5f25 100644 --- a/demo/Dockerfile.demo +++ b/demo/Dockerfile.demo @@ -1,4 +1,4 @@ -FROM bcgovimages/aries-cloudagent:py36-1.15-0_0.6.0rc0 +FROM bcgovimages/von-image:py36-1.15-1 ARG ACAPY="" RUN if [ -n "${ACAPY}" ]; then pip3 install ${ACAPY}; fi @@ -18,7 +18,7 @@ ADD https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 ./jq RUN chmod +x ./jq USER $user -RUN pip3 install -e ./aries-acapy-plugin-toolbox +RUN pip3 install --no-cache-dir -e ./aries-acapy-plugin-toolbox ENTRYPOINT ["/bin/bash", "-c", "./ngrok-wait.sh \"$@\"", "--"] CMD ["aca-py", "start", "--plugin", "acapy_plugin_toolbox", "--arg-file", "configs/default.yml"] diff --git a/docker/Dockerfile b/docker/Dockerfile index f8fd6c7a..49dfc9d6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM bcgovimages/aries-cloudagent:py36-1.15-0_0.6.0rc0 +FROM bcgovimages/von-image:py36-1.15-1 ARG ACAPY="" RUN if [ -n "${ACAPY}" ]; then pip3 install ${ACAPY}; fi @@ -15,7 +15,7 @@ USER $user # Make ACA-Py Install location more accessible RUN ln -s /home/indy/.pyenv/versions/3.6.9/lib/python3.6/site-packages site-packages -RUN pip3 install -e ./aries-acapy-plugin-toolbox +RUN pip3 install --no-cache-dir -e ./aries-acapy-plugin-toolbox ADD docker/default.yml . diff --git a/requirements.txt b/requirements.txt index 0fe6e670..21bca480 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aries-cloudagent[indy]@git+https://github.com/indicio-tech/aries-cloudagent-python@10b8134ddb9bc6a26058c929c35ec7b230787174 +aries-cloudagent[indy]@git+https://github.com/indicio-tech/aries-cloudagent-python@39f2c9a49d00f3c926ff56d9385c52dca6dc1c76 marshmallow==3.5.1 flake8 python-dateutil From a54b61a2eed7a1bddcb8ce6a139911dbc08b14fb Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Mon, 26 Apr 2021 14:02:43 -0400 Subject: [PATCH 46/59] feat: add pre-commit Signed-off-by: Daniel Bluhm --- .commitlint.config.js | 3 +++ .pre-commit-config.yaml | 19 +++++++++++++++++++ requirements.dev.txt | 1 + 3 files changed, 23 insertions(+) create mode 100644 .commitlint.config.js create mode 100644 .pre-commit-config.yaml diff --git a/.commitlint.config.js b/.commitlint.config.js new file mode 100644 index 00000000..c34aa79d --- /dev/null +++ b/.commitlint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'] +}; diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..1c72e986 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +--- +repos: + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v2.2.0 + hooks: + - id: commitlint + stages: [commit-msg] + args: ["--config", ".commitlint.config.js"] + additional_dependencies: ['@commitlint/config-conventional'] + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + stages: [commit] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.0 + hooks: + - id: flake8 + stages: [commit] diff --git a/requirements.dev.txt b/requirements.dev.txt index ba6cfc66..5b6cc1e3 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,3 +1,4 @@ pytest pytest-asyncio asynctest==0.13.0 +pre-commit From 1d7119da42d5386c9498c93c352f5572c06033f3 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Mon, 26 Apr 2021 14:16:04 -0400 Subject: [PATCH 47/59] fix: from_indy_dict errors Convert RequestPres message and handler to new style. Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/issuer.py | 305 ++++++++++++++++----------------- 1 file changed, 146 insertions(+), 159 deletions(-) diff --git a/acapy_plugin_toolbox/issuer.py b/acapy_plugin_toolbox/issuer.py index 183be616..440a43d6 100644 --- a/acapy_plugin_toolbox/issuer.py +++ b/acapy_plugin_toolbox/issuer.py @@ -2,105 +2,109 @@ # pylint: disable=invalid-name # pylint: disable=too-few-public-methods +from typing import Optional + from aries_cloudagent.connections.models.conn_record import ConnRecord from aries_cloudagent.core.profile import ProfileSession from aries_cloudagent.core.protocol_registry import ProtocolRegistry from aries_cloudagent.indy.util import generate_pr_nonce +from aries_cloudagent.messaging.agent_message import AgentMessage from aries_cloudagent.messaging.base_handler import ( - BaseHandler, BaseResponder, RequestContext -) -from aries_cloudagent.messaging.credential_definitions.util import ( - CRED_DEF_TAGS -) -from aries_cloudagent.messaging.decorators.attach_decorator import ( - AttachDecorator + BaseHandler, + BaseResponder, + RequestContext, ) -from aries_cloudagent.protocols.issue_credential.v1_0.manager import ( - CredentialManager -) -from aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_proposal import ( - CredentialProposal +from aries_cloudagent.messaging.credential_definitions.util import CRED_DEF_TAGS +from aries_cloudagent.messaging.decorators.attach_decorator import AttachDecorator +from aries_cloudagent.protocols.issue_credential.v1_0.manager import CredentialManager +from aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_proposal import ( # noqa: E501 + CredentialProposal, ) from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( - V10CredentialExchange, V10CredentialExchangeSchema + V10CredentialExchange, + V10CredentialExchangeSchema, ) from aries_cloudagent.protocols.issue_credential.v1_0.routes import ( V10CredentialExchangeListResultSchema, - V10CredentialProposalRequestMandSchema -) -from aries_cloudagent.protocols.present_proof.v1_0.manager import ( - PresentationManager + V10CredentialProposalRequestMandSchema, ) +from aries_cloudagent.protocols.present_proof.v1_0.manager import PresentationManager from aries_cloudagent.protocols.present_proof.v1_0.message_types import ( - ATTACH_DECO_IDS, PRESENTATION_REQUEST + ATTACH_DECO_IDS, + PRESENTATION_REQUEST, ) from aries_cloudagent.protocols.present_proof.v1_0.messages.presentation_request import ( - PresentationRequest + PresentationRequest, ) from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( - V10PresentationExchange, V10PresentationExchangeSchema + V10PresentationExchange, + V10PresentationExchangeSchema, ) from aries_cloudagent.protocols.present_proof.v1_0.routes import ( - V10PresentationExchangeListSchema, V10PresentationSendRequestRequestSchema -) -from aries_cloudagent.protocols.problem_report.v1_0.message import ( - ProblemReport + V10PresentationExchangeListSchema, + V10PresentationSendRequestRequestSchema, ) +from aries_cloudagent.protocols.problem_report.v1_0.message import ProblemReport from aries_cloudagent.storage.error import StorageNotFoundError from marshmallow import fields +from uuid import UUID + +from .util import ( + ExceptionReporter, + admin_only, + expand_message_class, + generate_model_schema, + get_connection, + with_generic_init, +) -from .util import admin_only, generate_model_schema - -PROTOCOL = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/admin-issuer/0.1' +PROTOCOL = "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/admin-issuer/0.1" -SEND_CREDENTIAL = '{}/send-credential'.format(PROTOCOL) -REQUEST_PRESENTATION = '{}/request-presentation'.format(PROTOCOL) -ISSUER_CRED_EXCHANGE = '{}/credential-exchange'.format(PROTOCOL) -ISSUER_PRES_EXCHANGE = '{}/presentation-exchange'.format(PROTOCOL) -CREDENTIALS_GET_LIST = '{}/credentials-get-list'.format(PROTOCOL) -CREDENTIALS_LIST = '{}/credentials-list'.format(PROTOCOL) -PRESENTATIONS_GET_LIST = '{}/presentations-get-list'.format(PROTOCOL) -PRESENTATIONS_LIST = '{}/presentations-list'.format(PROTOCOL) +SEND_CREDENTIAL = "{}/send-credential".format(PROTOCOL) +REQUEST_PRESENTATION = "{}/request-presentation".format(PROTOCOL) +ISSUER_CRED_EXCHANGE = "{}/credential-exchange".format(PROTOCOL) +ISSUER_PRES_EXCHANGE = "{}/presentation-exchange".format(PROTOCOL) +CREDENTIALS_GET_LIST = "{}/credentials-get-list".format(PROTOCOL) +CREDENTIALS_LIST = "{}/credentials-list".format(PROTOCOL) +PRESENTATIONS_GET_LIST = "{}/presentations-get-list".format(PROTOCOL) +PRESENTATIONS_LIST = "{}/presentations-list".format(PROTOCOL) MESSAGE_TYPES = { - SEND_CREDENTIAL: - 'acapy_plugin_toolbox.issuer.SendCred', - REQUEST_PRESENTATION: - 'acapy_plugin_toolbox.issuer.RequestPres', - CREDENTIALS_GET_LIST: - 'acapy_plugin_toolbox.issuer.CredGetList', - CREDENTIALS_LIST: - 'acapy_plugin_toolbox.issuer.CredList', - PRESENTATIONS_GET_LIST: - 'acapy_plugin_toolbox.issuer.PresGetList', - PRESENTATIONS_LIST: - 'acapy_plugin_toolbox.issuer.PresList', + SEND_CREDENTIAL: "acapy_plugin_toolbox.issuer.SendCred", + REQUEST_PRESENTATION: "acapy_plugin_toolbox.issuer.RequestPres", + CREDENTIALS_GET_LIST: "acapy_plugin_toolbox.issuer.CredGetList", + CREDENTIALS_LIST: "acapy_plugin_toolbox.issuer.CredList", + PRESENTATIONS_GET_LIST: "acapy_plugin_toolbox.issuer.PresGetList", + PRESENTATIONS_LIST: "acapy_plugin_toolbox.issuer.PresList", } async def setup( - session: ProfileSession, - protocol_registry: ProblemReport = None + session: ProfileSession, protocol_registry: Optional[ProtocolRegistry] = None ): - """Setup the issuer plugin.""" + """Set up the issuer plugin.""" if not protocol_registry: protocol_registry = session.inject(ProtocolRegistry) - protocol_registry.register_message_types( - MESSAGE_TYPES - ) + protocol_registry.register_message_types(MESSAGE_TYPES) + + +class AdminIssuerMessage(AgentMessage): + """Base Issuer Message class.""" + + protocol = PROTOCOL SendCred, SendCredSchema = generate_model_schema( - name='SendCred', - handler='acapy_plugin_toolbox.issuer.SendCredHandler', + name="SendCred", + handler="acapy_plugin_toolbox.issuer.SendCredHandler", msg_type=SEND_CREDENTIAL, - schema=V10CredentialProposalRequestMandSchema + schema=V10CredentialProposalRequestMandSchema, ) IssuerCredExchange, IssuerCredExchangeSchema = generate_model_schema( - name='IssuerCredExchange', - handler='acapy_plugin_toolbox.util.PassHandler', + name="IssuerCredExchange", + handler="acapy_plugin_toolbox.util.PassHandler", msg_type=ISSUER_CRED_EXCHANGE, - schema=V10CredentialExchangeSchema + schema=V10CredentialExchangeSchema, ) @@ -116,14 +120,10 @@ async def handle(self, context: RequestContext, responder: BaseResponder): session = await context.session() try: - conn_record = await ConnRecord.retrieve_by_id( - session, - connection_id - ) + conn_record = await ConnRecord.retrieve_by_id(session, connection_id) except StorageNotFoundError: report = ProblemReport( - explain_ltxt='Connection not found.', - who_retries='none' + explain_ltxt="Connection not found.", who_retries="none" ) report.assign_thread_from(context.message) await responder.send_reply(report) @@ -131,8 +131,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): if not conn_record.is_ready: report = ProblemReport( - explain_ltxt='Connection invalid.', - who_retries='none' + explain_ltxt="Connection invalid.", who_retries="none" ) report.assign_thread_from(context.message) await responder.send_reply(report) @@ -143,87 +142,63 @@ async def handle(self, context: RequestContext, responder: BaseResponder): credential_proposal=preview_spec, **{ t: getattr(context.message, t) - for t in CRED_DEF_TAGS if hasattr(context.message, t) + for t in CRED_DEF_TAGS + if hasattr(context.message, t) }, ) credential_manager = CredentialManager(context.profile) - cred_exchange_record, cred_offer_message = \ - await credential_manager.prepare_send( - connection_id, - credential_proposal=credential_proposal - ) + ( + cred_exchange_record, + cred_offer_message, + ) = await credential_manager.prepare_send( + connection_id, credential_proposal=credential_proposal + ) await responder.send( - cred_offer_message, - connection_id=cred_exchange_record.connection_id + cred_offer_message, connection_id=cred_exchange_record.connection_id ) cred_exchange = IssuerCredExchange(**cred_exchange_record.serialize()) cred_exchange.assign_thread_from(context.message) await responder.send_reply(cred_exchange) -RequestPres, RequestPresSchema = generate_model_schema( - name='RequestPres', - handler='acapy_plugin_toolbox.issuer.RequestPresHandler', - msg_type=REQUEST_PRESENTATION, - schema=V10PresentationSendRequestRequestSchema, -) -IssuerPresExchange, IssuerPresExchangeSchema = generate_model_schema( - name='IssuerPresExchange', - handler='acapy_plugin_toolbox.util.PassHandler', - msg_type=ISSUER_PRES_EXCHANGE, - schema=V10PresentationExchangeSchema -) +@expand_message_class +class RequestPres(AdminIssuerMessage): + """Request presentation message.""" + message_type = "request-presentation" + fields_from = V10PresentationSendRequestRequestSchema -class RequestPresHandler(BaseHandler): - """Handler for received presentation request request.""" + def __init__( + self, connection_id: UUID, proof_request: dict, comment: str = None, **kwargs + ): + """Initialize message.""" + super().__init__(**kwargs) + self.connection_id = connection_id + self.proof_request = proof_request + self.comment = comment @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received presentation request request.""" + connection_id = str(self.connection_id) + async with await context.session() as session: + async with ExceptionReporter(responder, StorageNotFoundError, self): + await get_connection(session, connection_id) - connection_id = str(context.message.connection_id) - session = await context.session() - try: - conn_record = await ConnRecord.retrieve_by_id( - session, - connection_id - ) - except StorageNotFoundError: - report = ProblemReport( - explain_ltxt='Connection not found.', - who_retries='none' - ) - report.assign_thread_from(context.message) - await responder.send_reply(report) - return - - if not conn_record.is_ready: - report = ProblemReport( - explain_ltxt='Connection invalid.', - who_retries='none' - ) - report.assign_thread_from(context.message) - await responder.send_reply(report) - return - - comment = context.message.comment - - indy_proof_request = context.message.proof_request - if not indy_proof_request.get('nonce'): - indy_proof_request['nonce'] = await generate_pr_nonce() + if not self.proof_request.get("nonce"): + self.proof_request["nonce"] = await generate_pr_nonce() presentation_request_message = PresentationRequest( - comment=comment, + comment=self.comment, request_presentations_attach=[ - AttachDecorator.from_indy_dict( - indy_dict=indy_proof_request, - ident=ATTACH_DECO_IDS[PRESENTATION_REQUEST] + AttachDecorator.data_base64( + mapping=self.proof_request, + ident=ATTACH_DECO_IDS[PRESENTATION_REQUEST], ) - ] + ], ) presentation_manager = PresentationManager(context.profile) @@ -231,36 +206,42 @@ async def handle(self, context: RequestContext, responder: BaseResponder): presentation_exchange_record = ( await presentation_manager.create_exchange_for_request( connection_id=connection_id, - presentation_request_message=presentation_request_message + presentation_request_message=presentation_request_message, ) ) - await responder.send( - presentation_request_message, - connection_id=connection_id - ) + await responder.send(presentation_request_message, connection_id=connection_id) pres_exchange = IssuerPresExchange(**presentation_exchange_record.serialize()) - pres_exchange.assign_thread_from(context.message) + pres_exchange.assign_thread_from(self) await responder.send_reply(pres_exchange) +@with_generic_init +@expand_message_class +class IssuerPresExchange(AdminIssuerMessage): + """Issuer Presentation Exchange report.""" + + message_type = "presentation-exchange" + fields_from = V10PresentationExchangeSchema + + CredGetList, CredGetListSchema = generate_model_schema( - name='CredGetList', - handler='acapy_plugin_toolbox.issuer.CredGetListHandler', + name="CredGetList", + handler="acapy_plugin_toolbox.issuer.CredGetListHandler", msg_type=CREDENTIALS_GET_LIST, schema={ - 'connection_id': fields.Str(required=False), - 'cred_def_id': fields.Str(required=False), - 'schema_id': fields.Str(required=False) - } + "connection_id": fields.Str(required=False), + "cred_def_id": fields.Str(required=False), + "schema_id": fields.Str(required=False), + }, ) CredList, CredListSchema = generate_model_schema( - name='CredList', - handler='acapy_plugin_toolbox.util.PassHandler', + name="CredList", + handler="acapy_plugin_toolbox.util.PassHandler", msg_type=CREDENTIALS_LIST, - schema=V10CredentialExchangeListResultSchema + schema=V10CredentialExchangeListResultSchema, ) @@ -272,13 +253,16 @@ async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received get cred list request.""" post_filter_positive = dict( - filter(lambda item: item[1] is not None, { - # 'state': V10CredentialExchange.STATE_ISSUED, - 'role': V10CredentialExchange.ROLE_ISSUER, - 'connection_id': context.message.connection_id, - 'credential_definition_id': context.message.cred_def_id, - 'schema_id': context.message.schema_id - }.items()) + filter( + lambda item: item[1] is not None, + { + # 'state': V10CredentialExchange.STATE_ISSUED, + "role": V10CredentialExchange.ROLE_ISSUER, + "connection_id": context.message.connection_id, + "credential_definition_id": context.message.cred_def_id, + "schema_id": context.message.schema_id, + }.items(), + ) ) session = await context.session() records = await V10CredentialExchange.query( @@ -289,18 +273,18 @@ async def handle(self, context: RequestContext, responder: BaseResponder): PresGetList, PresGetListSchema = generate_model_schema( - name='PresGetList', - handler='acapy_plugin_toolbox.issuer.PresGetListHandler', + name="PresGetList", + handler="acapy_plugin_toolbox.issuer.PresGetListHandler", msg_type=PRESENTATIONS_GET_LIST, schema={ - 'connection_id': fields.Str(required=False), - 'verified': fields.Str(required=False), - } + "connection_id": fields.Str(required=False), + "verified": fields.Str(required=False), + }, ) PresList, PresListSchema = generate_model_schema( - name='PresList', - handler='acapy_plugin_toolbox.util.PassHandler', + name="PresList", + handler="acapy_plugin_toolbox.util.PassHandler", msg_type=PRESENTATIONS_LIST, schema=V10PresentationExchangeListSchema # schema={ @@ -317,12 +301,15 @@ async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received get cred list request.""" post_filter_positive = dict( - filter(lambda item: item[1] is not None, { - # 'state': V10PresentialExchange.STATE_CREDENTIAL_RECEIVED, - 'role': V10PresentationExchange.ROLE_VERIFIER, - 'connection_id': context.message.connection_id, - 'verified': context.message.verified, - }.items()) + filter( + lambda item: item[1] is not None, + { + # 'state': V10PresentialExchange.STATE_CREDENTIAL_RECEIVED, + "role": V10PresentationExchange.ROLE_VERIFIER, + "connection_id": context.message.connection_id, + "verified": context.message.verified, + }.items(), + ) ) session = await context.session() records = await V10PresentationExchange.query( From ec1b915d4704e4cca98be22f57be6eff136c215b Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Mon, 26 Apr 2021 14:17:09 -0400 Subject: [PATCH 48/59] test: request presentation handler and message Signed-off-by: Daniel Bluhm --- tests/issuer/__init__.py | 0 tests/issuer/test_request_pres.py | 49 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/issuer/__init__.py create mode 100644 tests/issuer/test_request_pres.py diff --git a/tests/issuer/__init__.py b/tests/issuer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/issuer/test_request_pres.py b/tests/issuer/test_request_pres.py new file mode 100644 index 00000000..129b7abb --- /dev/null +++ b/tests/issuer/test_request_pres.py @@ -0,0 +1,49 @@ +"""Test RequestPres message and handler.""" +# pylint: disable=redefined-outer-name + +import uuid + +from aries_cloudagent.messaging.request_context import RequestContext +from aries_cloudagent.messaging.responder import MockResponder +from aries_cloudagent.protocols.present_proof.v1_0.messages.presentation_request import ( + PresentationRequest, +) +import pytest + +from acapy_plugin_toolbox.issuer import IssuerPresExchange, RequestPres +from acapy_plugin_toolbox import issuer as test_module + +TEST_CONN_ID = uuid.uuid4() + + +@pytest.fixture +def message(): + """Message fixture.""" + yield RequestPres(connection_id=TEST_CONN_ID, proof_request={}, comment="comment") + + +@pytest.fixture +def context(context, message): + """Context fixture.""" + context.message = message + yield context + + +@pytest.mark.asyncio +async def test_handler( + context: RequestContext, + mock_responder: MockResponder, + message: RequestPres, + mock_get_connection, +): + """Test RequestPres handler.""" + with mock_get_connection(test_module): + await message.handle(context, mock_responder) + + assert len(mock_responder.messages) == 2 + (req, req_recipient), (response, _) = mock_responder.messages + assert isinstance(req, PresentationRequest) + assert req.request_presentations_attach + assert req_recipient == {"connection_id": str(TEST_CONN_ID)} + assert isinstance(response, IssuerPresExchange) + assert response.connection_id == str(TEST_CONN_ID) From ccbc74cb86ebeb8f4c046c8b53a0f608d7a503f8 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Mon, 26 Apr 2021 15:21:32 -0400 Subject: [PATCH 49/59] test: get-matching-cred more error tests Signed-off-by: Daniel Bluhm --- acapy_plugin_toolbox/issuer.py | 2 + .../test_pres_get_matching_credentials.py | 58 ++++++++++++++++--- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/acapy_plugin_toolbox/issuer.py b/acapy_plugin_toolbox/issuer.py index 440a43d6..5e632935 100644 --- a/acapy_plugin_toolbox/issuer.py +++ b/acapy_plugin_toolbox/issuer.py @@ -55,6 +55,7 @@ expand_message_class, generate_model_schema, get_connection, + log_handling, with_generic_init, ) @@ -180,6 +181,7 @@ def __init__( self.proof_request = proof_request self.comment = comment + @log_handling @admin_only async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received presentation request request.""" diff --git a/tests/holder/test_pres_get_matching_credentials.py b/tests/holder/test_pres_get_matching_credentials.py index a0be3198..f167b4b1 100644 --- a/tests/holder/test_pres_get_matching_credentials.py +++ b/tests/holder/test_pres_get_matching_credentials.py @@ -2,15 +2,21 @@ # pylint: disable=redefined-outer-name -import pytest from aries_cloudagent.indy.holder import IndyHolder from aries_cloudagent.indy.sdk.holder import IndySdkHolder +from aries_cloudagent.protocols.problem_report.v1_0.message import ProblemReport +from aries_cloudagent.storage.error import StorageNotFoundError from asynctest import mock +import pytest from acapy_plugin_toolbox.decorators.pagination import Paginate from acapy_plugin_toolbox.holder import PresExRecord +from acapy_plugin_toolbox.holder import v0_1 as test_module from acapy_plugin_toolbox.holder.v0_1 import ( - PresGetMatchingCredentials, PresMatchingCredentials, PresRequestApprove + InvalidPresentationExchange, + PresGetMatchingCredentials, + PresMatchingCredentials, + PresRequestApprove, ) TEST_PRES_EX_ID = "test-pres-ex-id" @@ -21,8 +27,7 @@ def message(): """Message fixture.""" yield PresGetMatchingCredentials( - presentation_exchange_id=TEST_PRES_EX_ID, - paginate=Paginate(10) + presentation_exchange_id=TEST_PRES_EX_ID, paginate=Paginate(10) ) @@ -42,8 +47,7 @@ def context(context, message, holder): @pytest.fixture def record(): yield PresExRecord( - presentation_exchange_id=TEST_PRES_EX_ID, - connection_id=TEST_CONN_ID + presentation_exchange_id=TEST_PRES_EX_ID, connection_id=TEST_CONN_ID ) @@ -53,9 +57,7 @@ async def test_handler( ): holder.get_credentials_for_presentation_request_by_referent.return_value = () - with mock_get_pres_ex_record( - PresRequestApprove, record - ): + with mock_get_pres_ex_record(PresRequestApprove, record): await message.handle(context, mock_responder) assert len(mock_responder.messages) == 1 @@ -64,3 +66,41 @@ async def test_handler( assert reply.presentation_exchange_id == TEST_PRES_EX_ID assert reply.matching_credentials == () assert reply.page.count == message.paginate.limit + + +@pytest.mark.asyncio +async def test_handler_x_no_such_pres( + context, mock_responder, message, mock_get_pres_ex_record, record, holder +): + + holder.get_credentials_for_presentation_request_by_referent.return_value = () + with mock.patch.object( + test_module, "PresExRecord", mock.MagicMock() + ) as mock_pres_ex_record, pytest.raises(InvalidPresentationExchange): + mock_pres_ex_record.retrieve_by_id = mock.CoroutineMock( + side_effect=StorageNotFoundError + ) + await message.handle(context, mock_responder) + + assert len(mock_responder.messages) == 1 + reply, _reply_args = mock_responder.messages.pop() + assert isinstance(reply, ProblemReport) + + +@pytest.mark.asyncio +async def test_handler_x_pres_invalid_state( + context, mock_responder, message, mock_get_pres_ex_record, record, holder +): + + holder.get_credentials_for_presentation_request_by_referent.return_value = () + with mock.patch.object( + test_module, "PresExRecord", mock.MagicMock() + ) as mock_pres_ex_record, pytest.raises(InvalidPresentationExchange): + mock_pres_ex_record.retrieve_by_id = mock.CoroutineMock( + return_value=mock.MagicMock(state="not request_received") + ) + await message.handle(context, mock_responder) + + assert len(mock_responder.messages) == 1 + reply, _reply_args = mock_responder.messages.pop() + assert isinstance(reply, ProblemReport) From a9ddb06d8d12f6ae2292100e31168d08b99939b3 Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Mon, 26 Apr 2021 22:45:46 -0600 Subject: [PATCH 50/59] adding admin-holder/0.1/credential-delete Signed-off-by: Matthew Wright --- acapy_plugin_toolbox/holder/v0_1.py | 73 +++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 44a791e7..3cbd5220 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -742,6 +742,78 @@ def __init__( self.page = page +@expand_message_class +class CredDelete(AdminHolderMessage): + """Delete a credential.""" + message_type = "credential-delete" + + class Fields: + credential_exchange_id = fields.Str( + required=True, + description="ID of the credential exchange to delete", + example=UUIDFour.EXAMPLE + ) + + def __init__(self, credential_exchange_id: str = None, **kwargs): + super().__init__(**kwargs) + self.credential_exchange_id = credential_exchange_id + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle delete credential message.""" + + holder = cast(IndySdkHolder, context.inject(IndyHolder)) + async with context.session() as session: + async with ExceptionReporter( + responder, + ( + StorageError, + CredentialManagerError, + BaseModelError + ), + context.message + ): + cred_ex_record: CredExRecord = await CredExRecord.retrieve_by_id( + session, self.credential_exchange_id + ) + + await holder.delete_credential(cred_ex_record.credential_id) + await cred_ex_record.delete_record(session) + + message = CredDeleted( + credential_exchange_id=self.credential_exchange_id, + credential_id=cred_ex_record.credential_id, + ) + message.assign_thread_from(self) + await responder.send_reply(message) + + +@expand_message_class +class CredDeleted(AdminHolderMessage): + """Credential deleted.""" + message_type = "credential-deleted" + + class Fields: + credential_exchange_id = fields.Str( + required=True, + description="Credential exchange ID that was deleted.", + example=UUIDFour.EXAMPLE + ) + credential_id = fields.Str( + required=True, + description="Credential ID that was deleted.", + example=UUIDFour.EXAMPLE + ) + + def __init__( + self, credential_exchange_id: str, credential_id: str, **kwargs + ): + super().__init__(**kwargs) + self.credential_exchange_id = credential_exchange_id + self.credential_id = credential_id + + PROTOCOL = AdminHolderMessage.protocol TITLE = "Holder Admin Protocol" NAME = "admin-holder" @@ -764,6 +836,7 @@ def __init__( PresMatchingCredentials, SendPresProposal, PresExchange, + CredDelete, ] } From 0a55f153b755926d9f70211b06ccef2d28958ea7 Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Mon, 26 Apr 2021 22:05:17 -0600 Subject: [PATCH 51/59] adding admin-holder/0.1/presentation-exchange-delete Signed-off-by: Matthew Wright --- acapy_plugin_toolbox/holder/v0_1.py | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 3cbd5220..e9155722 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -814,6 +814,63 @@ def __init__( self.credential_id = credential_id +@expand_message_class +class PresDelete(AdminHolderMessage): + """Delete a presentation exchange message.""" + message_type = "presentation-exchange-delete" + + class Fields: + presentation_exchange_id = fields.Str( + required=True, + description="Presentation Exchange Message to delete.", + example=UUIDFour.EXAMPLE + ) + + def __init__( + self, presentation_exchange_id: str, **kwargs + ): + super().__init__(**kwargs) + self.presentation_exchange_id = presentation_exchange_id + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + async with context.session() as session: + async with ExceptionReporter( + responder, InvalidPresentationExchange, context.message + ): + pres_ex_record = await PresRequestApprove.get_pres_ex_record( + session, self.presentation_exchange_id + ) + + await pres_ex_record.delete_record(session) + + message = PresDeleted( + presentation_exchange_id=self.presentation_exchange_id, + ) + message.assign_thread_from(self) + await responder.send_reply(message) + + +@expand_message_class +class PresDeleted(AdminHolderMessage): + """Presentation exchange message deleted.""" + message_type = "presentation-exchange-deleted" + + class Fields: + presentation_exchange_id = fields.Str( + required=True, + description="Presentation Exchange Message to delete.", + example=UUIDFour.EXAMPLE + ) + + def __init__( + self, presentation_exchange_id: str, **kwargs + ): + super().__init__(**kwargs) + self.presentation_exchange_id = presentation_exchange_id + + PROTOCOL = AdminHolderMessage.protocol TITLE = "Holder Admin Protocol" NAME = "admin-holder" @@ -837,6 +894,7 @@ def __init__( SendPresProposal, PresExchange, CredDelete, + PresDelete, ] } From a6809c81ca1cb2e6c1598e6e628ef39306b9f35f Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Thu, 6 May 2021 16:15:45 +0000 Subject: [PATCH 52/59] ProblemReport explain_ltxt to description Signed-off-by: Matthew Wright --- acapy_plugin_toolbox/basicmessage.py | 2 +- acapy_plugin_toolbox/connections.py | 6 +-- .../credential_definitions.py | 2 +- acapy_plugin_toolbox/holder/v0_1.py | 4 +- acapy_plugin_toolbox/issuer.py | 4 +- acapy_plugin_toolbox/payments.py | 38 +++++++++---------- acapy_plugin_toolbox/routing.py | 2 +- acapy_plugin_toolbox/static_connections.py | 2 +- acapy_plugin_toolbox/taa.py | 10 ++--- acapy_plugin_toolbox/util.py | 6 +-- 10 files changed, 38 insertions(+), 38 deletions(-) diff --git a/acapy_plugin_toolbox/basicmessage.py b/acapy_plugin_toolbox/basicmessage.py index bf66f51d..b6a29e3d 100644 --- a/acapy_plugin_toolbox/basicmessage.py +++ b/acapy_plugin_toolbox/basicmessage.py @@ -309,7 +309,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) except StorageNotFoundError: report = ProblemReport( - explain_ltxt='Connection not found.', + description={"en":'Connection not found.'}, who_retries='none' ) report.assign_thread_from(context.message) diff --git a/acapy_plugin_toolbox/connections.py b/acapy_plugin_toolbox/connections.py index e16804ca..3a207785 100644 --- a/acapy_plugin_toolbox/connections.py +++ b/acapy_plugin_toolbox/connections.py @@ -229,7 +229,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) except StorageNotFoundError: report = ProblemReport( - explain_ltxt='Connection not found.', + description={"en":'Connection not found.'}, who_retries='none' ) report.assign_thread_from(context.message) @@ -274,7 +274,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.connection_record.connection_id: report = ProblemReport( - explain_ltxt='Current connection cannot be deleted.', + description={"en":'Current connection cannot be deleted.'}, who_retries='none' ) report.assign_thread_from(context.message) @@ -289,7 +289,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) except StorageNotFoundError: report = ProblemReport( - explain_ltxt='Connection not found.', + description={"en":'Connection not found.'}, who_retries='none' ) report.assign_thread_from(context.message) diff --git a/acapy_plugin_toolbox/credential_definitions.py b/acapy_plugin_toolbox/credential_definitions.py index c8af22b3..330fcdf7 100644 --- a/acapy_plugin_toolbox/credential_definitions.py +++ b/acapy_plugin_toolbox/credential_definitions.py @@ -214,7 +214,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) except Exception as err: report = ProblemReport( - explain_ltxt='Failed to send to ledger; Error: {}'.format(err), + description={"en":'Failed to send to ledger; Error: {}'.format(err)}, who_retries='none' ) LOGGER.exception("Failed to send cred def to ledger: %s", err) diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index e9155722..258c8fe7 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -207,7 +207,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) except StorageNotFoundError: report = ProblemReport( - explain_ltxt='Connection not found.', + description={"en":'Connection not found.'}, who_retries='none' ) report.assign_thread_from(context.message) @@ -216,7 +216,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): if not conn_record.is_ready: report = ProblemReport( - explain_ltxt='Connection invalid.', + description={"en":'Connection invalid.'}, who_retries='none' ) report.assign_thread_from(context.message) diff --git a/acapy_plugin_toolbox/issuer.py b/acapy_plugin_toolbox/issuer.py index 5e632935..815de03b 100644 --- a/acapy_plugin_toolbox/issuer.py +++ b/acapy_plugin_toolbox/issuer.py @@ -124,7 +124,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): conn_record = await ConnRecord.retrieve_by_id(session, connection_id) except StorageNotFoundError: report = ProblemReport( - explain_ltxt="Connection not found.", who_retries="none" + description={"en":"Connection not found."}, who_retries="none" ) report.assign_thread_from(context.message) await responder.send_reply(report) @@ -132,7 +132,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): if not conn_record.is_ready: report = ProblemReport( - explain_ltxt="Connection invalid.", who_retries="none" + description={"en":"Connection invalid."}, who_retries="none" ) report.assign_thread_from(context.message) await responder.send_reply(report) diff --git a/acapy_plugin_toolbox/payments.py b/acapy_plugin_toolbox/payments.py index 1e344c56..f4fcffde 100644 --- a/acapy_plugin_toolbox/payments.py +++ b/acapy_plugin_toolbox/payments.py @@ -169,10 +169,10 @@ async def handle(self, context: RequestContext, responder: BaseResponder): """Handle received address list requests.""" if context.message.method and context.message.method != SOV_METHOD: report = ProblemReport( - explain_ltxt=( + description={"en":( 'Method "{}" is not supported.' .format(context.message.method) - ), + )}, who_retries='none' ) report.assign_thread_from(context.message) @@ -204,7 +204,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): }) except (LedgerError, PaymentError) as err: report = ProblemReport( - explain_ltxt=(err), + description={"en":str(err)}, who_retries='none' ) await responder.send_reply(report) @@ -216,7 +216,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): if hasattr(err, 'message'): message += ': {}'.format(err.message) report = ProblemReport( - explain_ltxt=(message), + description={"en":message}, who_retries='none' ) await responder.send_reply(report) @@ -256,10 +256,10 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ledger: BaseLedger = session.inject(BaseLedger) if context.message.method != SOV_METHOD: report = ProblemReport( - explain_ltxt=( + description={"en":( 'Method "{}" is not supported.' .format(context.message.method) - ), + )}, who_retries='none' ) report.assign_thread_from(context.message) @@ -268,9 +268,9 @@ async def handle(self, context: RequestContext, responder: BaseResponder): if context.message.seed and len(context.message.seed) < 32: report = ProblemReport( - explain_ltxt=( + description={"en":( 'Seed must be 32 characters in length' - ), + )}, who_retries='none' ) report.assign_thread_from(context.message) @@ -291,7 +291,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): if hasattr(err, 'message'): message += ': {}'.format(err.message) report = ProblemReport( - explain_ltxt=(message), + description={"en":message}, who_retries='none' ) await responder.send_reply(report) @@ -307,7 +307,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): sources.append(source) except (LedgerError, PaymentError) as err: report = ProblemReport( - explain_ltxt=(err), + description={"en":str(err)}, who_retries='none' ) await responder.send_reply(report) @@ -319,7 +319,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): if hasattr(err, 'message'): message += ': {}'.format(err.message) report = ProblemReport( - explain_ltxt=(message), + description={"en":message}, who_retries='none' ) await responder.send_reply(report) @@ -410,10 +410,10 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ledger: BaseLedger = session.inject(BaseLedger) if context.message.method != SOV_METHOD: report = ProblemReport( - explain_ltxt=( + description={"en":( 'Method "{}" is not supported.' .format(context.message.method) - ), + )}, who_retries='none' ) report.assign_thread_from(context.message) @@ -425,7 +425,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): xfer_auth = await get_transfer_auth(ledger) except (LedgerError, PaymentError) as err: report = ProblemReport( - explain_ltxt=(err), + description={"en":str(err)}, who_retries='none' ) await responder.send_reply(report) @@ -437,7 +437,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): if hasattr(err, 'message'): message += ': {}'.format(err.message) report = ProblemReport( - explain_ltxt=(message), + description={"en":message}, who_retries='none' ) await responder.send_reply(report) @@ -612,10 +612,10 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ledger: BaseLedger = session.inject(BaseLedger) if context.message.method != SOV_METHOD: report = ProblemReport( - explain_ltxt=( + description={"en":( 'Method "{}" is not supported.' .format(context.message.method) - ), + )}, who_retries='none' ) report.assign_thread_from(context.message) @@ -637,7 +637,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) except (LedgerError, PaymentError) as err: report = ProblemReport( - explain_ltxt=(err), + description={"en":str(err)}, who_retries='none' ) await responder.send_reply(report) @@ -649,7 +649,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): if hasattr(err, 'message'): message += ': {}'.format(err.message) report = ProblemReport( - explain_ltxt=(message), + description={"en":message}, who_retries='none' ) await responder.send_reply(report) diff --git a/acapy_plugin_toolbox/routing.py b/acapy_plugin_toolbox/routing.py index 4ccd2eba..ebe1106f 100644 --- a/acapy_plugin_toolbox/routing.py +++ b/acapy_plugin_toolbox/routing.py @@ -140,7 +140,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) except StorageNotFoundError: report = ProblemReport( - explain_ltxt='Connection not found.', + description={"en":'Connection not found.'}, who_retries='none' ) report.assign_thread_from(context.message) diff --git a/acapy_plugin_toolbox/static_connections.py b/acapy_plugin_toolbox/static_connections.py index 2d07c198..1c7f2280 100644 --- a/acapy_plugin_toolbox/static_connections.py +++ b/acapy_plugin_toolbox/static_connections.py @@ -216,7 +216,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) except StorageNotFoundError: report = ProblemReport( - explain_ltxt='Connection not found.', + description={"en":'Connection not found.'}, who_retries='none' ) report.assign_thread_from(context.message) diff --git a/acapy_plugin_toolbox/taa.py b/acapy_plugin_toolbox/taa.py index 2bdcfb22..e91b9e77 100644 --- a/acapy_plugin_toolbox/taa.py +++ b/acapy_plugin_toolbox/taa.py @@ -83,7 +83,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ledger: BaseLedger = session.inject(BaseLedger, required=False) if not ledger or ledger.BACKEND_NAME != 'indy': report = ProblemReport( - explain_ltxt='Invalid ledger.', + description={"en":'Invalid ledger.'}, who_retries='none' ) report.assign_thread_from(context.message) @@ -148,7 +148,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ledger: BaseLedger = session.inject(BaseLedger, required=False) if not ledger or ledger.BACKEND_NAME != 'indy': report = ProblemReport( - explain_ltxt='Invalid ledger.', + description={"en":'Invalid ledger.'}, who_retries='none' ) report.assign_thread_from(context.message) @@ -169,10 +169,10 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) except Exception as err: report = ProblemReport( - explain_ltxt='An error occured while attempting to accept' + description={"en":'An error occured while attempting to accept' ' the Transaction Author Agreement: {}'.format( err - ), + )}, who_retries='none' ) report.assign_thread_from(context.message) @@ -227,7 +227,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ledger: BaseLedger = session.inject(BaseLedger, required=False) if not ledger or ledger.BACKEND_NAME != 'indy': report = ProblemReport( - explain_ltxt='Invalid ledger.', + description={"en":'Invalid ledger.'}, who_retries='none' ) report.assign_thread_from(context.message) diff --git a/acapy_plugin_toolbox/util.py b/acapy_plugin_toolbox/util.py index a5c38ab5..6aa7d69d 100644 --- a/acapy_plugin_toolbox/util.py +++ b/acapy_plugin_toolbox/util.py @@ -70,8 +70,8 @@ async def _wrapped(*args): return await func(*args) report = ProblemReport( - explain_ltxt='This connection is not authorized to perform' - ' the requested action.', + description={"en":'This connection is not authorized to perform' + ' the requested action.'}, who_retries='none' ) report.assign_thread_from(context.message) @@ -412,7 +412,7 @@ async def __aenter__(self): async def __aexit__(self, err_type, err_value, err_traceback): """Exit the context manager.""" if isinstance(err_value, self.exception): - report = ProblemReport(explain_ltxt=str(err_value)) + report = ProblemReport(description={"en":str(err_value)}) if self.original_message: report.assign_thread_from(self.original_message) await self.responder.send_reply(report) From 225aae0da0f3732e731cf90921d8df98c85d921f Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Wed, 12 May 2021 15:09:27 -0600 Subject: [PATCH 53/59] requirements.txt latest ACA-Py commit and adjustments so tests pass Signed-off-by: Matthew Wright --- acapy_plugin_toolbox/holder/__init__.py | 7 +++---- acapy_plugin_toolbox/holder/v0_1.py | 7 ++++--- requirements.txt | 2 +- tests/holder/conftest.py | 19 +++++++++++------- tests/holder/test_pres_approve.py | 20 +++++++++---------- .../test_pres_get_matching_credentials.py | 15 +++++++------- tests/holder/test_send_pres_proposal.py | 14 ++++++++++--- 7 files changed, 49 insertions(+), 35 deletions(-) diff --git a/acapy_plugin_toolbox/holder/__init__.py b/acapy_plugin_toolbox/holder/__init__.py index 96df6a2d..9d6d2917 100644 --- a/acapy_plugin_toolbox/holder/__init__.py +++ b/acapy_plugin_toolbox/holder/__init__.py @@ -14,11 +14,10 @@ V10CredentialExchangeSchema as CredExRecordSchema from aries_cloudagent.protocols.issue_credential.v1_0.routes import \ V10CredentialProposalRequestMandSchema as CredentialProposalRequestSchema -from aries_cloudagent.protocols.present_proof import v1_0 as present_proof from aries_cloudagent.protocols.present_proof.v1_0.manager import ( PresentationManager, PresentationManagerError ) -from aries_cloudagent.protocols.present_proof.indy.pres_preview import ( +from aries_cloudagent.indy.sdk.models.pres_preview import ( IndyPresPreview as PresentationPreview ) from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import \ @@ -28,7 +27,7 @@ from aries_cloudagent.protocols.present_proof.v1_0.routes import ( IndyCredPrecisSchema ) -from aries_cloudagent.protocols.present_proof.indy.requested_creds import ( +from aries_cloudagent.indy.sdk.models.requested_creds import ( IndyRequestedCredsRequestedAttrSchema, IndyRequestedCredsRequestedPredSchema, ) @@ -38,7 +37,7 @@ __all__ = [ "issue_credential", "CredentialManager", "CredentialManagerError", "CredentialAttributeSpec", "CredExRecord", "CredExRecordSchema", - "CredentialProposalRequestSchema", "present_proof", + "CredentialProposalRequestSchema", "PresentationManager", "PresentationManagerError", "PresentationPreview", "PresExRecord", "PresExRecordSchema", "IndyCredPrecisSchema", "PresentationProposalRequestSchema", "IndyRequestedCredsRequestedAttrSchema", diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py index 258c8fe7..215f29a4 100644 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ b/acapy_plugin_toolbox/holder/v0_1.py @@ -32,12 +32,13 @@ expand_model_class, get_connection, send_to_admins, with_generic_init, log_handling ) +from aries_cloudagent.protocols.present_proof.v1_0.messages.presentation_proposal import PresentationProposal from . import ( CredentialAttributeSpec, CredentialManager, CredentialManagerError, CredentialProposalRequestSchema, CredExRecord, CredExRecordSchema, IndyCredPrecisSchema, PresentationPreview, PresentationProposalRequestSchema, PresExRecord, PresExRecordSchema, - issue_credential, present_proof, PresentationManager, + issue_credential, PresentationManager, IndyRequestedCredsRequestedPredSchema, IndyRequestedCredsRequestedAttrSchema ) @@ -434,7 +435,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): comment = context.message.comment # Aries#0037 calls it a proposal in the proposal struct but it's of type preview - presentation_proposal = present_proof.messages.presentation_proposal.PresentationProposal( + presentation_proposal = PresentationProposal( comment=comment, presentation_proposal=context.message.presentation_proposal ) @@ -443,7 +444,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): context.settings.get("debug.auto_respond_presentation_request") ) - presentation_manager = present_proof.manager.PresentationManager(context.profile) + presentation_manager = PresentationManager(context.profile) presentation_exchange_record = ( await presentation_manager.create_exchange_for_proposal( diff --git a/requirements.txt b/requirements.txt index 21bca480..c4ae6132 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aries-cloudagent[indy]@git+https://github.com/indicio-tech/aries-cloudagent-python@39f2c9a49d00f3c926ff56d9385c52dca6dc1c76 +aries-cloudagent[indy]@git+https://github.com/hyperledger/aries-cloudagent-python@6292b5913605ff3cfe245093b7ce68b705e9b262 marshmallow==3.5.1 flake8 python-dateutil diff --git a/tests/holder/conftest.py b/tests/holder/conftest.py index 804b13cf..8a0fb748 100644 --- a/tests/holder/conftest.py +++ b/tests/holder/conftest.py @@ -2,31 +2,35 @@ from contextlib import contextmanager -from acapy_plugin_toolbox.holder import PresExRecord - import pytest +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchange as PresExRecord, +) from asynctest import mock @pytest.fixture def mock_record_query(): """Mock PresExRecord.query on a module.""" + @contextmanager def _mock_record_query(obj, result=None, spec=None): with mock.patch.object( - obj, "query", + obj, + "query", mock.CoroutineMock( - return_value=result or - mock.MagicMock(spec=spec) - ) + return_value=result or mock.MagicMock(spec=spec), + ), ) as record_query: yield record_query + yield _mock_record_query @pytest.fixture def mock_get_pres_ex_record(): """Mock get_pres_ex_record.""" + @contextmanager def _mock_get_pres_ex_record(obj, pres_ex_record: PresExRecord = None): with mock.patch.object( @@ -34,7 +38,8 @@ def _mock_get_pres_ex_record(obj, pres_ex_record: PresExRecord = None): "get_pres_ex_record", mock.CoroutineMock( return_value=pres_ex_record or mock.MagicMock(autospec=True) - ) + ), ) as get_pres_ex_record: yield get_pres_ex_record + yield _mock_get_pres_ex_record diff --git a/tests/holder/test_pres_approve.py b/tests/holder/test_pres_approve.py index 455a59a0..42b4b7a8 100644 --- a/tests/holder/test_pres_approve.py +++ b/tests/holder/test_pres_approve.py @@ -1,12 +1,14 @@ """Test PresRequestApprove message and handler.""" import pytest - -from asynctest import mock from acapy_plugin_toolbox.holder import v0_1 as test_module from acapy_plugin_toolbox.holder.v0_1 import PresRequestApprove -from acapy_plugin_toolbox.holder import PresentationManager, PresExRecord from aries_cloudagent.connections.models.conn_record import ConnRecord +from aries_cloudagent.protocols.present_proof.v1_0.manager import PresentationManager +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchange as PresExRecord, +) +from asynctest import mock TEST_PRES_EX_ID = "test-presentation_exchange_id" TEST_CONN_ID = "test-connection-id" @@ -24,7 +26,7 @@ def message(): self_attested_attributes=TEST_SELF_ATTESTED_ATTRS, requested_attributes=TEST_REQUESTED_ATTRS, requested_predicates=TEST_REQUESTED_PREDS, - comment=TEST_COMMENT + comment=TEST_COMMENT, ) @@ -38,7 +40,7 @@ def context(context, message): def record(): yield PresExRecord( presentation_exchange_id=TEST_PRES_EX_ID, - connection_id=TEST_CONN_ID + connection_id=TEST_CONN_ID, ) @@ -55,21 +57,19 @@ async def test_handler( mock_get_connection, mock_get_pres_ex_record, record, - conn_record + conn_record, ): """Test PresRequestApprove handler.""" mock_presentation_manager = mock.MagicMock(spec=PresentationManager) mock_presentation_manager.create_presentation = mock.CoroutineMock( return_value=(record, mock.MagicMock()) ) - with mock_get_connection( - test_module, conn_record - ), mock_get_pres_ex_record( + with mock_get_connection(test_module, conn_record), mock_get_pres_ex_record( PresRequestApprove, record ), mock.patch.object( test_module, "PresentationManager", - mock.MagicMock(return_value=mock_presentation_manager) + mock.MagicMock(return_value=mock_presentation_manager), ): await message.handle(context, mock_responder) diff --git a/tests/holder/test_pres_get_matching_credentials.py b/tests/holder/test_pres_get_matching_credentials.py index f167b4b1..d3db7858 100644 --- a/tests/holder/test_pres_get_matching_credentials.py +++ b/tests/holder/test_pres_get_matching_credentials.py @@ -2,15 +2,8 @@ # pylint: disable=redefined-outer-name -from aries_cloudagent.indy.holder import IndyHolder -from aries_cloudagent.indy.sdk.holder import IndySdkHolder -from aries_cloudagent.protocols.problem_report.v1_0.message import ProblemReport -from aries_cloudagent.storage.error import StorageNotFoundError -from asynctest import mock import pytest - from acapy_plugin_toolbox.decorators.pagination import Paginate -from acapy_plugin_toolbox.holder import PresExRecord from acapy_plugin_toolbox.holder import v0_1 as test_module from acapy_plugin_toolbox.holder.v0_1 import ( InvalidPresentationExchange, @@ -18,6 +11,14 @@ PresMatchingCredentials, PresRequestApprove, ) +from aries_cloudagent.indy.holder import IndyHolder +from aries_cloudagent.indy.sdk.holder import IndySdkHolder +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchange as PresExRecord, +) +from aries_cloudagent.protocols.problem_report.v1_0.message import ProblemReport +from aries_cloudagent.storage.error import StorageNotFoundError +from asynctest import mock TEST_PRES_EX_ID = "test-pres-ex-id" TEST_CONN_ID = "test-connection-id" diff --git a/tests/holder/test_send_pres_proposal.py b/tests/holder/test_send_pres_proposal.py index df059fc9..a7695b6e 100644 --- a/tests/holder/test_send_pres_proposal.py +++ b/tests/holder/test_send_pres_proposal.py @@ -1,12 +1,20 @@ """Test SendPresProposal message and handler.""" import pytest - from acapy_plugin_toolbox.holder import v0_1 as test_module from acapy_plugin_toolbox.holder.v0_1 import SendPresProposal +from aries_cloudagent.indy.sdk.models.pres_preview import ( + IndyPresAttrSpec, + IndyPresPreview, +) TEST_CONN_ID = "test-connection-id" -TEST_PROPOSAL = "test-proposal" +TEST_PROPOSAL = IndyPresPreview( + attributes=[ + IndyPresAttrSpec(name="test-proposal"), + ], + predicates=[], +) TEST_COMMENT = "test-comment" @@ -16,7 +24,7 @@ def message(): yield SendPresProposal( connection_id=TEST_CONN_ID, presentation_proposal=TEST_PROPOSAL, - comment=TEST_COMMENT + comment=TEST_COMMENT, ) From 158ba0fb2cbab703f1fa5bc1f7531eb03e1c5211 Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Wed, 12 May 2021 15:25:25 -0600 Subject: [PATCH 54/59] splitting out holder/v0_1/messages Signed-off-by: Matthew Wright --- acapy_plugin_toolbox/holder/__init__.py | 44 - acapy_plugin_toolbox/holder/v0_1.py | 964 ------------------ acapy_plugin_toolbox/holder/v0_1/__init__.py | 140 +++ acapy_plugin_toolbox/holder/v0_1/error.py | 2 + .../holder/v0_1/messages/__init__.py | 47 + .../holder/v0_1/messages/base.py | 7 + .../holder/v0_1/messages/cred_delete.py | 64 ++ .../holder/v0_1/messages/cred_deleted.py | 29 + .../holder/v0_1/messages/cred_exchange.py | 15 + .../holder/v0_1/messages/cred_get_list.py | 74 ++ .../holder/v0_1/messages/cred_list.py | 35 + .../holder/v0_1/messages/cred_offer_accept.py | 75 ++ .../holder/v0_1/messages/cred_offer_recv.py | 15 + .../holder/v0_1/messages/cred_received.py | 15 + .../holder/v0_1/messages/cred_request_sent.py | 15 + .../holder/v0_1/messages/pres_delete.py | 57 ++ .../holder/v0_1/messages/pres_deleted.py | 23 + .../holder/v0_1/messages/pres_exchange.py | 15 + .../holder/v0_1/messages/pres_get_list.py | 60 ++ .../messages/pres_get_matching_credentials.py | 68 ++ .../holder/v0_1/messages/pres_list.py | 28 + .../messages/pres_matching_credentials.py | 44 + .../v0_1/messages/pres_request_approve.py | 162 +++ .../v0_1/messages/pres_request_received.py | 57 ++ .../holder/v0_1/messages/pres_sent.py | 15 + .../v0_1/messages/send_cred_proposal.py | 73 ++ .../v0_1/messages/send_pres_proposal.py | 80 ++ tests/holder/test_events.py | 53 +- tests/holder/test_pres_approve.py | 8 +- .../test_pres_get_matching_credentials.py | 2 +- tests/holder/test_send_pres_proposal.py | 6 +- 31 files changed, 1256 insertions(+), 1036 deletions(-) delete mode 100644 acapy_plugin_toolbox/holder/v0_1.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/__init__.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/error.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/__init__.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/base.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/cred_delete.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/cred_deleted.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/cred_exchange.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/cred_get_list.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/cred_list.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_accept.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_recv.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/cred_received.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/cred_request_sent.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_delete.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_deleted.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_exchange.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_get_list.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_get_matching_credentials.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_list.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_matching_credentials.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_request_approve.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_request_received.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_sent.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/send_cred_proposal.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/send_pres_proposal.py diff --git a/acapy_plugin_toolbox/holder/__init__.py b/acapy_plugin_toolbox/holder/__init__.py index 9d6d2917..265113d9 100644 --- a/acapy_plugin_toolbox/holder/__init__.py +++ b/acapy_plugin_toolbox/holder/__init__.py @@ -1,45 +1 @@ """Holder admin protocol.""" - -# Shortcuts to deeply nested classes -from aries_cloudagent.protocols.issue_credential import \ - v1_0 as issue_credential -from aries_cloudagent.protocols.issue_credential.v1_0.manager import ( - CredentialManager, CredentialManagerError -) -from aries_cloudagent.protocols.issue_credential.v1_0.messages.inner.credential_preview import \ - CredAttrSpec as CredentialAttributeSpec -from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import \ - V10CredentialExchange as CredExRecord -from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import \ - V10CredentialExchangeSchema as CredExRecordSchema -from aries_cloudagent.protocols.issue_credential.v1_0.routes import \ - V10CredentialProposalRequestMandSchema as CredentialProposalRequestSchema -from aries_cloudagent.protocols.present_proof.v1_0.manager import ( - PresentationManager, PresentationManagerError -) -from aries_cloudagent.indy.sdk.models.pres_preview import ( - IndyPresPreview as PresentationPreview -) -from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import \ - V10PresentationExchange as PresExRecord -from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import \ - V10PresentationExchangeSchema as PresExRecordSchema -from aries_cloudagent.protocols.present_proof.v1_0.routes import ( - IndyCredPrecisSchema -) -from aries_cloudagent.indy.sdk.models.requested_creds import ( - IndyRequestedCredsRequestedAttrSchema, - IndyRequestedCredsRequestedPredSchema, -) -from aries_cloudagent.protocols.present_proof.v1_0.routes import \ - V10PresentationProposalRequestSchema as PresentationProposalRequestSchema - -__all__ = [ - "issue_credential", "CredentialManager", "CredentialManagerError", - "CredentialAttributeSpec", "CredExRecord", "CredExRecordSchema", - "CredentialProposalRequestSchema", - "PresentationManager", "PresentationManagerError", - "PresentationPreview", "PresExRecord", "PresExRecordSchema", "IndyCredPrecisSchema", - "PresentationProposalRequestSchema", "IndyRequestedCredsRequestedAttrSchema", - "IndyRequestedCredsRequestedPredSchema" -] diff --git a/acapy_plugin_toolbox/holder/v0_1.py b/acapy_plugin_toolbox/holder/v0_1.py deleted file mode 100644 index 215f29a4..00000000 --- a/acapy_plugin_toolbox/holder/v0_1.py +++ /dev/null @@ -1,964 +0,0 @@ -"""Define messages for credential holder admin protocols.""" - -# pylint: disable=invalid-name -# pylint: disable=too-few-public-methods - -import re -from typing import Sequence, List, Optional, Tuple, cast, Any -import logging - -from aries_cloudagent.config.injection_context import InjectionContext -from aries_cloudagent.connections.models.conn_record import ConnRecord -from aries_cloudagent.core.event_bus import Event, EventBus -from aries_cloudagent.core.profile import Profile, ProfileSession -from aries_cloudagent.core.protocol_registry import ProtocolRegistry -from aries_cloudagent.indy.holder import IndyHolder, IndyHolderError -from aries_cloudagent.indy.sdk.holder import IndySdkHolder -from aries_cloudagent.ledger.error import LedgerError -from aries_cloudagent.wallet.error import WalletNotFoundError -from aries_cloudagent.messaging.agent_message import AgentMessage -from aries_cloudagent.messaging.base_handler import ( - BaseResponder, RequestContext -) -from aries_cloudagent.messaging.models.base import BaseModel, BaseModelError -from aries_cloudagent.storage.error import StorageError, StorageNotFoundError -from aries_cloudagent.messaging.valid import UUIDFour -from marshmallow import fields, validate - -from .. import ProblemReport -from ..decorators.pagination import Page, Paginate -from ..util import ( - ExceptionReporter, InvalidConnection, admin_only, expand_message_class, - expand_model_class, get_connection, send_to_admins, with_generic_init, - log_handling -) -from aries_cloudagent.protocols.present_proof.v1_0.messages.presentation_proposal import PresentationProposal -from . import ( - CredentialAttributeSpec, CredentialManager, CredentialManagerError, - CredentialProposalRequestSchema, CredExRecord, CredExRecordSchema, - IndyCredPrecisSchema, PresentationPreview, - PresentationProposalRequestSchema, PresExRecord, PresExRecordSchema, - issue_credential, PresentationManager, - IndyRequestedCredsRequestedPredSchema, IndyRequestedCredsRequestedAttrSchema -) - - -LOGGER = logging.getLogger(__name__) - - -@expand_model_class -class CredentialRepresentation(BaseModel): - """Representation of Credentials in messages.""" - class Fields: - """Fields for Credential Representation.""" - issuer_did = fields.Str() - isser_connection_id = fields.Str() - name = fields.Str() - comment = fields.Str() - received_at = fields.DateTime(format="iso") - attributes = fields.List(fields.Nested(CredentialAttributeSpec)) - metadata = fields.Dict() - raw_repr = fields.Dict() - - def __init__( - self, - *, - issuer_did: str = None, - issuer_connection_id: str = None, - name: str = None, - comment: str = None, - received_at: str = None, - attributes: Sequence[CredentialAttributeSpec] = None, - metadata: dict = None, - raw_repr: dict = None - ): - """Initialize model.""" - self.issuer_did = issuer_did - self.issuer_connection_id = issuer_connection_id - self.name = name - self.comment = comment - self.received_at = received_at - self.attributes = attributes - self.metadata = metadata - self.raw_repr = raw_repr - - -class AdminHolderMessage(AgentMessage): - """Admin Holder Protocol Message Base class.""" - protocol = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/admin-holder/0.1' - - -@expand_message_class -class CredGetList(AdminHolderMessage): - """Credential list retrieval message.""" - message_type = "credentials-get-list" - - class Fields: - """Credential get list fields.""" - paginate = fields.Nested( - Paginate.Schema, - required=False, - data_key="~paginate", - missing=Paginate(limit=10, offset=0), - description="Pagination decorator." - ) - states = fields.List( - fields.Str(required=True), - required=False, - example=["offer_received"], - description="Filter listed credentials by state.", - validate=validate.OneOf( - [ - CredExRecord.STATE_ACKED, - CredExRecord.STATE_CREDENTIAL_RECEIVED, - CredExRecord.STATE_ISSUED, - CredExRecord.STATE_OFFER_RECEIVED, - CredExRecord.STATE_OFFER_SENT, - CredExRecord.STATE_PROPOSAL_RECEIVED, - CredExRecord.STATE_PROPOSAL_SENT, - CredExRecord.STATE_REQUEST_RECEIVED, - CredExRecord.STATE_REQUEST_SENT, - ] - ), - ) - - def __init__( - self, - paginate: Paginate = None, - states: Optional[List[str]] = None, - **kwargs - ): - super().__init__(**kwargs) - self.paginate = paginate - self.states = states - - @log_handling - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle received get cred list request.""" - session = await context.session() - - credentials = await CredExRecord.query(session) - - if self.states: - credentials = [c for c in credentials if c.state in self.states] - - credentials, page = self.paginate.apply(credentials) - - cred_list = CredList( - results=[credential.serialize() for credential in credentials], - page=page - ) - await responder.send_reply(cred_list) - - -@expand_message_class -class CredList(AdminHolderMessage): - """Credential list message.""" - message_type = "credentials-list" - - class Fields: - """Fields of credential list message.""" - results = fields.List( - fields.Dict(), - required=True, - description="List of requested credentials", - example=[], - ) - page = fields.Nested( - Page.Schema, - required=False, - data_key="~page", - description="Pagination decorator." - ) - - def __init__( - self, - results: Sequence[dict], - page: Page = None, - **kwargs - ): - super().__init__(**kwargs) - self.results = results - self.page = page - - -@with_generic_init -@expand_message_class -class SendCredProposal(AdminHolderMessage): - """Send Credential Proposal Message.""" - message_type = "send-credential-proposal" - fields_from = CredentialProposalRequestSchema - - @log_handling - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle received send proposal request.""" - connection_id = str(context.message.connection_id) - credential_definition_id = context.message.cred_def_id - comment = context.message.comment - - credential_manager = CredentialManager(context.profile) - - session = await context.session() - try: - conn_record = await ConnRecord.retrieve_by_id( - session, - connection_id - ) - except StorageNotFoundError: - report = ProblemReport( - description={"en":'Connection not found.'}, - who_retries='none' - ) - report.assign_thread_from(context.message) - await responder.send_reply(report) - return - - if not conn_record.is_ready: - report = ProblemReport( - description={"en":'Connection invalid.'}, - who_retries='none' - ) - report.assign_thread_from(context.message) - await responder.send_reply(report) - return - - credential_exchange_record = await credential_manager.create_proposal( - connection_id, - comment=comment, - credential_preview=context.message.credential_proposal, - cred_def_id=credential_definition_id - ) - - await responder.send( - issue_credential.messages.credential_proposal.CredentialProposal( - comment=context.message.comment, - credential_proposal=context.message.credential_proposal, - cred_def_id=credential_definition_id - ), - connection_id=connection_id - ) - cred_exchange = CredExchange(**credential_exchange_record.serialize()) - cred_exchange.assign_thread_from(context.message) - await responder.send_reply(cred_exchange) - - -@with_generic_init -@expand_message_class -class CredExchange(AdminHolderMessage): - """Credential exchange message.""" - message_type = "credential-exchange" - fields_from = CredExRecordSchema - - -@with_generic_init -@expand_message_class -class CredOfferRecv(AdminHolderMessage): - """Credential offer received message.""" - message_type = "credential-offer-received" - fields_from = CredExRecordSchema - - -@expand_message_class -class CredOfferAccept(AdminHolderMessage): - """Credential offer accept message.""" - message_type = "credential-offer-accept" - - class Fields: - """Fields of cred offer accept message.""" - credential_exchange_id = fields.Str( - required=True, - description="ID of the credential exchange to accept", - example=UUIDFour.EXAMPLE - ) - - def __init__(self, credential_exchange_id: str = None, **kwargs): - super().__init__(**kwargs) - self.credential_exchange_id = credential_exchange_id - - @log_handling - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle credential offer accept message.""" - - cred_ex_record = None - connection_record = None - async with context.session() as session: - async with ExceptionReporter( - responder, - (StorageError, CredentialManagerError, BaseModelError), - self - ): - cred_ex_record = await CredExRecord.retrieve_by_id( - session, self.credential_exchange_id - ) - connection_id = cred_ex_record.connection_id - connection_record = await get_connection(session, connection_id) - - credential_manager = CredentialManager(context.profile) - ( - cred_ex_record, - credential_request_message, - ) = await credential_manager.create_request( - cred_ex_record, connection_record.my_did - ) - - sent = CredRequestSent(**cred_ex_record.serialize()) - - await responder.send(credential_request_message, connection_id=connection_id) - await responder.send_reply(sent) - - -@with_generic_init -@expand_message_class -class CredRequestSent(AdminHolderMessage): - """Credential offer acceptance received and credential request sent.""" - message_type = "credential-request-sent" - fields_from = CredExRecordSchema - - -@with_generic_init -@expand_message_class -class CredReceived(AdminHolderMessage): - """Credential received notification message.""" - message_type = "credential-received" - fields_from = CredExRecordSchema - - -@expand_message_class -class PresGetList(AdminHolderMessage): - """Presentation get list message.""" - message_type = 'presentations-get-list' - - class Fields: - """Message fields.""" - connection_id = fields.Str( - required=False, - description="Filter presentations by connection_id" - ) - paginate = fields.Nested( - Paginate.Schema, - required=False, - data_key="~paginate", - missing=Paginate(limit=10, offset=0), - description="Pagination decorator." - ) - - def __init__( - self, - connection_id: str = None, - paginate: Paginate = None, - **kwargs - ): - super().__init__(**kwargs) - self.connection_id = connection_id - self.paginate = paginate or Paginate() - - @log_handling - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle received get cred list request.""" - - session = await context.session() - paginate: Paginate = context.message.paginate - - post_filter_positive = dict( - filter(lambda item: item[1] is not None, { - 'role': PresExRecord.ROLE_PROVER, - 'connection_id': context.message.connection_id, - }.items()) - ) - records = await PresExRecord.query( - session, {}, post_filter_positive=post_filter_positive - ) - records, page = paginate.apply(records) - pres_list = PresList([record.serialize() for record in records], page=page) - await responder.send_reply(pres_list) - - -@expand_message_class -class PresList(AdminHolderMessage): - """Presentation get list response message.""" - message_type = 'presentations-list' - - class Fields: - """Fields for presentation list message.""" - results = fields.List( - fields.Dict(), - description="Retrieved presentations." - ) - page = fields.Nested( - Page.Schema, - required=False, - data_key="~page", - description="Pagination decorator." - ) - - def __init__(self, results, page: Page = None, **kwargs): - super().__init__(**kwargs) - self.results = results - self.page = page - - -@expand_message_class -class SendPresProposal(AdminHolderMessage): - """Presentation proposal message.""" - message_type = 'send-presentation-proposal' - fields_from = PresentationProposalRequestSchema - - def __init__( - self, - *, - connection_id: str = None, - comment: str = None, - presentation_proposal: PresentationPreview = None, - auto_present: bool = None, - trace: bool = None, - **kwargs - ): - super().__init__(**kwargs) - self.connection_id = connection_id - self.comment = comment - self.presentation_proposal = presentation_proposal - self.auto_present = auto_present - self.trace = trace - - @log_handling - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle received send presentation proposal request.""" - session = await context.session() - connection_id = str(context.message.connection_id) - async with ExceptionReporter(responder, InvalidConnection, context.message): - await get_connection(session, connection_id) - - comment = context.message.comment - # Aries#0037 calls it a proposal in the proposal struct but it's of type preview - presentation_proposal = PresentationProposal( - comment=comment, - presentation_proposal=context.message.presentation_proposal - ) - auto_present = ( - context.message.auto_present or - context.settings.get("debug.auto_respond_presentation_request") - ) - - presentation_manager = PresentationManager(context.profile) - - presentation_exchange_record = ( - await presentation_manager.create_exchange_for_proposal( - connection_id=connection_id, - presentation_proposal_message=presentation_proposal, - auto_present=auto_present - ) - ) - await responder.send(presentation_proposal, connection_id=connection_id) - - pres_exchange = PresExchange(**presentation_exchange_record.serialize()) - pres_exchange.assign_thread_from(context.message) - await responder.send_reply(pres_exchange) - - -@with_generic_init -@expand_message_class -class PresExchange(AdminHolderMessage): - """Presentation Exchange message.""" - message_type = "presentation-exchange" - fields_from = PresExRecordSchema - - -@expand_message_class -class PresRequestReceived(AdminHolderMessage): - """Presentation Request Received.""" - message_type = "presentation-request-received" - - DEFAULT_COUNT = 10 - - class Fields: - """Fields of Presentation request received message.""" - record = fields.Nested( - PresExRecordSchema, - required=True, - description="Presentation details." - ) - matching_credentials = fields.Nested( - IndyCredPrecisSchema, - many=True, - description="Credentials matching the requested attributes." - ) - page = fields.Nested( - Page.Schema, - required=False, - description="Pagination decorator." - ) - - def __init__(self, record: PresExRecord, **kwargs): - super().__init__(**kwargs) - self.record = record - self.matching_credentials = [] - self.page = None - - async def retrieve_matching_credentials(self, profile: Profile): - holder = profile.inject(IndyHolder) - self.matching_credentials = await holder.get_credentials_for_presentation_request_by_referent( - self.record.presentation_request, - (), - 0, - self.DEFAULT_COUNT, - extra_query={}, - ) - self.page = Page(count_=self.DEFAULT_COUNT, offset=self.DEFAULT_COUNT) - - -class InvalidPresentationExchange(Exception): - """Raised when given Presentation Exchange ID or record is not valid.""" - - -@expand_message_class -class PresRequestApprove(AdminHolderMessage): - """Approve presentation request.""" - message_type = "presentation-request-approve" - - class Fields: - """Fields on pres request approve message.""" - presentation_exchange_id = fields.Str(required=True) - self_attested_attributes = fields.Dict( - description="Self-attested attributes to build into proof", - required=True, - keys=fields.Str(example="attr_name"), # marshmallow/apispec v3.0 ignores - values=fields.Str( - example="self_attested_value", - description=( - "Self-attested attribute values to use in requested-credentials " - "structure for proof construction" - ), - ), - ) - requested_attributes = fields.Dict( - description=( - "Nested object mapping proof request attribute referents to " - "requested-attribute specifiers" - ), - required=True, - keys=fields.Str(example="attr_referent"), # marshmallow/apispec v3.0 ignores - values=fields.Nested(IndyRequestedCredsRequestedAttrSchema()), - ) - requested_predicates = fields.Dict( - description=( - "Nested object mapping proof request predicate referents to " - "requested-predicate specifiers" - ), - required=True, - keys=fields.Str(example="pred_referent"), # marshmallow/apispec v3.0 ignores - values=fields.Nested(IndyRequestedCredsRequestedPredSchema()), - ) - comment = fields.Str( - required=False, - description="Optional comment.", - example="Nothing to see here." - ) - - def __init__( - self, - presentation_exchange_id: str, - self_attested_attributes: dict, - requested_attributes: dict, - requested_predicates: dict, - comment: str = None, - **kwargs - ): - super().__init__(**kwargs) - self.presentation_exchange_id = presentation_exchange_id - self.self_attested_attributes = self_attested_attributes - self.requested_attributes = requested_attributes - self.requested_predicates = requested_predicates - self.comment = comment - - @staticmethod - async def get_pres_ex_record( - session: ProfileSession, pres_ex_id: str - ) -> PresExRecord: - """Retrieve a presentation exchange record and validate its state.""" - try: - pres_ex_record = await PresExRecord.retrieve_by_id( - session, pres_ex_id - ) - pres_ex_record = cast(PresExRecord, pres_ex_record) - except StorageNotFoundError as err: - raise InvalidPresentationExchange( - "Presentation exchange ID not found" - ) from err - - if pres_ex_record.state != (PresExRecord.STATE_REQUEST_RECEIVED): - raise InvalidPresentationExchange( - "Presentation must be in request received state" - ) - - return pres_ex_record - - @log_handling - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle presentation request approved message.""" - async with context.session() as session: - async with ExceptionReporter( - responder, InvalidPresentationExchange, context.message - ): - pres_ex_record = await self.get_pres_ex_record( - session, self.presentation_exchange_id - ) - - async with ExceptionReporter( - responder, InvalidConnection, context.message - ): - conn_record = await get_connection( - session, pres_ex_record.connection_id - ) - - presentation_manager = PresentationManager(context.profile) - async with ExceptionReporter( - responder, - ( - BaseModelError, - IndyHolderError, - LedgerError, - StorageError, - WalletNotFoundError - ), - context.message - ): - pres_ex_record, message = await presentation_manager.create_presentation( - pres_ex_record, - { - "self_attested_attributes": self.self_attested_attributes, - "requested_attributes": self.requested_attributes, - "requested_predicates": self.requested_predicates - }, - comment=self.comment - ) - - await responder.send(message, connection_id=conn_record.connection_id) - - presentation_sent = PresSent(**pres_ex_record.serialize()) - presentation_sent.assign_thread_from(self) - await responder.send_reply(presentation_sent) - - -@with_generic_init -@expand_message_class -class PresSent(AdminHolderMessage): - """Presentation Exchange message.""" - message_type = "presentation-sent" - fields_from = PresExRecordSchema - - -@expand_message_class -class PresGetMatchingCredentials(AdminHolderMessage): - """Retrieve matching credentials for a presentation request.""" - message_type = "presentation-get-matching-credentials" - - class Fields: - presentation_exchange_id = fields.Str( - required=True, - description="Presentation to match credentials to.", - example=UUIDFour.EXAMPLE - ) - paginate = fields.Nested( - Paginate.Schema, - required=False, - data_key="~paginate", - missing=Paginate(limit=10, offset=0), - description="Pagination decorator." - ) - - def __init__( - self, presentation_exchange_id: str, paginate: Paginate = None, **kwargs - ): - super().__init__(**kwargs) - self.presentation_exchange_id = presentation_exchange_id - self.paginate = paginate - - @log_handling - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - holder = cast(IndySdkHolder, context.inject(IndyHolder)) - async with context.session() as session: - async with ExceptionReporter( - responder, InvalidPresentationExchange, context.message - ): - pres_ex_record = await PresRequestApprove.get_pres_ex_record( - session, self.presentation_exchange_id - ) - - matches = PresMatchingCredentials( - presentation_exchange_id=self.presentation_exchange_id, - matching_credentials=await holder.get_credentials_for_presentation_request_by_referent( - pres_ex_record.presentation_request, - (), - self.paginate.offset, - self.paginate.limit, - extra_query={}, - ), - page=Page(count_=self.paginate.limit, offset=self.paginate.offset) - ) - matches.assign_thread_from(self) - await responder.send_reply(matches) - - -@with_generic_init -@expand_message_class -class PresMatchingCredentials(AdminHolderMessage): - """Presentation Matching Credentials""" - message_type = "presentation-matching-credentials" - - class Fields: - """Fields for MatchingCredentials.""" - presentation_exchange_id = fields.Str( - required=True, - description="Exchange ID for matched credentials." - ) - matching_credentials = fields.Nested( - IndyCredPrecisSchema, - many=True, - description="Matched credentials." - ) - page = fields.Nested( - Page.Schema, - required=False, - description="Pagination info for matched credentials." - ) - - def __init__( - self, - presentation_exchange_id: str, - matching_credentials: Tuple[Any, ...], - page: Page = None, - **kwargs - ): - """Initialize PresMatchingCredentials""" - super().__init__(**kwargs) - self.presentation_exchange_id = presentation_exchange_id - self.matching_credentials = matching_credentials - self.page = page - - -@expand_message_class -class CredDelete(AdminHolderMessage): - """Delete a credential.""" - message_type = "credential-delete" - - class Fields: - credential_exchange_id = fields.Str( - required=True, - description="ID of the credential exchange to delete", - example=UUIDFour.EXAMPLE - ) - - def __init__(self, credential_exchange_id: str = None, **kwargs): - super().__init__(**kwargs) - self.credential_exchange_id = credential_exchange_id - - @log_handling - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - """Handle delete credential message.""" - - holder = cast(IndySdkHolder, context.inject(IndyHolder)) - async with context.session() as session: - async with ExceptionReporter( - responder, - ( - StorageError, - CredentialManagerError, - BaseModelError - ), - context.message - ): - cred_ex_record: CredExRecord = await CredExRecord.retrieve_by_id( - session, self.credential_exchange_id - ) - - await holder.delete_credential(cred_ex_record.credential_id) - await cred_ex_record.delete_record(session) - - message = CredDeleted( - credential_exchange_id=self.credential_exchange_id, - credential_id=cred_ex_record.credential_id, - ) - message.assign_thread_from(self) - await responder.send_reply(message) - - -@expand_message_class -class CredDeleted(AdminHolderMessage): - """Credential deleted.""" - message_type = "credential-deleted" - - class Fields: - credential_exchange_id = fields.Str( - required=True, - description="Credential exchange ID that was deleted.", - example=UUIDFour.EXAMPLE - ) - credential_id = fields.Str( - required=True, - description="Credential ID that was deleted.", - example=UUIDFour.EXAMPLE - ) - - def __init__( - self, credential_exchange_id: str, credential_id: str, **kwargs - ): - super().__init__(**kwargs) - self.credential_exchange_id = credential_exchange_id - self.credential_id = credential_id - - -@expand_message_class -class PresDelete(AdminHolderMessage): - """Delete a presentation exchange message.""" - message_type = "presentation-exchange-delete" - - class Fields: - presentation_exchange_id = fields.Str( - required=True, - description="Presentation Exchange Message to delete.", - example=UUIDFour.EXAMPLE - ) - - def __init__( - self, presentation_exchange_id: str, **kwargs - ): - super().__init__(**kwargs) - self.presentation_exchange_id = presentation_exchange_id - - @log_handling - @admin_only - async def handle(self, context: RequestContext, responder: BaseResponder): - async with context.session() as session: - async with ExceptionReporter( - responder, InvalidPresentationExchange, context.message - ): - pres_ex_record = await PresRequestApprove.get_pres_ex_record( - session, self.presentation_exchange_id - ) - - await pres_ex_record.delete_record(session) - - message = PresDeleted( - presentation_exchange_id=self.presentation_exchange_id, - ) - message.assign_thread_from(self) - await responder.send_reply(message) - - -@expand_message_class -class PresDeleted(AdminHolderMessage): - """Presentation exchange message deleted.""" - message_type = "presentation-exchange-deleted" - - class Fields: - presentation_exchange_id = fields.Str( - required=True, - description="Presentation Exchange Message to delete.", - example=UUIDFour.EXAMPLE - ) - - def __init__( - self, presentation_exchange_id: str, **kwargs - ): - super().__init__(**kwargs) - self.presentation_exchange_id = presentation_exchange_id - - -PROTOCOL = AdminHolderMessage.protocol -TITLE = "Holder Admin Protocol" -NAME = "admin-holder" -VERSION = "0.1" -MESSAGE_TYPES = { - msg_class.Meta.message_type: '{}.{}'.format(msg_class.__module__, msg_class.__name__) - for msg_class in [ - CredGetList, - CredList, - CredOfferAccept, - CredOfferRecv, - CredRequestSent, - CredReceived, - SendCredProposal, - CredExchange, - PresGetList, - PresList, - PresRequestApprove, - PresGetMatchingCredentials, - PresMatchingCredentials, - SendPresProposal, - PresExchange, - CredDelete, - PresDelete, - ] -} - - -async def setup( - context: InjectionContext, - protocol_registry: Optional[ProtocolRegistry] = None -): - """Setup the holder plugin.""" - if not protocol_registry: - protocol_registry = context.inject(ProtocolRegistry) - protocol_registry.register_message_types( - MESSAGE_TYPES - ) - bus: EventBus = context.inject(EventBus) - bus.subscribe( - re.compile(f"acapy::record::{CredExRecord.RECORD_TOPIC}::.*"), - issue_credential_event_handler - ) - bus.subscribe( - re.compile(f"acapy::record::{PresExRecord.RECORD_TOPIC}::.*"), - present_proof_event_handler - ) - - -async def issue_credential_event_handler(profile: Profile, event: Event): - """Handle issue credential events.""" - record: CredExRecord = CredExRecord.deserialize(event.payload) - LOGGER.debug("IssueCredential Event; %s: %s", event.topic, event.payload) - - if record.state not in ( - CredExRecord.STATE_OFFER_RECEIVED, - CredExRecord.STATE_CREDENTIAL_RECEIVED - ): - return - - responder = profile.inject(BaseResponder) - message = None - if record.state == CredExRecord.STATE_OFFER_RECEIVED: - message = CredOfferRecv(**record.serialize()) - LOGGER.debug("Prepared Message: %s", message.serialize()) - - if record.state == CredExRecord.STATE_CREDENTIAL_RECEIVED: - message = CredReceived(**record.serialize()) - LOGGER.debug("Prepared Message: %s", message.serialize()) - - async with profile.session() as session: - await send_to_admins( - session, - message, - responder - ) - - -async def present_proof_event_handler(profile: Profile, event: Event): - """Handle present proof events.""" - record: PresExRecord = PresExRecord.deserialize(event.payload) - LOGGER.debug("PresentProof Event; %s: %s", event.topic, event.payload) - - if record.state == PresExRecord.STATE_REQUEST_RECEIVED: - responder = profile.inject(BaseResponder) - message: PresRequestReceived = PresRequestReceived(record) - LOGGER.debug("Prepared Message: %s", message.serialize()) - await message.retrieve_matching_credentials(profile) - async with profile.session() as session: - await send_to_admins(session, message, responder) diff --git a/acapy_plugin_toolbox/holder/v0_1/__init__.py b/acapy_plugin_toolbox/holder/v0_1/__init__.py new file mode 100644 index 00000000..e0f6770e --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/__init__.py @@ -0,0 +1,140 @@ +"""Define messages for credential holder admin protocols.""" + +# pylint: disable=invalid-name +# pylint: disable=too-few-public-methods + +import logging +import re +from typing import Optional + +from aries_cloudagent.config.injection_context import InjectionContext +from aries_cloudagent.core.event_bus import Event, EventBus +from aries_cloudagent.core.profile import Profile +from aries_cloudagent.core.protocol_registry import ProtocolRegistry +from aries_cloudagent.messaging.agent_message import AgentMessage +from aries_cloudagent.messaging.base_handler import BaseResponder +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchange as CredExRecord, +) +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchange as PresExRecord, +) + +from ...util import send_to_admins +from .messages import ( + AdminHolderMessage, + CredDelete, + CredDeleted, + CredExchange, + CredGetList, + CredList, + CredOfferAccept, + CredOfferRecv, + CredReceived, + CredRequestSent, + PresDelete, + PresDeleted, + PresExchange, + PresGetList, + PresGetMatchingCredentials, + PresList, + PresMatchingCredentials, + PresRequestApprove, + PresRequestReceived, + PresSent, + SendCredProposal, + SendPresProposal, +) + +LOGGER = logging.getLogger(__name__) + + +PROTOCOL = AdminHolderMessage.protocol +TITLE = "Holder Admin Protocol" +NAME = "admin-holder" +VERSION = "0.1" +MESSAGE_TYPES = { + msg_class.Meta.message_type: "{}.{}".format( + msg_class.__module__, msg_class.__name__ + ) + for msg_class in [ + CredDelete, + CredDeleted, + CredExchange, + CredGetList, + CredList, + CredOfferAccept, + CredOfferRecv, + CredReceived, + CredRequestSent, + PresDelete, + PresDeleted, + PresExchange, + PresGetList, + PresGetMatchingCredentials, + PresList, + PresMatchingCredentials, + PresRequestApprove, + PresRequestReceived, + PresSent, + SendCredProposal, + SendPresProposal, + ] +} + + +async def setup( + context: InjectionContext, protocol_registry: Optional[ProtocolRegistry] = None +): + """Setup the holder plugin.""" + if not protocol_registry: + protocol_registry = context.inject(ProtocolRegistry) + protocol_registry.register_message_types(MESSAGE_TYPES) + bus: EventBus = context.inject(EventBus) + bus.subscribe( + re.compile(f"acapy::record::{CredExRecord.RECORD_TOPIC}::.*"), + issue_credential_event_handler, + ) + bus.subscribe( + re.compile(f"acapy::record::{PresExRecord.RECORD_TOPIC}::.*"), + present_proof_event_handler, + ) + + +async def issue_credential_event_handler(profile: Profile, event: Event): + """Handle issue credential events.""" + record: CredExRecord = CredExRecord.deserialize(event.payload) + LOGGER.debug("IssueCredential Event; %s: %s", event.topic, event.payload) + + if record.state not in ( + CredExRecord.STATE_OFFER_RECEIVED, + CredExRecord.STATE_CREDENTIAL_RECEIVED, + ): + return + + responder = profile.inject(BaseResponder) + message = None + if record.state == CredExRecord.STATE_OFFER_RECEIVED: + message = CredOfferRecv(**record.serialize()) + LOGGER.debug("Prepared Message: %s", message.serialize()) + + if record.state == CredExRecord.STATE_CREDENTIAL_RECEIVED: + message = CredReceived(**record.serialize()) + LOGGER.debug("Prepared Message: %s", message.serialize()) + + async with profile.session() as session: + await send_to_admins(session, message, responder) + + +async def present_proof_event_handler(profile: Profile, event: Event): + """Handle present proof events.""" + record: PresExRecord = PresExRecord.deserialize(event.payload) + LOGGER.debug("PresentProof Event; %s: %s", event.topic, event.payload) + + if record.state == PresExRecord.STATE_REQUEST_RECEIVED: + responder = profile.inject(BaseResponder) + message: PresRequestReceived = PresRequestReceived(record) + LOGGER.debug("Prepared Message: %s", message.serialize()) + await message.retrieve_matching_credentials(profile) + async with profile.session() as session: + await send_to_admins(session, message, responder) diff --git a/acapy_plugin_toolbox/holder/v0_1/error.py b/acapy_plugin_toolbox/holder/v0_1/error.py new file mode 100644 index 00000000..ae35d968 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/error.py @@ -0,0 +1,2 @@ +class InvalidPresentationExchange(Exception): + """Raised when given Presentation Exchange ID or record is not valid.""" diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/__init__.py b/acapy_plugin_toolbox/holder/v0_1/messages/__init__.py new file mode 100644 index 00000000..f77997a1 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/__init__.py @@ -0,0 +1,47 @@ +from .base import AdminHolderMessage +from .cred_delete import CredDelete +from .cred_deleted import CredDeleted +from .cred_exchange import CredExchange +from .cred_get_list import CredGetList +from .cred_list import CredList +from .cred_offer_accept import CredOfferAccept +from .cred_offer_recv import CredOfferRecv +from .cred_received import CredReceived +from .cred_request_sent import CredRequestSent +from .pres_delete import PresDelete +from .pres_deleted import PresDeleted +from .pres_exchange import PresExchange +from .pres_get_list import PresGetList +from .pres_get_matching_credentials import PresGetMatchingCredentials +from .pres_list import PresList +from .pres_matching_credentials import PresMatchingCredentials +from .pres_request_approve import PresRequestApprove +from .pres_request_received import PresRequestReceived +from .pres_sent import PresSent +from .send_cred_proposal import SendCredProposal +from .send_pres_proposal import SendPresProposal + +__all__ = [ + "AdminHolderMessage", + "CredDelete", + "CredDeleted", + "CredExchange", + "CredGetList", + "CredList", + "CredOfferAccept", + "CredOfferRecv", + "CredReceived", + "CredRequestSent", + "PresDelete", + "PresDeleted", + "PresExchange", + "PresGetList", + "PresGetMatchingCredentials", + "PresList", + "PresMatchingCredentials", + "PresRequestApprove", + "PresRequestReceived", + "PresSent", + "SendCredProposal", + "SendPresProposal", +] diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/base.py b/acapy_plugin_toolbox/holder/v0_1/messages/base.py new file mode 100644 index 00000000..07c55e11 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/base.py @@ -0,0 +1,7 @@ +from aries_cloudagent.messaging.agent_message import AgentMessage + + +class AdminHolderMessage(AgentMessage): + """Admin Holder Protocol Message Base class.""" + + protocol = "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/admin-holder/0.1" diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_delete.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_delete.py new file mode 100644 index 00000000..4d38daba --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_delete.py @@ -0,0 +1,64 @@ +from typing import cast + +from aries_cloudagent.indy.holder import IndyHolder +from aries_cloudagent.indy.sdk.holder import IndySdkHolder +from aries_cloudagent.messaging.base_handler import BaseResponder, RequestContext +from aries_cloudagent.messaging.models.base import BaseModelError +from aries_cloudagent.messaging.valid import UUIDFour +from aries_cloudagent.protocols.issue_credential.v1_0.manager import ( + CredentialManagerError, +) +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchange as CredExRecord, +) +from aries_cloudagent.storage.error import StorageError +from marshmallow import fields + +from ....util import ExceptionReporter, admin_only, expand_message_class, log_handling +from .base import AdminHolderMessage +from .cred_deleted import CredDeleted + + +@expand_message_class +class CredDelete(AdminHolderMessage): + """Delete a credential.""" + + message_type = "credential-delete" + + class Fields: + credential_exchange_id = fields.Str( + required=True, + description="ID of the credential exchange to delete", + example=UUIDFour.EXAMPLE, + ) + + def __init__(self, credential_exchange_id: str, **kwargs): + super().__init__(**kwargs) + self.credential_exchange_id = credential_exchange_id + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle delete credential message.""" + + holder = cast(IndySdkHolder, context.inject(IndyHolder)) + async with context.session() as session: + async with ExceptionReporter( + responder, + (StorageError, CredentialManagerError, BaseModelError), + context.message, + ): + cred_ex_record = await CredExRecord.retrieve_by_id( + session, self.credential_exchange_id + ) + cred_ex_record = cast(CredExRecord, cred_ex_record) + + await holder.delete_credential(cred_ex_record.credential_id) + await cred_ex_record.delete_record(session) + + message = CredDeleted( + credential_exchange_id=self.credential_exchange_id, + credential_id=cred_ex_record.credential_id, + ) + message.assign_thread_from(self) + await responder.send_reply(message) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_deleted.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_deleted.py new file mode 100644 index 00000000..42d0e53b --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_deleted.py @@ -0,0 +1,29 @@ +from aries_cloudagent.messaging.valid import UUIDFour +from marshmallow import fields + +from ....util import expand_message_class +from .base import AdminHolderMessage + + +@expand_message_class +class CredDeleted(AdminHolderMessage): + """Credential deleted.""" + + message_type = "credential-deleted" + + class Fields: + credential_exchange_id = fields.Str( + required=True, + description="Credential exchange ID that was deleted.", + example=UUIDFour.EXAMPLE, + ) + credential_id = fields.Str( + required=True, + description="Credential ID that was deleted.", + example=UUIDFour.EXAMPLE, + ) + + def __init__(self, credential_exchange_id: str, credential_id: str, **kwargs): + super().__init__(**kwargs) + self.credential_exchange_id = credential_exchange_id + self.credential_id = credential_id diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_exchange.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_exchange.py new file mode 100644 index 00000000..d9b5c97c --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_exchange.py @@ -0,0 +1,15 @@ +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchangeSchema as CredExRecordSchema, +) + +from ....util import expand_message_class, with_generic_init +from .base import AdminHolderMessage + + +@with_generic_init +@expand_message_class +class CredExchange(AdminHolderMessage): + """Credential exchange message.""" + + message_type = "credential-exchange" + fields_from = CredExRecordSchema diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_get_list.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_get_list.py new file mode 100644 index 00000000..610219f4 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_get_list.py @@ -0,0 +1,74 @@ +from typing import List, Optional + +from aries_cloudagent.messaging.base_handler import BaseResponder, RequestContext +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchange as CredExRecord, +) +from marshmallow import fields, validate + +from ....decorators.pagination import Paginate +from ....util import admin_only, expand_message_class, log_handling +from .base import AdminHolderMessage +from .cred_list import CredList + + +@expand_message_class +class CredGetList(AdminHolderMessage): + """Credential list retrieval message.""" + + message_type = "credentials-get-list" + + class Fields: + """Credential get list fields.""" + + paginate = fields.Nested( + Paginate.Schema, + required=False, + data_key="~paginate", + missing=Paginate(limit=10, offset=0), + description="Pagination decorator.", + ) + states = fields.List( + fields.Str(required=True), + required=False, + example=["offer_received"], + description="Filter listed credentials by state.", + validate=validate.OneOf( + [ + CredExRecord.STATE_ACKED, + CredExRecord.STATE_CREDENTIAL_RECEIVED, + CredExRecord.STATE_ISSUED, + CredExRecord.STATE_OFFER_RECEIVED, + CredExRecord.STATE_OFFER_SENT, + CredExRecord.STATE_PROPOSAL_RECEIVED, + CredExRecord.STATE_PROPOSAL_SENT, + CredExRecord.STATE_REQUEST_RECEIVED, + CredExRecord.STATE_REQUEST_SENT, + ] + ), + ) + + def __init__( + self, paginate: Paginate = None, states: Optional[List[str]] = None, **kwargs + ): + super().__init__(**kwargs) + self.paginate = paginate + self.states = states + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle received get cred list request.""" + session = await context.session() + + credentials = await CredExRecord.query(session) + + if self.states: + credentials = [c for c in credentials if c.state in self.states] + + credentials, page = self.paginate.apply(credentials) + + cred_list = CredList( + results=[credential.serialize() for credential in credentials], page=page + ) + await responder.send_reply(cred_list) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_list.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_list.py new file mode 100644 index 00000000..b13ba542 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_list.py @@ -0,0 +1,35 @@ +from typing import Sequence + +from marshmallow import fields + +from ....decorators.pagination import Page +from ....util import expand_message_class +from .base import AdminHolderMessage + + +@expand_message_class +class CredList(AdminHolderMessage): + """Credential list message.""" + + message_type = "credentials-list" + + class Fields: + """Fields of credential list message.""" + + results = fields.List( + fields.Dict(), + required=True, + description="List of requested credentials", + example=[], + ) + page = fields.Nested( + Page.Schema, + required=False, + data_key="~page", + description="Pagination decorator.", + ) + + def __init__(self, results: Sequence[dict], page: Page = None, **kwargs): + super().__init__(**kwargs) + self.results = results + self.page = page diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_accept.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_accept.py new file mode 100644 index 00000000..ac9aab77 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_accept.py @@ -0,0 +1,75 @@ +from typing import cast + +from aries_cloudagent.messaging.base_handler import BaseResponder, RequestContext +from aries_cloudagent.messaging.models.base import BaseModelError +from aries_cloudagent.messaging.valid import UUIDFour +from aries_cloudagent.protocols.issue_credential.v1_0.manager import ( + CredentialManager, + CredentialManagerError, +) +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchange as CredExRecord, +) +from aries_cloudagent.storage.error import StorageError +from marshmallow import fields + +from ....util import ( + ExceptionReporter, + admin_only, + expand_message_class, + get_connection, + log_handling, +) +from .base import AdminHolderMessage +from .cred_request_sent import CredRequestSent + + +@expand_message_class +class CredOfferAccept(AdminHolderMessage): + """Credential offer accept message.""" + + message_type = "credential-offer-accept" + + class Fields: + """Fields of cred offer accept message.""" + + credential_exchange_id = fields.Str( + required=True, + description="ID of the credential exchange to accept", + example=UUIDFour.EXAMPLE, + ) + + def __init__(self, credential_exchange_id: str, **kwargs): + super().__init__(**kwargs) + self.credential_exchange_id = credential_exchange_id + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle credential offer accept message.""" + + cred_ex_record = None + connection_record = None + async with context.session() as session: + async with ExceptionReporter( + responder, (StorageError, CredentialManagerError, BaseModelError), self + ): + cred_ex_record = await CredExRecord.retrieve_by_id( + session, self.credential_exchange_id + ) + cred_ex_record = cast(CredExRecord, cred_ex_record) + connection_id = cred_ex_record.connection_id + connection_record = await get_connection(session, connection_id) + + credential_manager = CredentialManager(context.profile) + ( + cred_ex_record, + credential_request_message, + ) = await credential_manager.create_request( + cred_ex_record, connection_record.my_did + ) + + sent = CredRequestSent(**cred_ex_record.serialize()) + + await responder.send(credential_request_message, connection_id=connection_id) + await responder.send_reply(sent) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_recv.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_recv.py new file mode 100644 index 00000000..032d3871 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_recv.py @@ -0,0 +1,15 @@ +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchangeSchema as CredExRecordSchema, +) + +from ....util import expand_message_class, with_generic_init +from .base import AdminHolderMessage + + +@with_generic_init +@expand_message_class +class CredOfferRecv(AdminHolderMessage): + """Credential offer received message.""" + + message_type = "credential-offer-received" + fields_from = CredExRecordSchema diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_received.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_received.py new file mode 100644 index 00000000..ceb3229d --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_received.py @@ -0,0 +1,15 @@ +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchangeSchema as CredExRecordSchema, +) + +from ....util import expand_message_class, with_generic_init +from .base import AdminHolderMessage + + +@with_generic_init +@expand_message_class +class CredReceived(AdminHolderMessage): + """Credential received notification message.""" + + message_type = "credential-received" + fields_from = CredExRecordSchema diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_request_sent.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_request_sent.py new file mode 100644 index 00000000..fa0669d1 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_request_sent.py @@ -0,0 +1,15 @@ +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchangeSchema as CredExRecordSchema, +) + +from ....util import expand_message_class, with_generic_init +from .base import AdminHolderMessage + + +@with_generic_init +@expand_message_class +class CredRequestSent(AdminHolderMessage): + """Credential offer acceptance received and credential request sent.""" + + message_type = "credential-request-sent" + fields_from = CredExRecordSchema diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_delete.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_delete.py new file mode 100644 index 00000000..4f102b0b --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_delete.py @@ -0,0 +1,57 @@ +from typing import cast + +from aries_cloudagent.messaging.base_handler import BaseResponder, RequestContext +from aries_cloudagent.messaging.valid import UUIDFour +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchange as PresExRecord, +) +from aries_cloudagent.storage.error import StorageNotFoundError +from marshmallow import fields + +from ....util import ExceptionReporter, admin_only, expand_message_class, log_handling +from ..error import InvalidPresentationExchange +from .base import AdminHolderMessage +from .pres_deleted import PresDeleted + + +@expand_message_class +class PresDelete(AdminHolderMessage): + """Delete a presentation exchange message.""" + + message_type = "presentation-exchange-delete" + + class Fields: + presentation_exchange_id = fields.Str( + required=True, + description="Presentation Exchange Message to delete.", + example=UUIDFour.EXAMPLE, + ) + + def __init__(self, presentation_exchange_id: str, **kwargs): + super().__init__(**kwargs) + self.presentation_exchange_id = presentation_exchange_id + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + async with context.session() as session: + async with ExceptionReporter( + responder, InvalidPresentationExchange, context.message + ): + try: + pres_ex_record = await PresExRecord.retrieve_by_id( + session, self.presentation_exchange_id + ) + pres_ex_record = cast(PresExRecord, pres_ex_record) + except StorageNotFoundError as err: + raise InvalidPresentationExchange( + "Presentation exchange ID not found" + ) from err + + await pres_ex_record.delete_record(session) + + message = PresDeleted( + presentation_exchange_id=self.presentation_exchange_id, + ) + message.assign_thread_from(self) + await responder.send_reply(message) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_deleted.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_deleted.py new file mode 100644 index 00000000..c1661b99 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_deleted.py @@ -0,0 +1,23 @@ +from aries_cloudagent.messaging.valid import UUIDFour +from marshmallow import fields + +from ....util import expand_message_class +from .base import AdminHolderMessage + + +@expand_message_class +class PresDeleted(AdminHolderMessage): + """Presentation exchange message deleted.""" + + message_type = "presentation-exchange-deleted" + + class Fields: + presentation_exchange_id = fields.Str( + required=True, + description="Presentation Exchange Message to delete.", + example=UUIDFour.EXAMPLE, + ) + + def __init__(self, presentation_exchange_id: str, **kwargs): + super().__init__(**kwargs) + self.presentation_exchange_id = presentation_exchange_id diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_exchange.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_exchange.py new file mode 100644 index 00000000..f33ba409 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_exchange.py @@ -0,0 +1,15 @@ +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchangeSchema as PresExRecordSchema, +) + +from ....util import expand_message_class, with_generic_init +from .base import AdminHolderMessage + + +@with_generic_init +@expand_message_class +class PresExchange(AdminHolderMessage): + """Presentation Exchange message.""" + + message_type = "presentation-exchange" + fields_from = PresExRecordSchema diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_get_list.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_get_list.py new file mode 100644 index 00000000..5c317934 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_get_list.py @@ -0,0 +1,60 @@ +from aries_cloudagent.messaging.base_handler import BaseResponder, RequestContext +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchange as PresExRecord, +) +from marshmallow import fields + +from ....decorators.pagination import Paginate +from ....util import admin_only, expand_message_class, log_handling +from .base import AdminHolderMessage +from .pres_list import PresList + + +@expand_message_class +class PresGetList(AdminHolderMessage): + """Presentation get list message.""" + + message_type = "presentations-get-list" + + class Fields: + """Message fields.""" + + connection_id = fields.Str( + required=False, description="Filter presentations by connection_id" + ) + paginate = fields.Nested( + Paginate.Schema, + required=False, + data_key="~paginate", + missing=Paginate(limit=10, offset=0), + description="Pagination decorator.", + ) + + def __init__(self, connection_id: str = None, paginate: Paginate = None, **kwargs): + super().__init__(**kwargs) + self.connection_id = connection_id + self.paginate = paginate or Paginate() + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle received get cred list request.""" + + session = await context.session() + paginate: Paginate = context.message.paginate + + post_filter_positive = dict( + filter( + lambda item: item[1] is not None, + { + "role": PresExRecord.ROLE_PROVER, + "connection_id": context.message.connection_id, + }.items(), + ) + ) + records = await PresExRecord.query( + session, {}, post_filter_positive=post_filter_positive + ) + records, page = paginate.apply(records) + pres_list = PresList([record.serialize() for record in records], page=page) + await responder.send_reply(pres_list) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_get_matching_credentials.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_get_matching_credentials.py new file mode 100644 index 00000000..7eb005e6 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_get_matching_credentials.py @@ -0,0 +1,68 @@ +from typing import cast + +from aries_cloudagent.indy.holder import IndyHolder +from aries_cloudagent.indy.sdk.holder import IndySdkHolder +from aries_cloudagent.messaging.base_handler import BaseResponder, RequestContext +from aries_cloudagent.messaging.valid import UUIDFour +from marshmallow import fields + +from ....decorators.pagination import Page, Paginate +from ....util import ExceptionReporter, admin_only, expand_message_class, log_handling +from ..error import InvalidPresentationExchange +from .base import AdminHolderMessage +from .pres_matching_credentials import PresMatchingCredentials +from .pres_request_approve import PresRequestApprove + + +@expand_message_class +class PresGetMatchingCredentials(AdminHolderMessage): + """Retrieve matching credentials for a presentation request.""" + + message_type = "presentation-get-matching-credentials" + + class Fields: + presentation_exchange_id = fields.Str( + required=True, + description="Presentation to match credentials to.", + example=UUIDFour.EXAMPLE, + ) + paginate = fields.Nested( + Paginate.Schema, + required=False, + data_key="~paginate", + missing=Paginate(limit=10, offset=0), + description="Pagination decorator.", + ) + + def __init__( + self, presentation_exchange_id: str, paginate: Paginate = None, **kwargs + ): + super().__init__(**kwargs) + self.presentation_exchange_id = presentation_exchange_id + self.paginate = paginate + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + holder = cast(IndySdkHolder, context.inject(IndyHolder)) + async with context.session() as session: + async with ExceptionReporter( + responder, InvalidPresentationExchange, context.message + ): + pres_ex_record = await PresRequestApprove.get_pres_ex_record( + session, self.presentation_exchange_id + ) + + matches = PresMatchingCredentials( + presentation_exchange_id=self.presentation_exchange_id, + matching_credentials=await holder.get_credentials_for_presentation_request_by_referent( + pres_ex_record.presentation_request, + (), + self.paginate.offset, + self.paginate.limit, + extra_query={}, + ), + page=Page(count_=self.paginate.limit, offset=self.paginate.offset), + ) + matches.assign_thread_from(self) + await responder.send_reply(matches) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_list.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_list.py new file mode 100644 index 00000000..835df54b --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_list.py @@ -0,0 +1,28 @@ +from marshmallow import fields + +from ....decorators.pagination import Page +from ....util import expand_message_class +from .base import AdminHolderMessage + + +@expand_message_class +class PresList(AdminHolderMessage): + """Presentation get list response message.""" + + message_type = "presentations-list" + + class Fields: + """Fields for presentation list message.""" + + results = fields.List(fields.Dict(), description="Retrieved presentations.") + page = fields.Nested( + Page.Schema, + required=False, + data_key="~page", + description="Pagination decorator.", + ) + + def __init__(self, results, page: Page = None, **kwargs): + super().__init__(**kwargs) + self.results = results + self.page = page diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_matching_credentials.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_matching_credentials.py new file mode 100644 index 00000000..416bf4e5 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_matching_credentials.py @@ -0,0 +1,44 @@ +from typing import Any, Tuple + +from aries_cloudagent.protocols.present_proof.v1_0.routes import IndyCredPrecisSchema +from marshmallow import fields + +from ....decorators.pagination import Page +from ....util import expand_message_class, with_generic_init +from .base import AdminHolderMessage + + +@with_generic_init +@expand_message_class +class PresMatchingCredentials(AdminHolderMessage): + """Presentation Matching Credentials""" + + message_type = "presentation-matching-credentials" + + class Fields: + """Fields for MatchingCredentials.""" + + presentation_exchange_id = fields.Str( + required=True, description="Exchange ID for matched credentials." + ) + matching_credentials = fields.Nested( + IndyCredPrecisSchema, many=True, description="Matched credentials." + ) + page = fields.Nested( + Page.Schema, + required=False, + description="Pagination info for matched credentials.", + ) + + def __init__( + self, + presentation_exchange_id: str, + matching_credentials: Tuple[Any, ...], + page: Page = None, + **kwargs, + ): + """Initialize PresMatchingCredentials""" + super().__init__(**kwargs) + self.presentation_exchange_id = presentation_exchange_id + self.matching_credentials = matching_credentials + self.page = page diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_approve.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_approve.py new file mode 100644 index 00000000..0adcbbbb --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_approve.py @@ -0,0 +1,162 @@ +from typing import cast + +from aries_cloudagent.core.profile import ProfileSession +from aries_cloudagent.indy.holder import IndyHolderError +from aries_cloudagent.indy.sdk.models.requested_creds import ( + IndyRequestedCredsRequestedAttrSchema, + IndyRequestedCredsRequestedPredSchema, +) +from aries_cloudagent.ledger.error import LedgerError +from aries_cloudagent.messaging.base_handler import BaseResponder, RequestContext +from aries_cloudagent.messaging.models.base import BaseModelError +from aries_cloudagent.protocols.present_proof.v1_0.manager import PresentationManager +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchange as PresExRecord, +) +from aries_cloudagent.storage.error import StorageError, StorageNotFoundError +from aries_cloudagent.wallet.error import WalletNotFoundError +from marshmallow import fields + +from ....util import ( + ExceptionReporter, + InvalidConnection, + admin_only, + expand_message_class, + get_connection, + log_handling, +) +from ..error import InvalidPresentationExchange +from .base import AdminHolderMessage +from .pres_sent import PresSent + + +@expand_message_class +class PresRequestApprove(AdminHolderMessage): + """Approve presentation request.""" + + message_type = "presentation-request-approve" + + class Fields: + """Fields on pres request approve message.""" + + presentation_exchange_id = fields.Str(required=True) + self_attested_attributes = fields.Dict( + description="Self-attested attributes to build into proof", + required=True, + keys=fields.Str(example="attr_name"), # marshmallow/apispec v3.0 ignores + values=fields.Str( + example="self_attested_value", + description=( + "Self-attested attribute values to use in requested-credentials " + "structure for proof construction" + ), + ), + ) + requested_attributes = fields.Dict( + description=( + "Nested object mapping proof request attribute referents to " + "requested-attribute specifiers" + ), + required=True, + keys=fields.Str( + example="attr_referent" + ), # marshmallow/apispec v3.0 ignores + values=fields.Nested(IndyRequestedCredsRequestedAttrSchema()), + ) + requested_predicates = fields.Dict( + description=( + "Nested object mapping proof request predicate referents to " + "requested-predicate specifiers" + ), + required=True, + keys=fields.Str( + example="pred_referent" + ), # marshmallow/apispec v3.0 ignores + values=fields.Nested(IndyRequestedCredsRequestedPredSchema()), + ) + comment = fields.Str( + required=False, + description="Optional comment.", + example="Nothing to see here.", + ) + + def __init__( + self, + presentation_exchange_id: str, + self_attested_attributes: dict, + requested_attributes: dict, + requested_predicates: dict, + comment: str = None, + **kwargs, + ): + super().__init__(**kwargs) + self.presentation_exchange_id = presentation_exchange_id + self.self_attested_attributes = self_attested_attributes + self.requested_attributes = requested_attributes + self.requested_predicates = requested_predicates + self.comment = comment + + @staticmethod + async def get_pres_ex_record( + session: ProfileSession, pres_ex_id: str + ) -> PresExRecord: + """Retrieve a presentation exchange record and validate its state.""" + try: + pres_ex_record = await PresExRecord.retrieve_by_id(session, pres_ex_id) + pres_ex_record = cast(PresExRecord, pres_ex_record) + except StorageNotFoundError as err: + raise InvalidPresentationExchange( + "Presentation exchange ID not found" + ) from err + + if pres_ex_record.state != (PresExRecord.STATE_REQUEST_RECEIVED): + raise InvalidPresentationExchange( + "Presentation must be in request received state" + ) + + return pres_ex_record + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle presentation request approved message.""" + async with context.session() as session: + async with ExceptionReporter( + responder, InvalidPresentationExchange, context.message + ): + pres_ex_record = await self.get_pres_ex_record( + session, self.presentation_exchange_id + ) + + async with ExceptionReporter(responder, InvalidConnection, context.message): + conn_record = await get_connection( + session, pres_ex_record.connection_id + ) + + presentation_manager = PresentationManager(context.profile) + async with ExceptionReporter( + responder, + ( + BaseModelError, + IndyHolderError, + LedgerError, + StorageError, + WalletNotFoundError, + ), + context.message, + ): + pres_ex_record, message = await presentation_manager.create_presentation( + pres_ex_record, + { + "self_attested_attributes": self.self_attested_attributes, + "requested_attributes": self.requested_attributes, + "requested_predicates": self.requested_predicates, + }, + comment=self.comment, + ) + + await responder.send(message, connection_id=conn_record.connection_id) + + presentation_sent = PresSent(**pres_ex_record.serialize()) + presentation_sent.assign_thread_from(self) + await responder.send_reply(presentation_sent) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_received.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_received.py new file mode 100644 index 00000000..ae67dacb --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_received.py @@ -0,0 +1,57 @@ +from aries_cloudagent.core.profile import Profile +from aries_cloudagent.indy.holder import IndyHolder +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchange as PresExRecord, +) +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchangeSchema as PresExRecordSchema, +) +from aries_cloudagent.protocols.present_proof.v1_0.routes import IndyCredPrecisSchema +from marshmallow import fields + +from ....decorators.pagination import Page +from ....util import expand_message_class +from .base import AdminHolderMessage + + +@expand_message_class +class PresRequestReceived(AdminHolderMessage): + """Presentation Request Received.""" + + message_type = "presentation-request-received" + + DEFAULT_COUNT = 10 + + class Fields: + """Fields of Presentation request received message.""" + + record = fields.Nested( + PresExRecordSchema, required=True, description="Presentation details." + ) + matching_credentials = fields.Nested( + IndyCredPrecisSchema, + many=True, + description="Credentials matching the requested attributes.", + ) + page = fields.Nested( + Page.Schema, required=False, description="Pagination decorator." + ) + + def __init__(self, record: PresExRecord, **kwargs): + super().__init__(**kwargs) + self.record = record + self.matching_credentials = [] + self.page = None + + async def retrieve_matching_credentials(self, profile: Profile): + holder = profile.inject(IndyHolder) + self.matching_credentials = ( + await holder.get_credentials_for_presentation_request_by_referent( + self.record.presentation_request, + (), + 0, + self.DEFAULT_COUNT, + extra_query={}, + ) + ) + self.page = Page(count_=self.DEFAULT_COUNT, offset=self.DEFAULT_COUNT) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_sent.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_sent.py new file mode 100644 index 00000000..eb973df2 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_sent.py @@ -0,0 +1,15 @@ +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchangeSchema as PresExRecordSchema, +) + +from ....util import expand_message_class, with_generic_init +from .base import AdminHolderMessage + + +@with_generic_init +@expand_message_class +class PresSent(AdminHolderMessage): + """Presentation Exchange message.""" + + message_type = "presentation-sent" + fields_from = PresExRecordSchema diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/send_cred_proposal.py b/acapy_plugin_toolbox/holder/v0_1/messages/send_cred_proposal.py new file mode 100644 index 00000000..72dfe334 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/send_cred_proposal.py @@ -0,0 +1,73 @@ +from typing import cast + +from aries_cloudagent.connections.models.conn_record import ConnRecord +from aries_cloudagent.messaging.base_handler import BaseResponder, RequestContext +from aries_cloudagent.protocols.issue_credential import v1_0 as issue_credential +from aries_cloudagent.protocols.issue_credential.v1_0.manager import CredentialManager +from aries_cloudagent.protocols.issue_credential.v1_0.routes import ( + V10CredentialProposalRequestMandSchema as CredentialProposalRequestSchema, +) +from aries_cloudagent.storage.error import StorageNotFoundError + +from .... import ProblemReport +from ....util import admin_only, expand_message_class, log_handling, with_generic_init +from .base import AdminHolderMessage +from .cred_exchange import CredExchange + + +@with_generic_init +@expand_message_class +class SendCredProposal(AdminHolderMessage): + """Send Credential Proposal Message.""" + + message_type = "send-credential-proposal" + fields_from = CredentialProposalRequestSchema + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle received send proposal request.""" + connection_id = str(context.message.connection_id) + credential_definition_id = context.message.cred_def_id + comment = context.message.comment + + credential_manager = CredentialManager(context.profile) + + session = await context.session() + try: + conn_record = await ConnRecord.retrieve_by_id(session, connection_id) + conn_record = cast(ConnRecord, conn_record) + except StorageNotFoundError: + report = ProblemReport( + description={"en": "Connection not found."}, who_retries="none" + ) + report.assign_thread_from(context.message) + await responder.send_reply(report) + return + + if not conn_record.is_ready: + report = ProblemReport( + description={"en": "Connection invalid."}, who_retries="none" + ) + report.assign_thread_from(context.message) + await responder.send_reply(report) + return + + credential_exchange_record = await credential_manager.create_proposal( + connection_id, + comment=comment, + credential_preview=context.message.credential_proposal, + cred_def_id=credential_definition_id, + ) + + await responder.send( + issue_credential.messages.credential_proposal.CredentialProposal( + comment=context.message.comment, + credential_proposal=context.message.credential_proposal, + cred_def_id=credential_definition_id, + ), + connection_id=connection_id, + ) + cred_exchange = CredExchange(**credential_exchange_record.serialize()) + cred_exchange.assign_thread_from(context.message) + await responder.send_reply(cred_exchange) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/send_pres_proposal.py b/acapy_plugin_toolbox/holder/v0_1/messages/send_pres_proposal.py new file mode 100644 index 00000000..ebc4ccde --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/send_pres_proposal.py @@ -0,0 +1,80 @@ +from aries_cloudagent.indy.sdk.models.pres_preview import ( + IndyPresPreview as PresentationPreview, +) +from aries_cloudagent.messaging.base_handler import BaseResponder, RequestContext +from aries_cloudagent.protocols.present_proof.v1_0.manager import PresentationManager +from aries_cloudagent.protocols.present_proof.v1_0.messages.presentation_proposal import ( + PresentationProposal, +) +from aries_cloudagent.protocols.present_proof.v1_0.routes import ( + V10PresentationProposalRequestSchema as PresentationProposalRequestSchema, +) + +from ....util import ( + ExceptionReporter, + InvalidConnection, + admin_only, + expand_message_class, + get_connection, + log_handling, +) +from .base import AdminHolderMessage +from .pres_exchange import PresExchange + + +@expand_message_class +class SendPresProposal(AdminHolderMessage): + """Presentation proposal message.""" + + message_type = "send-presentation-proposal" + fields_from = PresentationProposalRequestSchema + + def __init__( + self, + *, + connection_id: str = None, + comment: str = None, + presentation_proposal: PresentationPreview = None, + auto_present: bool = None, + trace: bool = None, + **kwargs, + ): + super().__init__(**kwargs) + self.connection_id = connection_id + self.comment = comment + self.presentation_proposal = presentation_proposal + self.auto_present = auto_present + self.trace = trace + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle received send presentation proposal request.""" + session = await context.session() + connection_id = str(context.message.connection_id) + async with ExceptionReporter(responder, InvalidConnection, context.message): + await get_connection(session, connection_id) + + comment = context.message.comment + # Aries#0037 calls it a proposal in the proposal struct but it's of type preview + presentation_proposal = PresentationProposal( + comment=comment, presentation_proposal=context.message.presentation_proposal + ) + auto_present = context.message.auto_present or context.settings.get( + "debug.auto_respond_presentation_request" + ) + + presentation_manager = PresentationManager(context.profile) + + presentation_exchange_record = ( + await presentation_manager.create_exchange_for_proposal( + connection_id=connection_id, + presentation_proposal_message=presentation_proposal, + auto_present=auto_present, + ) + ) + await responder.send(presentation_proposal, connection_id=connection_id) + + pres_exchange = PresExchange(**presentation_exchange_record.serialize()) + pres_exchange.assign_thread_from(context.message) + await responder.send_reply(pres_exchange) diff --git a/tests/holder/test_events.py b/tests/holder/test_events.py index 4c2d59ce..5096841e 100644 --- a/tests/holder/test_events.py +++ b/tests/holder/test_events.py @@ -3,20 +3,20 @@ # pylint: disable=redefined-outer-name import pytest +from acapy_plugin_toolbox.holder import v0_1 as test_module from aries_cloudagent.core.event_bus import Event, EventBus from aries_cloudagent.core.in_memory import InMemoryProfile from aries_cloudagent.core.protocol_registry import ProtocolRegistry +from aries_cloudagent.indy.holder import IndyHolder from aries_cloudagent.messaging.responder import BaseResponder, MockResponder from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( - V10CredentialExchange + V10CredentialExchange, ) from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( - V10PresentationExchange + V10PresentationExchange, ) from asynctest import mock -from acapy_plugin_toolbox.holder import v0_1 as test_module - @pytest.fixture def event_bus(): @@ -29,12 +29,14 @@ def profile(event_bus): """Profile fixture.""" holder = mock.MagicMock() holder.get_credentials_for_presentation_request_by_referent = mock.CoroutineMock() - yield InMemoryProfile.test_profile(bind={ - EventBus: event_bus, - BaseResponder: MockResponder(), - ProtocolRegistry: ProtocolRegistry(), - test_module.IndyHolder: holder - }) + yield InMemoryProfile.test_profile( + bind={ + EventBus: event_bus, + BaseResponder: MockResponder(), + ProtocolRegistry: ProtocolRegistry(), + IndyHolder: holder, + } + ) @pytest.fixture @@ -45,6 +47,7 @@ def context(profile): class MockSendToAdmins: """Mock send_to_admins method.""" + def __init__(self): self.message = None @@ -64,18 +67,22 @@ def mock_send_to_admins(): @pytest.mark.parametrize( "handler, topic", [ - ("issue_credential_event_handler", f"acapy::record::{V10CredentialExchange.RECORD_TOPIC}::test"), - ("present_proof_event_handler", f"acapy::record::{V10PresentationExchange.RECORD_TOPIC}::test") - ] + ( + "issue_credential_event_handler", + f"acapy::record::{V10CredentialExchange.RECORD_TOPIC}::test", + ), + ( + "present_proof_event_handler", + f"acapy::record::{V10PresentationExchange.RECORD_TOPIC}::test", + ), + ], ) async def test_events_subscribed_and_triggered( profile, context, event_bus, handler, topic ): """Test events are correctly registered and triggered.""" with mock.patch.object( - test_module, - handler, - mock.CoroutineMock() + test_module, handler, mock.CoroutineMock() ) as mock_event_handler: await test_module.setup(context) await event_bus.notify(profile, Event(topic, {"test": "payload"})) @@ -89,19 +96,19 @@ async def test_events_subscribed_and_triggered( ( test_module.issue_credential_event_handler, V10CredentialExchange.STATE_OFFER_RECEIVED, - test_module.CredOfferRecv + test_module.CredOfferRecv, ), ( test_module.issue_credential_event_handler, V10CredentialExchange.STATE_CREDENTIAL_RECEIVED, - test_module.CredReceived + test_module.CredReceived, ), ( test_module.present_proof_event_handler, V10PresentationExchange.STATE_REQUEST_RECEIVED, - test_module.PresRequestReceived - ) - ] + test_module.PresRequestReceived, + ), + ], ) async def test_message_sent_on_correct_state( profile, mock_send_to_admins, handler, state, message @@ -139,8 +146,8 @@ async def test_message_sent_on_correct_state( V10PresentationExchange.STATE_REQUEST_SENT, V10PresentationExchange.STATE_VERIFIED, ] - ] - ] + ], + ], ) async def test_message_not_sent_on_incorrect_state( profile, mock_send_to_admins, handler, state diff --git a/tests/holder/test_pres_approve.py b/tests/holder/test_pres_approve.py index 42b4b7a8..c90bffbd 100644 --- a/tests/holder/test_pres_approve.py +++ b/tests/holder/test_pres_approve.py @@ -1,8 +1,12 @@ """Test PresRequestApprove message and handler.""" import pytest -from acapy_plugin_toolbox.holder import v0_1 as test_module -from acapy_plugin_toolbox.holder.v0_1 import PresRequestApprove +from acapy_plugin_toolbox.holder.v0_1.messages import ( + pres_request_approve as test_module, +) +from acapy_plugin_toolbox.holder.v0_1.messages.pres_request_approve import ( + PresRequestApprove, +) from aries_cloudagent.connections.models.conn_record import ConnRecord from aries_cloudagent.protocols.present_proof.v1_0.manager import PresentationManager from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( diff --git a/tests/holder/test_pres_get_matching_credentials.py b/tests/holder/test_pres_get_matching_credentials.py index d3db7858..2d8aa4f4 100644 --- a/tests/holder/test_pres_get_matching_credentials.py +++ b/tests/holder/test_pres_get_matching_credentials.py @@ -6,11 +6,11 @@ from acapy_plugin_toolbox.decorators.pagination import Paginate from acapy_plugin_toolbox.holder import v0_1 as test_module from acapy_plugin_toolbox.holder.v0_1 import ( - InvalidPresentationExchange, PresGetMatchingCredentials, PresMatchingCredentials, PresRequestApprove, ) +from acapy_plugin_toolbox.holder.v0_1.error import InvalidPresentationExchange from aries_cloudagent.indy.holder import IndyHolder from aries_cloudagent.indy.sdk.holder import IndySdkHolder from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( diff --git a/tests/holder/test_send_pres_proposal.py b/tests/holder/test_send_pres_proposal.py index a7695b6e..033f0035 100644 --- a/tests/holder/test_send_pres_proposal.py +++ b/tests/holder/test_send_pres_proposal.py @@ -1,8 +1,10 @@ """Test SendPresProposal message and handler.""" import pytest -from acapy_plugin_toolbox.holder import v0_1 as test_module -from acapy_plugin_toolbox.holder.v0_1 import SendPresProposal +from acapy_plugin_toolbox.holder.v0_1.messages import send_pres_proposal as test_module +from acapy_plugin_toolbox.holder.v0_1.messages.send_pres_proposal import ( + SendPresProposal, +) from aries_cloudagent.indy.sdk.models.pres_preview import ( IndyPresAttrSpec, IndyPresPreview, From 7079b9c28cedbe8f8c0172aee93696ce5e5aaf1e Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Tue, 4 May 2021 23:12:00 -0600 Subject: [PATCH 55/59] adding admin-holder/0.1/credential-offer-reject Signed-off-by: Matthew Wright --- acapy_plugin_toolbox/holder/v0_1/__init__.py | 4 + .../holder/v0_1/messages/__init__.py | 4 + .../holder/v0_1/messages/cred_offer_reject.py | 83 +++++++++++++++++++ .../v0_1/messages/cred_offer_reject_sent.py | 15 ++++ 4 files changed, 106 insertions(+) create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_reject.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_reject_sent.py diff --git a/acapy_plugin_toolbox/holder/v0_1/__init__.py b/acapy_plugin_toolbox/holder/v0_1/__init__.py index e0f6770e..18581c53 100644 --- a/acapy_plugin_toolbox/holder/v0_1/__init__.py +++ b/acapy_plugin_toolbox/holder/v0_1/__init__.py @@ -30,6 +30,8 @@ CredList, CredOfferAccept, CredOfferRecv, + CredOfferReject, + CredOfferRejectSent, CredReceived, CredRequestSent, PresDelete, @@ -65,6 +67,8 @@ CredList, CredOfferAccept, CredOfferRecv, + CredOfferReject, + CredOfferRejectSent, CredReceived, CredRequestSent, PresDelete, diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/__init__.py b/acapy_plugin_toolbox/holder/v0_1/messages/__init__.py index f77997a1..247c832d 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/__init__.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/__init__.py @@ -6,6 +6,8 @@ from .cred_list import CredList from .cred_offer_accept import CredOfferAccept from .cred_offer_recv import CredOfferRecv +from .cred_offer_reject import CredOfferReject +from .cred_offer_reject_sent import CredOfferRejectSent from .cred_received import CredReceived from .cred_request_sent import CredRequestSent from .pres_delete import PresDelete @@ -30,6 +32,8 @@ "CredList", "CredOfferAccept", "CredOfferRecv", + "CredOfferReject", + "CredOfferRejectSent", "CredReceived", "CredRequestSent", "PresDelete", diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_reject.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_reject.py new file mode 100644 index 00000000..777aea87 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_reject.py @@ -0,0 +1,83 @@ +from typing import cast + +from aries_cloudagent.messaging.base_handler import BaseResponder, RequestContext +from aries_cloudagent.messaging.models.base import BaseModelError +from aries_cloudagent.messaging.valid import UUIDFour +from aries_cloudagent.protocols.issue_credential.v1_0.manager import ( + CredentialManagerError, +) +from aries_cloudagent.protocols.issue_credential.v1_0.messages.credential_problem_report import ( + CredentialProblemReport, +) +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchange as CredExRecord, +) +from aries_cloudagent.storage.error import StorageError +from marshmallow import fields + +from ....util import ( + ExceptionReporter, + admin_only, + expand_message_class, + get_connection, + log_handling, +) +from .base import AdminHolderMessage +from .cred_offer_reject_sent import CredOfferRejectSent + + +@expand_message_class +class CredOfferReject(AdminHolderMessage): + """Credential offer reject message.""" + + message_type = "credential-offer-reject" + + class Fields: + credential_exchange_id = fields.Str( + required=True, + description="ID of the credential exchange to reject.", + example=UUIDFour.EXAMPLE, + ) + + def __init__(self, credential_exchange_id: str, **kwargs): + super().__init__(**kwargs) + self.credential_exchange_id = credential_exchange_id + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle credential offer reject message.""" + async with context.session() as session: + async with ExceptionReporter( + responder, (StorageError, CredentialManagerError, BaseModelError), self + ): + cred_ex_record = await CredExRecord.retrieve_by_id( + session, self.credential_exchange_id + ) + cred_ex_record = cast(CredExRecord, cred_ex_record) + connection_id = cred_ex_record.connection_id + connection_record = await get_connection(session, connection_id) + + # TODO add credential_manager.reject(..) to ACA-Py + + cred_ex_record.state = ( + "reject-sent" # TODO add CredExRecord.STATE_REJECT_SENT to ACA-Py + ) + async with context.session() as session: + await cred_ex_record.save(session, reason="Rejected credential offer.") + + problem_report = CredentialProblemReport( + description={ + "en": "Rejected credential offer.", + "code": "rejected", # TODO add ProblemReportReason.REJECTED to ACA-Py + } + ) + problem_report.assign_thread_id(cred_ex_record.thread_id) + + sent = CredOfferRejectSent(**cred_ex_record.serialize()) + sent.assign_thread_from(self) + + await responder.send( + problem_report, connection_id=connection_record.connection_id + ) + await responder.send_reply(sent) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_reject_sent.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_reject_sent.py new file mode 100644 index 00000000..3fc6b7a0 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_reject_sent.py @@ -0,0 +1,15 @@ +from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( + V10CredentialExchangeSchema as CredExRecordSchema, +) + +from ....util import expand_message_class, with_generic_init +from .base import AdminHolderMessage + + +@with_generic_init +@expand_message_class +class CredOfferRejectSent(AdminHolderMessage): + """Credential offer reject sent.""" + + message_type = "credential-offer-reject-sent" + fields_from = CredExRecordSchema From f507b48c170197af90c8599f398de9370b9fcc1f Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Tue, 4 May 2021 22:22:02 -0600 Subject: [PATCH 56/59] adding admin-holder/0.1/presentation-request-reject Signed-off-by: Matthew Wright --- acapy_plugin_toolbox/holder/v0_1/__init__.py | 4 + .../holder/v0_1/messages/__init__.py | 4 + .../holder/v0_1/messages/pres_reject_sent.py | 15 ++++ .../v0_1/messages/pres_request_reject.py | 88 +++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_reject_sent.py create mode 100644 acapy_plugin_toolbox/holder/v0_1/messages/pres_request_reject.py diff --git a/acapy_plugin_toolbox/holder/v0_1/__init__.py b/acapy_plugin_toolbox/holder/v0_1/__init__.py index e0f6770e..5ada2e94 100644 --- a/acapy_plugin_toolbox/holder/v0_1/__init__.py +++ b/acapy_plugin_toolbox/holder/v0_1/__init__.py @@ -39,8 +39,10 @@ PresGetMatchingCredentials, PresList, PresMatchingCredentials, + PresRejectSent, PresRequestApprove, PresRequestReceived, + PresRequestReject, PresSent, SendCredProposal, SendPresProposal, @@ -74,8 +76,10 @@ PresGetMatchingCredentials, PresList, PresMatchingCredentials, + PresRejectSent, PresRequestApprove, PresRequestReceived, + PresRequestReject, PresSent, SendCredProposal, SendPresProposal, diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/__init__.py b/acapy_plugin_toolbox/holder/v0_1/messages/__init__.py index f77997a1..befe0771 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/__init__.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/__init__.py @@ -15,8 +15,10 @@ from .pres_get_matching_credentials import PresGetMatchingCredentials from .pres_list import PresList from .pres_matching_credentials import PresMatchingCredentials +from .pres_reject_sent import PresRejectSent from .pres_request_approve import PresRequestApprove from .pres_request_received import PresRequestReceived +from .pres_request_reject import PresRequestReject from .pres_sent import PresSent from .send_cred_proposal import SendCredProposal from .send_pres_proposal import SendPresProposal @@ -39,8 +41,10 @@ "PresGetMatchingCredentials", "PresList", "PresMatchingCredentials", + "PresRejectSent", "PresRequestApprove", "PresRequestReceived", + "PresRequestReject", "PresSent", "SendCredProposal", "SendPresProposal", diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_reject_sent.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_reject_sent.py new file mode 100644 index 00000000..dff85d6d --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_reject_sent.py @@ -0,0 +1,15 @@ +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchangeSchema as PresExRecordSchema, +) + +from ....util import expand_message_class, with_generic_init +from .base import AdminHolderMessage + + +@with_generic_init +@expand_message_class +class PresRejectSent(AdminHolderMessage): + """Presentation Exchange message.""" + + message_type = "presentation-reject-sent" + fields_from = PresExRecordSchema diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_reject.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_reject.py new file mode 100644 index 00000000..b3c32664 --- /dev/null +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_reject.py @@ -0,0 +1,88 @@ +from typing import cast + +from aries_cloudagent.messaging.base_handler import BaseResponder, RequestContext +from aries_cloudagent.messaging.valid import UUIDFour +from aries_cloudagent.protocols.present_proof.v1_0.messages.presentation_problem_report import ( + PresentationProblemReport, +) +from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( + V10PresentationExchange as PresExRecord, +) +from aries_cloudagent.storage.error import StorageNotFoundError +from marshmallow import fields + +from ....util import ( + ExceptionReporter, + InvalidConnection, + admin_only, + expand_message_class, + get_connection, + log_handling, +) +from ..error import InvalidPresentationExchange +from .base import AdminHolderMessage +from .pres_reject_sent import PresRejectSent + + +@expand_message_class +class PresRequestReject(AdminHolderMessage): + """Reject presentation request.""" + + message_type = "presentation-request-reject" + + class Fields: + presentation_exchange_id = fields.Str( + required=True, + description="Presentation request to reject.", + example=UUIDFour.EXAMPLE, + ) + + def __init__(self, presentation_exchange_id: str, **kwargs): + super().__init__(**kwargs) + self.presentation_exchange_id = presentation_exchange_id + + @log_handling + @admin_only + async def handle(self, context: RequestContext, responder: BaseResponder): + async with context.session() as session: + async with ExceptionReporter( + responder, + (InvalidPresentationExchange, InvalidConnection), + context.message, + ): + try: + pres_ex_record = await PresExRecord.retrieve_by_id( + session, self.presentation_exchange_id + ) + pres_ex_record = cast(PresExRecord, pres_ex_record) + except StorageNotFoundError as err: + raise InvalidPresentationExchange( + "Presentation exchange ID not found" + ) from err + + connection_id = pres_ex_record.connection_id + connection_record = await get_connection(session, connection_id) + + # TODO add presentation_manager.reject(..) to ACA-Py + + pres_ex_record.state = ( + "reject-sent" # TODO add PresExRecord.STATE_REJECT_SENT to ACA-Py + ) + async with context.session() as session: + await pres_ex_record.save(session, reason="created problem report") + + problem_report = PresentationProblemReport( + description={ + "en": "Rejected presentation request.", + "code": "rejected", # TODO add ProblemReportReason.REJECTED to ACA-Py + } + ) + problem_report.assign_thread_id(pres_ex_record.thread_id) + + sent = PresRejectSent(**pres_ex_record.serialize()) + sent.assign_thread_from(self) + + await responder.send( + problem_report, connection_id=connection_record.connection_id + ) + await responder.send_reply(sent) From 10643c0bfb63523cb98ecebe4f0b70c3d2d28bf8 Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Thu, 13 May 2021 16:54:10 +0000 Subject: [PATCH 57/59] updating wallet.create_local_did usage Signed-off-by: Matthew Wright --- acapy_plugin_toolbox/dids.py | 10 +++++++++- acapy_plugin_toolbox/static_connections.py | 7 ++++++- requirements.txt | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/acapy_plugin_toolbox/dids.py b/acapy_plugin_toolbox/dids.py index b600e2a6..8a1fe731 100644 --- a/acapy_plugin_toolbox/dids.py +++ b/acapy_plugin_toolbox/dids.py @@ -12,6 +12,8 @@ from aries_cloudagent.messaging.models.base_record import BaseRecord, BaseRecordSchema from aries_cloudagent.wallet.base import BaseWallet, DIDInfo from aries_cloudagent.wallet.error import WalletNotFoundError +from aries_cloudagent.wallet.did_method import DIDMethod +from aries_cloudagent.wallet.key_type import KeyType from .util import generate_model_schema, admin_only PROTOCOL = 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/admin-dids/0.1' @@ -233,7 +235,13 @@ async def handle(self, context: RequestContext, responder: BaseResponder): seed = context.message.seed if context.message.seed else None metadata = context.message.metadata if context.message.metadata else None - did_info = await wallet.create_local_did(seed, did, metadata) + did_info = await wallet.create_local_did( + method=DIDMethod.SOV, + key_type=KeyType.ED25519, + seed=seed, + did=did, + metadata=metadata, + ) result = get_reply_did(did_info) result.assign_thread_from(context.message) diff --git a/acapy_plugin_toolbox/static_connections.py b/acapy_plugin_toolbox/static_connections.py index 1c7f2280..06de3881 100644 --- a/acapy_plugin_toolbox/static_connections.py +++ b/acapy_plugin_toolbox/static_connections.py @@ -16,6 +16,8 @@ ) from aries_cloudagent.protocols.problem_report.v1_0.message import ProblemReport from aries_cloudagent.storage.error import StorageNotFoundError +from aries_cloudagent.wallet.did_method import DIDMethod +from aries_cloudagent.wallet.key_type import KeyType from .util import generate_model_schema, admin_only @@ -95,7 +97,10 @@ async def handle(self, context: RequestContext, responder: BaseResponder): wallet: BaseWallet = session.inject(BaseWallet) # Make our info for the connection - my_info = await wallet.create_local_did() + my_info = await wallet.create_local_did( + method=DIDMethod.SOV, + key_type=KeyType.ED25519, + ) # Create connection record connection = ConnRecord( diff --git a/requirements.txt b/requirements.txt index c4ae6132..79d081a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aries-cloudagent[indy]@git+https://github.com/hyperledger/aries-cloudagent-python@6292b5913605ff3cfe245093b7ce68b705e9b262 +aries-cloudagent[indy]@git+https://github.com/hyperledger/aries-cloudagent-python@9d65039638dae9276603c740648043f397a2d064 marshmallow==3.5.1 flake8 python-dateutil From 5cba8f116a810a9681029bef318c7a1686daae36 Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Thu, 13 May 2021 16:08:41 -0600 Subject: [PATCH 58/59] fixing credential serialization Signed-off-by: Matthew Wright --- acapy_plugin_toolbox/holder/v0_1/__init__.py | 4 +- .../holder/v0_1/messages/cred_exchange.py | 21 +++++++-- .../holder/v0_1/messages/cred_offer_accept.py | 2 +- .../holder/v0_1/messages/cred_offer_recv.py | 21 +++++++-- .../holder/v0_1/messages/cred_received.py | 21 +++++++-- .../holder/v0_1/messages/cred_request_sent.py | 21 +++++++-- .../v0_1/messages/send_cred_proposal.py | 2 +- acapy_plugin_toolbox/issuer.py | 47 ++++++++++++------- 8 files changed, 103 insertions(+), 36 deletions(-) diff --git a/acapy_plugin_toolbox/holder/v0_1/__init__.py b/acapy_plugin_toolbox/holder/v0_1/__init__.py index e0f6770e..7d26d364 100644 --- a/acapy_plugin_toolbox/holder/v0_1/__init__.py +++ b/acapy_plugin_toolbox/holder/v0_1/__init__.py @@ -115,11 +115,11 @@ async def issue_credential_event_handler(profile: Profile, event: Event): responder = profile.inject(BaseResponder) message = None if record.state == CredExRecord.STATE_OFFER_RECEIVED: - message = CredOfferRecv(**record.serialize()) + message = CredOfferRecv(record=record) LOGGER.debug("Prepared Message: %s", message.serialize()) if record.state == CredExRecord.STATE_CREDENTIAL_RECEIVED: - message = CredReceived(**record.serialize()) + message = CredReceived(record=record) LOGGER.debug("Prepared Message: %s", message.serialize()) async with profile.session() as session: diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_exchange.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_exchange.py index d9b5c97c..42a8ff74 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/cred_exchange.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_exchange.py @@ -1,15 +1,28 @@ +from typing import Mapping + from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( - V10CredentialExchangeSchema as CredExRecordSchema, + V10CredentialExchange, ) +from marshmallow import fields -from ....util import expand_message_class, with_generic_init +from ....util import expand_message_class from .base import AdminHolderMessage -@with_generic_init @expand_message_class class CredExchange(AdminHolderMessage): """Credential exchange message.""" message_type = "credential-exchange" - fields_from = CredExRecordSchema + + class Fields: + # TODO Use a toolbox CredentialRepresentation + raw_repr = fields.Mapping(required=True) + + def __init__(self, record: V10CredentialExchange, **kwargs): + super().__init__(**kwargs) + self.raw_repr = record.serialize() + + def serialize(self, **kwargs) -> Mapping: + base_msg = super().serialize(**kwargs) + return {**self.raw_repr, **base_msg} diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_accept.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_accept.py index ac9aab77..d50d988e 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_accept.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_accept.py @@ -69,7 +69,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): cred_ex_record, connection_record.my_did ) - sent = CredRequestSent(**cred_ex_record.serialize()) + sent = CredRequestSent(record=cred_ex_record) await responder.send(credential_request_message, connection_id=connection_id) await responder.send_reply(sent) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_recv.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_recv.py index 032d3871..c1935a87 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_recv.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_offer_recv.py @@ -1,15 +1,28 @@ +from typing import Mapping + from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( - V10CredentialExchangeSchema as CredExRecordSchema, + V10CredentialExchange, ) +from marshmallow import fields -from ....util import expand_message_class, with_generic_init +from ....util import expand_message_class from .base import AdminHolderMessage -@with_generic_init @expand_message_class class CredOfferRecv(AdminHolderMessage): """Credential offer received message.""" message_type = "credential-offer-received" - fields_from = CredExRecordSchema + + class Fields: + # TODO Use a toolbox CredentialRepresentation + raw_repr = fields.Mapping(required=True) + + def __init__(self, record: V10CredentialExchange, **kwargs): + super().__init__(**kwargs) + self.raw_repr = record.serialize() + + def serialize(self, **kwargs) -> Mapping: + base_msg = super().serialize(**kwargs) + return {**self.raw_repr, **base_msg} diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_received.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_received.py index ceb3229d..eea35210 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/cred_received.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_received.py @@ -1,15 +1,28 @@ +from typing import Mapping + from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( - V10CredentialExchangeSchema as CredExRecordSchema, + V10CredentialExchange, ) +from marshmallow import fields -from ....util import expand_message_class, with_generic_init +from ....util import expand_message_class from .base import AdminHolderMessage -@with_generic_init @expand_message_class class CredReceived(AdminHolderMessage): """Credential received notification message.""" message_type = "credential-received" - fields_from = CredExRecordSchema + + class Fields: + # TODO Use a toolbox CredentialRepresentation + raw_repr = fields.Mapping(required=True) + + def __init__(self, record: V10CredentialExchange, **kwargs): + super().__init__(**kwargs) + self.raw_repr = record.serialize() + + def serialize(self, **kwargs) -> Mapping: + base_msg = super().serialize(**kwargs) + return {**self.raw_repr, **base_msg} diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/cred_request_sent.py b/acapy_plugin_toolbox/holder/v0_1/messages/cred_request_sent.py index fa0669d1..10108555 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/cred_request_sent.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/cred_request_sent.py @@ -1,15 +1,28 @@ +from typing import Mapping + from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( - V10CredentialExchangeSchema as CredExRecordSchema, + V10CredentialExchange, ) +from marshmallow import fields -from ....util import expand_message_class, with_generic_init +from ....util import expand_message_class from .base import AdminHolderMessage -@with_generic_init @expand_message_class class CredRequestSent(AdminHolderMessage): """Credential offer acceptance received and credential request sent.""" message_type = "credential-request-sent" - fields_from = CredExRecordSchema + + class Fields: + # TODO Use a toolbox CredentialRepresentation + raw_repr = fields.Mapping(required=True) + + def __init__(self, record: V10CredentialExchange, **kwargs): + super().__init__(**kwargs) + self.raw_repr = record.serialize() + + def serialize(self, **kwargs) -> Mapping: + base_msg = super().serialize(**kwargs) + return {**self.raw_repr, **base_msg} diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/send_cred_proposal.py b/acapy_plugin_toolbox/holder/v0_1/messages/send_cred_proposal.py index 72dfe334..0fdb490f 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/send_cred_proposal.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/send_cred_proposal.py @@ -68,6 +68,6 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ), connection_id=connection_id, ) - cred_exchange = CredExchange(**credential_exchange_record.serialize()) + cred_exchange = CredExchange(record=credential_exchange_record) cred_exchange.assign_thread_from(context.message) await responder.send_reply(cred_exchange) diff --git a/acapy_plugin_toolbox/issuer.py b/acapy_plugin_toolbox/issuer.py index 815de03b..28ab9d70 100644 --- a/acapy_plugin_toolbox/issuer.py +++ b/acapy_plugin_toolbox/issuer.py @@ -2,7 +2,7 @@ # pylint: disable=invalid-name # pylint: disable=too-few-public-methods -from typing import Optional +from typing import Optional, Mapping from aries_cloudagent.connections.models.conn_record import ConnRecord from aries_cloudagent.core.profile import ProfileSession @@ -22,7 +22,6 @@ ) from aries_cloudagent.protocols.issue_credential.v1_0.models.credential_exchange import ( V10CredentialExchange, - V10CredentialExchangeSchema, ) from aries_cloudagent.protocols.issue_credential.v1_0.routes import ( V10CredentialExchangeListResultSchema, @@ -101,12 +100,22 @@ class AdminIssuerMessage(AgentMessage): msg_type=SEND_CREDENTIAL, schema=V10CredentialProposalRequestMandSchema, ) -IssuerCredExchange, IssuerCredExchangeSchema = generate_model_schema( - name="IssuerCredExchange", - handler="acapy_plugin_toolbox.util.PassHandler", - msg_type=ISSUER_CRED_EXCHANGE, - schema=V10CredentialExchangeSchema, -) + + +@expand_message_class +class IssuerCredExchange(AdminIssuerMessage): + message_type = ISSUER_CRED_EXCHANGE + class Fields: + # TODO Use a toolbox CredentialRepresentation + raw_repr = fields.Mapping(required=True) + + def __init__(self, record: V10CredentialExchange, **kwargs): + super().__init__(**kwargs) + self.raw_repr = record.serialize() + + def serialize(self, **kwargs) -> Mapping: + base_msg = super().serialize(**kwargs) + return {**self.raw_repr, **base_msg} class SendCredHandler(BaseHandler): @@ -160,7 +169,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): await responder.send( cred_offer_message, connection_id=cred_exchange_record.connection_id ) - cred_exchange = IssuerCredExchange(**cred_exchange_record.serialize()) + cred_exchange = IssuerCredExchange(record=cred_exchange_record) cred_exchange.assign_thread_from(context.message) await responder.send_reply(cred_exchange) @@ -239,12 +248,18 @@ class IssuerPresExchange(AdminIssuerMessage): }, ) -CredList, CredListSchema = generate_model_schema( - name="CredList", - handler="acapy_plugin_toolbox.util.PassHandler", - msg_type=CREDENTIALS_LIST, - schema=V10CredentialExchangeListResultSchema, -) + +@with_generic_init +@expand_message_class +class CredList(AdminIssuerMessage): + message_type = CREDENTIALS_LIST + class Fields: + results = fields.List( + fields.Dict(), + required=True, + description="List of credentials", + example=[], + ) class CredGetListHandler(BaseHandler): @@ -270,7 +285,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): records = await V10CredentialExchange.query( session, {}, post_filter_positive=post_filter_positive ) - cred_list = CredList(results=records) + cred_list = CredList(results=[record.serialize() for record in records]) await responder.send_reply(cred_list) From 433c8daec61cf949e9c1a26d826b428b9f0d5e9d Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Thu, 13 May 2021 16:53:46 -0600 Subject: [PATCH 59/59] fixing Presentation Exchange serialization Signed-off-by: Matthew Wright --- .../holder/v0_1/messages/pres_exchange.py | 22 ++++++++-- .../v0_1/messages/pres_request_approve.py | 2 +- .../v0_1/messages/pres_request_received.py | 9 ++-- .../holder/v0_1/messages/pres_sent.py | 22 ++++++++-- .../v0_1/messages/send_pres_proposal.py | 2 +- acapy_plugin_toolbox/issuer.py | 42 ++++++++++++------- tests/holder/test_pres_approve.py | 2 +- tests/holder/test_send_pres_proposal.py | 2 +- tests/issuer/test_request_pres.py | 2 +- 9 files changed, 73 insertions(+), 32 deletions(-) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_exchange.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_exchange.py index f33ba409..588f6723 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/pres_exchange.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_exchange.py @@ -1,15 +1,29 @@ +from typing import Mapping + from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( - V10PresentationExchangeSchema as PresExRecordSchema, + V10PresentationExchange, ) +from marshmallow import fields -from ....util import expand_message_class, with_generic_init +from ....util import expand_message_class from .base import AdminHolderMessage -@with_generic_init @expand_message_class class PresExchange(AdminHolderMessage): """Presentation Exchange message.""" message_type = "presentation-exchange" - fields_from = PresExRecordSchema + + class Fields: + # TODO Use a toolbox PresentationExchangeRepresentation + raw_repr = fields.Mapping(required=True) + + def __init__(self, record: V10PresentationExchange, **kwargs): + super().__init__(**kwargs) + self.record = record + self.raw_repr = record.serialize() + + def serialize(self, **kwargs) -> Mapping: + base_msg = super().serialize(**kwargs) + return {**self.raw_repr, **base_msg} diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_approve.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_approve.py index 0adcbbbb..b2e56ecd 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_approve.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_approve.py @@ -157,6 +157,6 @@ async def handle(self, context: RequestContext, responder: BaseResponder): await responder.send(message, connection_id=conn_record.connection_id) - presentation_sent = PresSent(**pres_ex_record.serialize()) + presentation_sent = PresSent(record=pres_ex_record) presentation_sent.assign_thread_from(self) await responder.send_reply(presentation_sent) diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_received.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_received.py index ae67dacb..db363285 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_received.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_request_received.py @@ -14,6 +14,11 @@ from .base import AdminHolderMessage +class PresExRecordField(fields.Field): + def _serialize(self, value: PresExRecord, attr, obj, **kwargs): + return value.serialize() + + @expand_message_class class PresRequestReceived(AdminHolderMessage): """Presentation Request Received.""" @@ -25,9 +30,7 @@ class PresRequestReceived(AdminHolderMessage): class Fields: """Fields of Presentation request received message.""" - record = fields.Nested( - PresExRecordSchema, required=True, description="Presentation details." - ) + record = PresExRecordField(required=True, description="Presentation details.") matching_credentials = fields.Nested( IndyCredPrecisSchema, many=True, diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/pres_sent.py b/acapy_plugin_toolbox/holder/v0_1/messages/pres_sent.py index eb973df2..48db41ef 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/pres_sent.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/pres_sent.py @@ -1,15 +1,29 @@ +from typing import Mapping + from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( - V10PresentationExchangeSchema as PresExRecordSchema, + V10PresentationExchange, ) +from marshmallow import fields -from ....util import expand_message_class, with_generic_init +from ....util import expand_message_class from .base import AdminHolderMessage -@with_generic_init @expand_message_class class PresSent(AdminHolderMessage): """Presentation Exchange message.""" message_type = "presentation-sent" - fields_from = PresExRecordSchema + + class Fields: + # TODO Use a toolbox PresentationExchangeRepresentation + raw_repr = fields.Mapping(required=True) + + def __init__(self, record: V10PresentationExchange, **kwargs): + super().__init__(**kwargs) + self.record = record + self.raw_repr = record.serialize() + + def serialize(self, **kwargs) -> Mapping: + base_msg = super().serialize(**kwargs) + return {**self.raw_repr, **base_msg} diff --git a/acapy_plugin_toolbox/holder/v0_1/messages/send_pres_proposal.py b/acapy_plugin_toolbox/holder/v0_1/messages/send_pres_proposal.py index ebc4ccde..b1b52b5a 100644 --- a/acapy_plugin_toolbox/holder/v0_1/messages/send_pres_proposal.py +++ b/acapy_plugin_toolbox/holder/v0_1/messages/send_pres_proposal.py @@ -75,6 +75,6 @@ async def handle(self, context: RequestContext, responder: BaseResponder): ) await responder.send(presentation_proposal, connection_id=connection_id) - pres_exchange = PresExchange(**presentation_exchange_record.serialize()) + pres_exchange = PresExchange(record=presentation_exchange_record) pres_exchange.assign_thread_from(context.message) await responder.send_reply(pres_exchange) diff --git a/acapy_plugin_toolbox/issuer.py b/acapy_plugin_toolbox/issuer.py index 28ab9d70..0dd2443e 100644 --- a/acapy_plugin_toolbox/issuer.py +++ b/acapy_plugin_toolbox/issuer.py @@ -24,7 +24,6 @@ V10CredentialExchange, ) from aries_cloudagent.protocols.issue_credential.v1_0.routes import ( - V10CredentialExchangeListResultSchema, V10CredentialProposalRequestMandSchema, ) from aries_cloudagent.protocols.present_proof.v1_0.manager import PresentationManager @@ -37,10 +36,8 @@ ) from aries_cloudagent.protocols.present_proof.v1_0.models.presentation_exchange import ( V10PresentationExchange, - V10PresentationExchangeSchema, ) from aries_cloudagent.protocols.present_proof.v1_0.routes import ( - V10PresentationExchangeListSchema, V10PresentationSendRequestRequestSchema, ) from aries_cloudagent.protocols.problem_report.v1_0.message import ProblemReport @@ -223,18 +220,28 @@ async def handle(self, context: RequestContext, responder: BaseResponder): await responder.send(presentation_request_message, connection_id=connection_id) - pres_exchange = IssuerPresExchange(**presentation_exchange_record.serialize()) + pres_exchange = IssuerPresExchange(record=presentation_exchange_record) pres_exchange.assign_thread_from(self) await responder.send_reply(pres_exchange) -@with_generic_init @expand_message_class class IssuerPresExchange(AdminIssuerMessage): """Issuer Presentation Exchange report.""" message_type = "presentation-exchange" - fields_from = V10PresentationExchangeSchema + class Fields: + # TODO Use a toolbox PresentationExchangeRepresentation + raw_repr = fields.Mapping(required=True) + + def __init__(self, record: V10PresentationExchange, **kwargs): + super().__init__(**kwargs) + self.record = record + self.raw_repr = record.serialize() + + def serialize(self, **kwargs) -> Mapping: + base_msg = super().serialize(**kwargs) + return {**self.raw_repr, **base_msg} CredGetList, CredGetListSchema = generate_model_schema( @@ -299,15 +306,18 @@ async def handle(self, context: RequestContext, responder: BaseResponder): }, ) -PresList, PresListSchema = generate_model_schema( - name="PresList", - handler="acapy_plugin_toolbox.util.PassHandler", - msg_type=PRESENTATIONS_LIST, - schema=V10PresentationExchangeListSchema - # schema={ - # 'results': fields.List(fields.Dict()) - # } -) + +@with_generic_init +@expand_message_class +class PresList(AdminIssuerMessage): + message_type = PRESENTATIONS_LIST + class Fields: + results = fields.List( + fields.Dict(), + required=True, + description="List of presentation exchange records", + example=[], + ) class PresGetListHandler(BaseHandler): @@ -332,5 +342,5 @@ async def handle(self, context: RequestContext, responder: BaseResponder): records = await V10PresentationExchange.query( session, {}, post_filter_positive=post_filter_positive ) - cred_list = PresList(results=records) + cred_list = PresList(results=[record.serialize() for record in records]) await responder.send_reply(cred_list) diff --git a/tests/holder/test_pres_approve.py b/tests/holder/test_pres_approve.py index c90bffbd..c3dbeb1d 100644 --- a/tests/holder/test_pres_approve.py +++ b/tests/holder/test_pres_approve.py @@ -80,7 +80,7 @@ async def test_handler( assert len(mock_responder.messages) == 2 reply, _reply_args = mock_responder.messages.pop() - assert reply.presentation_exchange_id == TEST_PRES_EX_ID + assert reply.record.presentation_exchange_id == TEST_PRES_EX_ID _pres, pres_args = mock_responder.messages.pop() assert "connection_id" in pres_args diff --git a/tests/holder/test_send_pres_proposal.py b/tests/holder/test_send_pres_proposal.py index 033f0035..03fe7cc8 100644 --- a/tests/holder/test_send_pres_proposal.py +++ b/tests/holder/test_send_pres_proposal.py @@ -48,4 +48,4 @@ async def test_handler(context, mock_responder, message, mock_get_connection): assert prop.comment == TEST_COMMENT assert prop_recipient["connection_id"] == TEST_CONN_ID assert isinstance(response, test_module.PresExchange) - assert response.connection_id == TEST_CONN_ID + assert response.record.connection_id == TEST_CONN_ID diff --git a/tests/issuer/test_request_pres.py b/tests/issuer/test_request_pres.py index 129b7abb..24e6646b 100644 --- a/tests/issuer/test_request_pres.py +++ b/tests/issuer/test_request_pres.py @@ -46,4 +46,4 @@ async def test_handler( assert req.request_presentations_attach assert req_recipient == {"connection_id": str(TEST_CONN_ID)} assert isinstance(response, IssuerPresExchange) - assert response.connection_id == str(TEST_CONN_ID) + assert response.record.connection_id == str(TEST_CONN_ID)