Skip to content

Commit

Permalink
Merge pull request #1316 from shaangill025/oob_cred_offer
Browse files Browse the repository at this point in the history
OOB Protocol - CredentialOffer Attachment Support
  • Loading branch information
andrewwhitehead authored Jul 16, 2021
2 parents 89324e2 + 4d37639 commit a58d7ba
Show file tree
Hide file tree
Showing 10 changed files with 1,132 additions and 602 deletions.
21 changes: 17 additions & 4 deletions aries_cloudagent/protocols/issue_credential/v1_0/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,11 +468,24 @@ async def receive_request(self, message: CredentialRequest, connection_id: str):
credential_request = message.indy_cred_req(0)

async with self._profile.session() as session:
cred_ex_record = await (
V10CredentialExchange.retrieve_by_connection_and_thread(
session, connection_id, message._thread_id
try:
cred_ex_record = await (
V10CredentialExchange.retrieve_by_connection_and_thread(
session, connection_id, message._thread_id
)
)
)
except StorageNotFoundError:
try:
cred_ex_record = await V10CredentialExchange.retrieve_by_tag_filter(
session,
{"thread_id": message._thread_id},
{"connection_id": None},
)
cred_ex_record.connection_id = connection_id
except StorageNotFoundError:
raise CredentialManagerError(
"Indy issue credential format can't start from credential request"
)
cred_ex_record.credential_request = credential_request
cred_ex_record.state = V10CredentialExchange.STATE_REQUEST_RECEIVED
await cred_ex_record.save(session, reason="receive credential request")
Expand Down
133 changes: 53 additions & 80 deletions aries_cloudagent/protocols/issue_credential/v1_0/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from ....messaging.models.base import BaseModelError
from ....messaging.models.openapi import OpenAPISchema
from ....messaging.valid import (
ENDPOINT,
INDY_CRED_DEF_ID,
INDY_DID,
INDY_SCHEMA_ID,
Expand All @@ -30,9 +29,6 @@
UUID4,
)
from ....storage.error import StorageError, StorageNotFoundError
from ....wallet.base import BaseWallet
from ....wallet.error import WalletError
from ....utils.outofband import serialize_outofband
from ....utils.tracing import trace_event, get_timer, AdminAPIMessageTracingSchema

from . import problem_report_for_record, report_problem
Expand Down Expand Up @@ -238,17 +234,33 @@ class V10CredentialFreeOfferRequestSchema(AdminAPIMessageTracingSchema):
credential_preview = fields.Nested(CredentialPreviewSchema, required=True)


class V10CreateFreeOfferResultSchema(OpenAPISchema):
"""Result schema for creating free offer."""
class V10CredentialConnFreeOfferRequestSchema(AdminAPIMessageTracingSchema):
"""Request schema for creating connection free credential offer."""

response = fields.Nested(
V10CredentialExchange(),
description="Credential exchange record",
cred_def_id = fields.Str(
description="Credential definition identifier",
required=True,
**INDY_CRED_DEF_ID,
)
auto_issue = fields.Bool(
description=(
"Whether to respond automatically to credential requests, creating "
"and issuing requested credentials"
),
required=False,
)
auto_remove = fields.Bool(
description=(
"Whether to remove the credential exchange record on completion "
"(overrides --preserve-exchange-records configuration setting)"
),
required=False,
default=True,
)
oob_url = fields.Str(
description="Out-of-band URL",
**ENDPOINT,
comment = fields.Str(
description="Human-readable comment", required=False, allow_none=True
)
credential_preview = fields.Nested(CredentialPreviewSchema, required=True)


class V10CredentialIssueRequestSchema(OpenAPISchema):
Expand Down Expand Up @@ -669,10 +681,10 @@ async def _create_free_offer(

@docs(
tags=["issue-credential v1.0"],
summary="Create a credential offer, independent of any proposal",
summary="Create a credential offer, independent of any proposal or connection",
)
@request_schema(V10CredentialFreeOfferRequestSchema())
@response_schema(V10CreateFreeOfferResultSchema(), 200, description="")
@request_schema(V10CredentialConnFreeOfferRequestSchema())
@response_schema(V10CredentialExchangeSchema(), 200, description="")
async def credential_exchange_create_free_offer(request: web.BaseRequest):
"""
Request handler for creating free credential offer.
Expand All @@ -690,7 +702,6 @@ async def credential_exchange_create_free_offer(request: web.BaseRequest):
r_time = get_timer()

context: AdminRequestContext = request["context"]
outbound_handler = request["outbound_message_router"]

body = await request.json()

Expand All @@ -707,57 +718,19 @@ async def credential_exchange_create_free_offer(request: web.BaseRequest):
if not preview_spec:
raise web.HTTPBadRequest(reason=("Missing credential_preview"))

connection_id = body.get("connection_id")
trace_msg = body.get("trace")

async with context.session() as session:
wallet = session.inject(BaseWallet)
if connection_id:
try:
connection_record = await ConnRecord.retrieve_by_id(
session, connection_id
)
conn_did = await wallet.get_local_did(connection_record.my_did)
except (WalletError, StorageError) as err:
raise web.HTTPBadRequest(reason=err.roll_up) from err
else:
conn_did = await wallet.get_public_did()
if not conn_did:
raise web.HTTPBadRequest(reason="Wallet has no public DID")
connection_id = None

did_info = await wallet.get_public_did()
del wallet

endpoint = did_info.metadata.get(
"endpoint", context.settings.get("default_endpoint")
)
if not endpoint:
raise web.HTTPBadRequest(reason="An endpoint for the public DID is required")

cred_ex_record = None
try:
(cred_ex_record, credential_offer_message) = await _create_free_offer(
context.profile,
cred_def_id,
connection_id,
auto_issue,
auto_remove,
preview_spec,
comment,
trace_msg,
)

trace_event(
context.settings,
credential_offer_message,
outcome="credential_exchange_create_free_offer.END",
perf_counter=r_time,
profile=context.profile,
cred_def_id=cred_def_id,
auto_issue=auto_issue,
auto_remove=auto_remove,
preview_spec=preview_spec,
comment=comment,
trace_msg=trace_msg,
)

oob_url = serialize_outofband(credential_offer_message, conn_did, endpoint)
result = cred_ex_record.serialize()

except (
BaseModelError,
CredentialManagerError,
Expand All @@ -768,16 +741,14 @@ async def credential_exchange_create_free_offer(request: web.BaseRequest):
if cred_ex_record:
async with context.session() as session:
await cred_ex_record.save_error_state(session, reason=err.roll_up)
await report_problem(
err,
ProblemReportReason.ISSUANCE_ABANDONED.value,
web.HTTPBadRequest,
cred_ex_record or connection_record,
outbound_handler,
)

response = {"record": result, "oob_url": oob_url}
return web.json_response(response)
raise web.HTTPBadRequest(reason=err.roll_up)
trace_event(
context.settings,
credential_offer_message,
outcome="credential_exchange_create_free_offer.END",
perf_counter=r_time,
)
return web.json_response(result)


@docs(
Expand Down Expand Up @@ -815,7 +786,6 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest):
auto_issue = body.get(
"auto_issue", context.settings.get("debug.auto_respond_credential_request")
)

auto_remove = body.get("auto_remove")
comment = body.get("comment")
preview_spec = body.get("credential_preview")
Expand All @@ -832,14 +802,14 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest):
raise web.HTTPForbidden(reason=f"Connection {connection_id} not ready")

cred_ex_record, credential_offer_message = await _create_free_offer(
context.profile,
cred_def_id,
connection_id,
auto_issue,
auto_remove,
preview_spec,
comment,
trace_msg,
profile=context.profile,
cred_def_id=cred_def_id,
connection_id=connection_id,
auto_issue=auto_issue,
auto_remove=auto_remove,
preview_spec=preview_spec,
comment=comment,
trace_msg=trace_msg,
)
result = cred_ex_record.serialize()

Expand Down Expand Up @@ -1312,6 +1282,9 @@ async def register(app: web.Application):
web.get(
"/issue-credential/records", credential_exchange_list, allow_head=False
),
web.post(
"/issue-credential/create-offer", credential_exchange_create_free_offer
),
web.get(
"/issue-credential/records/{cred_ex_id}",
credential_exchange_retrieve,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,75 @@ async def test_receive_request(self):
assert exchange.state == V10CredentialExchange.STATE_REQUEST_RECEIVED
assert exchange._credential_request.ser == INDY_CRED_REQ

async def test_receive_request_no_connection_cred_request(self):
stored_exchange = V10CredentialExchange(
credential_exchange_id="dummy-cxid",
initiator=V10CredentialExchange.INITIATOR_EXTERNAL,
role=V10CredentialExchange.ROLE_ISSUER,
)

request = CredentialRequest(
requests_attach=[CredentialRequest.wrap_indy_cred_req(INDY_CRED_REQ)]
)

with async_mock.patch.object(
V10CredentialExchange, "save", autospec=True
) as mock_save, async_mock.patch.object(
V10CredentialExchange,
"retrieve_by_connection_and_thread",
async_mock.CoroutineMock(),
) as mock_retrieve, async_mock.patch.object(
V10CredentialExchange, "retrieve_by_tag_filter", async_mock.CoroutineMock()
) as mock_retrieve_tag_filter:
mock_retrieve.side_effect = (StorageNotFoundError(),)
mock_retrieve_tag_filter.return_value = stored_exchange
cx_rec = await self.manager.receive_request(request, "test_conn_id")

mock_retrieve.assert_called_once_with(
self.session, "test_conn_id", request._thread_id
)
mock_retrieve_tag_filter.assert_called_once_with(
self.session, {"thread_id": request._thread_id}, {"connection_id": None}
)
mock_save.assert_called_once()
assert cx_rec.state == V10CredentialExchange.STATE_REQUEST_RECEIVED
assert cx_rec._credential_request.ser == INDY_CRED_REQ
assert cx_rec.connection_id == "test_conn_id"

async def test_receive_request_no_cred_ex_with_offer_found(self):
stored_exchange = V10CredentialExchange(
credential_exchange_id="dummy-cxid",
initiator=V10CredentialExchange.INITIATOR_EXTERNAL,
role=V10CredentialExchange.ROLE_ISSUER,
)

request = CredentialRequest(
requests_attach=[CredentialRequest.wrap_indy_cred_req(INDY_CRED_REQ)]
)

with async_mock.patch.object(
V10CredentialExchange, "save", autospec=True
) as mock_save, async_mock.patch.object(
V10CredentialExchange,
"retrieve_by_connection_and_thread",
async_mock.CoroutineMock(),
) as mock_retrieve, async_mock.patch.object(
V10CredentialExchange, "retrieve_by_tag_filter", async_mock.CoroutineMock()
) as mock_retrieve_tag_filter:
mock_retrieve.side_effect = (StorageNotFoundError(),)
mock_retrieve_tag_filter.side_effect = (StorageNotFoundError(),)
with self.assertRaises(CredentialManagerError):
cx_rec = await self.manager.receive_request(request, "test_conn_id")

mock_retrieve.assert_called_once_with(
self.session, "test_conn_id", request._thread_id
)
mock_retrieve_tag_filter.assert_called_once_with(
self.session,
{"thread_id": request._thread_id},
{"connection_id": None},
)

async def test_issue_credential(self):
connection_id = "test_conn_id"
comment = "comment"
Expand Down
Loading

0 comments on commit a58d7ba

Please sign in to comment.