diff --git a/.vscode-sample/multitenant-admin.yml b/.vscode-sample/multitenant-admin.yml index b6f98ef4fa..78240bb125 100644 --- a/.vscode-sample/multitenant-admin.yml +++ b/.vscode-sample/multitenant-admin.yml @@ -8,10 +8,11 @@ inbound-transport: outbound-transport: http -wallet-type: askar-anoncreds +wallet-type: askar wallet-storage-type: default wallet-name: multitenant-admin-wallet wallet-key: multitenant-admin-wallet-key +multitenancy-config: '{"wallet_type": "single-wallet-askar"}' admin-insecure-mode: true @@ -19,7 +20,7 @@ admin: [0.0.0.0, 9051] endpoint: http://localhost:9050 -genesis-url: https://localhost:9000/genesis +genesis-url: http://localhost:9000/genesis # Connections debug-connections: true @@ -35,3 +36,11 @@ multitenant-admin: true log-level: info tails-server-base-url: https://localhost:6543 + +plugin: + - multitenant_provider.v1_0 + +# This is used if you want to use the multitoken multitenant manager plugin +# plugin-config-value: +# - multitenant_provider.manager.class_name="multitenant_provider.v1_0.manager.SingleWalletAskarMultitokenMultitenantManager" +# - multitenant_provider.manager.always_check_provided_wallet_key=false diff --git a/aries_cloudagent/askar/tests/test_profile.py b/aries_cloudagent/askar/tests/test_profile.py index c9ad71a32e..82bf04dc53 100644 --- a/aries_cloudagent/askar/tests/test_profile.py +++ b/aries_cloudagent/askar/tests/test_profile.py @@ -1,12 +1,11 @@ import asyncio -import pytest - from unittest import mock +import pytest + from ...askar.profile import AskarProfile from ...config.injection_context import InjectionContext from ...ledger.base import BaseLedger - from .. import profile as test_module @@ -53,8 +52,7 @@ async def test_init_multi_ledger(open_store): assert askar_profile.opened == open_store assert askar_profile.settings["endorser.endorser_alias"] == "endorser_dev" assert ( - askar_profile.settings["endorser.endorser_public_did"] - == "9QPa6tHvBHttLg6U4xvviv" + askar_profile.settings["endorser.endorser_public_did"] == "9QPa6tHvBHttLg6U4xvviv" ) assert (askar_profile.inject_or(BaseLedger)).pool_name == "BCovrinDev" @@ -65,7 +63,7 @@ async def test_remove_success(open_store): context = InjectionContext() profile_id = "profile_id" context.settings = { - "multitenant.wallet_type": "askar-profile", + "multitenant.wallet_type": "single-wallet-askar", "wallet.askar_profile": profile_id, "ledger.genesis_transactions": mock.MagicMock(), } diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index 1d870cf2d5..cf94e1a0a4 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -58,9 +58,7 @@ def __call__(self, group_cls: ArgumentGroup): def get_registered(cls, category: str = None): """Fetch the set of registered classes in a category.""" return ( - grp - for (cats, grp) in cls._registered - if category is None or category in cats + grp for (cats, grp) in cls._registered if category is None or category in cats ) @@ -504,9 +502,7 @@ def get_settings(self, args: Namespace) -> dict: if "protocols" in provided_lists: settings["disclose_protocol_list"] = provided_lists.get("protocols") if "goal-codes" in provided_lists: - settings["disclose_goal_code_list"] = provided_lists.get( - "goal-codes" - ) + settings["disclose_goal_code_list"] = provided_lists.get("goal-codes") return settings @@ -1197,8 +1193,8 @@ def get_settings(self, args: Namespace) -> dict: if args.requests_through_public_did: if not args.public_invites: raise ArgsParseError( - "--public-invites is required to use " - "--requests-through-public-did" + "--public-invites is required to use ", + "--requests-through-public-did", ) settings["requests_through_public_did"] = True if args.timing: @@ -1416,9 +1412,7 @@ def get_settings(self, args: Namespace): settings["transport.outbound_configs"] = args.outbound_transports else: raise ArgsParseError("-ot/--outbound-transport is required") - settings["transport.enable_undelivered_queue"] = ( - args.enable_undelivered_queue - ) + settings["transport.enable_undelivered_queue"] = args.enable_undelivered_queue if args.max_message_size: settings["transport.max_message_size"] = args.max_message_size if args.max_outbound_retry: @@ -1525,9 +1519,7 @@ def get_settings(self, args: Namespace): settings["mediation.clear"] = True if args.clear_default_mediator and args.default_mediator_id: - raise ArgsParseError( - "Cannot both set and clear mediation at the same time." - ) + raise ArgsParseError("Cannot both set and clear mediation at the same time.") return settings @@ -1770,10 +1762,10 @@ def add_arguments(self, parser: ArgumentParser): env_var="ACAPY_MULTITENANCY_CONFIGURATION", help=( "Specify multitenancy configuration in key=value pairs. " - 'For example: "wallet_type=askar-profile wallet_name=askar-profile-name" ' + 'For example: "wallet_type=single-wallet-askar wallet_name=wallet-name" ' "Possible values: wallet_name, wallet_key, cache_size, " 'key_derivation_method. "wallet_name" is only used when ' - '"wallet_type" is "askar-profile"' + '"wallet_type" is "single-wallet-askar"' ), ) parser.add_argument( diff --git a/aries_cloudagent/core/conductor.py b/aries_cloudagent/core/conductor.py index f93900e577..8706e55cda 100644 --- a/aries_cloudagent/core/conductor.py +++ b/aries_cloudagent/core/conductor.py @@ -236,9 +236,7 @@ async def setup(self): ) # Bind default PyLD document loader - context.injector.bind_instance( - DocumentLoader, DocumentLoader(self.root_profile) - ) + context.injector.bind_instance(DocumentLoader, DocumentLoader(self.root_profile)) # Admin API if context.settings.get("admin.enabled"): @@ -480,8 +478,8 @@ async def start(self) -> None: try: async with self.root_profile.session() as session: invite_store = MediationInviteStore(session.context.inject(BaseStorage)) - mediation_invite_record = ( - await invite_store.get_mediation_invite_record(provided_invite) + mediation_invite_record = await invite_store.get_mediation_invite_record( + provided_invite ) except Exception: LOGGER.exception("Error retrieving mediator invitation") @@ -609,9 +607,7 @@ def inbound_message_router( def dispatch_complete(self, message: InboundMessage, completed: CompletedTask): """Handle completion of message dispatch.""" if completed.exc_info: - LOGGER.exception( - "Exception in message handler:", exc_info=completed.exc_info - ) + LOGGER.exception("Exception in message handler:", exc_info=completed.exc_info) if isinstance(completed.exc_info[1], LedgerConfigError) or isinstance( completed.exc_info[1], LedgerTransactionError ): @@ -725,9 +721,7 @@ async def queue_outbound( conn_mgr = ConnectionManager(profile) try: outbound.target_list = await self.dispatcher.run_task( - conn_mgr.get_connection_targets( - connection_id=outbound.connection_id - ) + conn_mgr.get_connection_targets(connection_id=outbound.connection_id) ) except ConnectionManagerError: LOGGER.exception("Error preparing outbound message for transmission") @@ -742,9 +736,7 @@ async def queue_outbound( elif not has_target and outbound.reply_thread_id: message_processor = profile.inject(OobMessageProcessor) outbound.target = await self.dispatcher.run_task( - message_processor.find_oob_target_for_outbound_message( - profile, outbound - ) + message_processor.find_oob_target_for_outbound_message(profile, outbound) ) return await self._queue_message(profile, outbound) @@ -864,7 +856,18 @@ async def check_for_valid_wallet_type(self, profile): async def check_for_wallet_upgrades_in_progress(self): """Check for upgrade and upgrade if needed.""" - multitenant_mgr = self.context.inject_or(BaseMultitenantManager) + + # We need to use the correct multitenant manager for single vs multiple wallets + # here because the multitenant provider hasn't been initialized yet. + manager_type = self.context.settings.get_value( + "multitenant.wallet_type", default="basic" + ).lower() + + manager_class = MultitenantManagerProvider.MANAGER_TYPES.get( + manager_type, manager_type + ) + + multitenant_mgr = self.context.inject_or(manager_class) if multitenant_mgr: subwallet_profiles = await get_subwallet_profiles_from_storage( self.root_profile diff --git a/aries_cloudagent/multitenant/manager_provider.py b/aries_cloudagent/multitenant/manager_provider.py index 4c986d2f67..0dfe957e17 100644 --- a/aries_cloudagent/multitenant/manager_provider.py +++ b/aries_cloudagent/multitenant/manager_provider.py @@ -2,10 +2,10 @@ import logging +from ..config.base import InjectionError +from ..config.injector import BaseInjector from ..config.provider import BaseProvider from ..config.settings import BaseSettings -from ..config.injector import BaseInjector -from ..config.base import InjectionError from ..utils.classloader import ClassLoader, ClassNotFoundError LOGGER = logging.getLogger(__name__) @@ -17,13 +17,13 @@ class MultitenantManagerProvider(BaseProvider): Decides which manager to use based on the settings. """ - askar_profile_manager_path = ( + single_wallet_askar_manager_path = ( "aries_cloudagent.multitenant." - "askar_profile_manager.AskarProfileMultitenantManager" + "single_wallet_askar_manager.SingleWalletAskarMultitenantManager" ) MANAGER_TYPES = { "basic": "aries_cloudagent.multitenant.manager.MultitenantManager", - "askar-profile": askar_profile_manager_path, + "single-wallet-askar": single_wallet_askar_manager_path, } def __init__(self, root_profile): @@ -34,9 +34,8 @@ def __init__(self, root_profile): def provide(self, settings: BaseSettings, injector: BaseInjector): """Create the multitenant manager instance.""" - multitenant_wallet_type = "multitenant.wallet_type" manager_type = settings.get_value( - multitenant_wallet_type, default="basic" + "multitenant.wallet_type", default="basic" ).lower() manager_class = self.MANAGER_TYPES.get(manager_type, manager_type) diff --git a/aries_cloudagent/multitenant/askar_profile_manager.py b/aries_cloudagent/multitenant/single_wallet_askar_manager.py similarity index 96% rename from aries_cloudagent/multitenant/askar_profile_manager.py rename to aries_cloudagent/multitenant/single_wallet_askar_manager.py index 9946535b84..1744adfb09 100644 --- a/aries_cloudagent/multitenant/askar_profile_manager.py +++ b/aries_cloudagent/multitenant/single_wallet_askar_manager.py @@ -9,11 +9,11 @@ from ..core.profile import ( Profile, ) -from ..multitenant.base import BaseMultitenantManager from ..wallet.models.wallet_record import WalletRecord +from .base import BaseMultitenantManager -class AskarProfileMultitenantManager(BaseMultitenantManager): +class SingleWalletAskarMultitenantManager(BaseMultitenantManager): """Class for handling askar profile multitenancy.""" DEFAULT_MULTITENANT_WALLET_NAME = "multitenant_sub_wallet" @@ -92,9 +92,7 @@ async def get_wallet_profile( profile_context = self._multitenant_profile.context.copy() if provision: - await self._multitenant_profile.store.create_profile( - wallet_record.wallet_id - ) + await self._multitenant_profile.store.create_profile(wallet_record.wallet_id) extra_settings = { "admin.webhook_urls": self.get_webhook_urls(base_context, wallet_record), diff --git a/aries_cloudagent/multitenant/tests/test_manager_provider.py b/aries_cloudagent/multitenant/tests/test_manager_provider.py index bacf0c18e6..d547a64463 100644 --- a/aries_cloudagent/multitenant/tests/test_manager_provider.py +++ b/aries_cloudagent/multitenant/tests/test_manager_provider.py @@ -1,9 +1,9 @@ from unittest import IsolatedAsyncioTestCase -from ...config.injection_context import InjectionContext from ...config.base import InjectionError -from ..manager_provider import MultitenantManagerProvider +from ...config.injection_context import InjectionContext from ...core.in_memory import InMemoryProfile +from ..manager_provider import MultitenantManagerProvider class TestProfileManagerProvider(IsolatedAsyncioTestCase): @@ -21,11 +21,11 @@ async def test_provide_askar_profile_manager(self): profile = InMemoryProfile.test_profile() provider = MultitenantManagerProvider(profile) context = InjectionContext() - context.settings["multitenant.wallet_type"] = "askar-profile" + context.settings["multitenant.wallet_type"] = "single-wallet-askar" self.assertEqual( provider.provide(context.settings, context.injector).__class__.__name__, - "AskarProfileMultitenantManager", + "SingleWalletAskarMultitenantManager", ) async def test_invalid_manager_type(self): diff --git a/aries_cloudagent/multitenant/tests/test_askar_profile_manager.py b/aries_cloudagent/multitenant/tests/test_single_wallet_askar_manager.py similarity index 87% rename from aries_cloudagent/multitenant/tests/test_askar_profile_manager.py rename to aries_cloudagent/multitenant/tests/test_single_wallet_askar_manager.py index 30892c1b2b..32188b7f67 100644 --- a/aries_cloudagent/multitenant/tests/test_askar_profile_manager.py +++ b/aries_cloudagent/multitenant/tests/test_single_wallet_askar_manager.py @@ -1,16 +1,16 @@ import asyncio - from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from ...config.injection_context import InjectionContext from ...core.in_memory import InMemoryProfile from ...messaging.responder import BaseResponder from ...wallet.models.wallet_record import WalletRecord -from ..askar_profile_manager import AskarProfileMultitenantManager +from ..single_wallet_askar_manager import SingleWalletAskarMultitenantManager -class TestAskarProfileMultitenantManager(IsolatedAsyncioTestCase): +class TestSingleWalletAskarMultitenantManager(IsolatedAsyncioTestCase): DEFAULT_MULTIENANT_WALLET_NAME = "multitenant_sub_wallet" async def asyncSetUp(self): @@ -20,7 +20,7 @@ async def asyncSetUp(self): self.responder = mock.CoroutineMock(send=mock.CoroutineMock()) self.context.injector.bind_instance(BaseResponder, self.responder) - self.manager = AskarProfileMultitenantManager(self.profile) + self.manager = SingleWalletAskarMultitenantManager(self.profile) async def test_get_wallet_profile_should_open_store_and_return_profile_with_wallet_context( self, @@ -42,9 +42,9 @@ async def test_get_wallet_profile_should_open_store_and_return_profile_with_wall ) with mock.patch( - "aries_cloudagent.multitenant.askar_profile_manager.wallet_config" + "aries_cloudagent.multitenant.single_wallet_askar_manager.wallet_config" ) as wallet_config, mock.patch( - "aries_cloudagent.multitenant.askar_profile_manager.AskarProfile", + "aries_cloudagent.multitenant.single_wallet_askar_manager.AskarProfile", ) as AskarProfile: sub_wallet_profile_context = InjectionContext() sub_wallet_profile = AskarProfile(None, None) @@ -74,9 +74,7 @@ def side_effect(context, provision): sub_wallet_profile.opened, sub_wallet_profile_context, profile_id="test" ) assert sub_wallet_profile_context.settings.get("wallet.seed") == "test_seed" - assert ( - sub_wallet_profile_context.settings.get("wallet.rekey") == "test_rekey" - ) + assert sub_wallet_profile_context.settings.get("wallet.rekey") == "test_rekey" assert sub_wallet_profile_context.settings.get("wallet.name") == "test_name" assert sub_wallet_profile_context.settings.get("wallet.type") == "test_type" assert sub_wallet_profile_context.settings.get("mediation.open") is True @@ -115,9 +113,9 @@ async def test_get_anoncreds_wallet_profile_should_open_store_and_return_anoncre ) with mock.patch( - "aries_cloudagent.multitenant.askar_profile_manager.wallet_config" + "aries_cloudagent.multitenant.single_wallet_askar_manager.wallet_config" ) as wallet_config, mock.patch( - "aries_cloudagent.multitenant.askar_profile_manager.AskarAnoncredsProfile", + "aries_cloudagent.multitenant.single_wallet_askar_manager.AskarAnoncredsProfile", ) as AskarAnoncredsProfile: sub_wallet_profile_context = InjectionContext() sub_wallet_profile = AskarAnoncredsProfile(None, None) @@ -141,7 +139,7 @@ async def test_get_wallet_profile_should_create_profile(self): create_profile_stub.set_result("") with mock.patch( - "aries_cloudagent.multitenant.askar_profile_manager.AskarProfile" + "aries_cloudagent.multitenant.single_wallet_askar_manager.AskarProfile" ) as AskarProfile: sub_wallet_profile = AskarProfile(None, None) sub_wallet_profile.context.copy.return_value = InjectionContext() @@ -164,10 +162,10 @@ async def test_get_wallet_profile_should_use_custom_subwallet_name(self): ) with mock.patch( - "aries_cloudagent.multitenant.askar_profile_manager.wallet_config" + "aries_cloudagent.multitenant.single_wallet_askar_manager.wallet_config" ) as wallet_config: with mock.patch( - "aries_cloudagent.multitenant.askar_profile_manager.AskarProfile" + "aries_cloudagent.multitenant.single_wallet_askar_manager.AskarProfile" ) as AskarProfile: sub_wallet_profile = AskarProfile(None, None) sub_wallet_profile.context.copy.return_value = InjectionContext() @@ -177,9 +175,7 @@ def side_effect(context, provision): wallet_config.side_effect = side_effect - await self.manager.get_wallet_profile( - self.profile.context, wallet_record - ) + await self.manager.get_wallet_profile(self.profile.context, wallet_record) wallet_config.assert_called_once() assert ( @@ -200,7 +196,7 @@ async def test_open_profiles(self): create_profile_stub = asyncio.Future() create_profile_stub.set_result("") with mock.patch( - "aries_cloudagent.multitenant.askar_profile_manager.AskarProfile" + "aries_cloudagent.multitenant.single_wallet_askar_manager.AskarProfile" ) as AskarProfile: sub_wallet_profile = AskarProfile(None, None) sub_wallet_profile.context.copy.return_value = InjectionContext() diff --git a/docs/features/Multitenancy.md b/docs/features/Multitenancy.md index c85756068a..135f55656b 100644 --- a/docs/features/Multitenancy.md +++ b/docs/features/Multitenancy.md @@ -64,6 +64,12 @@ multitenant-admin: true jwt-secret: Something very secret ``` +##### Single Wallet vs Multiple Wallets + +With askar wallets it's possible to have all tenant wallets in a single wallet or each have an individual wallet. The default is to have each tenant in a separate wallet. This is done to keep the wallets separate and to allow for more flexibility in the future. If you want to have all tenants in a single wallet you can set the `multitenancy-config` with the value `{"wallet_type": "single-wallet-askar"}`. If you want to explicitly set the wallet type for each tenant you can do so by setting the `multitenancy-config` with the value `{"wallet_type": "basic"}`. See .vscode-sample/multitenant-admin.yml for an example. + +```yaml + ## Multi-tenant Admin API The multi-tenant admin API allows you to manage wallets in ACA-Py. Only the base wallet can manage wallets, so you can't for example create a wallet in the context of sub wallet (using the `Authorization` header as specified in [Authentication](#authentication)).