Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pluggable DID Resolver Interface (cleaned) #1070

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
6e3ee60
feat: add base resolver classes
dbluhm Jan 20, 2021
4fd1c13
feat: add DIDUrl class
dbluhm Jan 21, 2021
fd8c086
feat: did doc dereferencing by url using index
dbluhm Jan 21, 2021
2eca9c1
rough draft resolver + formatting.
burdettadam Jan 26, 2021
0322381
feat: refine base did resolver interface
dbluhm Jan 26, 2021
75808fa
feat: add indy DID resolver
dbluhm Jan 28, 2021
eb3bcc9
did_resolver with supported changes.
burdettadam Feb 1, 2021
9422364
test: indy did resolver and fixes
dbluhm Feb 1, 2021
d5b0ca7
style: reformat with black
dbluhm Feb 1, 2021
b44e7dd
requested changes.
burdettadam Feb 1, 2021
bd9dfb3
some more passing tests.
burdettadam Feb 2, 2021
a05b38c
feat: indy did resolver correctly raises DID not found error
dbluhm Feb 2, 2021
8f631ba
improvements
burdettadam Feb 3, 2021
817d5a5
test: more did resolver tests and fixes
dbluhm Feb 3, 2021
be2ac21
test error paths
burdettadam Feb 4, 2021
9c56f3c
place holder for admin api
burdettadam Feb 4, 2021
b6acf29
chore: correct typos, more appropriately name attrs
dbluhm Feb 4, 2021
a4e4398
feat: drop fully_dereference method
dbluhm Feb 4, 2021
e583254
test: fix resolve parameters to match base class
dbluhm Feb 4, 2021
74b451a
test: dereference external did url
dbluhm Feb 4, 2021
5f7dbc9
chore: rename exceptions to better match DID vs Did convention
dbluhm Feb 4, 2021
46ecea9
chore: swap session for profile
dbluhm Feb 4, 2021
10cc62c
chore: rename dereference_external -> dereference
dbluhm Feb 4, 2021
9ee2030
one small bug.
burdettadam Feb 5, 2021
1f33fff
requested changes & patch bug.
burdettadam Feb 9, 2021
d0a7553
passing tests for resolver admin api.
burdettadam Feb 10, 2021
7490f89
re-use regex, better module description
burdettadam Feb 11, 2021
902d3f7
Correct typo in resolver routes
burdettadam Feb 12, 2021
969f38e
improve error handling in routes from resolver errors
burdettadam Feb 18, 2021
f4a3bdd
feat: initial HTTP Universal Resolver bindings
dbluhm Feb 10, 2021
5c671c4
refactor: base resolver wraps _resolve with checks
dbluhm Feb 23, 2021
c8a36fa
feat: http universal did resolver handle errors
dbluhm Feb 23, 2021
26eb0a4
fix: better errors in rotues, fixes found from testing
dbluhm Feb 23, 2021
50600af
fix: tests after merge and other fixes
dbluhm Feb 25, 2021
32424fe
passing tests.
burdettadam Feb 25, 2021
d67c7c8
fix: indy resolver registration
dbluhm Feb 26, 2021
b910996
fix: do not load indy when module not available
dbluhm Feb 26, 2021
7a5ab04
fix: drop http uni resolver as built in resolver
dbluhm Feb 26, 2021
dba442e
fix: doc tests
dbluhm Apr 7, 2021
99eac69
fix: did regex to include digits
dbluhm Mar 3, 2021
f685d16
fix: use resolver.did DID_PATTERN everywhere
dbluhm Mar 3, 2021
eb11791
remove unused import
Mar 23, 2021
b5d0f51
feat: use pydid in resolver package
dbluhm Mar 23, 2021
97e98dd
style: reformat with black
burdettadam Mar 24, 2021
ab6796e
chore: update pydid
burdettadam Mar 29, 2021
21041f9
simplify pluggin _resolve
Apr 6, 2021
f6bf5ca
test fixes
Apr 6, 2021
ad9f01e
fix: resolver test routes
dbluhm Apr 7, 2021
e1575fd
style: reformat with black
dbluhm Apr 7, 2021
21c28b9
Merge branch 'main' into feature/did-resolver-cleaned
dbluhm Apr 7, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion aries_cloudagent/config/default_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from ..core.plugin_registry import PluginRegistry
from ..core.profile import ProfileManager, ProfileManagerProvider
from ..core.protocol_registry import ProtocolRegistry
from ..resolver.did_resolver import DIDResolver
from ..resolver.did_resolver_registry import DIDResolverRegistry
from ..tails.base import BaseTailsServer
from ..ledger.indy import IndySdkLedgerPool, IndySdkLedgerPoolProvider

