From 5c658895f79037575925ab21c3aa7ddd889ff261 Mon Sep 17 00:00:00 2001 From: Jason Sherman Date: Fri, 22 Jul 2022 13:54:40 -0700 Subject: [PATCH 1/6] set auto-respond-presentation-proposal to true Signed-off-by: Jason Sherman --- charts/traction/values.yaml | 2 +- scripts/acapy-static-args.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/traction/values.yaml b/charts/traction/values.yaml index e9e3a13e9..885c4e716 100755 --- a/charts/traction/values.yaml +++ b/charts/traction/values.yaml @@ -122,7 +122,7 @@ acapy: autoRespondCredentialProposal: false autoRespondCredentialOffer: false autoRespondCredentialRequest: true - autoRespondPresentationProposal: false + autoRespondPresentationProposal: true autoRespondPresentationRequest: false autoStoreCredential: true autoVerifyPresentation: true diff --git a/scripts/acapy-static-args.yml b/scripts/acapy-static-args.yml index b799bb8da..a4b974a31 100755 --- a/scripts/acapy-static-args.yml +++ b/scripts/acapy-static-args.yml @@ -4,7 +4,7 @@ auto-respond-messages: false auto-respond-credential-proposal: false auto-respond-credential-offer: false auto-respond-credential-request: true -auto-respond-presentation-proposal: false +auto-respond-presentation-proposal: true auto-respond-presentation-request: false auto-store-credential: true auto-verify-presentation: true From 0b50f21ccf83e8bf96854a79030e40fa6209400f Mon Sep 17 00:00:00 2001 From: Jason Sherman Date: Fri, 22 Jul 2022 14:07:15 -0700 Subject: [PATCH 2/6] try longer wait for liveness on api Signed-off-by: Jason Sherman --- charts/traction/templates/traction_api_deployment.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/traction/templates/traction_api_deployment.yaml b/charts/traction/templates/traction_api_deployment.yaml index 1bec9e778..dd9f0ca12 100644 --- a/charts/traction/templates/traction_api_deployment.yaml +++ b/charts/traction/templates/traction_api_deployment.yaml @@ -39,13 +39,13 @@ spec: httpGet: path: / port: 5000 - initialDelaySeconds: 60 + initialDelaySeconds: 90 periodSeconds: 10 livenessProbe: httpGet: path: / port: 5000 - initialDelaySeconds: 60 + initialDelaySeconds: 90 periodSeconds: 30 env: - name: TRACTION_API_ADMIN_USER From b47feb61961da6f744946094e3b733b59085273e Mon Sep 17 00:00:00 2001 From: Jason Sherman Date: Wed, 27 Jul 2022 11:15:01 -0700 Subject: [PATCH 3/6] Holder Propose a Presentation Request. Signed-off-by: Jason Sherman --- .../api/endpoints/models/v1/holder.py | 31 ++++++ .../api/endpoints/models/v1/verifier.py | 3 +- .../routes/v1/tenant/holder/presentations.py | 15 +++ .../tenant/verifier/verifier_presentations.py | 2 +- .../v1/holder/holder_presentation_protocol.py | 52 +++++++--- .../api/protocols/v1/verifier/__init__.py | 2 + .../verifier/presentation_request_protocol.py | 96 +++++++------------ .../verifier_presentation_proposol_handler.py | 69 +++++++++++++ ...ier_presentation_request_status_updater.py | 31 ++++-- .../api/services/v1/holder_service.py | 67 +++++++++++++ .../api/services/v1/verifier_service.py | 3 +- .../bdd-tests/features/environment.py | 3 +- .../bdd-tests/features/steps/holder.py | 54 ++++++++++- .../bdd-tests/features/steps/v1_api.py | 10 ++ .../features/v1-holder-proposal.feature | 34 +++++++ 15 files changed, 381 insertions(+), 91 deletions(-) create mode 100644 services/traction/api/protocols/v1/verifier/verifier_presentation_proposol_handler.py create mode 100644 services/traction/bdd-tests/features/v1-holder-proposal.feature diff --git a/services/traction/api/endpoints/models/v1/holder.py b/services/traction/api/endpoints/models/v1/holder.py index 84a48bd86..e6d9c8879 100644 --- a/services/traction/api/endpoints/models/v1/holder.py +++ b/services/traction/api/endpoints/models/v1/holder.py @@ -42,6 +42,7 @@ class HolderCredentialStatusType(str, Enum): class HolderPresentationStatusType(str, Enum): # pending, nothing happened yet pending = "Pending" + proposol_sent = "Proposal Sent" # offer received, waiting for action request_received = "Request Received" presentation_sent = "Presentation Sent" @@ -217,3 +218,33 @@ class RejectPresentationRequestPayload(BaseModel): alias: str | None = None external_reference_id: str | None = None tags: List[str] | None = [] + + +class PresentationProposalAttribute(BaseModel): + name: str | None = None + cred_def_id: str | None = None + mime_type: str | None = None + referent: str | None = None + value: str | None = None + + +class PresentationProposalPredicate(BaseModel): + name: str | None = None + predicate: str | None = None + threshold: int | None = None + cred_def_id: str | None = None + + +class PresentationProposal(BaseModel): + attributes: List[PresentationProposalAttribute] | None = [] + predicates: List[PresentationProposalPredicate] | None = [] + + +class HolderSendProposalPayload(BaseModel): + contact_id: UUID + comment: str | None = None + presentation_proposal: PresentationProposal + + +class HolderSendProposalResponse(GetResponse[HolderPresentationItem]): + pass diff --git a/services/traction/api/endpoints/models/v1/verifier.py b/services/traction/api/endpoints/models/v1/verifier.py index d2dc6f415..73cc9f126 100644 --- a/services/traction/api/endpoints/models/v1/verifier.py +++ b/services/traction/api/endpoints/models/v1/verifier.py @@ -28,6 +28,7 @@ class VerifierPresentationStatusType(str, Enum): RECEIVED = "received" # Verification has been received but not verified VERIFIED = "verified" # Verified and proven to be correct REJECTED = "rejected" # request was rejected/abandoned + PROPOSED = "proposed" # holder is proposing a verification ERROR = "Error" # why is this capitalized? @@ -84,7 +85,7 @@ class VerifierPresentationItem( ): verifier_presentation_id: UUID contact_id: UUID - proof_request: ProofRequest + # proof_request: ProofRequest name: str version: str comment: Optional[str] diff --git a/services/traction/api/endpoints/routes/v1/tenant/holder/presentations.py b/services/traction/api/endpoints/routes/v1/tenant/holder/presentations.py index 8a979c656..1458b1c8f 100644 --- a/services/traction/api/endpoints/routes/v1/tenant/holder/presentations.py +++ b/services/traction/api/endpoints/routes/v1/tenant/holder/presentations.py @@ -10,6 +10,8 @@ HolderPresentationStatusType, HolderPresentationListParameters, HolderPresentationListResponse, + HolderSendProposalPayload, + HolderSendProposalResponse, ) from api.endpoints.routes.v1.link_utils import build_list_links @@ -59,3 +61,16 @@ async def list_holder_presentations( return HolderPresentationListResponse( items=items, count=len(items), total=total_count, links=links ) + + +@router.post("/send-proposal", status_code=status.HTTP_200_OK) +async def send_proposal( + payload: HolderSendProposalPayload, + save_in_traction: bool | None = False, +) -> HolderSendProposalResponse: + wallet_id = get_from_context("TENANT_WALLET_ID") + tenant_id = get_from_context("TENANT_ID") + + item = await holder_service.send_proposal(tenant_id, wallet_id, payload=payload) + links = [] # TODO + return HolderSendProposalResponse(item=item, links=links) diff --git a/services/traction/api/endpoints/routes/v1/tenant/verifier/verifier_presentations.py b/services/traction/api/endpoints/routes/v1/tenant/verifier/verifier_presentations.py index aef7fe1c7..4fcf54804 100644 --- a/services/traction/api/endpoints/routes/v1/tenant/verifier/verifier_presentations.py +++ b/services/traction/api/endpoints/routes/v1/tenant/verifier/verifier_presentations.py @@ -82,7 +82,7 @@ async def new_verifier_presentation( task_payload = { "verifier_presentation_id": item.verifier_presentation_id, "contact_id": item.contact_id, - "proof_request": item.proof_request, + "proof_request": payload.proof_request, } await SendPresentProofTask.assign(tenant_id, wallet_id, task_payload) diff --git a/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py b/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py index edf86a144..ec19fa6e3 100644 --- a/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py +++ b/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py @@ -13,6 +13,10 @@ DefaultPresentationRequestProtocol, ) +new_request_states = [ + AcapyPresentProofStateType.REQUEST_RECEIVED, +] + class DefaultHolderPresentationProtocol(DefaultPresentationRequestProtocol): def __init__(self): @@ -50,7 +54,7 @@ async def get_contact(self, profile: Profile, payload: dict) -> Contact: async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: self.logger.info("> approve_for_processing()") holder_presentation = await self.get_holder_presentation(profile, payload) - is_new_request = payload["state"] == AcapyPresentProofStateType.REQUEST_RECEIVED + is_new_request = payload["state"] in new_request_states is_existing_item = holder_presentation is not None approved = is_new_request or is_existing_item self.logger.info(f"is_new_request={is_new_request}") @@ -66,12 +70,18 @@ async def before_any(self, profile: Profile, payload: dict): self.logger.debug("holder presentation exists, proceed with status update") values = {"state": payload["state"]} + proposal_states = [ + AcapyPresentProofStateType.PROPOSAL_SENT, + ] sent_states = [AcapyPresentProofStateType.PRESENTATION_SENT] accepted_states = [ AcapyPresentProofStateType.PRESENTATION_ACKED, ] + if payload["state"] in proposal_states: + values["status"] = HolderPresentationStatusType.proposol_sent + if payload["state"] in sent_states: values["status"] = HolderPresentationStatusType.presentation_sent @@ -89,19 +99,30 @@ async def on_request_received(self, profile: Profile, payload: dict): self.logger.info("> on_request_received()") # create a new holder credential! contact = await self.get_contact(profile, payload) + holder_presentation = await self.get_holder_presentation(profile, payload) if contact: - offer = HolderPresentation( - tenant_id=profile.tenant_id, - contact_id=contact.contact_id, - status=HolderPresentationStatusType.request_received, - state=payload["state"], - thread_id=payload["thread_id"], - presentation_exchange_id=payload["presentation_exchange_id"], - connection_id=payload["connection_id"], - ) - async with async_session() as db: - db.add(offer) - await db.commit() + if holder_presentation: + # may have one due to a sent proposal + values = { + "status": HolderPresentationStatusType.request_received, + "state": payload["state"], + } + await HolderPresentation.update_by_id( + item_id=holder_presentation.holder_presentation_id, values=values + ) + else: + offer = HolderPresentation( + tenant_id=profile.tenant_id, + contact_id=contact.contact_id, + status=HolderPresentationStatusType.request_received, + state=payload["state"], + thread_id=payload["thread_id"], + presentation_exchange_id=payload["presentation_exchange_id"], + connection_id=payload["connection_id"], + ) + async with async_session() as db: + db.add(offer) + await db.commit() # TODO: create payload and send notification to tenant. else: @@ -110,6 +131,11 @@ async def on_request_received(self, profile: Profile, payload: dict): ) self.logger.info("< on_request_received()") + async def on_proposal_sent(self, profile: Profile, payload: dict): + self.logger.info("> on_proposal_sent()") + self.logger.info(f"##### payload = {payload}") + self.logger.info("< on_proposal_sent()") + async def on_presentation_sent(self, profile: Profile, payload: dict): self.logger.info("> on_presentation_sent()") self.logger.info("< on_presentation_sent()") diff --git a/services/traction/api/protocols/v1/verifier/__init__.py b/services/traction/api/protocols/v1/verifier/__init__.py index e4f5de3e3..98df14dcc 100644 --- a/services/traction/api/protocols/v1/verifier/__init__.py +++ b/services/traction/api/protocols/v1/verifier/__init__.py @@ -1,7 +1,9 @@ +from .verifier_presentation_proposol_handler import VerifierPresentationProposolHandler from .verifier_presentation_request_status_updater import ( VerifierPresentationRequestStatusUpdater, ) def subscribe_present_proof_protocol_listeners(): + VerifierPresentationProposolHandler() VerifierPresentationRequestStatusUpdater() diff --git a/services/traction/api/protocols/v1/verifier/presentation_request_protocol.py b/services/traction/api/protocols/v1/verifier/presentation_request_protocol.py index 6f410b7f0..bf3513013 100644 --- a/services/traction/api/protocols/v1/verifier/presentation_request_protocol.py +++ b/services/traction/api/protocols/v1/verifier/presentation_request_protocol.py @@ -25,67 +25,41 @@ async def notify(self, profile: Profile, event: Event): self.logger.debug(f"payload={payload}") self.logger.info(f"self.role={self.role} ? payload.role={payload['role']}") if self.role == payload["role"]: - if "state" in payload: - await self.before_all(profile=profile, payload=payload) - - if await self.approve_for_processing(profile=profile, payload=payload): - await self.before_any(profile=profile, payload=payload) - - if AcapyPresentProofStateType.PROPOSAL_SENT == payload["state"]: - await self.on_proposal_sent(profile=profile, payload=payload) - elif ( - AcapyPresentProofStateType.PROPOSAL_RECEIVED == payload["state"] - ): - await self.on_proposal_received( - profile=profile, payload=payload - ) - elif AcapyPresentProofStateType.REQUEST_SENT == payload["state"]: - await self.on_request_sent(profile=profile, payload=payload) - elif ( - AcapyPresentProofStateType.REQUEST_RECEIVED == payload["state"] - ): - await self.on_request_received(profile=profile, payload=payload) - elif ( - AcapyPresentProofStateType.PRESENTATION_SENT == payload["state"] - ): - await self.on_presentation_sent( - profile=profile, payload=payload - ) - elif ( - AcapyPresentProofStateType.PRESENTATION_RECEIVED - == payload["state"] - ): - await self.on_presentation_received( - profile=profile, payload=payload - ) - elif AcapyPresentProofStateType.VERIFIED == payload["state"]: - await self.on_verified(profile=profile, payload=payload) - elif ( - AcapyPresentProofStateType.PRESENTATION_ACKED - == payload["state"] - ): - await self.on_presentation_acked( - profile=profile, payload=payload - ) - elif AcapyPresentProofStateType.ABANDONED == payload["state"]: - await self.on_abandoned(profile=profile, payload=payload) - else: - pass - - await self.after_any(profile=profile, payload=payload) - - await self.after_all(profile=profile, payload=payload) - else: - # TODO: remove this when we update to acapy 7.4 - self.logger.info("payload has no key for 'state'") - await self.on_unknown_state(profile=profile, payload=payload) + await self.before_all(profile=profile, payload=payload) + + if await self.approve_for_processing(profile=profile, payload=payload): + await self.before_any(profile=profile, payload=payload) + + if AcapyPresentProofStateType.PROPOSAL_SENT == payload["state"]: + await self.on_proposal_sent(profile=profile, payload=payload) + elif AcapyPresentProofStateType.PROPOSAL_RECEIVED == payload["state"]: + await self.on_proposal_received(profile=profile, payload=payload) + elif AcapyPresentProofStateType.REQUEST_SENT == payload["state"]: + await self.on_request_sent(profile=profile, payload=payload) + elif AcapyPresentProofStateType.REQUEST_RECEIVED == payload["state"]: + await self.on_request_received(profile=profile, payload=payload) + elif AcapyPresentProofStateType.PRESENTATION_SENT == payload["state"]: + await self.on_presentation_sent(profile=profile, payload=payload) + elif ( + AcapyPresentProofStateType.PRESENTATION_RECEIVED == payload["state"] + ): + await self.on_presentation_received( + profile=profile, payload=payload + ) + elif AcapyPresentProofStateType.VERIFIED == payload["state"]: + await self.on_verified(profile=profile, payload=payload) + elif AcapyPresentProofStateType.PRESENTATION_ACKED == payload["state"]: + await self.on_presentation_acked(profile=profile, payload=payload) + elif AcapyPresentProofStateType.ABANDONED == payload["state"]: + await self.on_abandoned(profile=profile, payload=payload) + else: + pass + + await self.after_any(profile=profile, payload=payload) + + await self.after_all(profile=profile, payload=payload) self.logger.info("< notify()") - # TODO: remove this when we update to acapy 7.4, workaround for bug in 7.3 - @abstractmethod - async def on_unknown_state(self, profile: Profile, payload: dict): - pass - @abstractmethod async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: pass @@ -147,10 +121,6 @@ class DefaultPresentationRequestProtocol(PresentationRequestProtocol): def __init__(self): super().__init__() - # TODO: remove this when we update to acapy 7.4, workaround for bug in 7.3 - async def on_unknown_state(self, profile: Profile, payload: dict): - pass - async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: pass diff --git a/services/traction/api/protocols/v1/verifier/verifier_presentation_proposol_handler.py b/services/traction/api/protocols/v1/verifier/verifier_presentation_proposol_handler.py new file mode 100644 index 000000000..6471e010e --- /dev/null +++ b/services/traction/api/protocols/v1/verifier/verifier_presentation_proposol_handler.py @@ -0,0 +1,69 @@ +import uuid + +from api.db.models.v1.contact import Contact +from api.db.session import async_session +from api.endpoints.models.v1.errors import NotFoundError +from api.endpoints.models.v1.verifier import ( + VerifierPresentationStatusType, + AcapyPresentProofStateType, +) + +from .presentation_request_protocol import DefaultPresentationRequestProtocol + +from api.core.profile import Profile +from api.db.models.v1.verifier_presentation import VerifierPresentation + + +class VerifierPresentationProposolHandler(DefaultPresentationRequestProtocol): + def __init__(self): + super().__init__() + + def get_presentation_exchange_id(self, payload: dict) -> str: + try: + return payload["presentation_exchange_id"] + except KeyError: + return None + + async def get_contact(self, profile: Profile, payload: dict) -> Contact: + connection_id = uuid.UUID(payload["connection_id"]) + try: + async with async_session() as db: + return await Contact.get_by_connection_id( + db, profile.tenant_id, connection_id=connection_id + ) + except NotFoundError: + return None + + async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: + self.logger.info("> approve_for_processing()") + is_new_request = ( + payload["state"] in AcapyPresentProofStateType.PROPOSAL_RECEIVED + ) + approved = is_new_request + self.logger.info(f"is_new_request={is_new_request}") + self.logger.info(f"< approve_for_processing({approved})") + return approved + + async def on_proposal_received(self, profile: Profile, payload: dict): + self.logger.info("> on_proposal_received()") + # create a new verifier presentation! + self.logger.info(f"@@@@@ payload = {payload}") + contact = await self.get_contact(profile, payload) + if contact: + offer = VerifierPresentation( + tenant_id=profile.tenant_id, + contact_id=contact.contact_id, + status=VerifierPresentationStatusType.PROPOSED, + state=payload["state"], + pres_exch_id=payload["presentation_exchange_id"], + ) + async with async_session() as db: + db.add(offer) + await db.commit() + + # TODO: create payload and send notification to tenant. + else: + self.logger.warning( + f"No contact found for connection_id<{payload['connection_id']}>, cannot create verifier presentation." # noqa: E501 + ) + self.logger.info("< on_proposal_received()") diff --git a/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py b/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py index 6f583ca8f..e2bbc29bf 100644 --- a/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py +++ b/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py @@ -72,16 +72,6 @@ async def before_any(self, profile: Profile, payload: dict): ) self.logger.info("< before_any()") - # TODO: remove this when we update to acapy 7.4, workaround for bug in 7.3 - async def on_unknown_state(self, profile: Profile, payload: dict): - self.logger.info(f"> on_unknown_state({payload})") - verifier_presentation = await self.get_verifier_presentation(profile, payload) - if verifier_presentation: - # look for an error message - await self.handle_abandoned(verifier_presentation, payload) - - self.logger.info("< on_unknown_state()") - async def handle_abandoned(self, verifier_presentation, payload): if "error_msg" in payload: self.logger.debug(f"payload error_msg = {payload['error_msg']}") @@ -97,3 +87,24 @@ async def handle_abandoned(self, verifier_presentation, payload): verifier_presentation.verifier_presentation_id, values ) await db.commit() + + async def on_presentation_received(self, profile: Profile, payload: dict): + self.logger.info(f"##### on_presentation_received.payload = {payload}") + + async def on_request_sent(self, profile: Profile, payload: dict): + self.logger.info("> on_request_sent()") + verifier_presentation = await self.get_verifier_presentation(profile, payload) + if verifier_presentation: + # update the proof request to match what was sent. + values = { + "proof_request": payload["presentation_request"], + "name": payload["presentation_request"]["name"], + "version": payload["presentation_request"]["version"], + } + async with async_session() as db: + await VerifierPresentation.update_by_id( + verifier_presentation.verifier_presentation_id, values + ) + await db.commit() + + self.logger.info("< on_request_sent()") diff --git a/services/traction/api/services/v1/holder_service.py b/services/traction/api/services/v1/holder_service.py index 20a9074e7..745f2f88a 100644 --- a/services/traction/api/services/v1/holder_service.py +++ b/services/traction/api/services/v1/holder_service.py @@ -5,6 +5,9 @@ from sqlalchemy import select, func, desc, update from sqlalchemy.orm import selectinload +from acapy_client.model.indy_pres_attr_spec import IndyPresAttrSpec +from acapy_client.model.indy_pres_pred_spec import IndyPresPredSpec +from acapy_client.model.indy_pres_preview import IndyPresPreview from acapy_client.model.indy_pres_spec import IndyPresSpec from acapy_client.model.indy_requested_creds_requested_attr import ( IndyRequestedCredsRequestedAttr, @@ -18,8 +21,12 @@ from acapy_client.model.v10_presentation_problem_report_request import ( V10PresentationProblemReportRequest, ) +from acapy_client.model.v10_presentation_proposal_request import ( + V10PresentationProposalRequest, +) from api.db.models import Timeline +from api.db.models.v1.contact import Contact from api.db.models.v1.holder import HolderCredential, HolderPresentation from api.db.session import async_session from api.endpoints.models.credentials import CredentialStateType, CredPrecisForProof @@ -49,6 +56,7 @@ SendPresentationPayload, RejectPresentationRequestPayload, HolderPresentationAcapy, + HolderSendProposalPayload, ) from api.endpoints.models.v1.verifier import AcapyPresentProofStateType from api.services.v1 import acapy_service @@ -571,6 +579,7 @@ async def list_credentials_for_request( creds = present_proof_api.present_proof_records_pres_ex_id_credentials_get( item.presentation_exchange_id ) + logger.info(f"^^^^^^ {creds}") all_creds = [] for cred in creds: all_creds.append( @@ -736,3 +745,61 @@ def send_rejection_to_acapy(item, payload): item.presentation_exchange_id, **data, ) + + +async def send_proposal( + tenant_id: UUID, + wallet_id: UUID, + payload: HolderSendProposalPayload, +) -> HolderPresentationItem: + + async with async_session() as db: + contact = await Contact.get_by_id(db, tenant_id, payload.contact_id) + + attributes = [] + predicates = [] + for a in payload.presentation_proposal.attributes: + attributes.append( + IndyPresAttrSpec( + **a.dict(exclude_none=True, exclude_unset=True, exclude_defaults=False) + ) + ) + for p in payload.presentation_proposal.predicates: + predicates.append( + IndyPresPredSpec( + **p.dict(exclude_none=True, exclude_unset=True, exclude_defaults=False) + ) + ) + + presentation_proposal = IndyPresPreview( + attributes=attributes, + predicates=predicates, + ) + logger.info(f"presentation_proposal = {presentation_proposal}") + req = V10PresentationProposalRequest( + connection_id=str(contact.connection_id), + presentation_proposal=presentation_proposal, + comment=payload.comment, + auto_present=True, + trace=False, + ) + logger.info(f"V10PresentationProposalRequest = {req}") + resp = present_proof_api.present_proof_send_proposal_post(body=req) + logger.info(f"resp = {resp}") + + offer = HolderPresentation( + tenant_id=tenant_id, + contact_id=contact.contact_id, + status=HolderPresentationStatusType.proposol_sent, + state=AcapyPresentProofStateType.PROPOSAL_SENT, + presentation_exchange_id=resp["presentation_exchange_id"], + connection_id=resp["connection_id"], + thread_id=resp["thread_id"], + ) + async with async_session() as db: + db.add(offer) + await db.commit() + + return await get_holder_presentation( + tenant_id, wallet_id, offer.holder_presentation_id, True + ) diff --git a/services/traction/api/services/v1/verifier_service.py b/services/traction/api/services/v1/verifier_service.py index 019582a02..eaafedc18 100644 --- a/services/traction/api/services/v1/verifier_service.py +++ b/services/traction/api/services/v1/verifier_service.py @@ -50,10 +50,11 @@ def verifier_presentation_to_item( presentation_exchange = acapy_service.get_presentation_exchange_json( db_item.pres_exch_id ) + logger.info(f" & & & & & = presentation_exchange = {presentation_exchange}") acapy_item = PresentationExchangeAcapy( presentation_exchange=presentation_exchange ) - + logger.info(f"* * * * * db_item = {db_item}") item = VerifierPresentationItem(**db_item.dict(), acapy=acapy_item) return item diff --git a/services/traction/bdd-tests/features/environment.py b/services/traction/bdd-tests/features/environment.py index cb3549a3f..51cf8542e 100644 --- a/services/traction/bdd-tests/features/environment.py +++ b/services/traction/bdd-tests/features/environment.py @@ -26,5 +26,6 @@ def after_scenario(context, scenario): for tenant_config in tenants: if not tenant_config.get("deleted", False): - _hard_delete_tenant(context, tenant_config) + # _hard_delete_tenant(context, tenant_config) + pass pass diff --git a/services/traction/bdd-tests/features/steps/holder.py b/services/traction/bdd-tests/features/steps/holder.py index 6b721c4fa..66399e23d 100644 --- a/services/traction/bdd-tests/features/steps/holder.py +++ b/services/traction/bdd-tests/features/steps/holder.py @@ -156,11 +156,17 @@ def step_impl(context, holder: str): @step('"{holder}" will have {count:d} holder credential(s)') def step_impl(context, holder: str, count: int): - params = {} + params = {"acapy": True} response = list_holder_credentials(context, holder, params) assert response.status_code == status.HTTP_200_OK, response.__dict__ resp_json = json.loads(response.content) assert resp_json["total"] == count, resp_json + items = resp_json["items"] + creds = [] + for c in items: + creds.append(c["acapy"]["credential"]) + context.config.userdata[holder].setdefault("holder_credentials", items) + context.config.userdata[holder].setdefault("wallet_credentials", creds) @step('"{holder}" can find holder credential by alias "{alias}"') @@ -633,3 +639,49 @@ def send_valid_presentation(context, holder): context, holder, holder_presentation_id, payload ) return holder_presentation_id, response + + +@when('"{holder}" proposes a presentation to "{verifier}"') +def step_impl(context, holder: str, verifier: str): + contact_id = context.config.userdata[holder]["connections"][verifier]["contact_id"] + + pprint.pp(context.config.userdata[holder]["holder_credentials"]) + + c = context.config.userdata[holder]["holder_credentials"][0]["acapy"]["credential"] + + attributes = [] + # name: str | None = None + # cred_def_id: str | None = None + # mime_type: str | None = None + # referent: str | None = None + # value: str | None = None + for k in c["attrs"].keys(): + attributes.append( + { + "cred_def_id": c["cred_def_id"], + "referent": c["referent"], + "name": k, + "value": c["attrs"][k], + } + ) + + predicates = [] + # name: str | None = None + # predicate: str | None = None + # threshold: int | None = None + # cred_def_id: str | None = None + presentation_proposal = { + "attributes": attributes, + "predicates": predicates, + } + + payload = { + "contact_id": contact_id, + "presentation_proposal": presentation_proposal, + "comment": "sent for bdd test", + } + response = holder_send_proposal(context, holder, payload) + assert response.status_code == status.HTTP_200_OK, response.__dict__ + resp_json = json.loads(response.content) + item = resp_json["item"] + assert item["state"] == "proposal_sent", resp_json diff --git a/services/traction/bdd-tests/features/steps/v1_api.py b/services/traction/bdd-tests/features/steps/v1_api.py index 2f6b83a47..76493a698 100644 --- a/services/traction/bdd-tests/features/steps/v1_api.py +++ b/services/traction/bdd-tests/features/steps/v1_api.py @@ -411,3 +411,13 @@ def send_holder_presentation( headers=context.config.userdata[tenant]["auth_headers"], ) return response + + +def holder_send_proposal(context, tenant, payload: dict | None = {}): + response = requests.post( + context.config.userdata.get("traction_host") + + f"/tenant/v1/holder/presentations/send-proposal", + json=payload, + headers=context.config.userdata[tenant]["auth_headers"], + ) + return response diff --git a/services/traction/bdd-tests/features/v1-holder-proposal.feature b/services/traction/bdd-tests/features/v1-holder-proposal.feature new file mode 100644 index 000000000..551f5b47f --- /dev/null +++ b/services/traction/bdd-tests/features/v1-holder-proposal.feature @@ -0,0 +1,34 @@ +Feature: holding presentations + + Background: two tenants, 1 issuer/verifier and 1 holder + Given we have authenticated at the innkeeper + And we have "2" traction tenants + # verifier will also be the issuer + | name | role | + | faber | verifier | + | alice | prover | + And "faber" is an issuer + And we sadly wait for 5 seconds because we have not figured out how to listen for events + And "faber" and "alice" are connected + And issuer creates new schema(s) and cred def(s) + |issuer|schema_name|attrs|cred_def_tag|rev_reg_size| + |faber|useless-schema|name,title|test|0| + And check "faber" for 120 seconds for a status of "Active" for "useless-schema" + And "faber" issues "alice" a "useless-schema" credential + And we sadly wait for 10 seconds because we have not figured out how to listen for events + And "alice" will have a credential_offer from "faber" + And "alice" will accept credential_offer from "faber" + And we sadly wait for 10 seconds because we have not figured out how to listen for events + Then "alice" will have a holder credential with status "Accepted" + And "faber" will have an "Issued" issuer credential + And "alice" will have 1 holder credential(s) + + + Scenario: holder can propose a presentation to verifier + When "alice" proposes a presentation to "faber" + And we sadly wait for 5 seconds because we have not figured out how to listen for events + And "alice" will have 1 holder presentation(s) + And "alice" can find 1 credential(s) for holder presentation + And we sadly wait for 10 seconds because we have not figured out how to listen for events + Then "alice" will have a holder presentation with status "Presentation Sent" + And "faber" has a "received" verifier presentation From 43eecee703a864422cadd6925a23518f53a1e55b Mon Sep 17 00:00:00 2001 From: Jason Sherman Date: Wed, 27 Jul 2022 14:30:57 -0700 Subject: [PATCH 4/6] verifier to ask for verification when presentation comes from a proposal some refactoring (minor because protocols need an overhaul) Signed-off-by: Jason Sherman --- .../routes/v1/tenant/holder/presentations.py | 10 +++ .../v1/holder/holder_presentation_protocol.py | 36 --------- .../api/protocols/v1/verifier/__init__.py | 2 + .../verifier/presentation_request_protocol.py | 43 ++++++++++ .../verifier_presentation_proposol_handler.py | 22 +---- .../verifier_presentation_request_handler.py | 81 +++++++++++++++++++ ...ier_presentation_request_status_updater.py | 58 +------------ .../api/services/v1/holder_service.py | 4 - .../api/services/v1/verifier_service.py | 2 - .../bdd-tests/features/steps/holder.py | 1 + .../features/v1-holder-proposal.feature | 4 +- 11 files changed, 141 insertions(+), 122 deletions(-) create mode 100644 services/traction/api/protocols/v1/verifier/verifier_presentation_request_handler.py diff --git a/services/traction/api/endpoints/routes/v1/tenant/holder/presentations.py b/services/traction/api/endpoints/routes/v1/tenant/holder/presentations.py index 1458b1c8f..d262f6b9a 100644 --- a/services/traction/api/endpoints/routes/v1/tenant/holder/presentations.py +++ b/services/traction/api/endpoints/routes/v1/tenant/holder/presentations.py @@ -68,6 +68,16 @@ async def send_proposal( payload: HolderSendProposalPayload, save_in_traction: bool | None = False, ) -> HolderSendProposalResponse: + """Holder - Send Proposal + + This allows a holder to send a verifier a proposal of proof. + + Refer to the following RFC for more information on the flow: + https://github.com/hyperledger/aries-rfcs/tree/main/features/0037-present-proof#propose-presentation + + For the content structure the presentation_proposal attributes and predicates: + https://github.com/hyperledger/aries-rfcs/tree/main/features/0037-present-proof#presentation-preview + """ wallet_id = get_from_context("TENANT_WALLET_ID") tenant_id = get_from_context("TENANT_ID") diff --git a/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py b/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py index ec19fa6e3..bedf0422d 100644 --- a/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py +++ b/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py @@ -1,7 +1,4 @@ -import uuid - from api.core.profile import Profile -from api.db.models.v1.contact import Contact from api.db.models.v1.holder import HolderPresentation from api.db.session import async_session from api.endpoints.models.credentials import PresentationRoleType @@ -23,12 +20,6 @@ def __init__(self): super().__init__() self.role = PresentationRoleType.prover - def get_presentation_exchange_id(self, payload: dict) -> str: - try: - return payload["presentation_exchange_id"] - except KeyError: - return None - async def get_holder_presentation( self, profile: Profile, payload: dict ) -> HolderPresentation: @@ -41,16 +32,6 @@ async def get_holder_presentation( except NotFoundError: return None - async def get_contact(self, profile: Profile, payload: dict) -> Contact: - connection_id = uuid.UUID(payload["connection_id"]) - try: - async with async_session() as db: - return await Contact.get_by_connection_id( - db, profile.tenant_id, connection_id=connection_id - ) - except NotFoundError: - return None - async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: self.logger.info("> approve_for_processing()") holder_presentation = await self.get_holder_presentation(profile, payload) @@ -130,20 +111,3 @@ async def on_request_received(self, profile: Profile, payload: dict): f"No contact found for connection_id<{payload['connection_id']}>, cannot create holder presentation." # noqa: E501 ) self.logger.info("< on_request_received()") - - async def on_proposal_sent(self, profile: Profile, payload: dict): - self.logger.info("> on_proposal_sent()") - self.logger.info(f"##### payload = {payload}") - self.logger.info("< on_proposal_sent()") - - async def on_presentation_sent(self, profile: Profile, payload: dict): - self.logger.info("> on_presentation_sent()") - self.logger.info("< on_presentation_sent()") - - async def on_presentation_acked(self, profile: Profile, payload: dict): - self.logger.info("> on_presentation_acked()") - self.logger.info("< on_presentation_acked()") - - async def on_abandoned(self, profile: Profile, payload: dict): - self.logger.info("> on_abandoned()") - self.logger.info("< on_abandoned()") diff --git a/services/traction/api/protocols/v1/verifier/__init__.py b/services/traction/api/protocols/v1/verifier/__init__.py index 98df14dcc..b313eec41 100644 --- a/services/traction/api/protocols/v1/verifier/__init__.py +++ b/services/traction/api/protocols/v1/verifier/__init__.py @@ -1,4 +1,5 @@ from .verifier_presentation_proposol_handler import VerifierPresentationProposolHandler +from .verifier_presentation_request_handler import VerifierPresentationRequestHandler from .verifier_presentation_request_status_updater import ( VerifierPresentationRequestStatusUpdater, ) @@ -7,3 +8,4 @@ def subscribe_present_proof_protocol_listeners(): VerifierPresentationProposolHandler() VerifierPresentationRequestStatusUpdater() + VerifierPresentationRequestHandler() diff --git a/services/traction/api/protocols/v1/verifier/presentation_request_protocol.py b/services/traction/api/protocols/v1/verifier/presentation_request_protocol.py index bf3513013..fe79ea71c 100644 --- a/services/traction/api/protocols/v1/verifier/presentation_request_protocol.py +++ b/services/traction/api/protocols/v1/verifier/presentation_request_protocol.py @@ -1,10 +1,18 @@ import logging +import uuid from abc import ABC, abstractmethod +from sqlalchemy import select + from api.core.config import settings from api.core.event_bus import Event from api.core.profile import Profile +from api.db.models import Tenant +from api.db.models.v1.contact import Contact +from api.db.models.v1.verifier_presentation import VerifierPresentation +from api.db.session import async_session from api.endpoints.models.credentials import PresentationRoleType +from api.endpoints.models.v1.errors import NotFoundError from api.endpoints.models.v1.verifier import AcapyPresentProofStateType from api.endpoints.models.webhooks import WEBHOOK_PRESENT_LISTENER_PATTERN @@ -60,6 +68,29 @@ async def notify(self, profile: Profile, event: Event): await self.after_all(profile=profile, payload=payload) self.logger.info("< notify()") + async def get_tenant(self, profile: Profile) -> Tenant: + async with async_session() as db: + q = select(Tenant).where(Tenant.id == profile.tenant_id) + q_result = await db.execute(q) + db_rec = q_result.scalar_one_or_none() + return db_rec + + def get_presentation_exchange_id(self, payload: dict) -> str: + try: + return payload["presentation_exchange_id"] + except KeyError: + return None + + async def get_contact(self, profile: Profile, payload: dict) -> Contact: + connection_id = uuid.UUID(payload["connection_id"]) + try: + async with async_session() as db: + return await Contact.get_by_connection_id( + db, profile.tenant_id, connection_id=connection_id + ) + except NotFoundError: + return None + @abstractmethod async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: pass @@ -121,6 +152,18 @@ class DefaultPresentationRequestProtocol(PresentationRequestProtocol): def __init__(self): super().__init__() + async def get_verifier_presentation( + self, profile: Profile, payload: dict + ) -> VerifierPresentation: + pres_exch_id = self.get_presentation_exchange_id(payload=payload) + try: + async with async_session() as db: + return await VerifierPresentation.get_by_pres_exch_id( + db, profile.tenant_id, pres_exch_id + ) + except NotFoundError: + return None + async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: pass diff --git a/services/traction/api/protocols/v1/verifier/verifier_presentation_proposol_handler.py b/services/traction/api/protocols/v1/verifier/verifier_presentation_proposol_handler.py index 6471e010e..3b5add807 100644 --- a/services/traction/api/protocols/v1/verifier/verifier_presentation_proposol_handler.py +++ b/services/traction/api/protocols/v1/verifier/verifier_presentation_proposol_handler.py @@ -1,13 +1,10 @@ -import uuid - -from api.db.models.v1.contact import Contact from api.db.session import async_session -from api.endpoints.models.v1.errors import NotFoundError from api.endpoints.models.v1.verifier import ( VerifierPresentationStatusType, AcapyPresentProofStateType, ) + from .presentation_request_protocol import DefaultPresentationRequestProtocol from api.core.profile import Profile @@ -18,22 +15,6 @@ class VerifierPresentationProposolHandler(DefaultPresentationRequestProtocol): def __init__(self): super().__init__() - def get_presentation_exchange_id(self, payload: dict) -> str: - try: - return payload["presentation_exchange_id"] - except KeyError: - return None - - async def get_contact(self, profile: Profile, payload: dict) -> Contact: - connection_id = uuid.UUID(payload["connection_id"]) - try: - async with async_session() as db: - return await Contact.get_by_connection_id( - db, profile.tenant_id, connection_id=connection_id - ) - except NotFoundError: - return None - async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: self.logger.info("> approve_for_processing()") is_new_request = ( @@ -47,7 +28,6 @@ async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: async def on_proposal_received(self, profile: Profile, payload: dict): self.logger.info("> on_proposal_received()") # create a new verifier presentation! - self.logger.info(f"@@@@@ payload = {payload}") contact = await self.get_contact(profile, payload) if contact: offer = VerifierPresentation( diff --git a/services/traction/api/protocols/v1/verifier/verifier_presentation_request_handler.py b/services/traction/api/protocols/v1/verifier/verifier_presentation_request_handler.py new file mode 100644 index 000000000..226dc142d --- /dev/null +++ b/services/traction/api/protocols/v1/verifier/verifier_presentation_request_handler.py @@ -0,0 +1,81 @@ +from starlette_context import context + +from api.db.session import async_session +from api.endpoints.models.v1.verifier import ( + VerifierPresentationStatusType, + AcapyPresentProofStateType, +) +from api.services.v1.acapy_service import present_proof_api + +from .presentation_request_protocol import DefaultPresentationRequestProtocol + +from api.core.profile import Profile +from api.db.models.v1.verifier_presentation import VerifierPresentation + + +class VerifierPresentationRequestHandler(DefaultPresentationRequestProtocol): + def __init__(self): + super().__init__() + + async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: + self.logger.info("> approve_for_processing()") + verifier_presentation = await self.get_verifier_presentation(profile, payload) + has_record = verifier_presentation is not None + approved = has_record + self.logger.info(f"< approve_for_processing({approved})") + return approved + + async def handle_abandoned(self, verifier_presentation, payload): + if "error_msg" in payload: + self.logger.debug(f"payload error_msg = {payload['error_msg']}") + if str(payload["error_msg"]).startswith("abandoned"): + self.logger.info("presentation request abandoned, request rejected.") + values = { + "state": AcapyPresentProofStateType.ABANDONED, + "status": VerifierPresentationStatusType.REJECTED, + } + self.logger.debug(f"updating issuer credential = {values}") + async with async_session() as db: + await VerifierPresentation.update_by_id( + verifier_presentation.verifier_presentation_id, values + ) + await db.commit() + + async def on_request_sent(self, profile: Profile, payload: dict): + self.logger.info("> on_request_sent()") + verifier_presentation = await self.get_verifier_presentation(profile, payload) + if verifier_presentation: + # update the proof request to match what was sent. + values = { + "proof_request": payload["presentation_request"], + "name": payload["presentation_request"]["name"], + "version": payload["presentation_request"]["version"], + } + async with async_session() as db: + await VerifierPresentation.update_by_id( + verifier_presentation.verifier_presentation_id, values + ) + await db.commit() + + self.logger.info("< on_request_sent()") + + async def on_presentation_received(self, profile: Profile, payload: dict): + self.logger.info("> on_presentation_received()") + self.logger.debug(f"payload['auto_verify'] {payload['auto_verify']}") + if not payload["auto_verify"]: + self.logger.info("presentation request is not auto verify... verify it.") + verification_presentation = await self.get_verifier_presentation( + profile, payload + ) + if verification_presentation: + # TODO: put this in a task when tasks refactored? + tenant = await self.get_tenant(profile) + context["TENANT_WALLET_TOKEN"] = tenant.wallet_token + # need a check for from proposal... + self.logger.info("call for verification") + resp = present_proof_api.present_proof_records_pres_ex_id_verify_presentation_post( # noqa:E501 + str(verification_presentation.pres_exch_id) + ) + self.logger.info(f"call for verification resp = {resp}") + + self.logger.info("< on_presentation_received()") diff --git a/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py b/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py index e2bbc29bf..1acb2b989 100644 --- a/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py +++ b/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py @@ -1,10 +1,9 @@ -from api.db.session import async_session -from api.endpoints.models.v1.errors import NotFoundError from api.endpoints.models.v1.verifier import ( VerifierPresentationStatusType, AcapyPresentProofStateType, ) + from .presentation_request_protocol import DefaultPresentationRequestProtocol from api.core.profile import Profile @@ -15,24 +14,6 @@ class VerifierPresentationRequestStatusUpdater(DefaultPresentationRequestProtoco def __init__(self): super().__init__() - def get_presentation_exchange_id(self, payload: dict) -> str: - try: - return payload["presentation_exchange_id"] - except KeyError: - return None - - async def get_verifier_presentation( - self, profile: Profile, payload: dict - ) -> VerifierPresentation: - pres_exch_id = self.get_presentation_exchange_id(payload=payload) - try: - async with async_session() as db: - return await VerifierPresentation.get_by_pres_exch_id( - db, profile.tenant_id, pres_exch_id - ) - except NotFoundError: - return None - async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: self.logger.info("> approve_for_processing()") verifier_presentation = await self.get_verifier_presentation(profile, payload) @@ -71,40 +52,3 @@ async def before_any(self, profile: Profile, payload: dict): verifier_presentation.verifier_presentation_id, values ) self.logger.info("< before_any()") - - async def handle_abandoned(self, verifier_presentation, payload): - if "error_msg" in payload: - self.logger.debug(f"payload error_msg = {payload['error_msg']}") - if str(payload["error_msg"]).startswith("abandoned"): - self.logger.info("presentation request abandoned, request rejected.") - values = { - "state": AcapyPresentProofStateType.ABANDONED, - "status": VerifierPresentationStatusType.REJECTED, - } - self.logger.debug(f"updating issuer credential = {values}") - async with async_session() as db: - await VerifierPresentation.update_by_id( - verifier_presentation.verifier_presentation_id, values - ) - await db.commit() - - async def on_presentation_received(self, profile: Profile, payload: dict): - self.logger.info(f"##### on_presentation_received.payload = {payload}") - - async def on_request_sent(self, profile: Profile, payload: dict): - self.logger.info("> on_request_sent()") - verifier_presentation = await self.get_verifier_presentation(profile, payload) - if verifier_presentation: - # update the proof request to match what was sent. - values = { - "proof_request": payload["presentation_request"], - "name": payload["presentation_request"]["name"], - "version": payload["presentation_request"]["version"], - } - async with async_session() as db: - await VerifierPresentation.update_by_id( - verifier_presentation.verifier_presentation_id, values - ) - await db.commit() - - self.logger.info("< on_request_sent()") diff --git a/services/traction/api/services/v1/holder_service.py b/services/traction/api/services/v1/holder_service.py index 745f2f88a..43e011e35 100644 --- a/services/traction/api/services/v1/holder_service.py +++ b/services/traction/api/services/v1/holder_service.py @@ -579,7 +579,6 @@ async def list_credentials_for_request( creds = present_proof_api.present_proof_records_pres_ex_id_credentials_get( item.presentation_exchange_id ) - logger.info(f"^^^^^^ {creds}") all_creds = [] for cred in creds: all_creds.append( @@ -775,7 +774,6 @@ async def send_proposal( attributes=attributes, predicates=predicates, ) - logger.info(f"presentation_proposal = {presentation_proposal}") req = V10PresentationProposalRequest( connection_id=str(contact.connection_id), presentation_proposal=presentation_proposal, @@ -783,9 +781,7 @@ async def send_proposal( auto_present=True, trace=False, ) - logger.info(f"V10PresentationProposalRequest = {req}") resp = present_proof_api.present_proof_send_proposal_post(body=req) - logger.info(f"resp = {resp}") offer = HolderPresentation( tenant_id=tenant_id, diff --git a/services/traction/api/services/v1/verifier_service.py b/services/traction/api/services/v1/verifier_service.py index eaafedc18..9fe18d505 100644 --- a/services/traction/api/services/v1/verifier_service.py +++ b/services/traction/api/services/v1/verifier_service.py @@ -50,11 +50,9 @@ def verifier_presentation_to_item( presentation_exchange = acapy_service.get_presentation_exchange_json( db_item.pres_exch_id ) - logger.info(f" & & & & & = presentation_exchange = {presentation_exchange}") acapy_item = PresentationExchangeAcapy( presentation_exchange=presentation_exchange ) - logger.info(f"* * * * * db_item = {db_item}") item = VerifierPresentationItem(**db_item.dict(), acapy=acapy_item) return item diff --git a/services/traction/bdd-tests/features/steps/holder.py b/services/traction/bdd-tests/features/steps/holder.py index 66399e23d..f984d8a06 100644 --- a/services/traction/bdd-tests/features/steps/holder.py +++ b/services/traction/bdd-tests/features/steps/holder.py @@ -419,6 +419,7 @@ def step_impl(context, holder: str, count: int): response = list_holder_presentations(context, holder, params) assert response.status_code == status.HTTP_200_OK, response.__dict__ resp_json = json.loads(response.content) + pprint.pp(resp_json) assert resp_json["total"] == count, resp_json # store result, use for update and delete context.config.userdata[holder].setdefault( diff --git a/services/traction/bdd-tests/features/v1-holder-proposal.feature b/services/traction/bdd-tests/features/v1-holder-proposal.feature index 551f5b47f..7f419535b 100644 --- a/services/traction/bdd-tests/features/v1-holder-proposal.feature +++ b/services/traction/bdd-tests/features/v1-holder-proposal.feature @@ -30,5 +30,5 @@ Feature: holding presentations And "alice" will have 1 holder presentation(s) And "alice" can find 1 credential(s) for holder presentation And we sadly wait for 10 seconds because we have not figured out how to listen for events - Then "alice" will have a holder presentation with status "Presentation Sent" - And "faber" has a "received" verifier presentation + Then "alice" will have a holder presentation with status "Presentation Received" + And "faber" has a "verified" verifier presentation From 69790231d639727388898a9ed3408bf99f7f264e Mon Sep 17 00:00:00 2001 From: Jason Sherman Date: Wed, 27 Jul 2022 21:15:25 -0700 Subject: [PATCH 5/6] new test for invalid proposal, refactoring, adjusting abandoned status Signed-off-by: Jason Sherman --- .../v1/holder/holder_presentation_protocol.py | 48 ++++++++++++------- .../verifier_presentation_request_handler.py | 9 ++-- ...ier_presentation_request_status_updater.py | 7 ++- .../bdd-tests/features/steps/holder.py | 22 +++++++++ .../features/v1-holder-proposal.feature | 8 ++++ 5 files changed, 70 insertions(+), 24 deletions(-) diff --git a/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py b/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py index bedf0422d..9238bea78 100644 --- a/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py +++ b/services/traction/api/protocols/v1/holder/holder_presentation_protocol.py @@ -69,6 +69,10 @@ async def before_any(self, profile: Profile, payload: dict): if payload["state"] in accepted_states: values["status"] = HolderPresentationStatusType.presentation_acked + if "error_msg" in payload: + values["error_status_detail"] = payload["error_msg"] + values["status"] = HolderPresentationStatusType.error + self.logger.debug(f"update values = {values}") await HolderPresentation.update_by_id( item_id=item.holder_presentation_id, values=values @@ -78,20 +82,15 @@ async def before_any(self, profile: Profile, payload: dict): async def on_request_received(self, profile: Profile, payload: dict): self.logger.info("> on_request_received()") - # create a new holder credential! contact = await self.get_contact(profile, payload) - holder_presentation = await self.get_holder_presentation(profile, payload) - if contact: - if holder_presentation: - # may have one due to a sent proposal - values = { - "status": HolderPresentationStatusType.request_received, - "state": payload["state"], - } - await HolderPresentation.update_by_id( - item_id=holder_presentation.holder_presentation_id, values=values - ) - else: + if not contact: + self.logger.warning( + f"No contact found for connection_id<{payload['connection_id']}>, cannot create holder presentation." # noqa: E501 + ) + else: + holder_presentation = await self.get_holder_presentation(profile, payload) + if not holder_presentation: + # create a new holder credential! offer = HolderPresentation( tenant_id=profile.tenant_id, contact_id=contact.contact_id, @@ -106,8 +105,23 @@ async def on_request_received(self, profile: Profile, payload: dict): await db.commit() # TODO: create payload and send notification to tenant. - else: - self.logger.warning( - f"No contact found for connection_id<{payload['connection_id']}>, cannot create holder presentation." # noqa: E501 - ) self.logger.info("< on_request_received()") + + async def on_abandoned(self, profile: Profile, payload: dict): + if "error_msg" in payload: + self.logger.info(f"payload error_msg = {payload['error_msg']}") + if str(payload["error_msg"]).startswith("created problem report:"): + holder_presentation = await self.get_holder_presentation( + profile, payload + ) + self.logger.info("holder request abandoned, request rejected.") + values = { + "state": AcapyPresentProofStateType.ABANDONED, + "status": HolderPresentationStatusType.rejected, + } + self.logger.debug(f"updating holder presentation = {values}") + async with async_session() as db: + await HolderPresentation.update_by_id( + holder_presentation.holder_presentation_id, values + ) + await db.commit() diff --git a/services/traction/api/protocols/v1/verifier/verifier_presentation_request_handler.py b/services/traction/api/protocols/v1/verifier/verifier_presentation_request_handler.py index 226dc142d..9a82cad76 100644 --- a/services/traction/api/protocols/v1/verifier/verifier_presentation_request_handler.py +++ b/services/traction/api/protocols/v1/verifier/verifier_presentation_request_handler.py @@ -25,16 +25,19 @@ async def approve_for_processing(self, profile: Profile, payload: dict) -> bool: self.logger.info(f"< approve_for_processing({approved})") return approved - async def handle_abandoned(self, verifier_presentation, payload): + async def on_abandoned(self, profile: Profile, payload: dict): if "error_msg" in payload: - self.logger.debug(f"payload error_msg = {payload['error_msg']}") + self.logger.info(f"payload error_msg = {payload['error_msg']}") if str(payload["error_msg"]).startswith("abandoned"): + verifier_presentation = await self.get_verifier_presentation( + profile, payload + ) self.logger.info("presentation request abandoned, request rejected.") values = { "state": AcapyPresentProofStateType.ABANDONED, "status": VerifierPresentationStatusType.REJECTED, } - self.logger.debug(f"updating issuer credential = {values}") + self.logger.debug(f"updating verifier presentation = {values}") async with async_session() as db: await VerifierPresentation.update_by_id( verifier_presentation.verifier_presentation_id, values diff --git a/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py b/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py index 1acb2b989..dc9bb862a 100644 --- a/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py +++ b/services/traction/api/protocols/v1/verifier/verifier_presentation_request_status_updater.py @@ -35,16 +35,15 @@ async def before_any(self, profile: Profile, payload: dict): AcapyPresentProofStateType.PRESENTATION_RECEIVED, ] - # what to do about abandoned? - if payload["state"] in verified_states: values["status"] = VerifierPresentationStatusType.VERIFIED if payload["state"] in received_states: values["status"] = VerifierPresentationStatusType.RECEIVED - if payload["state"] == AcapyPresentProofStateType.ABANDONED: - values["status"] = VerifierPresentationStatusType.REJECTED + if "error_msg" in payload: + values["error_status_detail"] = payload["error_msg"] + values["status"] = VerifierPresentationStatusType.ERROR self.logger.debug(f"update values = {values}") diff --git a/services/traction/bdd-tests/features/steps/holder.py b/services/traction/bdd-tests/features/steps/holder.py index f984d8a06..27f241590 100644 --- a/services/traction/bdd-tests/features/steps/holder.py +++ b/services/traction/bdd-tests/features/steps/holder.py @@ -686,3 +686,25 @@ def step_impl(context, holder: str, verifier: str): resp_json = json.loads(response.content) item = resp_json["item"] assert item["state"] == "proposal_sent", resp_json + + +@step('"{holder}" proposes an empty presentation to "{verifier}"') +def step_impl(context, holder: str, verifier: str): + contact_id = context.config.userdata[holder]["connections"][verifier]["contact_id"] + + # don't ask for anything to prove... + presentation_proposal = { + "attributes": [], + "predicates": [], + } + + payload = { + "contact_id": contact_id, + "presentation_proposal": presentation_proposal, + "comment": "sent for bdd test", + } + response = holder_send_proposal(context, holder, payload) + assert response.status_code == status.HTTP_200_OK, response.__dict__ + resp_json = json.loads(response.content) + item = resp_json["item"] + assert item["state"] == "proposal_sent", resp_json diff --git a/services/traction/bdd-tests/features/v1-holder-proposal.feature b/services/traction/bdd-tests/features/v1-holder-proposal.feature index 7f419535b..eb4e84421 100644 --- a/services/traction/bdd-tests/features/v1-holder-proposal.feature +++ b/services/traction/bdd-tests/features/v1-holder-proposal.feature @@ -32,3 +32,11 @@ Feature: holding presentations And we sadly wait for 10 seconds because we have not figured out how to listen for events Then "alice" will have a holder presentation with status "Presentation Received" And "faber" has a "verified" verifier presentation + + + Scenario: holder proposes an empty presentation + When "alice" proposes an empty presentation to "faber" + And we sadly wait for 10 seconds because we have not figured out how to listen for events + And "alice" will have 1 holder presentation(s) + And "alice" will have a holder presentation with status "Error" + And "faber" has a "rejected" verifier presentation From 75cb4e59a5b98b4195dbdaedeeffab85db78c8c7 Mon Sep 17 00:00:00 2001 From: Jason Sherman Date: Thu, 28 Jul 2022 09:00:37 -0700 Subject: [PATCH 6/6] add a safety valve for getting credentials for a bad presentation request... Signed-off-by: Jason Sherman --- .../api/services/v1/holder_service.py | 24 ++++++++++++------- .../features/v1-holder-proposal.feature | 1 + 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/services/traction/api/services/v1/holder_service.py b/services/traction/api/services/v1/holder_service.py index 43e011e35..030330f4f 100644 --- a/services/traction/api/services/v1/holder_service.py +++ b/services/traction/api/services/v1/holder_service.py @@ -5,6 +5,7 @@ from sqlalchemy import select, func, desc, update from sqlalchemy.orm import selectinload +from acapy_client import ApiException from acapy_client.model.indy_pres_attr_spec import IndyPresAttrSpec from acapy_client.model.indy_pres_pred_spec import IndyPresPredSpec from acapy_client.model.indy_pres_preview import IndyPresPreview @@ -576,17 +577,22 @@ async def list_credentials_for_request( pass if item: - creds = present_proof_api.present_proof_records_pres_ex_id_credentials_get( - item.presentation_exchange_id - ) all_creds = [] - for cred in creds: - all_creds.append( - CredPrecisForProof( - cred_info=cred.get("cred_info"), - interval=cred.get("interval"), - presentation_referents=cred.get("presentation_referents"), + try: + creds = present_proof_api.present_proof_records_pres_ex_id_credentials_get( + item.presentation_exchange_id + ) + for cred in creds: + all_creds.append( + CredPrecisForProof( + cred_info=cred.get("cred_info"), + interval=cred.get("interval"), + presentation_referents=cred.get("presentation_referents"), + ) ) + except ApiException as e: + logger.warning( + f"Error getting credentials for presentation request. {e.reason}" ) return all_creds[skip : (skip + limit)], len(all_creds) else: diff --git a/services/traction/bdd-tests/features/v1-holder-proposal.feature b/services/traction/bdd-tests/features/v1-holder-proposal.feature index eb4e84421..4978858b7 100644 --- a/services/traction/bdd-tests/features/v1-holder-proposal.feature +++ b/services/traction/bdd-tests/features/v1-holder-proposal.feature @@ -39,4 +39,5 @@ Feature: holding presentations And we sadly wait for 10 seconds because we have not figured out how to listen for events And "alice" will have 1 holder presentation(s) And "alice" will have a holder presentation with status "Error" + And "alice" can find 0 credential(s) for holder presentation And "faber" has a "rejected" verifier presentation