Repository: hyperledger/aries-cloudagent-python

Issue:
Check for storage_config and storage_credentials are in proper escaped JSON format
This is in reference to #568 

I think it would be a huge timesaver to people working with k8 yaml files to deploy acapy if the postgres plugin intialization code did some check to see that the `storage_config` and `storage_credentials` passed in are in the proper format (escaped JSON string) before calling out to the static library. The error returned on bad parameters is error `210` and that doesn't give a good indication of what is going wrong.

```
E 2020-06-19T22:22:14.452583499Z 2020-06-19 22:22:14,452 aries_cloudagent.wallet.provider INFO Opening wallet type: indy
 
E 2020-06-19T22:22:14.461212446Z 2020-06-19 22:22:14,461 aries_cloudagent.wallet.plugin INFO Initializing postgres wallet
 
E 2020-06-19T22:22:14.478151798Z 2020-06-19 22:22:14,476 aries_cloudagent.wallet.plugin ERROR **Error unable to configure postgres stg: 210**
```

The proper format needs to end up resolving to the following on command line for `aca-py start`
```
--wallet-storage-config "{\"url\":\"localhost:5432\",\"wallet_scheme\":\"MultiWalletSingleTable\"}"
```
This also applies to `--wallet-storage-creds`

A fix can be added right in this code `aries_cloudagent/wallet/plugin.py`
```
def load_postgres_plugin(storage_config, storage_creds, raise_exc=False):
    """Load postgres dll and configure postgres wallet."""
    global LOADED, LOGGER

    if not LOADED:
        LOGGER.info("Initializing postgres wallet")
        stg_lib = cdll.LoadLibrary("libindystrgpostgres" + file_ext())
        result = stg_lib.postgresstorage_init()
        if result != 0:
            LOGGER.error("Error unable to load postgres wallet storage: %s", result)
            if raise_exc:
                raise OSError(f"Error unable to load postgres wallet storage: {result}")
            else:
                raise SystemExit(1)
        if "wallet_scheme" in storage_config:
            c_config = c_char_p(storage_config.encode("utf-8"))
            c_credentials = c_char_p(storage_creds.encode("utf-8"))
            result = stg_lib.init_storagetype(c_config, c_credentials)
            if result != 0:
                LOGGER.error("Error unable to configure postgres stg: %s", result)
                if raise_exc:
                    raise OSError(f"Error unable to configure postgres stg: {result}")
                else:
                    raise SystemExit(1)
        LOADED = True

        LOGGER.info("Success, loaded postgres wallet storage")
```

In addition, the argparse documentation should be updated to note fields such as `wallet_scheme` and discuss `admin_account` user requiring CREATEDB permissions.


Discussion:
Will try and submit a PR for this soon.

Resolving patch:
diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py
--- a/aries_cloudagent/config/argparse.py
+++ b/aries_cloudagent/config/argparse.py
@@ -787,9 +787,12 @@ def add_arguments(self, parser: ArgumentParser):
             "--wallet-storage-config",
             type=str,
             metavar="<storage-config>",
-            help="Specifies the storage configuration to use for the wallet.\
-            This is required if you are for using 'postgres_storage' wallet\
-            storage type. For example, '{\"url\":\"localhost:5432\"}'.",
+            help='Specifies the storage configuration to use for the wallet.\
+            This is required if you are for using \'postgres_storage\' wallet\
+            storage type. For example, \'{"url":"localhost:5432",\
+            "wallet_scheme":"MultiWalletSingleTable"}\'. This\
+            configuration maps to the indy sdk postgres plugin\
+            (PostgresConfig).',
         )
         parser.add_argument(
             "--wallet-storage-creds",
@@ -798,8 +801,11 @@ def add_arguments(self, parser: ArgumentParser):
             help='Specifies the storage credentials to use for the wallet.\
             This is required if you are for using \'postgres_storage\' wallet\
             For example, \'{"account":"postgres","password":\
-            "mysecretpassword","admin_account":"postgres","admin_password":\
-            "mysecretpassword"}\'',
+            "mysecretpassword","admin_account":"postgres",\
+            "admin_password":"mysecretpassword"}\'. This configuration maps\
+            to the indy sdk postgres plugin (PostgresCredentials). NOTE:\
+            admin_user must have the CREATEDB role or else initialization\
+            will fail.',
         )
         parser.add_argument(
             "--replace-public-did",
diff --git a/aries_cloudagent/wallet/plugin.py b/aries_cloudagent/wallet/plugin.py
--- a/aries_cloudagent/wallet/plugin.py
+++ b/aries_cloudagent/wallet/plugin.py
@@ -2,6 +2,7 @@
 
 import logging
 import platform
+import json
 from ctypes import cdll, c_char_p
 
 EXTENSION = {"darwin": ".dylib", "linux": ".so", "win32": ".dll", "windows": ".dll"}
@@ -20,6 +21,24 @@ def load_postgres_plugin(storage_config, storage_creds, raise_exc=False):
     global LOADED, LOGGER
 
     if not LOADED:
+        LOGGER.info(
+            "Checking input postgres storage_config and storage_creds arguments"
+        )
+        try:
+            json.loads(storage_config)
+            json.loads(storage_creds)
+        except json.decoder.JSONDecodeError:
+            LOGGER.error(
+                "Invalid stringified JSON input, check storage_config and storage_creds"
+            )
+            if raise_exc:
+                raise OSError(
+                    f"Invalid stringified JSON input,"
+                    "check storage_config and storage_creds"
+                )
+            else:
+                raise SystemExit(1)
+
         LOGGER.info("Initializing postgres wallet")
         stg_lib = cdll.LoadLibrary("libindystrgpostgres" + file_ext())
         result = stg_lib.postgresstorage_init()


Test patch:
diff --git a/aries_cloudagent/wallet/tests/test_plugin.py b/aries_cloudagent/wallet/tests/test_plugin.py
--- a/aries_cloudagent/wallet/tests/test_plugin.py
+++ b/aries_cloudagent/wallet/tests/test_plugin.py
@@ -16,7 +16,8 @@ async def test_file_ext(self):
         assert test_module.file_ext()
 
     def test_load_postgres_plugin(self):
-        storage_config = "wallet_scheme"
+        storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}'
+        storage_creds = '{"account":"test"}'
         mock_stg_lib = async_mock.MagicMock(
             postgresstorage_init=async_mock.MagicMock(return_value=0),
             init_storagetype=async_mock.MagicMock(return_value=0),
@@ -25,12 +26,13 @@ def test_load_postgres_plugin(self):
             test_module.cdll, "LoadLibrary", async_mock.Mock()
         ) as mock_load:
             mock_load.return_value = mock_stg_lib
