Skip to content

Commit

Permalink
Merge pull request #799 from bcgov/feat/w3c
Browse files Browse the repository at this point in the history
feat: add support w3c format cardinality
  • Loading branch information
esune authored Nov 25, 2024
2 parents 446ae0c + 45f08cf commit 9940a76
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from agent_webhooks.schemas import (
CredentialMappingDefSchema,
MappingDefSchema,
PathBaseSchema,
TopicDefSchema,
)

Expand All @@ -15,6 +16,7 @@ class CredentialTypeDefSchema(Schema):
origin_did = fields.String(required=True)
topic = fields.Nested(TopicDefSchema, required=True)
mappings = fields.List(fields.Nested(MappingDefSchema))
cardinality = fields.List(fields.Nested(PathBaseSchema))
credential = fields.Dict(
keys=fields.Enum(MappingTypeEnum, by_value=True),
values=fields.Nested(CredentialMappingDefSchema),
Expand Down
3 changes: 3 additions & 0 deletions server/vcr-server/agent_webhooks/tests/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
effective_date_mapping_def_spec,
expiry_date_mapping_def_spec,
],
"cardinality": [
{"path": "$.credentialSubject.issuedTo.id"},
],
}

credential_def_spec = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ def test_credential_type_registration(self, mock_credential_type_save):
saved_schema = test_schemas[0]

