Skip to content

Commit b573f0f

Browse files
authored
Merge pull request #3947 from TheTechmage/feat/auto-remove-failed
2 parents 64ec4cc + b8755ce commit b573f0f

10 files changed

Lines changed: 257 additions & 0 deletions

File tree

acapy_agent/config/argparse.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,6 +1292,13 @@ def add_arguments(self, parser: ArgumentParser):
12921292
help="Keep credential and presentation exchange records after "
12931293
"exchange has completed.",
12941294
)
1295+
parser.add_argument(
1296+
"--no-preserve-failed-exchange-records",
1297+
action="store_true",
1298+
env_var="ACAPY_NO_PRESERVE_FAILED_EXCHANGE_RECORDS",
1299+
help="Remove failed credential and presentation exchange records "
1300+
"upon failure.",
1301+
)
12951302
parser.add_argument(
12961303
"--emit-new-didcomm-prefix",
12971304
action="store_true",

acapy_agent/protocols/issue_credential/v2_0/manager.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ async def prepare_send(
5454
cred_proposal: V20CredProposal,
5555
verification_method: Optional[str] = None,
5656
auto_remove: Optional[bool] = None,
57+
auto_remove_on_failure: Optional[bool] = None,
5758
replacement_id: Optional[str] = None,
5859
) -> Tuple[V20CredExRecord, V20CredOffer]:
5960
"""Set up a new credential exchange record for an automated send.
@@ -63,6 +64,7 @@ async def prepare_send(
6364
cred_proposal: credential proposal with preview
6465
verification_method: an optional verification method to be used when issuing
6566
auto_remove: flag to remove the record automatically on completion
67+
auto_remove_on_failure: flag to remove the record automatically on failure
6668
replacement_id: identifier to help coordinate credential replacement
6769
6870
Returns:
@@ -71,6 +73,10 @@ async def prepare_send(
7173
"""
7274
if auto_remove is None:
7375
auto_remove = not self._profile.settings.get("preserve_exchange_records")
76+
if auto_remove_on_failure is None:
77+
auto_remove_on_failure = bool(
78+
self._profile.settings.get("no_preserve_failed_exchange_records")
79+
)
7480
cred_ex_record = V20CredExRecord(
7581
connection_id=connection_id,
7682
verification_method=verification_method,
@@ -79,6 +85,7 @@ async def prepare_send(
7985
cred_proposal=cred_proposal,
8086
auto_issue=True,
8187
auto_remove=auto_remove,
88+
auto_remove_on_failure=auto_remove_on_failure,
8289
trace=(cred_proposal._trace is not None),
8390
)
8491
return await self.create_offer(
@@ -766,4 +773,7 @@ async def receive_problem_report(
766773
cred_ex_record.error_msg = f"{code}: {message.description.get('en', code)}"
767774
await cred_ex_record.save(session, reason="received problem report")
768775

776+
if cred_ex_record.auto_remove_on_failure:
777+
await self.delete_cred_ex_record(cred_ex_record.cred_ex_id)
778+
769779
return cred_ex_record

acapy_agent/protocols/issue_credential/v2_0/models/cred_ex_record.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(
6969
auto_offer: bool = False,
7070
auto_issue: bool = False,
7171
auto_remove: bool = True,
72+
auto_remove_on_failure: bool = False,
7273
error_msg: Optional[str] = None,
7374
trace: bool = False, # backward compat: BaseRecord.from_storage()
7475
cred_id_stored: Optional[
@@ -95,6 +96,7 @@ def __init__(
9596
self.auto_offer = auto_offer
9697
self.auto_issue = auto_issue
9798
self.auto_remove = auto_remove
99+
self.auto_remove_on_failure = auto_remove_on_failure
98100
self.error_msg = error_msg
99101

100102
@property
@@ -225,6 +227,7 @@ def record_value(self) -> Mapping:
225227
"auto_offer",
226228
"auto_issue",
227229
"auto_remove",
230+
"auto_remove_on_failure",
228231
"error_msg",
229232
"trace",
230233
)
@@ -428,6 +431,16 @@ class Meta:
428431
"example": False,
429432
},
430433
)
434+
auto_remove_on_failure = fields.Bool(
435+
required=False,
436+
dump_default=True,
437+
metadata={
438+
"description": (
439+
"Issuer choice to remove this credential exchange record when failed"
440+
),
441+
"example": False,
442+
},
443+
)
431444
error_msg = fields.Str(
432445
required=False,
433446
metadata={"description": "Error message", "example": "The front fell off"},

acapy_agent/protocols/issue_credential/v2_0/routes.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,15 @@ class V20IssueCredSchemaCore(AdminAPIMessageTracingSchema):
332332
)
333333
},
334334
)
335+
auto_remove_on_failure = fields.Bool(
336+
required=False,
337+
metadata={
338+
"description": (
339+
"Whether to remove the credential exchange record on failure"
340+
" (overrides --no-preserve-failed-exchange-records configuration setting)"
341+
)
342+
},
343+
)
335344
comment = fields.Str(
336345
required=False,
337346
allow_none=True,
@@ -393,6 +402,15 @@ class V20CredRequestFreeSchema(AdminAPIMessageTracingSchema):
393402
)
394403
},
395404
)
405+
auto_remove_on_failure = fields.Bool(
406+
required=False,
407+
metadata={
408+
"description": (
409+
"Whether to remove the credential exchange record on failure"
410+
" (overrides --no-preserve-failed-exchange-records configuration setting)"
411+
)
412+
},
413+
)
396414
comment = fields.Str(
397415
required=False,
398416
allow_none=True,
@@ -512,6 +530,16 @@ class V20CredRequestRequestSchema(OpenAPISchema):
512530
)
513531
},
514532
)
533+
auto_remove_on_failure = fields.Bool(
534+
required=False,
535+
dump_default=False,
536+
metadata={
537+
"description": (
538+
"Whether to remove the credential exchange record on failure"
539+
" (overrides --no-preserve-failed-exchange-records configuration setting)"
540+
)
541+
},
542+
)
515543

