Skip to content

Commit 322b88f

Browse files
jamshaleff137
authored andcommitted
Allow schema id to be used during anoncreds issuance (openwallet-foundation#3497)
* Allow schema id to be used during anoncreds issuance Signed-off-by: jamshale <jamiehalebc@gmail.com> * Fix scenario test Signed-off-by: jamshale <jamiehalebc@gmail.com> * Fix mistake in handler / Add unit tests Signed-off-by: jamshale <jamiehalebc@gmail.com> * Try a sleep in between upgrades. Issue with github actions Signed-off-by: jamshale <jamiehalebc@gmail.com> * Refactor based on PR review comments Signed-off-by: jamshale <jamiehalebc@gmail.com> * Repair _fetch_schema_attr_names function call Signed-off-by: jamshale <jamiehalebc@gmail.com> * Remove unnessecary self usage Signed-off-by: jamshale <jamiehalebc@gmail.com> --------- Signed-off-by: jamshale <jamiehalebc@gmail.com>
1 parent 8f74b76 commit 322b88f

File tree

5 files changed

+314
-92
lines changed

5 files changed

+314
-92
lines changed

acapy_agent/protocols/issue_credential/v2_0/formats/anoncreds/handler.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
import json
44
import logging
5-
from typing import Mapping, Optional, Tuple
5+
from typing import List, Mapping, Optional, Tuple
66

7-
from anoncreds import CredentialDefinition, Schema
7+
from anoncreds import CredentialDefinition
88
from marshmallow import RAISE
99

10-
from ......anoncreds.base import AnonCredsResolutionError
10+
from ......anoncreds.base import AnonCredsObjectNotFound, AnonCredsResolutionError
1111
from ......anoncreds.holder import AnonCredsHolder, AnonCredsHolderError
12-
from ......anoncreds.issuer import CATEGORY_CRED_DEF, CATEGORY_SCHEMA, AnonCredsIssuer
12+
from ......anoncreds.issuer import CATEGORY_CRED_DEF, AnonCredsIssuer
1313
from ......anoncreds.models.credential import AnoncredsCredentialSchema
1414
from ......anoncreds.models.credential_offer import AnoncredsCredentialOfferSchema
1515
from ......anoncreds.models.credential_proposal import (
@@ -204,15 +204,54 @@ async def _create():
204204
offer_json = await issuer.create_credential_offer(cred_def_id)
205205
return json.loads(offer_json)
206206

207-
async with self.profile.session() as session:
208-
cred_def_entry = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id)
209-
cred_def_dict = CredentialDefinition.load(cred_def_entry.value).to_dict()
210-
schema_entry = await session.handle.fetch(
211-
CATEGORY_SCHEMA, cred_def_dict["schemaId"]
207+
async def _get_attr_names(schema_id) -> List[str] | None:
208+
"""Fetch attribute names for a given schema ID from the registry."""
209+
if not schema_id:
210+
return None
211+
try:
212+
schema_result = await registry.get_schema(self.profile, schema_id)
213+
return schema_result.schema.attr_names
214+
except AnonCredsObjectNotFound:
215+
LOGGER.info(f"Schema not found for schema_id={schema_id}")
216+
return None
217+
except AnonCredsResolutionError as e:
218+
LOGGER.warning(f"Schema resolution failed for schema_id={schema_id}: {e}")
219+
return None
220+
221+
async def _fetch_schema_attr_names(
222+
anoncreds_attachment, cred_def_id
223+
) -> List[str] | None:
224+
"""Determine schema attribute names from schema_id or cred_def_id."""
225+
schema_id = anoncreds_attachment.get("schema_id")
226+
attr_names = await _get_attr_names(schema_id)
227+
228+
if attr_names:
229+
return attr_names
230+
231+
if cred_def_id:
232+
async with self.profile.session() as session:
233+
cred_def_entry = await session.handle.fetch(
234+
CATEGORY_CRED_DEF, cred_def_id
235+
)
236+
cred_def_dict = CredentialDefinition.load(
237+
cred_def_entry.value
238+
).to_dict()
239+
return await _get_attr_names(cred_def_dict.get("schemaId"))
240+
241+
return None
242+
243+
attr_names = None
244+
registry = self.profile.inject(AnonCredsRegistry)
245+
246+
attr_names = await _fetch_schema_attr_names(anoncreds_attachment, cred_def_id)
247+
248+
if not attr_names:
249+
raise V20CredFormatError(
250+
"Could not determine schema attributes. If you did not create the "
251+
"schema, then you need to provide the schema_id."
212252
)
213-
schema_dict = Schema.load(schema_entry.value).to_dict()
214253

215-
schema_attrs = set(schema_dict["attrNames"])
254+
schema_attrs = set(attr_names)
216255
preview_attrs = set(cred_proposal_message.credential_preview.attr_dict())
217256
if preview_attrs != schema_attrs:
218257
raise V20CredFormatError(

acapy_agent/protocols/issue_credential/v2_0/formats/anoncreds/tests/test_handler.py

Lines changed: 151 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,22 @@
44
from unittest import IsolatedAsyncioTestCase
55

66
import pytest
7+
from anoncreds import CredentialDefinition
78
from marshmallow import ValidationError
89

910
from .......anoncreds.holder import AnonCredsHolder
1011
from .......anoncreds.issuer import AnonCredsIssuer
12+
from .......anoncreds.models.credential_definition import (
13+
CredDef,
14+
CredDefValue,
15+
CredDefValuePrimary,
16+
)
17+
from .......anoncreds.registry import AnonCredsRegistry
1118
from .......anoncreds.revocation import AnonCredsRevocationRegistryFullError
1219
from .......cache.base import BaseCache
1320
from .......cache.in_memory import InMemoryCache
21+
from .......config.provider import ClassProvider
22+
from .......indy.credx.issuer import CATEGORY_CRED_DEF
1423
from .......ledger.base import BaseLedger
1524
from .......ledger.multiple_ledger.ledger_requests_executor import (
1625
IndyLedgerRequestsExecutor,
@@ -193,9 +202,38 @@
193202

194203
class TestV20AnonCredsCredFormatHandler(IsolatedAsyncioTestCase):
195204
async def asyncSetUp(self):
196-
self.profile = await create_test_profile()
205+
self.profile = await create_test_profile(
206+
{
207+
"wallet.type": "askar-anoncreds",
208+
}
209+
)
197210
self.context = self.profile.context
198211

212+
# Context
213+
self.cache = InMemoryCache()
214+
self.profile.context.injector.bind_instance(BaseCache, self.cache)
215+
216+
# Issuer
217+
self.issuer = mock.MagicMock(AnonCredsIssuer, autospec=True)
218+
self.profile.context.injector.bind_instance(AnonCredsIssuer, self.issuer)
219+
220+
# Holder
221+
self.holder = mock.MagicMock(AnonCredsHolder, autospec=True)
222+
self.profile.context.injector.bind_instance(AnonCredsHolder, self.holder)
223+
224+
# Anoncreds registry
225+
self.profile.context.injector.bind_instance(
226+
AnonCredsRegistry, AnonCredsRegistry()
227+
)
228+
registry = self.profile.context.inject_or(AnonCredsRegistry)
229+
legacy_indy_registry = ClassProvider(
230+
"acapy_agent.anoncreds.default.legacy_indy.registry.LegacyIndyRegistry",
231+
# supported_identifiers=[],
232+
# method_name="",
233+
).provide(self.profile.context.settings, self.profile.context.injector)
234+
await legacy_indy_registry.setup(self.profile.context)
235+
registry.register(legacy_indy_registry)
236+
199237
# Ledger
200238
self.ledger = mock.MagicMock(BaseLedger, autospec=True)
201239
self.ledger.get_schema = mock.CoroutineMock(return_value=SCHEMA)
@@ -214,18 +252,6 @@ async def asyncSetUp(self):
214252
)
215253
),
216254
)
217-
# Context
218-
self.cache = InMemoryCache()
219-
self.profile.context.injector.bind_instance(BaseCache, self.cache)
220-
221-
# Issuer
222-
self.issuer = mock.MagicMock(AnonCredsIssuer, autospec=True)
223-
self.profile.context.injector.bind_instance(AnonCredsIssuer, self.issuer)
224-
225-
# Holder
226-
self.holder = mock.MagicMock(AnonCredsHolder, autospec=True)
227-
self.profile.context.injector.bind_instance(AnonCredsHolder, self.holder)
228-
229255
self.handler = AnonCredsCredFormatHandler(self.profile)
230256
assert self.handler.profile
231257

@@ -338,68 +364,123 @@ async def test_receive_proposal(self):
338364
# Not much to assert. Receive proposal doesn't do anything
339365
await self.handler.receive_proposal(cred_ex_record, cred_proposal_message)
340366

341-
@pytest.mark.skip(reason="Anoncreds-break")
342-
async def test_create_offer(self):
343-
schema_id_parts = SCHEMA_ID.split(":")
344-
345-
cred_preview = V20CredPreview(
346-
attributes=(
347-
V20CredAttrSpec(name="legalName", value="value"),
348-
V20CredAttrSpec(name="jurisdictionId", value="value"),
349-
V20CredAttrSpec(name="incorporationDate", value="value"),
350-
)
351-
)
352-
353-
cred_proposal = V20CredProposal(
354-
credential_preview=cred_preview,
355-
formats=[
356-
V20CredFormat(
357-
attach_id="0",
358-
format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][
359-
V20CredFormat.Format.ANONCREDS.api
367+
async def test_create_offer_cant_find_schema_in_wallet_or_data_registry(self):
368+
with self.assertRaises(V20CredFormatError):
369+
await self.handler.create_offer(
370+
V20CredProposal(
371+
formats=[
372+
V20CredFormat(
373+
attach_id="0",
374+
format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][
375+
V20CredFormat.Format.ANONCREDS.api
376+
],
377+
)
378+
],
379+
filters_attach=[
380+
AttachDecorator.data_base64(
381+
{"cred_def_id": CRED_DEF_ID}, ident="0"
382+
)
360383
],
361384
)
362-
],
363-
filters_attach=[
364-
AttachDecorator.data_base64({"cred_def_id": CRED_DEF_ID}, ident="0")
365-
],
366-
)
385+
)
367386

368-
cred_def_record = StorageRecord(
369-
CRED_DEF_SENT_RECORD_TYPE,
370-
CRED_DEF_ID,
371-
{
372-
"schema_id": SCHEMA_ID,
373-
"schema_issuer_did": schema_id_parts[0],
374-
"schema_name": schema_id_parts[-2],
375-
"schema_version": schema_id_parts[-1],
376-
"issuer_did": TEST_DID,
377-
"cred_def_id": CRED_DEF_ID,
378-
"epoch": str(int(time())),
379-
},
387+
@mock.patch.object(
388+
AnonCredsRegistry,
389+
"get_schema",
390+
mock.CoroutineMock(
391+
return_value=mock.MagicMock(schema=mock.MagicMock(attr_names=["score"]))
392+
),
393+
)
394+
@mock.patch.object(
395+
AnonCredsIssuer,
396+
"create_credential_offer",
397+
mock.CoroutineMock(return_value=json.dumps(ANONCREDS_OFFER)),
398+
)
399+
@mock.patch.object(
400+
CredentialDefinition,
401+
"load",
402+
mock.MagicMock(to_dict=mock.MagicMock(return_value={"schemaId": SCHEMA_ID})),
403+
)
404+
async def test_create_offer(self):
405+
self.issuer.create_credential_offer = mock.CoroutineMock({})
406+
# With a schema_id
407+
await self.handler.create_offer(
408+
V20CredProposal(
409+
credential_preview=V20CredPreview(
410+
attributes=(V20CredAttrSpec(name="score", value="0"),)
411+
),
412+
formats=[
413+
V20CredFormat(
414+
attach_id="0",
415+
format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][
416+
V20CredFormat.Format.ANONCREDS.api
417+
],
418+
)
419+
],
420+
filters_attach=[
421+
AttachDecorator.data_base64(
422+
{"cred_def_id": CRED_DEF_ID, "schema_id": SCHEMA_ID}, ident="0"
423+
)
424+
],
425+
)
380426
)
381-
await self.session.storage.add_record(cred_def_record)
382-
383-
self.issuer.create_credential_offer = mock.CoroutineMock(
384-
return_value=json.dumps(ANONCREDS_OFFER)
427+
# Only with cred_def_id
428+
async with self.profile.session() as session:
429+
await session.handle.insert(
430+
CATEGORY_CRED_DEF,
431+
CRED_DEF_ID,
432+
CredDef(
433+
issuer_id=TEST_DID,
434+
schema_id=SCHEMA_ID,
435+
tag="tag",
436+
type="CL",
437+
value=CredDefValue(
438+
primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z")
439+
),
440+
).to_json(),
441+
tags={},
442+
)
443+
await self.handler.create_offer(
444+
V20CredProposal(
445+
credential_preview=V20CredPreview(
446+
attributes=(V20CredAttrSpec(name="score", value="0"),)
447+
),
448+
formats=[
449+
V20CredFormat(
450+
attach_id="0",
451+
format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][
452+
V20CredFormat.Format.ANONCREDS.api
453+
],
454+
)
455+
],
456+
filters_attach=[
457+
AttachDecorator.data_base64({"cred_def_id": CRED_DEF_ID}, ident="0")
458+
],
459+
)
385460
)
386-
387-
(cred_format, attachment) = await self.handler.create_offer(cred_proposal)
388-
389-
self.issuer.create_credential_offer.assert_called_once_with(CRED_DEF_ID)
390-
391-
# assert identifier match
392-
assert cred_format.attach_id == self.handler.format.api == attachment.ident
393-
394-
# assert content of attachment is proposal data
395-
assert attachment.content == ANONCREDS_OFFER
396-
397-
# assert data is encoded as base64
398-
assert attachment.data.base64
399-
400-
self.issuer.create_credential_offer.reset_mock()
401-
await self.handler.create_offer(cred_proposal)
402-
self.issuer.create_credential_offer.assert_not_called()
461+
# Wrong attribute name
462+
with self.assertRaises(V20CredFormatError):
463+
await self.handler.create_offer(
464+
V20CredProposal(
465+
credential_preview=V20CredPreview(
466+
attributes=(V20CredAttrSpec(name="wrong", value="0"),)
467+
),
468+
formats=[
469+
V20CredFormat(
470+
attach_id="0",
471+
format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][
472+
V20CredFormat.Format.ANONCREDS.api
473+
],
474+
)
475+
],
476+
filters_attach=[
477+
AttachDecorator.data_base64(
478+
{"cred_def_id": CRED_DEF_ID, "schema_id": SCHEMA_ID},
479+
ident="0",
480+
)
481+
],
482+
)
483+
)
403484

404485
@pytest.mark.skip(reason="Anoncreds-break")
405486
async def test_create_offer_no_cache(self):

0 commit comments

Comments
 (0)