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

feat: universal resolver - configurable authentication #2095

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
17 changes: 17 additions & 0 deletions aries_cloudagent/config/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,14 @@ def add_arguments(self, parser: ArgumentParser):
"resolver instance."
),
)
parser.add_argument(
"--universal-resolver-bearer-token",
type=str,
nargs="?",
metavar="<universal_resolver_token>",
env_var="ACAPY_UNIVERSAL_RESOLVER_BEARER_TOKEN",
help="Bearer token if universal resolver instance requires authentication.",
),

def get_settings(self, args: Namespace) -> dict:
"""Extract general settings."""
Expand Down Expand Up @@ -688,12 +696,21 @@ def get_settings(self, args: Namespace) -> dict:
"--universal-resolver-regex cannot be used without --universal-resolver"
)

if args.universal_resolver_bearer_token and not args.universal_resolver:
raise ArgsParseError(
"--universal-resolver-bearer-token "
+ "cannot be used without --universal-resolver"
)

if args.universal_resolver:
settings["resolver.universal"] = args.universal_resolver

if args.universal_resolver_regex:
settings["resolver.universal.supported"] = args.universal_resolver_regex

if args.universal_resolver_bearer_token:
settings["resolver.universal.token"] = args.universal_resolver_bearer_token

return settings


Expand Down
18 changes: 9 additions & 9 deletions aries_cloudagent/resolver/default/tests/test_universal.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class MockClientSession:
def __init__(self, response: MockResponse = None):
self.response = response

def __call__(self):
def __call__(self, headers):
return self

async def __aenter__(self):
Expand Down Expand Up @@ -101,7 +101,7 @@ async def test_resolve_not_found(profile, resolver, mock_client_session):