Expand All @@ -17,7 +19,6 @@
from ..protocols.didcomm_prefix import DIDCommPrefix
from ..protocols.introduction.v0_1.base_service import BaseIntroductionService
from ..protocols.introduction.v0_1.demo_service import DemoIntroductionService

from ..transport.wire_format import BaseWireFormat
from ..utils.stats import Collector

Expand All @@ -41,6 +42,13 @@ async def build_context(self) -> InjectionContext:
# Global protocol registry
context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry())

# Global did resolver registry
did_resolver_registry = DIDResolverRegistry()
context.injector.bind_instance(DIDResolverRegistry, did_resolver_registry)

# Global did resolver
context.injector.bind_instance(DIDResolver, DIDResolver(did_resolver_registry))

await self.bind_providers(context)
await self.load_plugins(context)

Expand Down Expand Up @@ -103,7 +111,9 @@ async def load_plugins(self, context: InjectionContext):
"aries_cloudagent.messaging.credential_definitions"
)
plugin_registry.register_plugin("aries_cloudagent.messaging.schemas")
# plugin_registry.register_plugin("aries_cloudagent.messaging.jsonld")
plugin_registry.register_plugin("aries_cloudagent.revocation")
plugin_registry.register_plugin("aries_cloudagent.resolver")
plugin_registry.register_plugin("aries_cloudagent.wallet")

if context.settings.get("multitenant.admin_enabled"):
Expand Down
27 changes: 27 additions & 0 deletions aries_cloudagent/resolver/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Interfaces and base classes for DID Resolution."""

import logging

from ..config.injection_context import InjectionContext
from ..config.provider import ClassProvider
from .did_resolver_registry import DIDResolverRegistry

LOGGER = logging.getLogger(__name__)


async def setup(context: InjectionContext):
"""Set up default resolvers."""
registry = context.inject(DIDResolverRegistry, required=False)
if not registry:
LOGGER.warning("No DID Resolver Registry instance found in context")
return

if context.settings.get("ledger.disabled"):
LOGGER.warning("Ledger is not configured, not loading IndyDIDResolver")
return

resolver = ClassProvider(
"aries_cloudagent.resolver.default.indy.IndyDIDResolver"
).provide(context.settings, context.injector)
await resolver.setup(context)
registry.register(resolver)
79 changes: 79 additions & 0 deletions aries_cloudagent/resolver/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Base Class for DID Resolvers."""

from abc import ABC, abstractmethod
from enum import Enum
from typing import Sequence, Union

from pydid import DID, DIDDocument

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


class ResolverError(BaseError):
"""Base class for resolver exceptions."""


class DIDNotFound(ResolverError):
"""Raised when DID is not found in verifiable data registry."""


class DIDMethodNotSupported(ResolverError):
"""Raised when no resolver is registered for a given did method."""


class ResolverType(Enum):
"""Resolver Type declarations."""

NATIVE = "native"
NON_NATIVE = "non-native"


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

def __init__(self, type_: ResolverType = None):
"""Initialize BaseDIDResolver.

Args:
type_ (Type): Type of resolver, native or non-native
"""
self.type = type_ or ResolverType.NON_NATIVE

@abstractmethod
async def setup(self, context: InjectionContext):
"""Do asynchronous resolver setup."""

@property
def native(self):
"""Return if this resolver is native."""
return self.type == ResolverType.NATIVE