516544

517545
class V20CredIssueRequestSchema(OpenAPISchema):
@@ -735,6 +763,10 @@ async def credential_exchange_create(request: web.BaseRequest):
735763
auto_remove = body.get(
736764
"auto_remove", not profile.settings.get("preserve_exchange_records")
737765
)
766+
auto_remove_on_failure = body.get(
767+
"auto_remove_on_failure",
768+
profile.settings.get("no_preserve_failed_exchange_records"),
769+
)
738770
if not filt_spec:
739771
raise web.HTTPBadRequest(reason="Missing filter")
740772
trace_msg = body.get("trace")
@@ -763,6 +795,7 @@ async def credential_exchange_create(request: web.BaseRequest):
763795
connection_id=None,
764796
cred_proposal=cred_proposal,
765797
auto_remove=auto_remove,
798+
auto_remove_on_failure=auto_remove_on_failure,
766799
)
767800
except (StorageError, BaseModelError) as err:
768801
raise web.HTTPBadRequest(reason=err.roll_up) from err
@@ -1338,6 +1371,10 @@ async def credential_exchange_send_free_request(request: web.BaseRequest):
13381371
auto_remove = body.get(
13391372
"auto_remove", not profile.settings.get("preserve_exchange_records")
13401373
)
1374+
auto_remove_on_failure = body.get(
1375+
"auto_remove_on_failure",
1376+
profile.settings.get("no_preserve_failed_exchange_records"),
1377+
)
13411378
trace_msg = body.get("trace")
13421379
holder_did = body.get("holder_did")
13431380

