Skip to content

Commit

Permalink
Merge pull request openwallet-foundation#27 from burdettadam/feature/…
Browse files Browse the repository at this point in the history
…resolver-admin-api

feature/resolver-admin-api
  • Loading branch information
dbluhm authored Feb 11, 2021
2 parents 01b58a8 + d3cdf84 commit b71b167
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 9 deletions.
2 changes: 2 additions & 0 deletions aries_cloudagent/config/default_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,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
1 change: 1 addition & 0 deletions aries_cloudagent/resolver/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Interfaces and base classes for DID Resolution."""
8 changes: 2 additions & 6 deletions aries_cloudagent/resolver/did_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ def __init__(self, registry: DIDResolverRegistry):
"""Initialize a `didresolver` instance."""
self.did_resolver_registry = registry

async def resolve(
self, profile: Profile, did: Union[str, DID]
) -> ResolvedDIDDoc:
async def resolve(self, profile: Profile, did: Union[str, DID]) -> ResolvedDIDDoc:
"""Retrieve did doc from public registry."""
# TODO Cache results
if isinstance(did, str):
Expand Down Expand Up @@ -62,9 +60,7 @@ def _match_did_to_resolver(self, did: DID) -> BaseDIDResolver:
raise DIDMethodNotSupported(f"{did.method} not supported")
return resolvers

async def dereference(
self, profile: Profile, did_url: str
) -> ResolvedDIDDoc:
async def dereference(self, profile: Profile, did_url: str) -> ResolvedDIDDoc:
"""Dereference a DID URL to its corresponding DID Doc object."""
# TODO Use cached DID Docs when possible
did_url = DIDUrl.parse(did_url)
Expand Down
4 changes: 4 additions & 0 deletions aries_cloudagent/resolver/diddoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ def did(self):
"""Return the DID subject of this Document."""
return self._did

def serialize(self):
"""Return the did document."""
return self._doc

def didcomm_services(self) -> Sequence[dict]:
"""Return agent services in priority order."""

Expand Down
146 changes: 146 additions & 0 deletions aries_cloudagent/resolver/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""
Resolve did document admin routes.
"/resolver/resolve/{did}": {
"get": {
"responses": {
"200": {
"schema": {
"$ref": "#/definitions/DIDDoc"
},
"description": null
}
},
"parameters": [
{
"in": "path",
"name": "did",
"required": true,
"type": "string",
"pattern": "did:([a-z]+):((?:[a-zA-Z0-9._-]*:)*[a-zA-Z0-9._-]+)",
"description": "decentralize identifier(DID)",
"example": "did:ted:WgWxqztrNooG92RXvxSTWv"
}
],
"tags": [ "resolver" ],
"summary": "Retrieve doc for requested did",
"produces": [ "application/json" ]
}
}
"""

import re
from aiohttp import web
from aiohttp_apispec import (
docs,
match_info_schema,
response_schema,
)
from marshmallow import fields, validate

from ..admin.request_context import AdminRequestContext
from ..messaging.models.base import BaseModelError
from ..messaging.models.openapi import OpenAPISchema
from ..storage.error import StorageError, StorageNotFoundError
from .did_resolver import DIDResolver
from .did import DID_PATTERN

class W3cDIDDoc(validate.Regexp):
"""Validate value against w3c DID document."""

EXAMPLE = "*"
PATTERN = DID_PATTERN

def __init__(self):
"""Initializer."""

super().__init__(
W3cDIDDoc.PATTERN,
error="Value {input} is not a w3c decentralized identifier (DID) doc.",
)


_W3cDIDDoc = {"validate": W3cDIDDoc(), "example": W3cDIDDoc.EXAMPLE}


class DIDDocSchema(OpenAPISchema):
"""Result schema for did document query."""

did_doc = fields.Str(
description="decentralize identifier(DID) document", required=True, **_W3cDIDDoc
)


class W3cDID(validate.Regexp):
"""Validate value against w3c DID."""

EXAMPLE = "did:ted:WgWxqztrNooG92RXvxSTWv"
PATTERN = re.compile(r"did:([a-z]+):((?:[a-zA-Z0-9._-]*:)*[a-zA-Z0-9._-]+)")

def __init__(self):
"""Initializer."""

super().__init__(
W3cDID.PATTERN,
error="Value {input} is not a w3c decentralized identifier (DID)",
)


_W3cDID = {"validate": W3cDID(), "example": W3cDID.EXAMPLE}


class DIDMatchInfoSchema(OpenAPISchema):
"""Path parameters and validators for request taking DID."""

did = fields.Str(
description="decentralize identifier(DID)", required=True, **_W3cDID
)


@docs(tags=["resolver"], summary="Retrieve doc for requested did")
@match_info_schema(DIDMatchInfoSchema())
@response_schema(DIDDocSchema(), 200)
async def resolve_did(request: web.BaseRequest):
"""Retrieve a did document."""
context: AdminRequestContext = request["context"]