@property
@abstractmethod
def supported_methods(self) -> Sequence[str]:
"""Return list of DID methods supported by this resolver."""

def supports(self, method: str) -> bool:
"""Return if this resolver supports the given method."""
return method in self.supported_methods

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

if not self.supports(did.method):
raise DIDMethodNotSupported(
f"{did.method} is not supported by {self.__class__.__name__} resolver."
)

did = str(did)
did_document = await self._resolve(profile, did)
result = DIDDocument.deserialize(did_document)
return result

@abstractmethod
async def _resolve(self, profile: Profile, did: DID) -> dict:
"""Resolve a DID using this resolver."""
1 change: 1 addition & 0 deletions aries_cloudagent/resolver/default/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Resolvers included in ACA-Py by Default."""
70 changes: 70 additions & 0 deletions aries_cloudagent/resolver/default/indy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Indy DID Resolver.

Resolution is performed using the IndyLedger class.
"""
from typing import Sequence

from pydid import DID, DIDDocumentBuilder, VerificationSuite

from ...config.injection_context import InjectionContext
from ...core.profile import Profile
from ...ledger.indy import IndySdkLedger
from ...ledger.base import BaseLedger
from ...ledger.error import LedgerError
from ..base import BaseDIDResolver, DIDNotFound, ResolverError, ResolverType


class NoIndyLedger(ResolverError):
"""Raised when there is no indy ledger instance configured."""


class IndyDIDResolver(BaseDIDResolver):
"""Indy DID Resolver."""

VERIFICATION_METHOD_TYPE = "Ed25519VerificationKey2018"
AGENT_SERVICE_TYPE = "did-communication"
SUITE = VerificationSuite(VERIFICATION_METHOD_TYPE, "publicKeyBase58")

def __init__(self):
"""Initialize Indy Resolver."""
super().__init__(ResolverType.NATIVE)

async def setup(self, context: InjectionContext):
"""Perform required setup for Indy DID resolution."""

@property
def supported_methods(self) -> Sequence[str]:
"""Return supported methods of Indy DID Resolver."""
return ["sov"]

async def _resolve(self, profile: Profile, did: str) -> dict:
"""Resolve an indy DID."""
did = DID(did)
ledger = profile.inject(BaseLedger, required=False)
if not ledger or not isinstance(ledger, IndySdkLedger):
raise NoIndyLedger("No Indy ledger instance is configured.")

try:
async with ledger:
recipient_key = await ledger.get_key_for_did(did)
endpoint = await ledger.get_endpoint_for_did(did)
except LedgerError as err:
raise DIDNotFound(f"DID {did} could not be resolved") from err

builder = DIDDocumentBuilder(did)

