Repository: great-expectations/great_expectations

Issue:
Use cleaner solution for non-truncating division in python 2
Prefer `from __future__ import division` to `1.*x/y`


Discussion:
Done inline during improve_partitions push.

Resolving patch:
diff --git a/great_expectations/data_context/data_context.py b/great_expectations/data_context/data_context.py
--- a/great_expectations/data_context/data_context.py
+++ b/great_expectations/data_context/data_context.py
@@ -2082,22 +2082,13 @@ def list_datasources(self):
         """
         datasources = []
         for (
-            key,
+            name,
             value,
         ) in self.project_config_with_variables_substituted.datasources.items():
-            value["name"] = key
-
-            if "credentials" in value:
-                if "password" in value["credentials"]:
-                    value["credentials"][
-                        "password"
-                    ] = PasswordMasker.MASKED_PASSWORD_STRING
-                if "url" in value["credentials"]:
-                    value["credentials"]["url"] = PasswordMasker.mask_db_url(
-                        value["credentials"]["url"]
-                    )
-
-            datasources.append(value)
+            datasource_config = copy.deepcopy(value)
+            datasource_config["name"] = name
+            masked_config = PasswordMasker.sanitize_config(datasource_config)
+            datasources.append(masked_config)
         return datasources
 
     def list_stores(self):
@@ -2110,7 +2101,8 @@ def list_stores(self):
         ) in self.project_config_with_variables_substituted.stores.items():
             store_config = copy.deepcopy(value)
             store_config["name"] = name
-            stores.append(store_config)
+            masked_config = PasswordMasker.sanitize_config(store_config)
+            stores.append(masked_config)
         return stores
 
     def list_active_stores(self):
diff --git a/great_expectations/data_context/types/base.py b/great_expectations/data_context/types/base.py
--- a/great_expectations/data_context/types/base.py
+++ b/great_expectations/data_context/types/base.py
@@ -1256,10 +1256,13 @@ def account_id(self):
         return self.organization_id
 
     def to_json_dict(self):
+        # postpone importing to avoid circular imports
+        from great_expectations.data_context.util import PasswordMasker
+
         return {
             "base_url": self.base_url,
             "organization_id": self.organization_id,
-            "access_token": self.access_token,
+            "access_token": PasswordMasker.MASKED_PASSWORD_STRING,
             "account_id": self.account_id,  # TODO: remove when account_id is deprecated
         }
 
@@ -2085,6 +2088,16 @@ def to_json_dict(self) -> dict:
         serializeable_dict: dict = convert_to_json_serializable(data=dict_obj)
         return serializeable_dict
 
+    def to_sanitized_json_dict(self) -> dict:
+        """
+        Wrapper for `to_json_dict` which ensures sensitive fields are properly masked.
+        """
+        # postpone importing to avoid circular imports
+        from great_expectations.data_context.util import PasswordMasker
+
+        serializeable_dict = self.to_json_dict()
+        return PasswordMasker.sanitize_config(serializeable_dict)
+
     def __repr__(self) -> str:
         """
         # TODO: <Alex>2/4/2022</Alex>
@@ -2093,7 +2106,7 @@ def __repr__(self) -> str:
         location of the "great_expectations/types/__init__.py" and "great_expectations/core/util.py" modules make this
         refactoring infeasible at the present time.
         """
-        json_dict: dict = self.to_json_dict()
+        json_dict: dict = self.to_sanitized_json_dict()
         deep_filter_properties_iterable(
             properties=json_dict,
             inplace=True,
diff --git a/great_expectations/data_context/util.py b/great_expectations/data_context/util.py
--- a/great_expectations/data_context/util.py
+++ b/great_expectations/data_context/util.py
@@ -8,9 +8,11 @@
 import warnings
 from collections import OrderedDict
 from functools import lru_cache
-from typing import Optional
+from typing import Any, Optional
 from urllib.parse import urlparse
 
+from great_expectations.types import safe_deep_copy
+
 try:
     from azure.identity import DefaultAzureCredential
     from azure.keyvault.secrets import SecretClient
@@ -520,8 +522,14 @@ class PasswordMasker:
 
     MASKED_PASSWORD_STRING = "***"
 
-    @staticmethod
-    def mask_db_url(url: str, use_urlparse: bool = False, **kwargs) -> str:
+    # values with the following keys will be processed with cls.mask_db_url:
+    URL_KEYS = {"connection_string", "url"}
+
+    # values with these keys will be directly replaced with cls.MASKED_PASSWORD_STRING:
+    PASSWORD_KEYS = {"access_token", "password"}
+
+    @classmethod
+    def mask_db_url(cls, url: str, use_urlparse: bool = False, **kwargs) -> str:
         """
         Mask password in database url.
         Uses sqlalchemy engine parsing if sqlalchemy is installed, otherwise defaults to using urlparse from the stdlib which does not handle kwargs.
