Skip to content

Commit

Permalink
Merge pull request openwallet-foundation#81 from sicpa-dlab/task/reso…
Browse files Browse the repository at this point in the history
…lver-metadata

resolver_metadata added
  • Loading branch information
dbluhm authored May 14, 2021
2 parents f84fd31 + 4a40e40 commit 43a0575
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 31 deletions.
8 changes: 5 additions & 3 deletions aries_cloudagent/connections/base_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import logging
from typing import List, Sequence, Tuple

from pydid import DIDDocument as ResolvedDocument
from pydid.doc.didcomm_service import DIDCommService
from pydid.doc.verification_method import VerificationMethod

Expand All @@ -19,7 +18,7 @@
from ..protocols.coordinate_mediation.v1_0.models.mediation_record import (
MediationRecord,
)
from ..resolver.base import ResolverError
from ..resolver.base import ResolverError, ResolutionResult
from ..resolver.did_resolver import DIDResolver
from ..storage.base import BaseStorage
from ..storage.error import StorageNotFoundError
Expand Down Expand Up @@ -223,7 +222,10 @@ async def resolve_invitation(self, did: str):

resolver = self._session.inject(DIDResolver)
try:
doc: ResolvedDocument = await resolver.resolve(self._session.profile, did)
resolution: ResolutionResult = await resolver.resolve(
self._session.profile, did
)
doc = resolution.did_doc
except ResolverError as error:
raise BaseConnectionManagerError(
"Failed to resolve public DID in invitation"
Expand Down
2 changes: 1 addition & 1 deletion aries_cloudagent/messaging/jsonld/tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
@pytest.fixture
def mock_resolver():
did_resolver = async_mock.MagicMock()
did_resolver.resolve = async_mock.CoroutineMock(return_value=did_doc)
did_resolver.resolve = async_mock.CoroutineMock(return_value=(did_doc, {}))
url = "did:example:1234abcd#4"
did_resolver.dereference = async_mock.CoroutineMock(
return_value=did_doc.dereference(url)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest.mock import call
from asynctest import mock as async_mock, TestCase as AsyncTestCase


from .....cache.base import BaseCache
from .....cache.in_memory import InMemoryCache
from .....config.base import InjectionError
Expand All @@ -15,6 +16,7 @@
from .....messaging.responder import BaseResponder, MockResponder
from .....protocols.routing.v1_0.manager import RoutingManager
from .....resolver.did_resolver import DIDResolver
from .....resolver.base import ResolutionResult
from .....resolver.did_resolver_registry import DIDResolverRegistry
from .....storage.error import StorageNotFoundError
from .....transport.inbound.receipt import MessageReceipt
Expand Down Expand Up @@ -2039,7 +2041,7 @@ async def test_fetch_connection_targets_conn_invitation_did_resolver(self):
self.resolver.get_endpoint_for_did = async_mock.CoroutineMock(
return_value=self.test_endpoint
)
self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc)
self.resolver.resolve = async_mock.CoroutineMock(return_value=ResolutionResult(did_doc, {}))
self.context.injector.bind_instance(DIDResolver, self.resolver)

local_did = await self.session.wallet.create_local_did(
Expand Down Expand Up @@ -2106,7 +2108,7 @@ async def test_fetch_connection_targets_conn_invitation_btcr_resolver(self):
self.resolver.get_endpoint_for_did = async_mock.CoroutineMock(
return_value=self.test_endpoint
)
self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc)
self.resolver.resolve = async_mock.CoroutineMock(return_value=ResolutionResult(did_doc, {}))
self.context.injector.bind_instance(DIDResolver, self.resolver)

local_did = await self.session.wallet.create_local_did(
Expand Down Expand Up @@ -2169,7 +2171,7 @@ async def test_fetch_connection_targets_conn_invitation_btcr_without_services(se
self.resolver.get_endpoint_for_did = async_mock.CoroutineMock(
return_value=self.test_endpoint
)
self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc)
self.resolver.resolve = async_mock.CoroutineMock(return_value=ResolutionResult(did_doc, {}))
self.context.injector.bind_instance(DIDResolver, self.resolver)

local_did = await self.session.wallet.create_local_did(
Expand Down Expand Up @@ -2211,7 +2213,7 @@ async def test_fetch_connection_targets_conn_invitation_no_didcomm_services(self
self.resolver.get_endpoint_for_did = async_mock.CoroutineMock(
return_value=self.test_endpoint
)
self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc)
self.resolver.resolve = async_mock.CoroutineMock(return_value=ResolutionResult(did_doc, {}))
self.context.injector.bind_instance(DIDResolver, self.resolver)

local_did = await self.session.wallet.create_local_did(
Expand Down Expand Up @@ -2252,7 +2254,7 @@ async def test_fetch_connection_targets_conn_invitation_unsupported_key_type(sel
self.resolver.get_endpoint_for_did = async_mock.CoroutineMock(
return_value=self.test_endpoint
)
self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc)
self.resolver.resolve = async_mock.CoroutineMock(return_value=ResolutionResult(did_doc, {}))
self.context.injector.bind_instance(DIDResolver, self.resolver)

local_did = await self.session.wallet.create_local_did(
Expand Down Expand Up @@ -2313,7 +2315,7 @@ async def test_fetch_connection_targets_oob_invitation_svc_did_resolver(self):
did_doc = builder.build()

self.resolver = async_mock.MagicMock()
self.resolver.resolve = async_mock.CoroutineMock(return_value=did_doc)
self.resolver.resolve = async_mock.CoroutineMock(return_value=ResolutionResult(did_doc, {}))
self.context.injector.bind_instance(DIDResolver, self.resolver)

local_did = await self.session.wallet.create_local_did(
Expand Down
63 changes: 59 additions & 4 deletions aries_cloudagent/resolver/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from abc import ABC, abstractmethod
from enum import Enum
from typing import Sequence, Union
from datetime import datetime
import re

from pydid import DID, DIDDocument
Expand All @@ -13,11 +14,15 @@
vm_allow_missing_controller,
vm_allow_type_list,
)

from collections import namedtuple
from ..config.injection_context import InjectionContext
from ..core.error import BaseError
from ..core.profile import Profile

ResolverMetadata = namedtuple(
"resolver_metadata", ["type", "driverId", "resolver", "retrieved", "duration"]
)


class ResolverError(BaseError):
"""Base class for resolver exceptions."""
Expand All @@ -38,6 +43,20 @@ class ResolverType(Enum):
NON_NATIVE = "non-native"


class ResolutionResult:
"""Resolution Class to pack the DID Doc and the resolution information."""

def __init__(self, did_doc: DIDDocument, metadata: ResolverMetadata = None):
"""Initialize Resolution.
Args:
did_doc: DID Document resolved
resolver_metadata: Resolving details
"""
self.did_doc = did_doc
self.metadata = metadata


class BaseDIDResolver(ABC):
"""Base Class for DID Resolvers."""

Expand Down Expand Up @@ -75,16 +94,35 @@ async def supports(self, profile: Profile, did: Union[str, DID]) -> bool:

return False

async def resolve(self, profile: Profile, did: Union[str, DID]) -> DIDDocument:
async def resolve(
self, profile: Profile, did: Union[str, DID], retrieve_metadata: bool = False
) -> ResolutionResult:
"""Resolve a DID using this resolver."""

async def resolve_with_metadata(py_did):
resolution_start_time = datetime.utcnow()

did_document = await self._resolve(profile, str(py_did))

resolver_metadata = await self._retrieve_resolver_metadata(
py_did.method, resolution_start_time
)

return did_document, resolver_metadata

py_did = DID(did) if isinstance(did, str) else did

if not await self.supports(profile, py_did.method):
raise DIDMethodNotSupported(
f"{self.__class__.__name__} does not support DID method {py_did.method}"
)
if retrieve_metadata:
did_document, resolver_metadata = await resolve_with_metadata(py_did)

else:
did_document = await self._resolve(profile, str(py_did))
resolver_metadata = None

did_document = await self._resolve(profile, str(py_did))
result = DIDDocument.deserialize(
did_document,
options={
Expand All @@ -95,8 +133,25 @@ async def resolve(self, profile: Profile, did: Union[str, DID]) -> DIDDocument:
vm_allow_type_list,
},
)
return result
return ResolutionResult(result, resolver_metadata)

@abstractmethod
async def _resolve(self, profile: Profile, did: str) -> dict:
"""Resolve a DID using this resolver."""

async def _retrieve_resolver_metadata(self, method, resolution_start_time):

time_now = datetime.utcnow()
duration = int((time_now - resolution_start_time).total_seconds() * 1000)
retrieved_time = time_now.strftime("%Y-%m-%dT%H:%M:%SZ")

internal_class = self.__class__
module = internal_class.__module__
class_name = internal_class.__qualname__
resolver = module + "." + class_name

resolver_metadata = ResolverMetadata(
self.type.value, f"did:{method}", resolver, retrieved_time, duration
)

return resolver_metadata
9 changes: 7 additions & 2 deletions aries_cloudagent/resolver/default/indy.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ async def _resolve(self, profile: Profile, did: str) -> dict:
except LedgerError as err:
raise DIDNotFound(f"DID {did} could not be resolved") from err

did_doc = self._did_doc_builder(did, endpoint, recipient_key)

return did_doc.serialize()

def _did_doc_builder(self, did, endpoint, recipient_key):
"""Build DID Document."""
builder = DIDDocumentBuilder(DID(did))

vmethod = builder.verification_methods.add(
Expand All @@ -70,5 +76,4 @@ async def _resolve(self, profile: Profile, did: str) -> dict:
recipient_keys=[vmethod],
routing_keys=[],
)
result = builder.build()
return result.serialize()
return builder.build()
23 changes: 18 additions & 5 deletions aries_cloudagent/resolver/did_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from itertools import chain
from typing import Sequence, Union

from pydid import DID, DIDDocument, DIDError, DIDUrl, Service, VerificationMethod
from pydid import DID, DIDError, DIDUrl, Service, VerificationMethod

from ..core.profile import Profile

Expand All @@ -18,6 +18,7 @@
DIDMethodNotSupported,
DIDNotFound,
ResolverError,
ResolutionResult,
)
from .did_resolver_registry import DIDResolverRegistry

Expand All @@ -31,14 +32,26 @@ def __init__(self, registry: DIDResolverRegistry):
"""Initialize a `didresolver` instance."""
self.did_resolver_registry = registry

async def resolve(self, profile: Profile, did: Union[str, DID]) -> DIDDocument:
async def resolve(
self, profile: Profile, did: Union[str, DID], retrieve_metadata: bool = False
) -> ResolutionResult:
"""Retrieve did doc from public registry."""
# TODO Cache results
py_did: DID = DID(did) if isinstance(did, str) else did
for resolver in await self._match_did_to_resolver(profile, py_did):
try:
LOGGER.debug("Resolving DID %s with %s", did, resolver)
return await resolver.resolve(profile, py_did)
resolution: ResolutionResult = await resolver.resolve(
profile,
py_did,
)
if resolution.metadata:
LOGGER.debug(
"Resolution metadata for did %s: %s",
did,
resolution.metadata._asdict(),
)
return resolution
except DIDNotFound:
LOGGER.debug("DID %s not found by resolver %s", did, resolver)

Expand Down Expand Up @@ -74,8 +87,8 @@ async def dereference(
# TODO Use cached DID Docs when possible
try:
did_url = DIDUrl.parse(did_url)
doc = await self.resolve(profile, did_url.did)
return doc.dereference(did_url)
resolution: ResolutionResult = await self.resolve(profile, did_url.did)
return resolution.did_doc.dereference(did_url)
except DIDError as err:
raise ResolverError(
"Failed to parse DID URL from {}".format(did_url)
Expand Down
29 changes: 25 additions & 4 deletions aries_cloudagent/resolver/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@
}
}
"""
from distutils import util

from aiohttp import web
from aiohttp_apispec import docs, match_info_schema, response_schema
from aiohttp_apispec import docs, match_info_schema, response_schema, querystring_schema
from marshmallow import fields, validate

from ..admin.request_context import AdminRequestContext
from ..messaging.models.openapi import OpenAPISchema
from .base import DIDMethodNotSupported, DIDNotFound, ResolverError
from .base import DIDMethodNotSupported, DIDNotFound, ResolverError, ResolutionResult
from pydid.common import DID_PATTERN
from .did_resolver import DIDResolver

Expand Down Expand Up @@ -92,8 +93,17 @@ class DIDMatchInfoSchema(OpenAPISchema):
)


class VerboseSchema(OpenAPISchema):
"""Request schema for signing a jsonld doc."""

verbose = fields.Boolean(
required=False, description="Verbose to show the resolver metadata"
)


@docs(tags=["resolver"], summary="Retrieve doc for requested did")
@match_info_schema(DIDMatchInfoSchema())
@querystring_schema(VerboseSchema)
@response_schema(DIDDocSchema(), 200)
async def resolve_did(request: web.BaseRequest):
"""Retrieve a did document."""
Expand All @@ -103,8 +113,19 @@ async def resolve_did(request: web.BaseRequest):
try:
session = await context.session()
resolver = session.inject(DIDResolver)
document = await resolver.resolve(context.profile, did)
result = document.serialize()
retrieve_metadata = False
query = request.rel_url.query
if query and util.strtobool(query["verbose"]):
retrieve_metadata = True
resolution: ResolutionResult = await resolver.resolve(
context.profile, did, retrieve_metadata
)
doc = resolution.did_doc.serialize()
result = {"did_doc": doc}

if resolution.metadata:
result["resolver_metadata"] = resolution.metadata._asdict()

except DIDNotFound as err:
raise web.HTTPNotFound(reason=err.roll_up) from err
except DIDMethodNotSupported as err:
Expand Down
9 changes: 5 additions & 4 deletions aries_cloudagent/resolver/tests/test_did_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
DIDNotFound,
ResolverError,
ResolverType,
ResolutionResult,
)
from ..did_resolver import DIDResolver
from ..did_resolver_registry import DIDResolverRegistry
Expand Down Expand Up @@ -162,15 +163,15 @@ async def test_dereference_x(resolver, profile):
@pytest.mark.asyncio
@pytest.mark.parametrize("did", TEST_DIDS)
async def test_resolve(resolver, profile, did):
did_doc = await resolver.resolve(profile, did)
assert isinstance(did_doc, DIDDocument)
resolution: ResolutionResult = await resolver.resolve(profile, did)
assert isinstance(resolution.did_doc, DIDDocument)


@pytest.mark.asyncio
@pytest.mark.parametrize("did", TEST_DIDS)
async def test_resolve_did(resolver, profile, did):
did_doc = await resolver.resolve(profile, DID(did))
assert isinstance(did_doc, DIDDocument)
resolution: ResolutionResult = await resolver.resolve(profile, DID(did))
assert isinstance(resolution.did_doc, DIDDocument)


@pytest.mark.asyncio
Expand Down
Loading

0 comments on commit 43a0575

Please sign in to comment.