vmethod = builder.verification_methods.add(
ident="keys-1", suite=self.SUITE, material=recipient_key
)
builder.authentication.reference(vmethod.id)
if endpoint:
# TODO add priority
builder.services.add_didcomm(
ident=self.AGENT_SERVICE_TYPE,
type_=self.AGENT_SERVICE_TYPE,
endpoint=endpoint,
recipient_keys=[vmethod],
routing_keys=[],
)
result = builder.build()
return result.serialize()
Empty file.
70 changes: 70 additions & 0 deletions aries_cloudagent/resolver/default/tests/test_indy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Test IndyDIDResolver."""

import pytest
from asynctest import mock as async_mock

from ....core.in_memory import InMemoryProfile
from ....core.profile import Profile
from ....ledger.base import BaseLedger
from ....ledger.error import LedgerError
from ...base import DIDNotFound, ResolverError
from .. import indy as test_module
from ..indy import IndyDIDResolver

# pylint: disable=W0621
TEST_DID0 = "did:sov:123"


@pytest.fixture
def resolver():
"""Resolver fixture."""
yield IndyDIDResolver()


@pytest.fixture
def ledger():
"""Ledger fixture."""
ledger = async_mock.MagicMock(spec=test_module.IndySdkLedger)
ledger.get_endpoint_for_did = async_mock.CoroutineMock(
return_value="https://github.com/"
)
ledger.get_key_for_did = async_mock.CoroutineMock(return_value="key")
yield ledger


@pytest.fixture
def profile(ledger):
"""Profile fixture."""
profile = InMemoryProfile.test_profile()
profile.context.injector.bind_instance(BaseLedger, ledger)
yield profile


def test_supported_methods(resolver: IndyDIDResolver):
"""Test the supported_methods."""
assert resolver.supported_methods == ["sov"]
assert resolver.supports("sov")


@pytest.mark.asyncio
async def test_resolve(resolver: IndyDIDResolver, profile: Profile):
"""Test resolve method."""
assert await resolver.resolve(profile, TEST_DID0)


@pytest.mark.asyncio
async def test_resolve_x_no_ledger(resolver: IndyDIDResolver, profile: Profile):
"""Test resolve method with no ledger."""
profile.context.injector.clear_binding(BaseLedger)
with pytest.raises(ResolverError):
await resolver.resolve(profile, TEST_DID0)


@pytest.mark.asyncio
async def test_resolve_x_did_not_found(
resolver: IndyDIDResolver, ledger: BaseLedger, profile: Profile
):
"""Test resolve method when no did is found."""
ledger.get_key_for_did.side_effect = LedgerError
with pytest.raises(DIDNotFound):
await resolver.resolve(profile, TEST_DID0)
70 changes: 70 additions & 0 deletions aries_cloudagent/resolver/did_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
the did resolver.

responsible for keeping track of all resolvers. more importantly
retrieving did's from different sources provided by the method type.
"""

import logging
from itertools import chain
from typing import Union

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

from ..core.profile import Profile
from ..resolver.base import BaseDIDResolver, DIDMethodNotSupported, DIDNotFound
from .did_resolver_registry import DIDResolverRegistry

LOGGER = logging.getLogger(__name__)


class DIDResolver:
"""did resolver singleton."""

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:
"""Retrieve did doc from public registry."""
# TODO Cache results
if isinstance(did, str):
did = DID(did)
for resolver in self._match_did_to_resolver(did):
try:
LOGGER.debug("Resolving DID %s with %s", did, resolver)
return await resolver.resolve(profile, did)
except DIDNotFound:
LOGGER.debug("DID %s not found by resolver %s", did, resolver)

raise DIDNotFound(f"DID {did} could not be resolved.")

def _match_did_to_resolver(self, did: DID) -> BaseDIDResolver:
"""Generate supported DID Resolvers.

Native resolvers are yielded first, in registered order followed by
non-native resolvers in registered order.
"""
valid_resolvers = list(
filter(
lambda resolver: resolver.supports(did.method),
self.did_resolver_registry.resolvers,
)
)
native_resolvers = filter(lambda resolver: resolver.native, valid_resolvers)
non_native_resolvers = filter(
lambda resolver: not resolver.native, valid_resolvers
)
resolvers = list(chain(native_resolvers, non_native_resolvers))
if not resolvers:
raise DIDMethodNotSupported(f"DID method '{did.method}' not supported")
return resolvers

async def dereference(
self, profile: Profile, did_url: str
) -> Union[Service, VerificationMethod]:
"""Dereference a DID URL to its corresponding DID Doc object."""
# TODO Use cached DID Docs when possible
did_url = DIDUrl.parse(did_url)
doc = await self.resolve(profile, did_url.did)
return doc.dereference(did_url)
26 changes: 26 additions & 0 deletions aries_cloudagent/resolver/did_resolver_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""In memmory storage for registering did resolvers."""

import logging
from typing import Sequence

LOGGER = logging.getLogger(__name__)


class DIDResolverRegistry:
"""Registry for did resolvers."""

def __init__(self):
"""Initialize list for did resolvers."""
self._resolvers = []

@property
def resolvers(
self,
) -> Sequence[str]:
"""Accessor for a list of all did resolvers."""
return self._resolvers

def register(self, resolver) -> None:
"""Register a resolver."""
LOGGER.debug("Registering resolver %s", resolver)
self._resolvers.append(resolver)
Loading