@@ -557,7 +565,7 @@ def mask_db_url(url: str, use_urlparse: bool = False, **kwargs) -> str:
 
             colon = ":" if parsed_url.port is not None else ""
             masked_url = (
-                f"{parsed_url.scheme}://{parsed_url.username}:{PasswordMasker.MASKED_PASSWORD_STRING}"
+                f"{parsed_url.scheme}://{parsed_url.username}:{cls.MASKED_PASSWORD_STRING}"
                 f"@{parsed_url.hostname}{colon}{parsed_url.port or ''}{parsed_url.path or ''}"
             )
 
@@ -567,3 +575,37 @@ def mask_db_url(url: str, use_urlparse: bool = False, **kwargs) -> str:
                 )
 
             return masked_url
+
+    @classmethod
+    def sanitize_config(cls, config: dict) -> dict:
+        """
+        Mask sensitive fields in a Dict.
+        """
+
+        # be defensive, since it would be logical to expect this method works with DataContextConfig
+        if not isinstance(config, dict):
+            raise TypeError(
+                f"PasswordMasker.sanitize_config expects param `config` "
+                + f"to be of type Dict, not of type {type(config)}"
+            )
+
+        config_copy = safe_deep_copy(config)  # be immutable
+
+        def recursive_cleaner_method(config: Any) -> None:
+            if isinstance(config, dict):
+                for key, val in config.items():
+                    if not isinstance(val, str):
+                        recursive_cleaner_method(val)
+                    elif key in cls.URL_KEYS:
+                        config[key] = cls.mask_db_url(val)
+                    elif key in cls.PASSWORD_KEYS:
+                        config[key] = cls.MASKED_PASSWORD_STRING
+                    else:
+                        pass  # this string is not sensitive
+            elif isinstance(config, list):
+                for val in config:
+                    recursive_cleaner_method(val)
+
+        recursive_cleaner_method(config_copy)  # Perform anonymization in place
+
+        return config_copy


Test patch:
diff --git a/tests/data_context/conftest.py b/tests/data_context/conftest.py
--- a/tests/data_context/conftest.py
+++ b/tests/data_context/conftest.py
@@ -164,6 +164,245 @@ def basic_data_context_config():
     )
 
 
