From b2f0cb7e360b94e666148543fe14e0a67cce9556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Humbert?= Date: Thu, 26 Jan 2023 11:44:17 +0100 Subject: [PATCH] feat: universal resolver - configurable authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add `--universal-resolver-bearer-token` to allow authentication against privately run universal resolvers BREAKING CHANGE: * Configured url should now include the `/1.0` suffix. The default url reflects that changes Signed-off-by: Clément Humbert --- aries_cloudagent/config/argparse.py | 17 ++++++ .../resolver/default/tests/test_universal.py | 18 +++--- .../resolver/default/universal.py | 60 +++++++++++-------- 3 files changed, 61 insertions(+), 34 deletions(-) diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index a00fffa8b2..3ed950a6f5 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -643,6 +643,14 @@ def add_arguments(self, parser: ArgumentParser): "resolver instance." ), ) + parser.add_argument( + "--universal-resolver-bearer-token", + type=str, + nargs="?", + metavar="", + 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.""" @@ -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 diff --git a/aries_cloudagent/resolver/default/tests/test_universal.py b/aries_cloudagent/resolver/default/tests/test_universal.py index 381e9194e3..8d9df9638d 100644 --- a/aries_cloudagent/resolver/default/tests/test_universal.py +++ b/aries_cloudagent/resolver/default/tests/test_universal.py @@ -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): @@ -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" ) @@ -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") @@ -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"), ): @@ -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"), ): @@ -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"), ): diff --git a/aries_cloudagent/resolver/default/universal.py b/aries_cloudagent/resolver/default/universal.py index 85ca9e2dba..2efee46009 100644 --- a/aries_cloudagent/resolver/default/universal.py +++ b/aries_cloudagent/resolver/default/universal.py @@ -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]]): @@ -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 @@ -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"] @@ -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() + )