Skip to content

Commit

Permalink
Allow schema id to be used during anoncreds issuance (openwallet-foun…
Browse files Browse the repository at this point in the history
…dation#3497)

* Allow schema id to be used during anoncreds issuance

Signed-off-by: jamshale <[email protected]>

* Fix scenario test

Signed-off-by: jamshale <[email protected]>

* Fix mistake in handler / Add unit tests

Signed-off-by: jamshale <[email protected]>

* Try a sleep in between upgrades. Issue with github actions

Signed-off-by: jamshale <[email protected]>

* Refactor based on PR review comments

Signed-off-by: jamshale <[email protected]>

* Repair _fetch_schema_attr_names function call

Signed-off-by: jamshale <[email protected]>

* Remove unnessecary self usage

Signed-off-by: jamshale <[email protected]>

---------

Signed-off-by: jamshale <[email protected]>
  • Loading branch information
jamshale authored and ff137 committed Feb 13, 2025
1 parent 8f74b76 commit 322b88f
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import json
import logging
from typing import Mapping, Optional, Tuple
from typing import List, Mapping, Optional, Tuple

from anoncreds import CredentialDefinition, Schema
from anoncreds import CredentialDefinition
from marshmallow import RAISE

from ......anoncreds.base import AnonCredsResolutionError
from ......anoncreds.base import AnonCredsObjectNotFound, AnonCredsResolutionError
from ......anoncreds.holder import AnonCredsHolder, AnonCredsHolderError
from ......anoncreds.issuer import CATEGORY_CRED_DEF, CATEGORY_SCHEMA, AnonCredsIssuer
from ......anoncreds.issuer import CATEGORY_CRED_DEF, AnonCredsIssuer
from ......anoncreds.models.credential import AnoncredsCredentialSchema
from ......anoncreds.models.credential_offer import AnoncredsCredentialOfferSchema
from ......anoncreds.models.credential_proposal import (
Expand Down Expand Up @@ -204,15 +204,54 @@ async def _create():
offer_json = await issuer.create_credential_offer(cred_def_id)
return json.loads(offer_json)

async with self.profile.session() as session:
cred_def_entry = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id)
cred_def_dict = CredentialDefinition.load(cred_def_entry.value).to_dict()
schema_entry = await session.handle.fetch(
CATEGORY_SCHEMA, cred_def_dict["schemaId"]
async def _get_attr_names(schema_id) -> List[str] | None:
"""Fetch attribute names for a given schema ID from the registry."""
if not schema_id:
return None
try:
schema_result = await registry.get_schema(self.profile, schema_id)
return schema_result.schema.attr_names
except AnonCredsObjectNotFound:
LOGGER.info(f"Schema not found for schema_id={schema_id}")
return None
except AnonCredsResolutionError as e:
LOGGER.warning(f"Schema resolution failed for schema_id={schema_id}: {e}")
return None

async def _fetch_schema_attr_names(
anoncreds_attachment, cred_def_id
) -> List[str] | None:
"""Determine schema attribute names from schema_id or cred_def_id."""
schema_id = anoncreds_attachment.get("schema_id")
attr_names = await _get_attr_names(schema_id)

if attr_names:
return attr_names

if cred_def_id:
async with self.profile.session() as session:
cred_def_entry = await session.handle.fetch(
CATEGORY_CRED_DEF, cred_def_id
)
cred_def_dict = CredentialDefinition.load(
cred_def_entry.value
).to_dict()
return await _get_attr_names(cred_def_dict.get("schemaId"))

return None

attr_names = None
registry = self.profile.inject(AnonCredsRegistry)

attr_names = await _fetch_schema_attr_names(anoncreds_attachment, cred_def_id)

if not attr_names:
raise V20CredFormatError(
"Could not determine schema attributes. If you did not create the "
"schema, then you need to provide the schema_id."
)
schema_dict = Schema.load(schema_entry.value).to_dict()

schema_attrs = set(schema_dict["attrNames"])
schema_attrs = set(attr_names)
preview_attrs = set(cred_proposal_message.credential_preview.attr_dict())
if preview_attrs != schema_attrs:
raise V20CredFormatError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@
from unittest import IsolatedAsyncioTestCase

import pytest
from anoncreds import CredentialDefinition
from marshmallow import ValidationError

from .......anoncreds.holder import AnonCredsHolder
from .......anoncreds.issuer import AnonCredsIssuer
from .......anoncreds.models.credential_definition import (
CredDef,
CredDefValue,
CredDefValuePrimary,
)
from .......anoncreds.registry import AnonCredsRegistry
from .......anoncreds.revocation import AnonCredsRevocationRegistryFullError
from .......cache.base import BaseCache
from .......cache.in_memory import InMemoryCache
from .......config.provider import ClassProvider
from .......indy.credx.issuer import CATEGORY_CRED_DEF
from .......ledger.base import BaseLedger
from .......ledger.multiple_ledger.ledger_requests_executor import (
IndyLedgerRequestsExecutor,
Expand Down Expand Up @@ -193,9 +202,38 @@

class TestV20AnonCredsCredFormatHandler(IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self.profile = await create_test_profile()
self.profile = await create_test_profile(
{
"wallet.type": "askar-anoncreds",
}
)
self.context = self.profile.context

# Context
self.cache = InMemoryCache()
self.profile.context.injector.bind_instance(BaseCache, self.cache)

# Issuer
self.issuer = mock.MagicMock(AnonCredsIssuer, autospec=True)
self.profile.context.injector.bind_instance(AnonCredsIssuer, self.issuer)

# Holder
self.holder = mock.MagicMock(AnonCredsHolder, autospec=True)
self.profile.context.injector.bind_instance(AnonCredsHolder, self.holder)

# Anoncreds registry
self.profile.context.injector.bind_instance(
AnonCredsRegistry, AnonCredsRegistry()
)
registry = self.profile.context.inject_or(AnonCredsRegistry)
legacy_indy_registry = ClassProvider(
"acapy_agent.anoncreds.default.legacy_indy.registry.LegacyIndyRegistry",
# supported_identifiers=[],
# method_name="",
).provide(self.profile.context.settings, self.profile.context.injector)
await legacy_indy_registry.setup(self.profile.context)
registry.register(legacy_indy_registry)

# Ledger
self.ledger = mock.MagicMock(BaseLedger, autospec=True)
self.ledger.get_schema = mock.CoroutineMock(return_value=SCHEMA)
Expand All @@ -214,18 +252,6 @@ async def asyncSetUp(self):
)
),
)
# Context
self.cache = InMemoryCache()
self.profile.context.injector.bind_instance(BaseCache, self.cache)

# Issuer
self.issuer = mock.MagicMock(AnonCredsIssuer, autospec=True)
self.profile.context.injector.bind_instance(AnonCredsIssuer, self.issuer)

# Holder
self.holder = mock.MagicMock(AnonCredsHolder, autospec=True)
self.profile.context.injector.bind_instance(AnonCredsHolder, self.holder)

self.handler = AnonCredsCredFormatHandler(self.profile)
assert self.handler.profile

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

@pytest.mark.skip(reason="Anoncreds-break")
async def test_create_offer(self):
schema_id_parts = SCHEMA_ID.split(":")

cred_preview = V20CredPreview(
attributes=(
V20CredAttrSpec(name="legalName", value="value"),
V20CredAttrSpec(name="jurisdictionId", value="value"),
V20CredAttrSpec(name="incorporationDate", value="value"),
)
)

cred_proposal = V20CredProposal(
credential_preview=cred_preview,
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][
V20CredFormat.Format.ANONCREDS.api
async def test_create_offer_cant_find_schema_in_wallet_or_data_registry(self):
with self.assertRaises(V20CredFormatError):
await self.handler.create_offer(
V20CredProposal(
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][
V20CredFormat.Format.ANONCREDS.api
],
)
],
filters_attach=[
AttachDecorator.data_base64(
{"cred_def_id": CRED_DEF_ID}, ident="0"
)
],
)
],
filters_attach=[
AttachDecorator.data_base64({"cred_def_id": CRED_DEF_ID}, ident="0")
],
)
)