+@pytest.fixture
+def basic_data_context_config_dict(basic_data_context_config):
+    """Wrapper fixture to transform `basic_data_context_config` to a json dict"""
+    return basic_data_context_config.to_json_dict()
+
+
+@pytest.fixture
+def conn_string_password():
+    """Returns a stable password for mocking connection strings"""
+    return "not_a_password"
+
+
+@pytest.fixture
+def conn_string_with_embedded_password(conn_string_password):
+    """
+    A mock connection string with the `conn_string_password` fixture embedded.
+    """
+    return f"redshift+psycopg2://no_user:{conn_string_password}@111.11.1.1:1111/foo"
+
+
+@pytest.fixture
+def data_context_config_with_datasources(conn_string_password):
+    return DataContextConfig(
+        **{
+            "commented_map": {},
+            "config_version": 2,
+            "plugins_directory": "plugins/",
+            "evaluation_parameter_store_name": "evaluation_parameter_store",
+            "validations_store_name": "does_not_have_to_be_real",
+            "expectations_store_name": "expectations_store",
+            "config_variables_file_path": "uncommitted/config_variables.yml",
+            "datasources": {
+                "Datasource 1: Redshift": {
+                    "class_name": "Datasource",
+                    "data_connectors": {
+                        "default_configured_asset_sql_data_connector": {
+                            "assets": {},
+                            "batch_spec_passthrough": {"sample": "value"},
+                            "class_name": "ConfiguredAssetSqlDataConnector",
+                        }
+                    },
+                    "execution_engine": {
+                        "class_name": "SqlAlchemyExecutionEngine",
+                        "connection_string": f"redshift+psycopg2://no_user:{conn_string_password}@111.11.1.1:1111/foo",
+                    },
+                    "module_name": "great_expectations.datasource",
+                },
+                "Datasource 2: Postgres": {
+                    "class_name": "Datasource",
+                    "data_connectors": {
+                        "default_configured_asset_sql_data_connector_sqlalchemy": {
+                            "assets": {},
+                            "batch_spec_passthrough": {"sample": "value"},
+                            "class_name": "ConfiguredAssetSqlDataConnector",
+                        }
+                    },
+                    "execution_engine": {
+                        "class_name": "SqlAlchemyExecutionEngine",
+                        "connection_string": f"postgresql://no_user:{conn_string_password}@some_url:1111/postgres?sslmode=prefer",
+                    },
+                    "module_name": "great_expectations.datasource",
+                },
+                "Datasource 3: MySQL": {
+                    "class_name": "Datasource",
+                    "data_connectors": {
+                        "default_configured_asset_sql_data_connector_sqlalchemy": {
+                            "assets": {},
+                            "batch_spec_passthrough": {"sample": "value"},
+                            "class_name": "ConfiguredAssetSqlDataConnector",
+                        }
+                    },
+                    "execution_engine": {
+                        "class_name": "SqlAlchemyExecutionEngine",
+                        "connection_string": f"mysql+pymysql://no_user:{conn_string_password}@some_url:1111/foo",
+                    },
+                    "module_name": "great_expectations.datasource",
+                },
+                "Datasource 4: Pandas": {
+                    # no creds to be masked here, shouldnt be affected
+                    "class_name": "Datasource",
+                    "data_connectors": {
+                        "default_runtime_data_connector": {
+                            "batch_identifiers": ["col"],
+                            "batch_spec_passthrough": {"sample": "value"},
+                            "class_name": "RuntimeDataConnector",
+                        }
+                    },
+                    "execution_engine": {"class_name": "PandasExecutionEngine"},
+                    "module_name": "great_expectations.datasource",
+                },
+                "Datasource 5: Snowflake": {
+                    "class_name": "Datasource",
+                    "data_connectors": {
+                        "default_configured_asset_sql_data_connector_snowflake": {
+                            "assets": {
+                                "taxi_data": {
+                                    "table_name": "taxi_data",
+                                    "type": "table",
+                                }
+                            },
+                            "batch_spec_passthrough": {"sample": "value"},
+                            "class_name": "ConfiguredAssetSqlDataConnector",
+                        }
+                    },
+                    "execution_engine": {
+                        "class_name": "SqlAlchemyExecutionEngine",
+                        "connection_string": f"snowflake://no_user:{conn_string_password}@some_url/foo/PUBLIC?role=PUBLIC&warehouse=bar",
+                    },
+                    "module_name": "great_expectations.datasource",
+                },
+                "Datasource 6: Spark": {
+                    "class_name": "Datasource",
+                    "data_connectors": {
+                        "default_runtime_data_connector": {
+                            "batch_identifiers": ["batch", "identifiers", "here"],
+                            "batch_spec_passthrough": {"sample": "value"},
+                            "class_name": "RuntimeDataConnector",
+                        }
+                    },
+                    "execution_engine": {"class_name": "SparkDFExecutionEngine"},
+                    "module_name": "great_expectations.datasource",
+                },
+            },
+            "stores": {
+                "expectations_store": {
+                    "class_name": "ExpectationsStore",
+                    "store_backend": {
+                        "class_name": "TupleFilesystemStoreBackend",
+                        "base_directory": "expectations/",
+                    },
+                },
+                "evaluation_parameter_store": {
+                    "module_name": "great_expectations.data_context.store",
+                    "class_name": "EvaluationParameterStore",
+                },
+            },
+            "data_docs_sites": {},
+            "validation_operators": {
+                "default": {
+                    "class_name": "ActionListValidationOperator",
+                    "action_list": [],
+                }
+            },
+            "anonymous_usage_statistics": {
+                "enabled": True,
+                "data_context_id": "6a52bdfa-e182-455b-a825-e69f076e67d6",
+                "usage_statistics_url": USAGE_STATISTICS_QA_URL,
+            },
+        }
+    )
+
+
+@pytest.fixture
+def data_context_config_dict_with_datasources(data_context_config_with_datasources):
+    """Wrapper fixture to transform `data_context_config_with_datasources` to a json dict"""
+    return data_context_config_with_datasources.to_json_dict()
+
+
+@pytest.fixture
+def data_context_config_with_cloud_backed_stores(ge_cloud_access_token):
+    org_id = "a34595b2-267e-4469-b18f-774e65dc556f"
+    return DataContextConfig(
+        **{
+            "commented_map": {},
+            "config_version": 2,
+            "plugins_directory": "plugins/",
+            "evaluation_parameter_store_name": "evaluation_parameter_store",
+            "validations_store_name": "does_not_have_to_be_real",
+            "expectations_store_name": "expectations_store",
+            "config_variables_file_path": "uncommitted/config_variables.yml",
+            "datasources": {},
+            "stores": {
+                "default_checkpoint_store": {
+                    "class_name": "CheckpointStore",
+                    "store_backend": {
+                        "class_name": "GeCloudStoreBackend",
+                        "ge_cloud_base_url": "http://foo/bar/",
+                        "ge_cloud_credentials": {
+                            "access_token": ge_cloud_access_token,
+                            "organization_id": org_id,
+                        },
+                        "ge_cloud_resource_type": "contract",
+                        "suppress_store_backend_id": True,
+                    },
+                },
+                "default_evaluation_parameter_store": {
+                    "class_name": "EvaluationParameterStore"
+                },
+                "default_expectations_store": {
+                    "class_name": "ExpectationsStore",
+                    "store_backend": {
+                        "class_name": "GeCloudStoreBackend",
+                        "ge_cloud_base_url": "http://foo/bar/",
+                        "ge_cloud_credentials": {
+                            "access_token": ge_cloud_access_token,
+                            "organization_id": org_id,
+                        },
+                        "ge_cloud_resource_type": "expectation_suite",
+                        "suppress_store_backend_id": True,
+                    },
+                },
+                "default_validations_store": {
+                    "class_name": "ValidationsStore",
+                    "store_backend": {
+                        "class_name": "GeCloudStoreBackend",
+                        "ge_cloud_base_url": "http://foo/bar/",
+                        "ge_cloud_credentials": {
+                            "access_token": ge_cloud_access_token,
+                            "organization_id": org_id,
+                        },
+                        "ge_cloud_resource_type": "suite_validation_result",
+                        "suppress_store_backend_id": True,
+                    },
+                },
+            },
+            "data_docs_sites": {},
+            "validation_operators": {
+                "default": {
+                    "class_name": "ActionListValidationOperator",
+                    "action_list": [],
+                }
+            },
+            "anonymous_usage_statistics": {
+                "enabled": True,
+                "data_context_id": "6a52bdfa-e182-455b-a825-e69f076e67d6",
+                "usage_statistics_url": USAGE_STATISTICS_QA_URL,
+            },
+        }
+    )
+
+
+@pytest.fixture
+def data_context_config_dict_with_cloud_backed_stores(
+    data_context_config_with_cloud_backed_stores,
+):
+    """Wrapper fixture to transform `data_context_config_with_cloud_backed_stores` to a json dict"""
+    return data_context_config_with_cloud_backed_stores.to_json_dict()
+
+
 @pytest.fixture
 def ge_cloud_runtime_base_url():
     return "https://api.dev.greatexpectations.io/runtime"