@pytest.mark.asyncio
async def test_resolve_unexpeceted_status(profile, resolver, mock_client_session):
async def test_resolve_unexpected_status(profile, resolver, mock_client_session):
mock_client_session.response = MockResponse(
500, "Server failed to complete request"
)
Expand All @@ -112,21 +112,21 @@ async def test_resolve_unexpeceted_status(profile, resolver, mock_client_session
@pytest.mark.asyncio
async def test_fetch_resolver_props(mock_client_session: MockClientSession):
mock_client_session.response = MockResponse(200, {"test": "json"})
assert await test_module._fetch_resolver_props("test") == {"test": "json"}
assert await UniversalResolver()._fetch_resolver_props() == {"test": "json"}
mock_client_session.response = MockResponse(404, "Not found")
with pytest.raises(ResolverError):
await test_module._fetch_resolver_props("test")
await UniversalResolver()._fetch_resolver_props()


@pytest.mark.asyncio
async def test_get_supported_did_regex():
props = {"example": {"http": {"pattern": "match a test string"}}}
with async_mock.patch.object(
test_module,
UniversalResolver,
"_fetch_resolver_props",
async_mock.CoroutineMock(return_value=props),
):
pattern = await test_module._get_supported_did_regex("test")
pattern = await UniversalResolver()._get_supported_did_regex()
assert pattern.fullmatch("match a test string")


Expand Down Expand Up @@ -169,7 +169,7 @@ async def test_setup_endpoint_set(resolver: UniversalResolver):
context = async_mock.MagicMock()
context.settings = settings
with async_mock.patch.object(
test_module,
UniversalResolver,
"_get_supported_did_regex",
async_mock.CoroutineMock(return_value="pattern"),
):
Expand All @@ -189,7 +189,7 @@ async def test_setup_endpoint_default(resolver: UniversalResolver):
context = async_mock.MagicMock()
context.settings = settings
with async_mock.patch.object(
test_module,
UniversalResolver,
"_get_supported_did_regex",
async_mock.CoroutineMock(return_value="pattern"),
):
Expand All @@ -205,7 +205,7 @@ async def test_setup_endpoint_unset(resolver: UniversalResolver):
context = async_mock.MagicMock()
context.settings = settings
with async_mock.patch.object(
test_module,
UniversalResolver,
"_get_supported_did_regex",
async_mock.CoroutineMock(return_value="pattern"),
):
Expand Down
60 changes: 35 additions & 25 deletions aries_cloudagent/resolver/default/universal.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,7 @@
from ..base import BaseDIDResolver, DIDNotFound, ResolverError, ResolverType

LOGGER = logging.getLogger(__name__)
DEFAULT_ENDPOINT = "https://dev.uniresolver.io"


async def _fetch_resolver_props(endpoint: str) -> dict:
"""Retrieve universal resolver properties."""
async with aiohttp.ClientSession() as session:
async with session.get(f"{endpoint}/1.0/properties/") as resp:
if resp.status >= 200 and resp.status < 400:
return await resp.json()
raise ResolverError(
"Failed to retrieve resolver properties: " + await resp.text()
)


async def _get_supported_did_regex(endpoint: str) -> Pattern:
props = await _fetch_resolver_props(endpoint)
return _compile_supported_did_regex(
driver["http"]["pattern"] for driver in props.values()
)
DEFAULT_ENDPOINT = "https://dev.uniresolver.io/1.0"


def _compile_supported_did_regex(patterns: Iterable[Union[str, Pattern]]):
Expand All @@ -54,25 +36,37 @@ def __init__(
*,
endpoint: Optional[str] = None,
supported_did_regex: Optional[Pattern] = None,
bearer_token: Optional[str] = None,
):
"""Initialize UniversalResolver."""
super().__init__(ResolverType.NON_NATIVE)
self._endpoint = endpoint
self._supported_did_regex = supported_did_regex

self.__default_headers = (
{"Authorization": f"Bearer {bearer_token}"} if bearer_token else {}
)

async def setup(self, context: InjectionContext):
"""Preform setup, populate supported method list, configuration."""
"""Perform setup, populate supported method list, configuration."""

# configure endpoint
endpoint = context.settings.get_str("resolver.universal")
if endpoint == "DEFAULT" or not endpoint:
endpoint = DEFAULT_ENDPOINT
self._endpoint = endpoint

# configure authorization
token = context.settings.get_str("resolver.universal.token")
self.__default_headers = {"Authorization": f"Bearer {token}"} if token else {}

# configure supported methods
supported = context.settings.get("resolver.universal.supported")
if supported is None:
supported_did_regex = await _get_supported_did_regex(endpoint)
supported_did_regex = await self._get_supported_did_regex()
else:
supported_did_regex = _compile_supported_did_regex(supported)

self._endpoint = endpoint
self._supported_did_regex = supported_did_regex

@property
Expand All @@ -91,8 +85,8 @@ async def _resolve(
) -> dict:
"""Resolve DID through remote universal resolver."""

async with aiohttp.ClientSession() as session:
async with session.get(f"{self._endpoint}/1.0/identifiers/{did}") as resp:
async with aiohttp.ClientSession(headers=self.__default_headers) as session:
async with session.get(f"{self._endpoint}/identifiers/{did}") as resp:
if resp.status == 200:
doc = await resp.json()
did_doc = doc["didDocument"]
Expand All @@ -103,5 +97,21 @@ async def _resolve(

text = await resp.text()
raise ResolverError(
f"Unexecpted status from universal resolver ({resp.status}): {text}"
f"Unexpected status from universal resolver ({resp.status}): {text}"
)

async def _fetch_resolver_props(self) -> dict:
"""Retrieve universal resolver properties."""
async with aiohttp.ClientSession(headers=self.__default_headers) as session:
async with session.get(f"{self._endpoint}/properties/") as resp:
if 200 <= resp.status < 400:
return await resp.json()
raise ResolverError(
"Failed to retrieve resolver properties: " + await resp.text()
)

async def _get_supported_did_regex(self) -> Pattern:
props = await self._fetch_resolver_props()
return _compile_supported_did_regex(
driver["http"]["pattern"] for driver in props.values()
)