@@ -1362,6 +1399,7 @@ async def credential_exchange_send_free_request(request: web.BaseRequest):
13621399
cred_ex_record = V20CredExRecord(
13631400
connection_id=connection_id,
13641401
auto_remove=auto_remove,
1402+
auto_remove_on_failure=auto_remove_on_failure,
13651403
cred_proposal=cred_proposal.serialize(),
13661404
initiator=V20CredExRecord.INITIATOR_SELF,
13671405
role=V20CredExRecord.ROLE_HOLDER,
@@ -1433,9 +1471,16 @@ async def credential_exchange_send_bound_request(request: web.BaseRequest):
14331471
auto_remove = body.get(
14341472
"auto_remove", not profile.settings.get("preserve_exchange_records")
14351473
)
1474+
auto_remove_on_failure = body.get(
1475+
"auto_remove_on_failure",
1476+
profile.settings.get("no_preserve_failed_exchange_records"),
1477+
)
14361478
except JSONDecodeError:
14371479
holder_did = None
14381480
auto_remove = not profile.settings.get("preserve_exchange_records")
1481+
auto_remove_on_failure = profile.settings.get(
1482+
"no_preserve_failed_exchange_records"
1483+
)
14391484

14401485
cred_ex_id = request.match_info["cred_ex_id"]
14411486

@@ -1479,6 +1524,7 @@ async def credential_exchange_send_bound_request(request: web.BaseRequest):
14791524

14801525
# assign the auto_remove flag from above...
14811526
cred_ex_record.auto_remove = auto_remove
1527+
cred_ex_record.auto_remove_on_failure = auto_remove_on_failure
14821528

14831529
cred_manager = V20CredManager(profile)
14841530
cred_ex_record, cred_request_message = await cred_manager.create_request(

acapy_agent/protocols/issue_credential/v2_0/tests/test_manager.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,6 +1466,48 @@ async def test_receive_problem_report_x(self):
14661466
with self.assertRaises(test_module.StorageNotFoundError):
14671467
await self.manager.receive_problem_report(problem, connection_id)
14681468

1469+
async def test_receive_problem_report_removal(self):
1470+
connection_id = "connection-id"
1471+
stored_exchange = V20CredExRecord(
1472+
cred_ex_id="dummy-cxid",
1473+
connection_id=connection_id,
1474+
initiator=V20CredExRecord.INITIATOR_SELF,
1475+
role=V20CredExRecord.ROLE_ISSUER,
1476+
auto_remove_on_failure=True,
1477+
)
1478+
problem = V20CredProblemReport(
1479+
description={
1480+
"code": test_module.ProblemReportReason.ISSUANCE_ABANDONED.value,
1481+
"en": "Insufficient privilege",
1482+
}
1483+
)
1484+
1485+
with (
1486+
mock.patch.object(V20CredExRecord, "save", autospec=True) as save_ex,
1487+
mock.patch.object(
1488+
V20CredExRecord,
1489+
"retrieve_by_conn_and_thread",
1490+
mock.CoroutineMock(),
1491+
) as retrieve_ex,
1492+
mock.patch.object(
1493+
V20CredExRecord, "retrieve_by_id", mock.CoroutineMock()
1494+
) as mock_retrieve,
1495+
mock.patch.object(
1496+
V20CredExRecord, "delete_record", autospec=True
1497+
) as delete_ex,
1498+
):
1499+
retrieve_ex.return_value = stored_exchange
1500+
mock_retrieve.return_value = stored_exchange
1501+
1502+
ret_exchange = await self.manager.receive_problem_report(
1503+
problem, connection_id
1504+
)
1505+
retrieve_ex.assert_called()
1506+
save_ex.assert_called_once()
1507+
delete_ex.assert_called_once()
1508+
1509+
assert ret_exchange.state == V20CredExRecord.STATE_ABANDONED
1510+
14691511
async def test_retrieve_records(self):
14701512
self.profile.context.injector.bind_instance(InMemoryCache, InMemoryCache())
14711513

acapy_agent/protocols/present_proof/v2_0/manager.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ async def create_exchange_for_proposal(
4141
pres_proposal_message: V20PresProposal,
4242
auto_present: Optional[bool] = None,
4343
auto_remove: Optional[bool] = None,
44+
auto_remove_on_failure: Optional[bool] = None,
4445
):
4546
"""Create a presentation exchange record for input presentation proposal.
4647
@@ -51,13 +52,19 @@ async def create_exchange_for_proposal(
5152
auto_present: whether to present proof upon receiving proof request
5253
(default to configuration setting)
5354
auto_remove: whether to remove this presentation exchange upon completion
55+
auto_remove_on_failure: whether to remove this presentation exchange upon
56+
failure
5457
5558
Returns:
5659
Presentation exchange record, created
5760
5861
"""
5962
if auto_remove is None:
6063
auto_remove = not self._profile.settings.get("preserve_exchange_records")
64+
if auto_remove_on_failure is None:
65+
auto_remove_on_failure = bool(
66+
self._profile.settings.get("no_preserve_failed_exchange_records")
67+
)
6168
pres_ex_record = V20PresExRecord(
6269
connection_id=connection_id,
6370
thread_id=pres_proposal_message._thread_id,
@@ -68,6 +75,7 @@ async def create_exchange_for_proposal(
6875
auto_present=auto_present,
6976
trace=(pres_proposal_message._trace is not None),
7077
auto_remove=auto_remove,
78+
auto_remove_on_failure=auto_remove_on_failure,
7179
)
7280

7381
async with self._profile.session() as session:
@@ -162,6 +170,7 @@ async def create_exchange_for_request(
162170
pres_request_message: V20PresRequest,
163171
auto_verify: Optional[bool] = None,
164172
auto_remove: Optional[bool] = None,
173+
auto_remove_on_failure: Optional[bool] = None,
165174
):
166175
"""Create a presentation exchange record for input presentation request.
167176
@@ -171,13 +180,19 @@ async def create_exchange_for_request(
171180
exchange record, extracting indy proof request and thread id
172181
auto_verify: whether to auto-verify presentation exchange
173182
auto_remove: whether to remove this presentation exchange upon completion
183+
auto_remove_on_failure: whether to remove this presentation exchange upon
184+
failure
174185
175186
Returns:
176187
Presentation exchange record, updated
177188
178189
"""
179190
if auto_remove is None:
180191
auto_remove = not self._profile.settings.get("preserve_exchange_records")
192+
if auto_remove_on_failure is None:
193+
auto_remove_on_failure = bool(
194+
self._profile.settings.get("no_preserve_failed_exchange_records")
195+
)
181196
pres_ex_record = V20PresExRecord(
182197
connection_id=connection_id,
183198
thread_id=pres_request_message._thread_id,
@@ -188,6 +203,7 @@ async def create_exchange_for_request(
188203
auto_verify=auto_verify,
189204
trace=(pres_request_message._trace is not None),
190205
auto_remove=auto_remove,
206+
auto_remove_on_failure=auto_remove_on_failure,
191207
)
192208
async with self._profile.session() as session:
193209
await pres_ex_record.save(
@@ -494,4 +510,8 @@ async def receive_problem_report(
494510
pres_ex_record.error_msg = f"{code}: {message.description.get('en', code)}"
495511
await pres_ex_record.save(session, reason="received problem report")
496512

513+
# all done: delete
514+
if pres_ex_record.auto_remove_on_failure:
515+
await pres_ex_record.delete_record(session)
516+
497517
return pres_ex_record

acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def __init__(
6767
trace: bool = False, # backward compat: BaseRecord.FromStorage()
6868
by_format: Optional[Mapping] = None, # backward compat: BaseRecord.FromStorage()
6969
auto_remove: bool = False,
70+
auto_remove_on_failure: bool = False,
7071
**kwargs,
7172
):
7273
"""Initialize a new PresExRecord."""
@@ -85,6 +86,7 @@ def __init__(
8586
self.auto_verify = auto_verify
8687
self.error_msg = error_msg
8788
self.auto_remove = auto_remove
89+
self.auto_remove_on_failure = auto_remove_on_failure
8890

8991
@property
9092
def pres_ex_id(self) -> str:
@@ -225,6 +227,7 @@ def record_value(self) -> Mapping:
225227
"error_msg",
226228
"trace",
227229
"auto_remove",
230+
"auto_remove_on_failure",
228231
)
229232
},
230233
**{
@@ -366,3 +369,13 @@ class Meta:
366369
"example": False,
367370
},
368371
)
372+
auto_remove_on_failure = fields.Bool(
373+
required=False,
374+
dump_default=True,
375+
metadata={
376+
"description": (
377+
"Verifier choice to remove this presentation exchange record when failed"
378+
),
379+
"example": False,
380+
},
381+
)

acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ async def test_record(self):
109109
"error_msg": "error",
110110
"trace": False,
111111
"auto_remove": True,
112+
"auto_remove_on_failure": False,
112113
}
113114

114115
bx_record = BasexRecordImpl()

0 commit comments

Comments
 (0)