diff --git a/tests/data_context/test_data_context.py b/tests/data_context/test_data_context.py
--- a/tests/data_context/test_data_context.py
+++ b/tests/data_context/test_data_context.py
@@ -31,7 +31,7 @@
     ConfigurationIdentifier,
     ExpectationSuiteIdentifier,
 )
-from great_expectations.data_context.util import file_relative_path
+from great_expectations.data_context.util import PasswordMasker, file_relative_path
 from great_expectations.dataset import Dataset
 from great_expectations.datasource import (
     Datasource,
@@ -365,7 +365,7 @@ def test_list_datasources(data_context_parameterized_expectation_suite):
                     "host": os.getenv("GE_TEST_LOCAL_DB_HOSTNAME", "localhost"),
                     "port": "65432",
                     "username": "username_str",
-                    "password": "***",
+                    "password": PasswordMasker.MASKED_PASSWORD_STRING,
                     "database": "database_str",
                 },
             },
@@ -378,7 +378,7 @@ def test_list_datasources(data_context_parameterized_expectation_suite):
                     "module_name": "great_expectations.dataset",
                 },
                 "credentials": {
-                    "url": "postgresql+psycopg2://username:***@host:65432/database",
+                    "url": f"postgresql+psycopg2://username:{PasswordMasker.MASKED_PASSWORD_STRING}@host:65432/database",
                 },
             },
         ]