did = request.match_info["did"]
try:
session = await context.session()
resolver = session.inject(DIDResolver)
document = await resolver.resolve(context.profile, did)
result = document.serialize()
except StorageNotFoundError as err:
raise web.HTTPNotFound(reason=err.roll_up) from err
except (BaseModelError, StorageError) as err:
raise web.HTTPBadRequest(reason=err.roll_up) from err
return web.json_response(result)


async def register(app: web.Application):
"""Register routes."""

app.add_routes(
[
web.get(
"/resolver/resolve/{did}",
resolve_did,
allow_head=False,
),
]
)


def post_process_routes(app: web.Application):
"""Amend swagger API."""

# Add top-level tags description
if "tags" not in app._state["swagger_dict"]:
app._state["swagger_dict"]["tags"] = []
app._state["swagger_dict"]["tags"].append(
{
"name": "resolver",
"description": "universal resolvers",
"externalDocs": {"description": "Specification"}, # , "url": SPEC_URI},
}
)
4 changes: 1 addition & 3 deletions aries_cloudagent/resolver/tests/test_did_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,7 @@ def test_match_did_to_resolver_registration_order():
@pytest.mark.asyncio
async def test_dereference(resolver, profile):
url = "did:example:1234abcd#4"
assert DOC["verificationMethod"][1] == await resolver.dereference(
profile, url
)
assert DOC["verificationMethod"][1] == await resolver.dereference(profile, url)


@pytest.mark.asyncio
Expand Down
104 changes: 104 additions & 0 deletions aries_cloudagent/resolver/tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import unittest
import asyncio
import pytest
from asynctest import mock as async_mock
from ...storage.error import StorageError, StorageNotFoundError
from ...messaging.models.base import BaseModelError

from ..base import (
BaseDIDResolver,
DIDMethodNotSupported,
DIDNotFound,
ResolverType,
)
from ...resolver.did import DID
from ...resolver.diddoc import ResolvedDIDDoc
from ..did_resolver import DIDResolver
from ..did_resolver_registry import DIDResolverRegistry
from .test_did_resolver import TEST_DID_METHODS
from ...admin.request_context import AdminRequestContext
from .test_diddoc import DOC
from .. import routes as test_module
from ...core.in_memory import InMemoryProfile

did_doc = ResolvedDIDDoc(DOC)


@pytest.fixture
def mock_response():
json_response = async_mock.MagicMock()
temp_value = test_module.web.json_response
test_module.web.json_response = json_response
yield json_response
test_module.web.json_response = temp_value


@pytest.fixture
def mock_resolver():
did_resolver = async_mock.MagicMock()
did_resolver.resolve = async_mock.CoroutineMock(return_value=did_doc)
yield did_resolver


@pytest.fixture
def mock_request(mock_resolver):
context = AdminRequestContext.test_context({DIDResolver: mock_resolver})
outbound_message_router = async_mock.CoroutineMock()
request_dict = {
"context": context,
"outbound_message_router": outbound_message_router,
}
request = async_mock.MagicMock(
match_info={
"did": "did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a",
},
query={},
json=async_mock.CoroutineMock(return_value={}),
__getitem__=lambda _, k: request_dict[k],
)
yield request


@pytest.mark.asyncio
async def test_resolver(mock_request, mock_response):
await test_module.resolve_did(mock_request)
mock_response.assert_called_once_with(did_doc.serialize())
# TODO: test http response codes


@pytest.mark.asyncio
async def test_resolver_not_found_error(mock_resolver, mock_request, mock_response):
mock_resolver.resolve = async_mock.CoroutineMock(side_effect=StorageNotFoundError())
with pytest.raises(test_module.web.HTTPNotFound):
await test_module.resolve_did(mock_request)


@pytest.mark.asyncio
async def test_resolver_bad_req_error(mock_resolver, mock_request, mock_response):
mock_resolver.resolve = async_mock.CoroutineMock(side_effect=BaseModelError())
with pytest.raises(test_module.web.HTTPBadRequest):
await test_module.resolve_did(mock_request)


@pytest.mark.asyncio
async def test_resolver_bad_req_storage_error(
mock_resolver, mock_request, mock_response
):
mock_resolver.resolve = async_mock.CoroutineMock(side_effect=StorageError())
with pytest.raises(test_module.web.HTTPBadRequest):
await test_module.resolve_did(mock_request)


@pytest.mark.asyncio
async def test_register():
mock_app = async_mock.MagicMock()
mock_app.add_routes = async_mock.MagicMock()
await test_module.register(mock_app)
mock_app.add_routes.assert_called_once()


@pytest.mark.asyncio
async def test_post_process_routes():
mock_app = async_mock.MagicMock(_state={"swagger_dict": {}})
test_module.post_process_routes(mock_app)
assert "tags" in mock_app._state["swagger_dict"]

0 comments on commit b71b167

Please sign in to comment.