-            test_module.load_postgres_plugin(storage_config, "creds")
+            test_module.load_postgres_plugin(storage_config, storage_creds)
 
             assert test_module.LOADED
 
     def test_load_postgres_plugin_init_x_raise(self):
-        storage_config = "wallet_scheme"
+        storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}'
+        storage_creds = '{"account":"test"}'
         mock_stg_lib = async_mock.MagicMock(
             postgresstorage_init=async_mock.MagicMock(return_value=2)
         )
@@ -40,12 +42,13 @@ def test_load_postgres_plugin_init_x_raise(self):
             mock_load.return_value = mock_stg_lib
             with self.assertRaises(OSError) as context:
                 test_module.load_postgres_plugin(
-                    storage_config, "creds", raise_exc=True
+                    storage_config, storage_creds, raise_exc=True
                 )
             assert "unable to load postgres" in str(context.exception)
 
     def test_load_postgres_plugin_init_x_exit(self):
-        storage_config = "wallet_scheme"
+        storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}'
+        storage_creds = '{"account":"test"}'
         mock_stg_lib = async_mock.MagicMock(
             postgresstorage_init=async_mock.MagicMock(return_value=2)
         )
@@ -55,11 +58,12 @@ def test_load_postgres_plugin_init_x_exit(self):
             mock_load.return_value = mock_stg_lib
             with self.assertRaises(SystemExit):
                 test_module.load_postgres_plugin(
-                    storage_config, "creds", raise_exc=False
+                    storage_config, storage_creds, raise_exc=False
                 )
 
     def test_load_postgres_plugin_config_x_raise(self):
-        storage_config = "wallet_scheme"
+        storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}'
+        storage_creds = '{"account":"test"}'
         mock_stg_lib = async_mock.MagicMock(
             postgresstorage_init=async_mock.MagicMock(return_value=0),
             init_storagetype=async_mock.MagicMock(return_value=2),
@@ -70,12 +74,13 @@ def test_load_postgres_plugin_config_x_raise(self):
             mock_load.return_value = mock_stg_lib
             with self.assertRaises(OSError) as context:
                 test_module.load_postgres_plugin(
-                    storage_config, "creds", raise_exc=True
+                    storage_config, storage_creds, raise_exc=True
                 )
             assert "unable to configure postgres" in str(context.exception)
 
     def test_load_postgres_plugin_config_x_exit(self):
-        storage_config = "wallet_scheme"
+        storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}'
+        storage_creds = '{"account":"test"}'
         mock_stg_lib = async_mock.MagicMock(
             postgresstorage_init=async_mock.MagicMock(return_value=0),
             init_storagetype=async_mock.MagicMock(return_value=2),
@@ -86,5 +91,38 @@ def test_load_postgres_plugin_config_x_exit(self):
             mock_load.return_value = mock_stg_lib
             with self.assertRaises(SystemExit):
                 test_module.load_postgres_plugin(
-                    storage_config, "creds", raise_exc=False
+                    storage_config, storage_creds, raise_exc=False
+                )
+
+    def test_load_postgres_plugin_bad_json_x_raise(self):
+        storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}'
+        storage_creds = '"account":"test"'
+        mock_stg_lib = async_mock.MagicMock(
+            postgresstorage_init=async_mock.MagicMock(return_value=0),
+            init_storagetype=async_mock.MagicMock(return_value=2),
+        )
+        with async_mock.patch.object(
+            test_module.cdll, "LoadLibrary", async_mock.Mock()
+        ) as mock_load:
+            mock_load.return_value = mock_stg_lib
+            with self.assertRaises(OSError) as context:
+                test_module.load_postgres_plugin(
+                    storage_config, storage_creds, raise_exc=True
+                )
+            assert "Invalid stringified JSON input" in str(context.exception)
+
+    def test_load_postgres_plugin_bad_json_x_exit(self):
+        storage_config = '"wallet_scheme":"MultiWalletSingleTable"'
+        storage_creds = '{"account":"test"}'
+        mock_stg_lib = async_mock.MagicMock(
+            postgresstorage_init=async_mock.MagicMock(return_value=0),
+            init_storagetype=async_mock.MagicMock(return_value=2),
+        )
+        with async_mock.patch.object(
+            test_module.cdll, "LoadLibrary", async_mock.Mock()
+        ) as mock_load:
+            mock_load.return_value = mock_stg_lib
+            with self.assertRaises(SystemExit):
+                test_module.load_postgres_plugin(
+                    storage_config, storage_creds, raise_exc=False
                 )