cred_def_record = StorageRecord(
CRED_DEF_SENT_RECORD_TYPE,
CRED_DEF_ID,
{
"schema_id": SCHEMA_ID,
"schema_issuer_did": schema_id_parts[0],
"schema_name": schema_id_parts[-2],
"schema_version": schema_id_parts[-1],
"issuer_did": TEST_DID,
"cred_def_id": CRED_DEF_ID,
"epoch": str(int(time())),
},
@mock.patch.object(
AnonCredsRegistry,
"get_schema",
mock.CoroutineMock(
return_value=mock.MagicMock(schema=mock.MagicMock(attr_names=["score"]))
),
)
@mock.patch.object(
AnonCredsIssuer,
"create_credential_offer",
mock.CoroutineMock(return_value=json.dumps(ANONCREDS_OFFER)),
)
@mock.patch.object(
CredentialDefinition,
"load",
mock.MagicMock(to_dict=mock.MagicMock(return_value={"schemaId": SCHEMA_ID})),
)
async def test_create_offer(self):
self.issuer.create_credential_offer = mock.CoroutineMock({})
# With a schema_id
await self.handler.create_offer(
V20CredProposal(
credential_preview=V20CredPreview(
attributes=(V20CredAttrSpec(name="score", value="0"),)
),
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][
V20CredFormat.Format.ANONCREDS.api
],
)
],
filters_attach=[
AttachDecorator.data_base64(
{"cred_def_id": CRED_DEF_ID, "schema_id": SCHEMA_ID}, ident="0"
)
],
)
)
await self.session.storage.add_record(cred_def_record)

