From 0b52306401e7152275b40108cfde523cd3b2fc1b Mon Sep 17 00:00:00 2001 From: Shaanjot Gill Date: Tue, 27 Jun 2023 09:53:03 -0700 Subject: [PATCH] wallet record access on settings endpoint Signed-off-by: Shaanjot Gill --- aries_cloudagent/admin/request_context.py | 16 ++- aries_cloudagent/admin/server.py | 23 +++- .../admin/tests/test_request_context.py | 14 +++ aries_cloudagent/multitenant/admin/routes.py | 38 +++--- aries_cloudagent/multitenant/base.py | 24 +++- .../multitenant/tests/test_base.py | 58 +++++++++ aries_cloudagent/settings/routes.py | 60 +++++++-- .../settings/tests/test_routes.py | 118 ++++++++++++++++-- 8 files changed, 311 insertions(+), 40 deletions(-) diff --git a/aries_cloudagent/admin/request_context.py b/aries_cloudagent/admin/request_context.py index 1fe7f79076..c7a64a11b0 100644 --- a/aries_cloudagent/admin/request_context.py +++ b/aries_cloudagent/admin/request_context.py @@ -23,11 +23,15 @@ def __init__( profile: Profile, *, context: InjectionContext = None, - settings: Mapping[str, object] = None + settings: Mapping[str, object] = None, + root_profile: Profile = None, + metadata: dict = None ): """Initialize an instance of AdminRequestContext.""" self._context = (context or profile.context).start_scope("admin", settings) self._profile = profile + self._root_profile = root_profile + self._metadata = metadata @property def injector(self) -> Injector: @@ -39,6 +43,16 @@ def profile(self) -> Profile: """Accessor for the associated `Profile` instance.""" return self._profile + @property + def root_profile(self) -> Optional[Profile]: + """Accessor for the associated root_profile instance.""" + return self._root_profile + + @property + def metadata(self) -> dict: + """Accessor for the associated metadata.""" + return self._metadata + @property def settings(self) -> Settings: """Accessor for the context settings.""" diff --git a/aries_cloudagent/admin/server.py b/aries_cloudagent/admin/server.py index 544bc8f585..91dcaf94a7 100644 --- a/aries_cloudagent/admin/server.py +++ b/aries_cloudagent/admin/server.py @@ -384,7 +384,7 @@ async def check_multitenant_authorization(request: web.Request, handler): async def setup_context(request: web.Request, handler): authorization_header = request.headers.get("Authorization") profile = self.root_profile - + meta_data = {} # Multitenancy context setup if self.multitenant_manager and authorization_header: try: @@ -397,6 +397,16 @@ async def setup_context(request: web.Request, handler): profile = await self.multitenant_manager.get_profile_for_token( self.context, token ) + ( + walletid, + walletkey, + ) = self.multitenant_manager.get_wallet_details_from_token( + token=token + ) + meta_data = { + "wallet_id": walletid, + "wallet_key": walletkey, + } except MultitenantManagerError as err: raise web.HTTPUnauthorized(reason=err.roll_up) except (jwt.InvalidTokenError, StorageNotFoundError): @@ -411,7 +421,16 @@ async def setup_context(request: web.Request, handler): # TODO may dynamically adjust the profile used here according to # headers or other parameters - admin_context = AdminRequestContext(profile) + if self.multitenant_manager and authorization_header: + admin_context = AdminRequestContext( + profile=profile, + root_profile=self.root_profile, + metadata=meta_data, + ) + else: + admin_context = AdminRequestContext( + profile=profile, + ) request["context"] = admin_context request["outbound_message_router"] = responder.send diff --git a/aries_cloudagent/admin/tests/test_request_context.py b/aries_cloudagent/admin/tests/test_request_context.py index 7775262638..e74763004e 100644 --- a/aries_cloudagent/admin/tests/test_request_context.py +++ b/aries_cloudagent/admin/tests/test_request_context.py @@ -12,12 +12,26 @@ def setUp(self): self.ctx = test_module.AdminRequestContext(InMemoryProfile.test_profile()) assert self.ctx.__class__.__name__ in str(self.ctx) + self.ctx_with_added_attrs = test_module.AdminRequestContext( + profile=InMemoryProfile.test_profile(), + root_profile=InMemoryProfile.test_profile(), + metadata={"test_attrib_key": "test_attrib_value"}, + ) + assert self.ctx_with_added_attrs.__class__.__name__ in str( + self.ctx_with_added_attrs + ) + def test_session_transaction(self): sesn = self.ctx.session() assert isinstance(sesn, ProfileSession) txn = self.ctx.transaction() assert isinstance(txn, ProfileSession) + sesn = self.ctx_with_added_attrs.session() + assert isinstance(sesn, ProfileSession) + txn = self.ctx_with_added_attrs.transaction() + assert isinstance(txn, ProfileSession) + async def test_session_inject_x(self): test_ctx = test_module.AdminRequestContext.test_context({Collector: None}) async with test_ctx.session() as test_sesn: diff --git a/aries_cloudagent/multitenant/admin/routes.py b/aries_cloudagent/multitenant/admin/routes.py index 42b7348b79..b9c78f9e20 100644 --- a/aries_cloudagent/multitenant/admin/routes.py +++ b/aries_cloudagent/multitenant/admin/routes.py @@ -43,31 +43,31 @@ } ACAPY_LIFECYCLE_CONFIG_FLAG_ARGS_MAP = { - "log_level": "log.level", - "invite_public": "debug.invite_public", - "public_invites": "public_invites", - "auto_accept_invites": "debug.auto_accept_invites", - "auto_accept_requests": "debug.auto_accept_requests", - "auto_ping_connection": "auto_ping_connection", - "monitor_ping": "debug.monitor_ping", - "auto_respond_messages": "debug.auto_respond_messages", - "auto_respond_credential_offer": "debug.auto_respond_credential_offer", - "auto_respond_credential_request": "debug.auto_respond_credential_request", - "auto_verify_presentation": "debug.auto_verify_presentation", - "notify_revocation": "revocation.notify", - "auto_request_endorsement": "endorser.auto_request", - "auto_write_transactions": "endorser.auto_write", - "auto_create_revocation_transactions": "endorser.auto_create_rev_reg", - "endorser_protocol_role": "endorser.protocol_role", + "log-level": "log.level", + "invite-public": "debug.invite_public", + "public-invites": "public_invites", + "auto-accept-invites": "debug.auto_accept_invites", + "auto-accept-requests": "debug.auto_accept_requests", + "auto-ping-connection": "auto_ping_connection", + "monitor-ping": "debug.monitor_ping", + "auto-respond-messages": "debug.auto_respond_messages", + "auto-respond-credential-offer": "debug.auto_respond_credential_offer", + "auto-respond-credential-request": "debug.auto_respond_credential_request", + "auto-verify-presentation": "debug.auto_verify_presentation", + "notify-revocation": "revocation.notify", + "auto-request-endorsement": "endorser.auto_request", + "auto-write-transactions": "endorser.auto_write", + "auto-create-revocation-transactions": "endorser.auto_create_rev_reg", + "endorser-protocol-role": "endorser.protocol_role", } ACAPY_ENDORSER_FLAGS_DEPENDENT_ON_AUTHOR_ROLE = [ "ACAPY_AUTO_REQUEST_ENDORSEMENT", "ACAPY_AUTO_WRITE_TRANSACTIONS", "ACAPY_CREATE_REVOCATION_TRANSACTIONS", - "auto_request_endorsement", - "auto_write_transactions", - "auto_create_revocation_transactions", + "auto-request-endorsement", + "auto-write-transactions", + "auto-create-revocation-transactions", ] diff --git a/aries_cloudagent/multitenant/base.py b/aries_cloudagent/multitenant/base.py index 2afec03b00..03fbb7a515 100644 --- a/aries_cloudagent/multitenant/base.py +++ b/aries_cloudagent/multitenant/base.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from datetime import datetime import logging -from typing import Iterable, List, Optional, cast +from typing import Iterable, List, Optional, cast, Tuple import jwt @@ -318,6 +318,28 @@ async def create_auth_token( return token + def get_wallet_details_from_token(self, token: str) -> Tuple[str, str]: + """Get the wallet_id and wallet_key from provided token.""" + jwt_secret = self._profile.context.settings.get("multitenant.jwt_secret") + token_body = jwt.decode(token, jwt_secret, algorithms=["HS256"]) + wallet_id = token_body.get("wallet_id") + wallet_key = token_body.get("wallet_key") + return wallet_id, wallet_key + + async def get_wallet_and_profile( + self, context: InjectionContext, wallet_id: str, wallet_key: str + ) -> Tuple[WalletRecord, Profile]: + """Get the wallet_record and profile associated with wallet id and key.""" + extra_settings = {} + async with self._profile.session() as session: + wallet = await WalletRecord.retrieve_by_id(session, wallet_id) + if wallet.requires_external_key: + if not wallet_key: + raise WalletKeyMissingError() + extra_settings["wallet.key"] = wallet_key + profile = await self.get_wallet_profile(context, wallet, extra_settings) + return (wallet, profile) + async def get_profile_for_token( self, context: InjectionContext, token: str ) -> Profile: diff --git a/aries_cloudagent/multitenant/tests/test_base.py b/aries_cloudagent/multitenant/tests/test_base.py index dd1c5fba31..1e28b90b18 100644 --- a/aries_cloudagent/multitenant/tests/test_base.py +++ b/aries_cloudagent/multitenant/tests/test_base.py @@ -418,6 +418,64 @@ async def test_create_auth_token_unmanaged(self): assert wallet_record.jwt_iat == iat assert expected_token == token + async def test_get_wallet_details_from_token(self): + self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" + wallet_record = WalletRecord( + key_management_mode=WalletRecord.MODE_MANAGED, + settings={"wallet.type": "indy", "wallet.key": "wallet_key"}, + jwt_iat=100, + ) + session = await self.profile.session() + await wallet_record.save(session) + token = jwt.encode( + {"wallet_id": wallet_record.wallet_id, "iat": 100}, + "very_secret_jwt", + algorithm="HS256", + ) + ret_wallet_id, ret_wallet_key = self.manager.get_wallet_details_from_token( + token + ) + assert ret_wallet_id == wallet_record.wallet_id + assert not ret_wallet_key + + token = jwt.encode( + { + "wallet_id": wallet_record.wallet_id, + "iat": 100, + "wallet_key": "wallet_key", + }, + "very_secret_jwt", + algorithm="HS256", + ) + ret_wallet_id, ret_wallet_key = self.manager.get_wallet_details_from_token( + token + ) + assert ret_wallet_id == wallet_record.wallet_id + assert ret_wallet_key == "wallet_key" + + async def test_get_wallet_and_profile(self): + self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" + wallet_record = WalletRecord( + key_management_mode=WalletRecord.MODE_MANAGED, + settings={"wallet.type": "indy", "wallet.key": "wallet_key"}, + jwt_iat=100, + ) + + session = await self.profile.session() + await wallet_record.save(session) + + with async_mock.patch.object( + self.manager, "get_wallet_profile" + ) as get_wallet_profile: + mock_profile = InMemoryProfile.test_profile() + get_wallet_profile.return_value = mock_profile + + wallet, profile = await self.manager.get_wallet_and_profile( + self.profile.context, wallet_record.wallet_id, "wallet_key" + ) + assert wallet == wallet_record + assert profile == mock_profile + async def test_get_profile_for_token_invalid_token_raises(self): self.profile.settings["multitenant.jwt_secret"] = "very_secret_jwt" diff --git a/aries_cloudagent/settings/routes.py b/aries_cloudagent/settings/routes.py index 8b1231d503..625fa59b58 100644 --- a/aries_cloudagent/settings/routes.py +++ b/aries_cloudagent/settings/routes.py @@ -7,9 +7,13 @@ from marshmallow import fields from ..admin.request_context import AdminRequestContext +from ..multitenant.base import BaseMultitenantManager from ..core.error import BaseError from ..messaging.models.openapi import OpenAPISchema -from ..multitenant.admin.routes import get_extra_settings_dict_per_tenant +from ..multitenant.admin.routes import ( + get_extra_settings_dict_per_tenant, + ACAPY_LIFECYCLE_CONFIG_FLAG_ARGS_MAP, +) LOGGER = logging.getLogger(__name__) @@ -41,6 +45,16 @@ class ProfileSettingsSchema(OpenAPISchema): ) +def _get_filtered_settings_dict(wallet_settings: dict): + """Get filtered settings dict to display.""" + filter_param_list = list(ACAPY_LIFECYCLE_CONFIG_FLAG_ARGS_MAP.values()) + settings_dict = {} + for param in filter_param_list: + if param in wallet_settings: + settings_dict[param] = wallet_settings.get(param) + return settings_dict + + @docs( tags=["settings"], summary="Update settings or config associated with the profile.", @@ -55,21 +69,34 @@ async def update_profile_settings(request: web.BaseRequest): request: aiohttp request object """ context: AdminRequestContext = request["context"] + root_profile = context.root_profile or context.profile try: body = await request.json() - extra_setting = get_extra_settings_dict_per_tenant( + extra_settings = get_extra_settings_dict_per_tenant( body.get("extra_settings") or {} ) - context.profile.settings.update(extra_setting) - result = context.profile.settings + async with root_profile.session() as session: + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + wallet_id = context.metadata.get("wallet_id") + wallet_record = await multitenant_mgr.update_wallet( + wallet_id, extra_settings + ) + wallet_settings = wallet_record.settings + settings_dict = _get_filtered_settings_dict(wallet_settings) + else: + root_profile.context.update_settings(extra_settings) + settings_dict = _get_filtered_settings_dict( + (context.profile.settings).to_dict() + ) except BaseError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - return web.json_response(result.to_dict()) + return web.json_response(settings_dict) @docs( tags=["settings"], - summary="Get the settings or config associated with the profile.", + summary="Get the configurable settings associated with the profile.", ) @response_schema(ProfileSettingsSchema(), 200, description="") async def get_profile_settings(request: web.BaseRequest): @@ -80,12 +107,27 @@ async def get_profile_settings(request: web.BaseRequest): request: aiohttp request object """ context: AdminRequestContext = request["context"] - + root_profile = context.root_profile or context.profile try: - result = context.profile.settings + async with root_profile.session() as session: + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + wallet_id = context.metadata.get("wallet_id") + wallet_key = context.metadata.get("wallet_key") + wallet_record, profile = await multitenant_mgr.get_wallet_and_profile( + root_profile.context, wallet_id, wallet_key + ) + profile_settings = profile.settings.to_dict() + wallet_settings = wallet_record.settings + all_settings = {**profile_settings, **wallet_settings} + settings_dict = _get_filtered_settings_dict(all_settings) + else: + settings_dict = _get_filtered_settings_dict( + (root_profile.settings).to_dict() + ) except BaseError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - return web.json_response(result.to_dict()) + return web.json_response(settings_dict) async def register(app: web.Application): diff --git a/aries_cloudagent/settings/tests/test_routes.py b/aries_cloudagent/settings/tests/test_routes.py index 83aba881c2..474b792cc4 100644 --- a/aries_cloudagent/settings/tests/test_routes.py +++ b/aries_cloudagent/settings/tests/test_routes.py @@ -4,7 +4,11 @@ import pytest from asynctest import mock as async_mock + +from ...admin.request_context import AdminRequestContext from ...core.in_memory import InMemoryProfile +from ...multitenant.base import BaseMultitenantManager +from ...multitenant.manager import MultitenantManager from .. import routes as test_module @@ -32,10 +36,10 @@ async def test_get_profile_settings(mock_response): "debug.auto_accept_requests": True, } ) - context = profile.context - setattr(context, "profile", profile) request_dict = { - "context": context, + "context": AdminRequestContext( + profile=profile, + ), } request = async_mock.MagicMock( query={}, @@ -44,10 +48,55 @@ async def test_get_profile_settings(mock_response): ) await test_module.get_profile_settings(request) assert mock_response.call_args[0][0] == { - "admin.admin_client_max_request_size": 1, "debug.auto_respond_credential_offer": True, "debug.auto_respond_credential_request": True, - "debug.auto_respond_presentation_proposal": True, + "debug.auto_verify_presentation": True, + "debug.auto_accept_invites": True, + "debug.auto_accept_requests": True, + } + # Multitenant + profile = InMemoryProfile.test_profile() + multi_tenant_manager = MultitenantManager(profile) + profile.context.injector.bind_instance( + BaseMultitenantManager, + multi_tenant_manager, + ) + request_dict = { + "context": AdminRequestContext( + profile=profile, + root_profile=profile, + metadata={ + "wallet_id": "walletid", + "wallet_key": "walletkey", + }, + ), + } + request = async_mock.MagicMock( + query={}, + json=async_mock.CoroutineMock(return_value={}), + __getitem__=lambda _, k: request_dict[k], + ) + with async_mock.patch.object( + multi_tenant_manager, "get_wallet_and_profile" + ) as get_wallet_and_profile: + get_wallet_and_profile.return_value = ( + async_mock.MagicMock( + settings={ + "admin.admin_client_max_request_size": 1, + "debug.auto_respond_credential_offer": True, + "debug.auto_respond_credential_request": True, + "debug.auto_respond_presentation_proposal": True, + "debug.auto_verify_presentation": True, + "debug.auto_accept_invites": True, + "debug.auto_accept_requests": True, + } + ), + profile, + ) + await test_module.get_profile_settings(request) + assert mock_response.call_args[0][0] == { + "debug.auto_respond_credential_offer": True, + "debug.auto_respond_credential_request": True, "debug.auto_verify_presentation": True, "debug.auto_accept_invites": True, "debug.auto_accept_requests": True, @@ -66,10 +115,10 @@ async def test_update_profile_settings(mock_response): "auto_ping_connection": True, } ) - context = profile.context - setattr(context, "profile", profile) request_dict = { - "context": context, + "context": AdminRequestContext( + profile=profile, + ), } request = async_mock.MagicMock( query={}, @@ -94,3 +143,56 @@ async def test_update_profile_settings(mock_response): "debug.auto_accept_requests": False, "auto_ping_connection": False, } + # Multitenant + profile = InMemoryProfile.test_profile() + multi_tenant_manager = MultitenantManager(profile) + profile.context.injector.bind_instance( + BaseMultitenantManager, + multi_tenant_manager, + ) + + request_dict = { + "context": AdminRequestContext( + profile=profile, + root_profile=profile, + metadata={ + "wallet_id": "walletid", + "wallet_key": "walletkey", + }, + ), + } + request = async_mock.MagicMock( + query={}, + json=async_mock.CoroutineMock( + return_value={ + "extra_settings": { + "ACAPY_INVITE_PUBLIC": False, + "ACAPY_PUBLIC_INVITES": False, + "ACAPY_AUTO_ACCEPT_INVITES": False, + "ACAPY_AUTO_ACCEPT_REQUESTS": False, + "ACAPY_AUTO_PING_CONNECTION": False, + } + } + ), + __getitem__=lambda _, k: request_dict[k], + ) + with async_mock.patch.object( + multi_tenant_manager, "update_wallet" + ) as update_wallet: + update_wallet.return_value = async_mock.MagicMock( + settings={ + "public_invites": False, + "debug.invite_public": False, + "debug.auto_accept_invites": False, + "debug.auto_accept_requests": False, + "auto_ping_connection": False, + } + ) + await test_module.update_profile_settings(request) + assert mock_response.call_args[0][0] == { + "public_invites": False, + "debug.invite_public": False, + "debug.auto_accept_invites": False, + "debug.auto_accept_requests": False, + "auto_ping_connection": False, + }