From 8c810dfee26f0a65605c720510ba1a618851eb2b Mon Sep 17 00:00:00 2001 From: jamshale <31809382+jamshale@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:24:41 -0700 Subject: [PATCH] Tenant Import Feature (#55) * Add tenant import feature Signed-off-by: jamshale * Add additonal configs and raw key support Signed-off-by: jamshale --------- Signed-off-by: jamshale --- askar_tools/README.md | 37 ++- askar_tools/__main__.py | 147 +++++++++++- askar_tools/exporter.py | 17 +- askar_tools/key_methods.py | 7 + askar_tools/multi_wallet_converter.py | 12 +- askar_tools/pg_connection.py | 4 +- askar_tools/tenant_importer.py | 225 ++++++++++++++++++ askar_tools/tests/e2e/cases.py | 5 + askar_tools/tests/e2e/containers.py | 14 +- askar_tools/tests/e2e/test_export.py | 61 +++-- .../tests/e2e/test_mt_convert_to_mw.py | 17 +- askar_tools/tests/e2e/test_tenant_import.py | 117 +++++++++ 12 files changed, 613 insertions(+), 50 deletions(-) create mode 100644 askar_tools/key_methods.py create mode 100644 askar_tools/tenant_importer.py create mode 100644 askar_tools/tests/e2e/test_tenant_import.py diff --git a/askar_tools/README.md b/askar_tools/README.md index cb98e23..8ffb8e3 100644 --- a/askar_tools/README.md +++ b/askar_tools/README.md @@ -10,7 +10,6 @@ poetry install ``` - ### Export Wallet: * Exports a wallet into a file with a readable json format. This can be useful for debugging or for sharing wallet information with others. @@ -22,11 +21,13 @@ poetry install poetry run askar-tools \ --strategy export \ --uri postgres://:@:/ \ - --base-wallet-name \ - --base-wallet-key + --wallet-name \ + --wallet-key \ + --wallet-key-derivation-method \ + --export-filename ``` -### Multitenant Wallet - Switch from single wallet to multi wallet: +### Multi-tenant Wallet - Switch from single wallet to multi wallet: ##### Prerequisites: Backup sub-wallet. This operation will delete the sub-wallet when finished. If the wallet is broken for some reason you will not be able to recover it without a backup. @@ -34,7 +35,7 @@ poetry install * Converts the profiles in the sub-wallet to individual wallets and databases. * After completion, the sub-wallet will be deleted and the deployment should no longer use the `--multitenancy-config '{"wallet_type": "single-wallet-askar"}'` configuration. -- `export` (Output the contents of a wallet to a json file): +- `mt-convert-to-mw` (Convert from single wallet to multi-wallet multi-tenant agent): ``` poetry run askar-tools \ @@ -42,5 +43,31 @@ poetry install --uri postgres://:@:/ \ --wallet-name \ --wallet-key \ + --wallet-key-derivation-method \ --multitenant-sub-wallet-name + ``` + +### Import Wallet: + + * Imports a wallet from a database location into a multi-tenant multi-wallet admin and database location. + +- `tenant-import` (Import a wallet into a multi-wallet multi-tenant agent): + + ``` + poetry run askar-tools \ + --strategy tenant-import \ + --uri postgres://:@:/ \ + --wallet-name \ + --wallet-key \ + --wallet-key-derivation-method \ + --tenant-uri postgres://:@:/ \ + --tenant-wallet-name \ + --tenant-wallet-key \ + --tenant-wallet-key-derivation-method \ + --tenant-wallet-type \ + --tenant-label \ + --tenant-image-url \ + --tenant-webhook-urls \ + --tenant-extra-settings \ + --tenant-dispatch-type ``` \ No newline at end of file diff --git a/askar_tools/__main__.py b/askar_tools/__main__.py index 8214bfe..810d677 100644 --- a/askar_tools/__main__.py +++ b/askar_tools/__main__.py @@ -11,20 +11,25 @@ from .multi_wallet_converter import MultiWalletConverter from .pg_connection import PgConnection from .sqlite_connection import SqliteConnection +from .tenant_importer import TenantImporter def config(): """Parse command line arguments.""" parser = argparse.ArgumentParser("askar-wallet-tools") + + # Strategy parser.add_argument( "--strategy", required=True, - choices=["export", "mt-convert-to-mw"], + choices=["export", "mt-convert-to-mw", "tenant-import"], help=( "Specify migration strategy depending on database type, wallet " "management mode, and agent type." ), ) + + # Main wallet parser.add_argument( "--uri", required=True, @@ -32,6 +37,7 @@ def config(): ) parser.add_argument( "--wallet-name", + required=True, type=str, help=( "Specify name of wallet to be migrated for DatabasePerWallet " @@ -40,12 +46,30 @@ def config(): ) parser.add_argument( "--wallet-key", + required=True, type=str, help=( "Specify key corresponding to the given name of the wallet to " "be migrated for database per wallet (export) migration strategy." ), ) + parser.add_argument( + "--wallet-key-derivation-method", + type=str, + help=("Specify key derivation method for the wallet. Default is 'ARGON2I_MOD'."), + ) + + # Export + parser.add_argument( + "--export-filename", + type=str, + help=( + "Specify the filename to export the data to. Default is 'wallet_export.json'." + ), + default="wallet_export.json", + ) + + # Multiwallet conversion parser.add_argument( "--multitenant-sub-wallet-name", type=str, @@ -55,13 +79,75 @@ def config(): ), default="multitenant_sub_wallet", ) + + # Tenant import + parser.add_argument( + "--tenant-uri", + help=("Specify URI of the tenant database to be imported."), + ) + parser.add_argument( + "--tenant-wallet-name", + type=str, + help=("Specify name of tenant wallet to be imported."), + ) + parser.add_argument( + "--tenant-wallet-key", + type=str, + help=("Specify key corresponding of the tenant wallet to be imported."), + ) + parser.add_argument( + "--tenant-wallet-key-derivation-method", + type=str, + help=( + "Specify key derivation method for the tenant wallet. Default is 'ARGON2I_MOD'." + ), + ) + parser.add_argument( + "--tenant-wallet-type", + type=str, + help=( + """Specify the wallet type of the tenant wallet. Either 'askar' + or 'askar-anoncreds'. Default is 'askar'.""" + ), + ) + parser.add_argument( + "--tenant-label", + type=str, + help=("Specify the label for the tenant wallet."), + ) + parser.add_argument( + "--tenant-image-url", + type=str, + help=("Specify the image URL for the tenant wallet."), + ) + parser.add_argument( + "--tenant-webhook-urls", + type=list, + help=("Specify the webhook URLs for the tenant wallet."), + ) + parser.add_argument( + "--tenant-extra-settings", + type=dict, + help=("Specify extra settings for the tenant wallet."), + ) + parser.add_argument( + "--tenant-dispatch-type", + type=str, + help=("Specify the dispatch type for the tenant wallet."), + ) + args, _ = parser.parse_known_args(sys.argv[1:]) - if args.strategy == "export": - if not args.wallet_name: - raise ValueError("Wallet name required for export strategy") - if not args.wallet_key: - raise ValueError("Wallet key required for export strategy") + if args.strategy == "tenant-import": + if ( + not args.tenant_uri + or not args.tenant_wallet_name + or not args.tenant_wallet_key + ): + parser.error( + """For tenant-import strategy, tenant-uri, tenant-wallet-name, and + tenant-wallet-key are required.""" + ) return args @@ -71,7 +157,19 @@ async def main( uri: str, wallet_name: Optional[str] = None, wallet_key: Optional[str] = None, + wallet_key_derivation_method: Optional[str] = "ARGON2I_MOD", multitenant_sub_wallet_name: Optional[str] = "multitenant_sub_wallet", + tenant_uri: Optional[str] = None, + tenant_wallet_name: Optional[str] = None, + tenant_wallet_key: Optional[str] = None, + tenant_wallet_type: Optional[str] = "askar", + tenant_wallet_key_derivation_method: Optional[str] = "ARGON2I_MOD", + tenant_label: Optional[str] = None, + tenant_image_url: Optional[str] = None, + tenant_webhook_urls: Optional[list] = None, + tenant_extra_settings: Optional[dict] = None, + tenant_dispatch_type: Optional[str] = "default", + export_filename: Optional[str] = "wallet_export.json", ): """Run the main function.""" logging.basicConfig(level=logging.WARN) @@ -88,16 +186,49 @@ async def main( # Strategy setup if strategy == "export": await conn.connect() - print("wallet_name", wallet_name) - method = Exporter(conn=conn, wallet_name=wallet_name, wallet_key=wallet_key) + method = Exporter( + conn=conn, + wallet_name=wallet_name, + wallet_key=wallet_key, + wallet_key_derivation_method=wallet_key_derivation_method, + export_filename=export_filename, + ) elif strategy == "mt-convert-to-mw": await conn.connect() method = MultiWalletConverter( conn=conn, wallet_name=wallet_name, wallet_key=wallet_key, + wallet_key_derivation_method=wallet_key_derivation_method, sub_wallet_name=multitenant_sub_wallet_name, ) + elif strategy == "tenant-import": + tenant_parsed = urlparse(tenant_uri) + if tenant_parsed.scheme == "sqlite": + tenant_conn = SqliteConnection(tenant_uri) + elif tenant_parsed.scheme == "postgres": + tenant_conn = PgConnection(tenant_uri) + else: + raise ValueError("Unexpected tenant DB URI scheme") + + await conn.connect() + await tenant_conn.connect() + method = TenantImporter( + admin_conn=conn, + admin_wallet_name=wallet_name, + admin_wallet_key=wallet_key, + admin_wallet_key_derivation_method=wallet_key_derivation_method, + tenant_conn=tenant_conn, + tenant_wallet_name=tenant_wallet_name, + tenant_wallet_key=tenant_wallet_key, + tenant_wallet_type=tenant_wallet_type, + tenant_wallet_key_derivation_method=tenant_wallet_key_derivation_method, + tenant_label=tenant_label, + tenant_image_url=tenant_image_url, + tenant_webhook_urls=tenant_webhook_urls, + tenant_extra_settings=tenant_extra_settings, + tenant_dispatch_type=tenant_dispatch_type, + ) else: raise Exception("Invalid strategy") diff --git a/askar_tools/exporter.py b/askar_tools/exporter.py index 77655e1..d64e44a 100644 --- a/askar_tools/exporter.py +++ b/askar_tools/exporter.py @@ -5,6 +5,7 @@ from aries_askar import Store +from .key_methods import KEY_METHODS from .pg_connection import PgConnection from .sqlite_connection import SqliteConnection @@ -17,6 +18,8 @@ def __init__( conn: SqliteConnection | PgConnection, wallet_name: str, wallet_key: str, + wallet_key_derivation_method: str = "ARGON2I_MOD", + export_filename: str = "wallet_export.json", ): """Initialize the Exporter object. @@ -24,10 +27,14 @@ def __init__( conn: The connection object. wallet_name: The name of the wallet. wallet_key: The key for the wallet. + wallet_key_derivation_method: The key derivation method for the wallet. + export_filename: The name of the export file. """ self.conn = conn self.wallet_name = wallet_name self.wallet_key = wallet_key + self.wallet_key_derivation_method = wallet_key_derivation_method + self.export_filename = export_filename async def _get_decoded_items_and_tags(self, store): scan = store.scan() @@ -51,10 +58,14 @@ async def _get_decoded_items_and_tags(self, store): async def export(self): """Export the wallet data.""" - print("Exporting wallet to wallet_export.json...") + print(f"Exporting wallet to {self.export_filename}...") tables = {"config": {}, "items": {}, "profiles": {}} - store = await Store.open(self.conn.uri, pass_key=self.wallet_key) + store = await Store.open( + self.conn.uri, + pass_key=self.wallet_key, + key_method=KEY_METHODS[self.wallet_key_derivation_method], + ) tables["items"] = await self._get_decoded_items_and_tags(store) @@ -62,7 +73,7 @@ async def export(self): tables["profiles"] = await self.conn.get_profiles() - with open("wallet_export.json", "w") as json_file: + with open(self.export_filename, "w") as json_file: json.dump(tables, json_file, indent=4) await store.close() diff --git a/askar_tools/key_methods.py b/askar_tools/key_methods.py new file mode 100644 index 0000000..fd737ae --- /dev/null +++ b/askar_tools/key_methods.py @@ -0,0 +1,7 @@ +""".Key methods for Askar wallet.""" + +KEY_METHODS = { + "RAW": "RAW", + "ARGON2I_INT": "kdf:argon2i:int", + "ARGON2I_MOD": "kdf:argon2i:mod", +} diff --git a/askar_tools/multi_wallet_converter.py b/askar_tools/multi_wallet_converter.py index 4f30177..942ee6c 100644 --- a/askar_tools/multi_wallet_converter.py +++ b/askar_tools/multi_wallet_converter.py @@ -3,15 +3,10 @@ from aries_askar import Store from .error import ConversionError +from .key_methods import KEY_METHODS from .pg_connection import PgConnection from .sqlite_connection import SqliteConnection -KEY_METHODS = { - "KEY_DERIVATION_RAW": "RAW", - "KEY_DERIVATION_ARGON2I_INT": "kdf:argon2i:int", - "KEY_DERIVATION_ARGON2I_MOD": "kdf:argon2i:mod", -} - class MultiWalletConverter: """Util class for converting multi-tenant wallets between single wallet and multi wallet.""" # noqa: E501 @@ -21,6 +16,7 @@ def __init__( conn: SqliteConnection | PgConnection, wallet_name: str, wallet_key: str, + wallet_key_derivation_method: str, sub_wallet_name: str, ): """Initialize the MultiWalletConverter instance. @@ -29,11 +25,13 @@ def __init__( conn (SqliteConnection): The SQLite connection object. wallet_name (str): The name of the wallet. wallet_key (str): The key for the wallet. + wallet_key_derivation_method (str): The key derivation method for the wallet. sub_wallet_name (str): The name of the sub wallet. """ self.conn = conn self.admin_wallet_name = wallet_name self.admin_wallet_key = wallet_key + self.wallet_key_derivation_method = wallet_key_derivation_method self.sub_wallet_name = sub_wallet_name def get_wallet_records(self, entries): @@ -87,7 +85,7 @@ async def convert_single_wallet_to_multi_wallet(self): ) key_method = KEY_METHODS.get( wallet_record["settings"].get( - "wallet.key_derivation_method", "KEY_DERIVATION_ARGON2I_MOD" + "wallet.key_derivation_method", "ARGON2I_MOD" ) ) print( diff --git a/askar_tools/pg_connection.py b/askar_tools/pg_connection.py index a26fe88..cc2a164 100644 --- a/askar_tools/pg_connection.py +++ b/askar_tools/pg_connection.py @@ -86,7 +86,7 @@ async def get_profiles(self): return result - async def create_database(self, base_wallet_name, sub_wallet_name): + async def create_database(self, admin_wallet_name, sub_wallet_name): """Create an postgres database.""" await self._conn.execute( f""" @@ -94,7 +94,7 @@ async def create_database(self, base_wallet_name, sub_wallet_name): """ ) - async def remove_wallet(self, base_wallet_name, sub_wallet_name): + async def remove_wallet(self, admin_wallet_name, sub_wallet_name): """Remove the postgres wallet.""" # Kill any connections to the database await self._conn.execute( diff --git a/askar_tools/tenant_importer.py b/askar_tools/tenant_importer.py new file mode 100644 index 0000000..584d923 --- /dev/null +++ b/askar_tools/tenant_importer.py @@ -0,0 +1,225 @@ +"""This module contains the Tenant Importer class.""" + +import time +import uuid + +from aries_askar import Store + +from .key_methods import KEY_METHODS +from .pg_connection import PgConnection +from .sqlite_connection import SqliteConnection + + +class TenantImporter: + """The Tenant Importer class.""" + + def __init__( + self, + admin_conn: SqliteConnection | PgConnection, + admin_wallet_name: str, + admin_wallet_key: str, + tenant_conn: SqliteConnection | PgConnection, + tenant_wallet_name: str, + tenant_wallet_key: str, + tenant_wallet_type: str = "askar", + tenant_label: str = None, + tenant_image_url: str = None, + tenant_webhook_urls: list = None, + tenant_extra_settings: dict = None, + tenant_dispatch_type: str = "default", + admin_wallet_key_derivation_method: str = "ARGON2I_MOD", + tenant_wallet_key_derivation_method: str = "ARGON2I_MOD", + ): + """Initialize the Tenant Importer object. + + Args: + admin_conn: The admin connection object. + admin_wallet_name: The name of the admin wallet. + admin_wallet_key: The key for the admin wallet. + tenant_conn: The tenant connection object. + tenant_wallet_name: The name of the tenant wallet. + tenant_wallet_key: The key for the tenant wallet. + tenant_wallet_type: The type of the tenant wallet. + tenant_label: The label for the tenant wallet. + tenant_image_url: The image URL for the tenant wallet. + tenant_webhook_urls: The webhook URLs for the tenant wallet. + tenant_extra_settings: Extra settings for the tenant wallet. + tenant_dispatch_type: The dispatch type for the tenant wallet. + admin_wallet_key_derivation_method: The key derivation method for the admin + wallet. + tenant_wallet_key_derivation_method: The key derivation method for the tenant + wallet. + """ + self.admin_conn = admin_conn + self.admin_wallet_name = admin_wallet_name + self.admin_wallet_key = admin_wallet_key + self.tenant_conn = tenant_conn + self.tenant_wallet_name = tenant_wallet_name + self.tenant_wallet_key = tenant_wallet_key + self.tenant_wallet_type = tenant_wallet_type + self.tenant_label = tenant_label + self.tenant_image_url = tenant_image_url + self.tenant_webhook_urls = tenant_webhook_urls + self.tenant_extra_settings = tenant_extra_settings + self.tenant_dispatch_type = tenant_dispatch_type + self.admin_wallet_key_derivation_method = admin_wallet_key_derivation_method + self.tenant_wallet_key_derivation_method = tenant_wallet_key_derivation_method + + async def _create_tenant(self, wallet_id: str, admin_txn, current_time: str): + # Create wallet record in admin wallet + + value_json = { + "wallet_name": self.tenant_wallet_name, + "created_at": current_time, + "updated_at": current_time, + "settings": { + "wallet.type": self.tenant_wallet_type, + "wallet.name": self.tenant_wallet_name, + "wallet.key": self.tenant_wallet_key, + "wallet.id": wallet_id, + "wallet.key_derivation_method": KEY_METHODS[ + self.tenant_wallet_key_derivation_method + ], + "wallet.dispatch_type": self.tenant_dispatch_type, + }, + "key_management_mode": "managed", + "jwt_iat": current_time, + } + + if self.tenant_label: + value_json["settings"]["default_label"] = self.tenant_label + + if self.tenant_image_url: + value_json["settings"]["image_url"] = self.tenant_image_url + + if self.tenant_extra_settings: + value_json["settings"].update(self.tenant_extra_settings) + + if self.tenant_webhook_urls: + value_json["settings"]["wallet.webhook_urls"] = self.tenant_webhook_urls + + await admin_txn.insert( + category="wallet_record", + name=wallet_id, + value_json=value_json, + tags={ + "wallet_name": self.tenant_wallet_name, + }, + ) + + async def _create_forward_routes( + self, tenant_wallet: Store, admin_txn, wallet_id: str, current_time: str + ): + # Import DIDs, connections, and DID keys in forward route table + tenant_did_scan = tenant_wallet.scan(category="did") + tenant_dids = await tenant_did_scan.fetch_all() + for did in tenant_dids: + print(f"Importing DID: {did.value_json}") + await admin_txn.insert( + category="forward_route", + name=str(uuid.uuid4()), + value_json={ + "recipient_key": did.value_json["verkey"], + "wallet_id": wallet_id, + "created_at": current_time, + "updated_at": current_time, + "connection_id": None, + }, + tags={ + "recipient_key": did.value_json["verkey"], + "role": "server", + "wallet_id": wallet_id, + }, + ) + tenant_connection_scan = tenant_wallet.scan(category="connection") + tenant_connections = await tenant_connection_scan.fetch_all() + for connection in tenant_connections: + print(f"Importing connection: {connection.value_json}") + await admin_txn.insert( + category="forward_route", + name=str(uuid.uuid4()), + value_json={ + "recipient_key": connection.value_json["invitation_key"], + "wallet_id": wallet_id, + "created_at": current_time, + "updated_at": current_time, + "connection_id": None, + }, + tags={ + "recipient_key": connection.value_json["invitation_key"], + "role": "server", + "wallet_id": wallet_id, + }, + ) + tenant_did_key_scan = tenant_wallet.scan(category="did_key") + tenant_did_keys = await tenant_did_key_scan.fetch_all() + for did_key in tenant_did_keys: + print(f"Importing did key: {did_key.value}") + await admin_txn.insert( + category="forward_route", + name=str(uuid.uuid4()), + value_json={ + "recipient_key": did_key.tags["key"], + "wallet_id": wallet_id, + "created_at": current_time, + "updated_at": current_time, + "connection_id": None, + }, + tags={ + "recipient_key": did_key.tags["key"], + "role": "server", + "wallet_id": wallet_id, + }, + ) + + async def import_tenant(self): + """Import the tenant wallet into the admin wallet.""" + print("Importing tenant wallet into admin wallet") + + # Make wallet/db in admin location for tenant + await self.admin_conn.create_database( + admin_wallet_name=self.admin_wallet_name, + sub_wallet_name=self.tenant_wallet_name, + ) + # Copy the tenant wallet to the admin wallet location + tenant_wallet = await Store.open( + uri=self.tenant_conn.uri, + pass_key=self.tenant_wallet_key, + key_method=KEY_METHODS[self.tenant_wallet_key_derivation_method], + ) + await tenant_wallet.copy_to( + target_uri=self.admin_conn.uri.replace( + self.admin_wallet_name, self.tenant_wallet_name + ), + pass_key=self.tenant_wallet_key, + key_method=KEY_METHODS[self.tenant_wallet_key_derivation_method], + ) + + # Import the tenant wallet into the admin wallet + admin_store = await Store.open( + uri=self.admin_conn.uri, + pass_key=self.admin_wallet_key, + key_method=KEY_METHODS[self.admin_wallet_key_derivation_method], + ) + async with admin_store.transaction() as admin_txn: + wallet_id = str(uuid.uuid4()) + current_time = time.time() + await self._create_tenant( + wallet_id=wallet_id, + admin_txn=admin_txn, + current_time=current_time, + ) + await self._create_forward_routes( + tenant_wallet=tenant_wallet, + admin_txn=admin_txn, + wallet_id=wallet_id, + current_time=current_time, + ) + await admin_txn.commit() + + await self.admin_conn.close() + await self.tenant_conn.close() + + async def run(self): + """Run the importer.""" + await self.import_tenant() diff --git a/askar_tools/tests/e2e/cases.py b/askar_tools/tests/e2e/cases.py index 6eda53d..d65f53b 100644 --- a/askar_tools/tests/e2e/cases.py +++ b/askar_tools/tests/e2e/cases.py @@ -97,3 +97,8 @@ def __init__(self): class ExportTestCases(BaseTestCases): def __init__(self): self._cases = (self.connections(), self.credentials_with_revocation()) + + +class TenantImportTestCases(BaseTestCases): + def __init__(self): + self._cases = (self.connections(), self.credentials_with_revocation()) diff --git a/askar_tools/tests/e2e/containers.py b/askar_tools/tests/e2e/containers.py index 68d1f93..8af0716 100644 --- a/askar_tools/tests/e2e/containers.py +++ b/askar_tools/tests/e2e/containers.py @@ -99,11 +99,13 @@ def stop(self, container: Container): self.containers.remove(container) container.stop() - def postgres(self, port: int, volume: Optional[str] = None) -> Container: + def postgres( + self, port: int, volume: Optional[str] = None, name: Optional[str] = "postgres" + ) -> Container: """Create a postgres container.""" container = self.client.containers.run( self.POSTGRES_IMAGE, - name="postgres", + name=name, volumes={volume: {"bind": "/var/lib/postgresql/data", "mode": "rw,z"}} if volume else None, @@ -172,8 +174,9 @@ def acapy_sqlite( self, name: str, wallet_key: str, - admin_port: int, + wallet_key_derivation_method: str, wallet_type: str, + admin_port: int, volume_src: str, volume_dst: str, sub_wallet_src: Optional[str] = None, @@ -196,6 +199,7 @@ def acapy_sqlite( --wallet-type {wallet_type} --wallet-name {name} --wallet-key {wallet_key} + --wallet-key-derivation-method {wallet_key_derivation_method} --preserve-exchange-records --auto-provision """ @@ -237,8 +241,9 @@ def acapy_postgres( self, name: str, wallet_key: str, - admin_port: int, + wallet_key_derivation_method: str, wallet_type: str, + admin_port: int, postgres: Container, mwst: bool = False, mt: bool = False, @@ -259,6 +264,7 @@ def acapy_postgres( --wallet-type {wallet_type} --wallet-name {name} --wallet-key {wallet_key} + --wallet-key-derivation-method {wallet_key_derivation_method} --wallet-storage-type postgres_storage --preserve-exchange-records --auto-provision diff --git a/askar_tools/tests/e2e/test_export.py b/askar_tools/tests/e2e/test_export.py index 5bc1aa2..4258647 100644 --- a/askar_tools/tests/e2e/test_export.py +++ b/askar_tools/tests/e2e/test_export.py @@ -16,10 +16,15 @@ async def test_export_pg(self, containers: Containers): # Prepare postgres = containers.postgres(5432) alice_container = containers.acapy_postgres( - "alice", "insecure", 3001, "askar", postgres + "alice", + "3cAZj1hPvUhKeBkzCKPTHhTxRRmYv5abDbjmaYwtk6Nf", + "RAW", + "askar", + 3001, + postgres, ) bob_container = containers.acapy_postgres( - "bob", "insecure", 3002, "askar", postgres + "bob", "insecure", "kdf:argon2i:mod", "askar", 3002, postgres ) containers.wait_until_healthy(alice_container) containers.wait_until_healthy(bob_container) @@ -35,21 +40,33 @@ async def test_export_pg(self, containers: Containers): strategy="export", uri="postgres://postgres:mysecretpassword@localhost:5432/alice", wallet_name="alice", + wallet_key="3cAZj1hPvUhKeBkzCKPTHhTxRRmYv5abDbjmaYwtk6Nf", + wallet_key_derivation_method="RAW", + export_filename="wallet_export_alice.json", + ) + await main( + strategy="export", + uri="postgres://postgres:mysecretpassword@localhost:5432/bob", + wallet_name="bob", wallet_key="insecure", + export_filename="wallet_export_bob.json", ) - found_export_file = False + found_alice_export_file = False + found_bob_export_file = False for root, dirs, files in os.walk("../"): - if "wallet_export.json" in files: - found_export_file = True - # TODO: Delete the file + if "wallet_export_alice.json" in files: + found_alice_export_file = True + if "wallet_export_bob.json" in files: + found_bob_export_file = True containers.stop(alice_container) containers.stop(bob_container) containers.stop(postgres) # Assert: TODO: check file contents - assert found_export_file + assert found_alice_export_file + assert found_bob_export_file @pytest.mark.asyncio @pytest.mark.e2e @@ -58,9 +75,10 @@ async def test_export_sqlite(self, containers: Containers, tmp_path_factory): containers.fix_permissions(alice_volume_path, user=1001, group=1001) alice_container = containers.acapy_sqlite( "alice", - "insecure", - 3001, + "3cAZj1hPvUhKeBkzCKPTHhTxRRmYv5abDbjmaYwtk6Nf", + "RAW", "askar", + 3001, alice_volume_path, "/home/aries/.aries_cloudagent/wallet/alice", ) @@ -69,8 +87,9 @@ async def test_export_sqlite(self, containers: Containers, tmp_path_factory): bob_container = containers.acapy_sqlite( "bob", "insecure", - 3002, + "kdf:argon2i:mod", "askar", + 3002, bob_volume_path, "/home/aries/.aries_cloudagent/wallet/bob", ) @@ -92,17 +111,29 @@ async def test_export_sqlite(self, containers: Containers, tmp_path_factory): strategy="export", uri=f"sqlite://{alice_volume_path}/sqlite.db", wallet_name="alice", + wallet_key="3cAZj1hPvUhKeBkzCKPTHhTxRRmYv5abDbjmaYwtk6Nf", + wallet_key_derivation_method="RAW", + export_filename="wallet_export_alice.json", + ) + await main( + strategy="export", + uri=f"sqlite://{bob_volume_path}/sqlite.db", + wallet_name="bob", wallet_key="insecure", + export_filename="wallet_export_alice.json", ) - found_export_file = False + found_alice_export_file = False + found_bob_export_file = False for root, dirs, files in os.walk("../"): - if "wallet_export.json" in files: - found_export_file = True - # TODO: Delete the file + if "wallet_export_alice.json" in files: + found_alice_export_file = True + if "wallet_export_bob.json" in files: + found_bob_export_file = True containers.stop(alice_container) containers.stop(bob_container) # Assert: TODO: check file contents - assert found_export_file + assert found_alice_export_file + assert found_bob_export_file diff --git a/askar_tools/tests/e2e/test_mt_convert_to_mw.py b/askar_tools/tests/e2e/test_mt_convert_to_mw.py index d8f56e4..8d36ea3 100644 --- a/askar_tools/tests/e2e/test_mt_convert_to_mw.py +++ b/askar_tools/tests/e2e/test_mt_convert_to_mw.py @@ -8,7 +8,7 @@ from .containers import Containers -class TestPgMtConvertToMw(WalletTypeToBeTested): +class TestMultitenantConvertToMultiwallet(WalletTypeToBeTested): @pytest.mark.asyncio @pytest.mark.e2e async def test_conversion_pg(self, containers: Containers): @@ -18,8 +18,9 @@ async def test_conversion_pg(self, containers: Containers): admin_container = containers.acapy_postgres( "admin", "insecure", - 3001, + "kdf:argon2i:mod", "askar", + 3001, postgres, mwst=True, mt=True, @@ -35,7 +36,8 @@ async def test_conversion_pg(self, containers: Containers): json={ "label": "Alice", "wallet_name": "alice", - "wallet_key": "alice_insecure1", + "wallet_key": "3cAZj1hPvUhKeBkzCKPTHhTxRRmYv5abDbjmaYwtk6Nf", + "wallet_key_derivation": "RAW", "wallet_type": "askar", }, response=CreateWalletResponse, @@ -77,8 +79,9 @@ async def test_conversion_pg(self, containers: Containers): admin_container = containers.acapy_postgres( "admin", "insecure", - 3001, + "kdf:argon2i:mod", "askar", + 3001, postgres, mt=True, askar_profile=False, @@ -114,8 +117,9 @@ async def test_conversion_sqlite(self, containers: Containers, tmp_path_factory) admin_container = containers.acapy_sqlite( "admin", "insecure", - 3001, + "kdf:argon2i:mod", "askar", + 3001, admin_volume_path, "/home/aries/.aries_cloudagent/wallet/admin", sub_wallet_volume_path, @@ -133,7 +137,8 @@ async def test_conversion_sqlite(self, containers: Containers, tmp_path_factory) json={ "label": "Alice", "wallet_name": "alice", - "wallet_key": "alice_insecure1", + "wallet_key": "3cAZj1hPvUhKeBkzCKPTHhTxRRmYv5abDbjmaYwtk6Nf", + "wallet_key_derivation": "RAW", "wallet_type": "askar", }, response=CreateWalletResponse, diff --git a/askar_tools/tests/e2e/test_tenant_import.py b/askar_tools/tests/e2e/test_tenant_import.py new file mode 100644 index 0000000..3f338e3 --- /dev/null +++ b/askar_tools/tests/e2e/test_tenant_import.py @@ -0,0 +1,117 @@ +import pytest +from acapy_controller import Controller +from acapy_controller.models import CreateWalletResponse +from askar_tools.__main__ import main + +from . import WalletTypeToBeTested +from .cases import TenantImportTestCases +from .containers import Containers + + +class TestTenantImport(WalletTypeToBeTested): + @pytest.mark.asyncio + @pytest.mark.e2e + async def test_tenant_import_pg(self, containers: Containers): + # Prepare + admin_postgres = containers.postgres(5432, name="admin_postgres") + tenant_postgres = containers.postgres(5433, name="tenant_postgres") + # Create an admin container with a single wallet + admin_container = containers.acapy_postgres( + "admin", + "insecure", + "kdf:argon2i:mod", + "askar", + 3001, + admin_postgres, + mwst=True, + mt=True, + askar_profile=False, + ) + tenant_container = containers.acapy_postgres( + "tenant", + "3cAZj1hPvUhKeBkzCKPTHhTxRRmYv5abDbjmaYwtk6Nf", + "RAW", + "askar", + 3002, + tenant_postgres, + mwst=False, + mt=False, + ) + containers.wait_until_healthy(admin_container) + containers.wait_until_healthy(tenant_container) + + async with Controller("http://localhost:3001") as admin: + test_cases = TenantImportTestCases() + # Create sub wallet with admin + alice_wallet = await admin.post( + "/multitenancy/wallet", + json={ + "label": "Alice", + "wallet_name": "alice", + "wallet_key": "alice_insecure1", + "wallet_type": "askar", + }, + response=CreateWalletResponse, + ) + + # Start the alice subwallet controller and create the separate db tenant controller + async with Controller( + "http://localhost:3001", + wallet_id=alice_wallet.wallet_id, + subwallet_token=alice_wallet.token, + ) as alice, Controller( + "http://localhost:3002", + ) as tenant: + await test_cases.pre(alice, tenant) + + # Action the import + await main( + strategy="tenant-import", + uri="postgres://postgres:mysecretpassword@localhost:5432/admin", + wallet_name="admin", + wallet_key="insecure", + tenant_uri="postgres://postgres:mysecretpassword@localhost:5433/tenant", + tenant_wallet_name="tenant", + tenant_wallet_key="3cAZj1hPvUhKeBkzCKPTHhTxRRmYv5abDbjmaYwtk6Nf", + tenant_label="Tenant", + tenant_image_url="https://example.com/image.png", + tenant_extra_settings={"extra": "settings"}, + tenant_webhook_urls=["http://example.com/webhook"], + tenant_dispatch_type="default", + tenant_wallet_key_derivation_method="RAW", + ) + + async with Controller("http://localhost:3001") as admin: + # Get the tenant wallet id and token + wallets = await admin.get("/multitenancy/wallets") + + tenant_wallet_id = None + for wallet in wallets["results"]: + if wallet["settings"]["wallet.name"] == "tenant": + tenant_wallet_id = wallet["wallet_id"] + break + assert tenant_wallet_id is not None + + tenant_wallet_token = ( + await admin.post(f"/multitenancy/wallet/{tenant_wallet_id}/token") + )["token"] + + assert tenant_wallet_token is not None + + # Re-run the test cases with the new tenant wallet + test_cases = TenantImportTestCases() + async with Controller( + "http://localhost:3001", + wallet_id=alice_wallet.wallet_id, + subwallet_token=alice_wallet.token, + ) as alice, Controller( + "http://localhost:3001", + wallet_id=tenant_wallet_id, + subwallet_token=tenant_wallet_token, + ) as tenant: + await test_cases.pre(alice, tenant) + + containers.stop(admin_container) + containers.stop(admin_postgres) + containers.stop(tenant_container) + containers.stop(tenant_postgres)