self.issuer.create_credential_offer = mock.CoroutineMock(
return_value=json.dumps(ANONCREDS_OFFER)
# Only with cred_def_id
async with self.profile.session() as session:
await session.handle.insert(
CATEGORY_CRED_DEF,
CRED_DEF_ID,
CredDef(
issuer_id=TEST_DID,
schema_id=SCHEMA_ID,
tag="tag",
type="CL",
value=CredDefValue(
primary=CredDefValuePrimary("n", "s", {}, "rctxt", "z")
),
).to_json(),
tags={},
)
await self.handler.create_offer(
V20CredProposal(
credential_preview=V20CredPreview(
attributes=(V20CredAttrSpec(name="score", value="0"),)
),
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][
V20CredFormat.Format.ANONCREDS.api
],
)
],
filters_attach=[
AttachDecorator.data_base64({"cred_def_id": CRED_DEF_ID}, ident="0")
],
)
)

(cred_format, attachment) = await self.handler.create_offer(cred_proposal)

self.issuer.create_credential_offer.assert_called_once_with(CRED_DEF_ID)

# assert identifier match
assert cred_format.attach_id == self.handler.format.api == attachment.ident

# assert content of attachment is proposal data
assert attachment.content == ANONCREDS_OFFER

# assert data is encoded as base64
assert attachment.data.base64

self.issuer.create_credential_offer.reset_mock()
await self.handler.create_offer(cred_proposal)
self.issuer.create_credential_offer.assert_not_called()
# Wrong attribute name
with self.assertRaises(V20CredFormatError):
await self.handler.create_offer(
V20CredProposal(
credential_preview=V20CredPreview(
attributes=(V20CredAttrSpec(name="wrong", value="0"),)
),
formats=[
V20CredFormat(
attach_id="0",
format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][
V20CredFormat.Format.ANONCREDS.api
],
)
],
filters_attach=[
AttachDecorator.data_base64(
{"cred_def_id": CRED_DEF_ID, "schema_id": SCHEMA_ID},
ident="0",
)
],
)
)

@pytest.mark.skip(reason="Anoncreds-break")
async def test_create_offer_no_cache(self):
Expand Down
Loading

0 comments on commit 322b88f

Please sign in to comment.