diff --git a/tests/data_context/test_data_context_config_variables.py b/tests/data_context/test_data_context_config_variables.py
--- a/tests/data_context/test_data_context_config_variables.py
+++ b/tests/data_context/test_data_context_config_variables.py
@@ -12,6 +12,7 @@
     DatasourceConfigSchema,
 )
 from great_expectations.data_context.util import (
+    PasswordMasker,
     file_relative_path,
     substitute_config_variable,
 )
@@ -807,7 +808,7 @@ def test_create_data_context_and_config_vars_in_code(tmp_path_factory, monkeypat
         "database": "DB_NAME",
         "username": "DB_USER",
         # Note masking of "password" field
-        "password": "***",
+        "password": PasswordMasker.MASKED_PASSWORD_STRING,
     }
 
     # Check context substitutes escaped variables appropriately
diff --git a/tests/data_context/test_data_context_utils.py b/tests/data_context/test_data_context_utils.py
--- a/tests/data_context/test_data_context_utils.py
+++ b/tests/data_context/test_data_context_utils.py
@@ -14,6 +14,7 @@
     substitute_value_from_gcp_secret_manager,
     substitute_value_from_secret_store,
 )
+from great_expectations.types import safe_deep_copy
 from great_expectations.util import load_class
 
 
@@ -64,7 +65,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
             PasswordMasker.mask_db_url(
                 f"postgresql://scott:tiger@{db_hostname}:65432/mydatabase"
             )
-            == f"postgresql://scott:***@{db_hostname}:65432/mydatabase"
+            == f"postgresql://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/mydatabase"
         )
     except ModuleNotFoundError:
         pass
@@ -73,14 +74,14 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
             f"postgresql://scott:tiger@{db_hostname}:65432/mydatabase",
             use_urlparse=True,
         )
-        == f"postgresql://scott:***@{db_hostname}:65432/mydatabase"
+        == f"postgresql://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/mydatabase"
     )
     # missing port number, using urlparse
     assert (
         PasswordMasker.mask_db_url(
             f"postgresql://scott:tiger@{db_hostname}/mydatabase", use_urlparse=True
         )
-        == f"postgresql://scott:***@{db_hostname}/mydatabase"
+        == f"postgresql://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}/mydatabase"
     )
 
     # psycopg2 (if installed in test environment)
@@ -89,7 +90,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
             PasswordMasker.mask_db_url(
                 f"postgresql+psycopg2://scott:tiger@{db_hostname}:65432/mydatabase"
             )
-            == f"postgresql+psycopg2://scott:***@{db_hostname}:65432/mydatabase"
+            == f"postgresql+psycopg2://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/mydatabase"
         )
     except ModuleNotFoundError:
         pass
@@ -98,7 +99,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
             f"postgresql+psycopg2://scott:tiger@{db_hostname}:65432/mydatabase",
             use_urlparse=True,
         )
-        == f"postgresql+psycopg2://scott:***@{db_hostname}:65432/mydatabase"
+        == f"postgresql+psycopg2://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/mydatabase"
     )
 
     # pg8000 (if installed in test environment)