assert saved_credential_type.processor_config == {
"cardinality_fields": credential_type_def_spec.get("cardinality_fields"),
"cardinality": credential_type_def_spec.get("cardinality"),
"credential": credential_type_def_spec.get("credential"),
"mappings": credential_type_def_spec.get("mappings"),
"topic": credential_type_def_spec.get("topic"),
}
assert "cardinality_fields" not in saved_credential_type.processor_config
assert "mapping" not in saved_credential_type.processor_config
assert saved_credential_type.issuer_id == test_issuer.id
assert saved_credential_type.schema_id == saved_schema.id
8 changes: 6 additions & 2 deletions server/vcr-server/agent_webhooks/tests/test_issuer_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class TestIssuerManager(TestCase):
"topic": [{"source_id": {"input": "topic_id", "from": "claim"}}],
"endpoint": "endpoint",
"cardinality_fields": ["field"],
"mappings": {},
"mapping": {},
"logo_b64": "logo base64",
}
],
Expand Down Expand Up @@ -94,9 +94,11 @@ def test_legacy_issuer_registration(

assert credential_type.description == "cred type name"
assert credential_type.processor_config == {
"cardinality": None,
"cardinality_fields": ["field"],
"credential": {"effective_date": {"input": "eff_date", "from": "claim"}},
"mappings": {},
"mapping": {},
"mappings": None,
"topic": [{"source_id": {"input": "topic_id", "from": "claim"}}],
}
assert schema.name == "schema"
Expand Down Expand Up @@ -127,11 +129,13 @@ def test_issuer_registration(self, mock_schema_save, mock_credential_type_save):

assert credential_type.description == "cred type name"
assert credential_type.processor_config == {
"cardinality": credential_type_def_spec.get("cardinality"),
"cardinality_fields": ["field"],
"credential": credential_type_def_spec.get("credential"),
"mappings": credential_type_def_spec.get("mappings"),
"topic": topic_def_spec,
}
assert "mapping" not in credential_type.processor_config
assert schema.name == credential_type_def_spec.get("schema")
assert schema.version == credential_type_def_spec.get("version")
assert schema.origin_did == issuer_def_spec.get("did")
Expand Down
14 changes: 12 additions & 2 deletions server/vcr-server/agent_webhooks/utils/credential_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,21 @@ def _build_processor_config(
self, credential_type_def: CredentialTypeDefSchema
) -> dict:
processor_config = {
"cardinality_fields": credential_type_def.get("cardinality_fields"),
"cardinality_fields": credential_type_def.get("cardinality_fields"), # DEPRECATED
"cardinality": credential_type_def.get("cardinality"),
"credential": credential_type_def.get("credential"),
"mapping": credential_type_def.get('mapping'),
"mapping": credential_type_def.get('mapping'), # DEPRECATED
"mappings": credential_type_def.get("mappings"),
"topic": credential_type_def.get("topic"),
}

# Remove deprecated fields for vc_di format
# Eventually older formats will be catch up and this can be removed
if credential_type_def.get("format") == "vc_di":
if not processor_config.get("cardinality_fields"):
processor_config.pop("cardinality_fields", None)
if not processor_config.get("mapping"):
processor_config.pop("mapping", None)


return processor_config
75 changes: 57 additions & 18 deletions server/vcr-server/agent_webhooks/utils/vc_di_credential.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import base64
from datetime import datetime
import hashlib
import logging
import re

Expand Down Expand Up @@ -45,20 +47,31 @@ def update_credential(self, credential_def: CredentialDefSchema) -> Credential:

# Resolve the topic for the credential
topic = self._resolve_credential_topic(credential_def, processor_config)

# Resolve the cardinality for the credential
cardinality = self._process_credential_cardinality(
credential_def, processor_config
)

# Resolve the credential properties
credential_properties = self._process_credential_properties(
credential_def, processor_config
)
credential_properties["credential_type"] = credential_type

# Need this in a separate method for testing
self._set_additional_properties(credential_def, credential_properties)

# Set our own credential properties
credential_properties["credential_type"] = credential_type
credential_properties["cardinality_hash"] = (
cardinality["hash"] if cardinality else None
)

# Update the database with the credential data
credential = topic.credentials.create(**credential_properties)

# Update the credential set
self._resolve_credential_set(credential)
self._resolve_credential_set(credential, cardinality)

credential_attributes = self._process_credential_attributes(
credential_def, processor_config
Expand All @@ -69,8 +82,8 @@ def update_credential(self, credential_def: CredentialDefSchema) -> Credential:
return credential

def _set_additional_properties(self, credential_def, credential_properties):
credential_properties['format'] = credential_def.get('format')
credential_properties['raw_data'] = credential_def.get('raw_data')
credential_properties["format"] = credential_def.get("format")
credential_properties["raw_data"] = credential_def.get("raw_data")

def _resolve_credential_type(
self, credential_def: CredentialDefSchema
Expand Down Expand Up @@ -223,7 +236,7 @@ def _resolve_credential_set(
return credential_set

def _process_credential_properties(
self, credential_def: CredentialDefSchema, processor_config: dict
self, credential_def: CredentialDefSchema, processor_config: any
) -> dict:
"""
Generate a dictionary of additional credential properties from the processor config
Expand Down Expand Up @@ -258,6 +271,32 @@ def _process_credential_properties(

return {}

def _process_credential_cardinality(
self, credential_def: CredentialDefSchema, processor_config: any
) -> any:
"""Extract the credential cardinality values and hash"""

cardinality = processor_config.get("cardinality") or []
values = {}
if cardinality and len(cardinality):
for attribute in cardinality:
path = attribute.get("path")
try:
values[path] = self._process_mapping(attribute, credential_def)
except AttributeError:
raise CredentialException(
"Processor config specifies field '{}' in cardinality, "
+ "but value does not exist in credential.".format(path)
)
if values:
hash_paths = ["{}::{}".format(k, values[k]) for k in values]
hash = base64.b64encode(
hashlib.sha256(",".join(hash_paths).encode("utf-8")).digest()
)
return {"values": values, "hash": hash}

return None

def _process_credential_attributes(
self, credential_def: CredentialDefSchema, processor_config: dict
) -> list[dict]:
Expand All @@ -277,6 +316,19 @@ def _process_credential_attributes(

return attributes

def _process_mapping(self, rule: dict, credential_def: CredentialDefSchema) -> any:
"""Takes our mapping rules and returns a value from the credential data."""

if not (raw_data := credential_def and credential_def.get("raw_data")):
return None

if path := rule and rule.get("path"):
json_path = parse(path)
matches = [match.value for match in json_path.find(raw_data)]
return matches[0] if matches and len(matches) else None

return None

def _process_config_date(
self, config: dict, credential_def: CredentialDefSchema, field: str
) -> datetime:
Expand Down Expand Up @@ -313,16 +365,3 @@ def _process_config_date(
parsed_date = parsed_date.astimezone(timezone.utc)

return parsed_date

def _process_mapping(self, rule: dict, credential_def: CredentialDefSchema) -> any:
"""Takes our mapping rules and returns a value from the credential data."""

if not (raw_data := credential_def and credential_def.get("raw_data")):
return None

if path := rule and rule.get("path"):
json_path = parse(path)
matches = [match.value for match in json_path.find(raw_data)]
return matches[0] if matches and len(matches) else None

return None

0 comments on commit 9940a76

Please sign in to comment.