@@ -107,7 +108,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
             PasswordMasker.mask_db_url(
                 f"postgresql+pg8000://scott:tiger@{db_hostname}:65432/mydatabase"
             )
-            == f"postgresql+pg8000://scott:***@{db_hostname}:65432/mydatabase"
+            == f"postgresql+pg8000://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/mydatabase"
         )
     except ModuleNotFoundError:
         pass
@@ -116,7 +117,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
             f"postgresql+pg8000://scott:tiger@{db_hostname}:65432/mydatabase",
             use_urlparse=True,
         )
-        == f"postgresql+pg8000://scott:***@{db_hostname}:65432/mydatabase"
+        == f"postgresql+pg8000://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/mydatabase"
     )
 
     # MySQL
@@ -124,7 +125,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
     try:
         assert (
             PasswordMasker.mask_db_url(f"mysql://scott:tiger@{db_hostname}:65432/foo")
-            == f"mysql://scott:***@{db_hostname}:65432/foo"
+            == f"mysql://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/foo"
         )
     except ModuleNotFoundError:
         pass
@@ -133,7 +134,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
         PasswordMasker.mask_db_url(
             f"mysql://scott:tiger@{db_hostname}:65432/foo", use_urlparse=True
         )
-        == f"mysql://scott:***@{db_hostname}:65432/foo"
+        == f"mysql://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/foo"
     )
 
     # mysqlclient (a maintained fork of MySQL-Python) (if installed in test environment)
@@ -142,7 +143,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
             PasswordMasker.mask_db_url(
                 f"mysql+mysqldb://scott:tiger@{db_hostname}:65432/foo"
             )
-            == f"mysql+mysqldb://scott:***@{db_hostname}:65432/foo"
+            == f"mysql+mysqldb://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/foo"
         )
     except ModuleNotFoundError:
         pass
@@ -150,7 +151,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
         PasswordMasker.mask_db_url(
             f"mysql+mysqldb://scott:tiger@{db_hostname}:65432/foo", use_urlparse=True
         )
-        == f"mysql+mysqldb://scott:***@{db_hostname}:65432/foo"
+        == f"mysql+mysqldb://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/foo"
     )
 
     # PyMySQL (if installed in test environment)
@@ -159,7 +160,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
             PasswordMasker.mask_db_url(
                 f"mysql+pymysql://scott:tiger@{db_hostname}:65432/foo"
             )
-            == f"mysql+pymysql://scott:***@{db_hostname}:65432/foo"
+            == f"mysql+pymysql://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/foo"
         )
     except ModuleNotFoundError:
         pass
@@ -167,7 +168,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
         PasswordMasker.mask_db_url(
             f"mysql+pymysql://scott:tiger@{db_hostname}:65432/foo", use_urlparse=True
         )
-        == f"mysql+pymysql://scott:***@{db_hostname}:65432/foo"
+        == f"mysql+pymysql://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:65432/foo"
     )
 
     # Oracle (if installed in test environment)
@@ -184,13 +185,13 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
         PasswordMasker.mask_db_url(
             f"oracle://scott:tiger@{url_host}:1521/sidname", use_urlparse=True
         )
-        == f"oracle://scott:***@{url_host}:1521/sidname"
+        == f"oracle://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{url_host}:1521/sidname"
     )
 
     try:
         assert (
             PasswordMasker.mask_db_url("oracle+cx_oracle://scott:tiger@tnsname")
-            == "oracle+cx_oracle://scott:***@tnsname"
+            == f"oracle+cx_oracle://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@tnsname"
         )
     except ModuleNotFoundError:
         pass
@@ -198,7 +199,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
         PasswordMasker.mask_db_url(
             "oracle+cx_oracle://scott:tiger@tnsname", use_urlparse=True
         )
-        == "oracle+cx_oracle://scott:***@tnsname"
+        == f"oracle+cx_oracle://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@tnsname"
     )
 
     # Microsoft SQL Server
@@ -214,7 +215,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
         PasswordMasker.mask_db_url(
             "mssql+pyodbc://scott:tiger@mydsn", use_urlparse=True
         )
-        == "mssql+pyodbc://scott:***@mydsn"
+        == f"mssql+pyodbc://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@mydsn"
     )
 
     # pymssql (if installed in test environment)
@@ -223,7 +224,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
             PasswordMasker.mask_db_url(
                 f"mssql+pymssql://scott:tiger@{db_hostname}:12345/dbname"
             )
-            == f"mssql+pymssql://scott:***@{db_hostname}:12345/dbname"
+            == f"mssql+pymssql://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:12345/dbname"
         )
     except ModuleNotFoundError:
         pass
@@ -231,7 +232,7 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
         PasswordMasker.mask_db_url(
             f"mssql+pymssql://scott:tiger@{db_hostname}:12345/dbname", use_urlparse=True
         )
-        == f"mssql+pymssql://scott:***@{db_hostname}:12345/dbname"
+        == f"mssql+pymssql://scott:{PasswordMasker.MASKED_PASSWORD_STRING}@{db_hostname}:12345/dbname"
     )
 
     # SQLite
@@ -286,6 +287,199 @@ def test_password_masker_mask_db_url(monkeypatch, tmp_path):
     assert PasswordMasker.mask_db_url("sqlite://", use_urlparse=True) == "sqlite://"
 
 
+def test_sanitize_config_raises_exception_with_bad_input(
+    basic_data_context_config,
+):
+
+    # expect that an Exception is raised if something other than a dict is passed
+    with pytest.raises(TypeError):
+        PasswordMasker.sanitize_config(basic_data_context_config)
+
+
+def test_sanitize_config_doesnt_change_config_without_datasources(
+    basic_data_context_config_dict,
+):
+
+    # expect no change without datasources
+    config_without_creds = PasswordMasker.sanitize_config(
+        basic_data_context_config_dict
+    )
+    assert config_without_creds == basic_data_context_config_dict
+
+
+def test_sanitize_config_masks_cloud_store_backend_access_tokens(
+    data_context_config_dict_with_cloud_backed_stores, ge_cloud_access_token
+):
+
+    # test that cloud store backend tokens have been properly masked
+    config_with_creds_in_stores = PasswordMasker.sanitize_config(
+        data_context_config_dict_with_cloud_backed_stores
+    )
+    for name, store_config in config_with_creds_in_stores["stores"].items():
+
+        if (
+            not store_config.get("store_backend")
+            or not store_config["store_backend"].get("ge_cloud_credentials")
+            or not store_config["store_backend"]["ge_cloud_credentials"].get(
+                "access_token"
+            )
+        ):
+            # a field in store_config["store_backend"]["ge_cloud_credentials"]["access_token"]
+            # doesn't exist, so we expect this config to be unchanged
+            assert (
+                store_config
+                == data_context_config_dict_with_cloud_backed_stores["stores"][name]
+            )
+        else:
+            # check that the original token exists
+            assert (
+                data_context_config_dict_with_cloud_backed_stores["stores"][name][
+                    "store_backend"
+                ]["ge_cloud_credentials"]["access_token"]
+                == ge_cloud_access_token
+            )
+            # expect that the GE Cloud token has been obscured
+            assert (
+                store_config["store_backend"]["ge_cloud_credentials"]["access_token"]
+                != ge_cloud_access_token
+            )
+
+
+def test_sanitize_config_masks_execution_engine_connection_strings(
+    data_context_config_dict_with_datasources, conn_string_password
+):
+
+    # test that datasource credentials have been properly masked
+    unaltered_datasources = data_context_config_dict_with_datasources["datasources"]
+    config_with_creds_masked = PasswordMasker.sanitize_config(
+        data_context_config_dict_with_datasources
+    )
+    masked_datasources = config_with_creds_masked["datasources"]
+
+    # iterate through the processed datasources and check for correctness
+    for name, processed_config in masked_datasources.items():
+
+        # check if processed_config["execution_engine"]["connection_string"] exists
+        if processed_config.get("execution_engine") and processed_config[
+            "execution_engine"
+        ].get("connection_string"):
+
+            # check if the connection string contains a password
+            if (
+                conn_string_password
+                in unaltered_datasources[name]["execution_engine"]["connection_string"]
+            ):
+                # it does contain a password, so make sure its masked
+                assert (
+                    conn_string_password
+                    not in processed_config["execution_engine"]["connection_string"]
+                )
+            else:
+                # it doesn't contain a password, so make sure it's unaltered
+                assert processed_config == unaltered_datasources[name]
+
+        # processed_config either doesn't have an `execution_engine` field,
+        # or a `connection_string` field
+        else:
+            # expect this config to be unaltered
+            assert processed_config == unaltered_datasources[name]
+
+
+def test_sanitize_config_with_arbitrarily_nested_sensitive_keys():
+
+    # base case - this config should pass through unaffected
+    config = {
+        "some_field": "and a value",
+        "some_other_field": {"password": "expect this to be found"},
+    }
+    config_copy = safe_deep_copy(config)
+    res = PasswordMasker.sanitize_config(config_copy)
+    assert res != config
+    assert res["some_other_field"]["password"] == PasswordMasker.MASKED_PASSWORD_STRING
+
+
+def test_sanitize_config_with_password_field():
+
+    # this case has a password field inside a credentials dict - expect it to be masked
+    config = {"credentials": {"password": "my-super-duper-secure-passphrase-123"}}
+    config_copy = safe_deep_copy(config)
+    res = PasswordMasker.sanitize_config(config_copy)
+    assert res != config
+    assert res["credentials"]["password"] == PasswordMasker.MASKED_PASSWORD_STRING
+
+
+def test_sanitize_config_with_url_field(
+    conn_string_with_embedded_password, conn_string_password
+):
+
+    # this case has a url field inside a credentials dict - expect the password inside
+    # of it to be masked
+    config = {"credentials": {"url": conn_string_with_embedded_password}}
+    config_copy = safe_deep_copy(config)
+    res = PasswordMasker.sanitize_config(config_copy)
+    assert res != config
+    assert conn_string_password not in res["credentials"]["url"]
+    assert PasswordMasker.MASKED_PASSWORD_STRING in res["credentials"]["url"]
+
+
+def test_sanitize_config_with_nested_url_field(
+    conn_string_password, conn_string_with_embedded_password
+):
+
+    # this case has a connection string in an execution_engine dict
+    config = {
+        "execution_engine": {"connection_string": conn_string_with_embedded_password}
+    }
+    config_copy = safe_deep_copy(config)
+    res = PasswordMasker.sanitize_config(config_copy)
+    assert res != config
+    assert conn_string_password not in res["execution_engine"]["connection_string"]
+    assert (
+        PasswordMasker.MASKED_PASSWORD_STRING
+        in res["execution_engine"]["connection_string"]
+    )
+
+
+def test_sanitize_config_regardless_of_parent_key():
+
+    # expect this config still be masked
+    config = {
+        "some_field": "and a value",
+        "some_other_field": {"access_token": "but this won't be found"},
+    }
+    config_copy = safe_deep_copy(config)
+    res = PasswordMasker.sanitize_config(config_copy)
+    assert res != config
+    assert (
+        res["some_other_field"]["access_token"] == PasswordMasker.MASKED_PASSWORD_STRING
+    )
+
+
+def test_sanitize_config_masks_cloud_access_token(ge_cloud_access_token):
+
+    # expect the access token to be found and masked
+    config = {
+        "store_backend": {
+            "ge_cloud_credentials": {"access_token": ge_cloud_access_token}
+        }
+    }
+    config_copy = safe_deep_copy(config)
+    res = PasswordMasker.sanitize_config(config_copy)
+    assert res != config
+    assert (
+        res["store_backend"]["ge_cloud_credentials"]["access_token"]
+        == PasswordMasker.MASKED_PASSWORD_STRING
+    )
+
+
+def test_sanitize_config_works_with_list():
+    config = {"some_key": [{"access_token": "12345"}]}
+    config_copy = safe_deep_copy(config)
+    res = PasswordMasker.sanitize_config(config_copy)
+    assert res != config
+    assert res["some_key"][0]["access_token"] == PasswordMasker.MASKED_PASSWORD_STRING
+
+
 def test_parse_substitution_variable():
     """
     What does this test and why?