This is an automated email from the ASF dual-hosted git repository. hainenber pushed a commit to branch fix/mask-sensitive-data-in-debug-log-for-failed-import-validation in repository https://gitbox.apache.org/repos/asf/superset.git
commit 2f438d47a9b2d47a64f9dda1b7fa4330352acb14 Author: hainenber <[email protected]> AuthorDate: Sun Jul 12 20:00:41 2026 +0700 fix(sec): redact sensitive data in debug log for failed import validation Signed-off-by: hainenber <[email protected]> --- superset/commands/importers/v1/utils.py | 17 +++++- .../unit_tests/commands/importers/v1/utils_test.py | 66 ++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/superset/commands/importers/v1/utils.py b/superset/commands/importers/v1/utils.py index f2fe9eaba0a..e2bba1e645a 100644 --- a/superset/commands/importers/v1/utils.py +++ b/superset/commands/importers/v1/utils.py @@ -149,6 +149,7 @@ def load_configs( prefix = file_name.split("/")[0] schema = schemas.get(f"{prefix}/") if schema: + config = None try: config = load_yaml(file_name, content) @@ -230,7 +231,21 @@ def load_configs( prefix, exc.messages, ) - logger.debug("Config content that failed validation: %s", config) + # Mask sensitive data before logging out for debug + if config: + redacted_config = json.redact_sensitive( + config, + { + "$.password", + "$.ssh_tunnel.password", + "$.ssh_tunnel.private_key", + "$.ssh_tunnel.private_key_password", + "$.masked_encrypted_extra", + }, + ) + logger.debug( + "Config content that failed validation: %s.", redacted_config + ) exc.messages = {file_name: exc.messages} exceptions.append(exc) diff --git a/tests/unit_tests/commands/importers/v1/utils_test.py b/tests/unit_tests/commands/importers/v1/utils_test.py index eef9d4ea2e9..f6df6a92fab 100644 --- a/tests/unit_tests/commands/importers/v1/utils_test.py +++ b/tests/unit_tests/commands/importers/v1/utils_test.py @@ -16,10 +16,17 @@ # under the License. """Tests for superset/commands/dataset/importers/v1/utils.py temporal helpers.""" +import logging from unittest.mock import patch import pandas as pd import pytest +from jsonpath_ng import parse +from marshmallow import Schema +from marshmallow.exceptions import ValidationError + +from superset.constants import PASSWORD_MASK +from superset.utils import json class TestConvertTemporalColumns: @@ -117,3 +124,62 @@ class TestConvertTemporalColumns: call_args = mock_logger.warning.call_args[0] assert call_args[1] == 2 # 2 out-of-bounds, 1 pre-existing null + + +class TestLoadConfigs: + def test_load_configs_caught_exception_and_log_redacted_configs(self) -> None: + logging.basicConfig(level=logging.DEBUG) + from superset.commands.importers.v1.utils import ( + load_configs, + ) + + with ( + patch("superset.db") as mock_db, + patch("yaml.safe_load") as mock_yaml_safe_load, + patch("superset.commands.importers.v1.utils.logger") as mock_logger, + ): + mock_yaml_safe_load.return_value = { + "masked_encrypted_extra": json.dumps( + {"oauth2_client_info": {"secret": "MASKED"}} + ), + "ssh_tunnel": {}, + } + mock_db.session.query.return_value = {"uuid-1": "SECRET_TO_BE_SEEN"} + + class RaiseValidationSchema(Schema): + def load(self, value): + raise ValidationError("Exception when validating config") + + failed_schema = RaiseValidationSchema() + + load_configs( + contents={"file1/": "here is some content"}, + schemas={ + "file1/": failed_schema, + }, + passwords={"file1/": "PASSWORD"}, + exceptions=[], + ssh_tunnel_passwords={"file1/": "SECRET_SSH_TUNNEL_PASSWORD"}, + ssh_tunnel_private_keys={"file1/": "SECRET_SSH_TUNNEL_PRIVATE_KEY"}, + ssh_tunnel_priv_key_passwords={ + "file1/": "SECRET_SSH_TUNNEL_PRIV_KEY_PASSWORD" + }, + encrypted_extra_secrets={ + "file1/": { + "$.oauth2_client_info.secret": "SECRET_OAUTH2_CLIENT_INFO" + } + }, + ) + + logged_config = mock_logger.debug.call_args[0][1] + + for redacted_field in [ + "$.password", + "$.ssh_tunnel.password", + "$.ssh_tunnel.private_key", + "$.ssh_tunnel.private_key_password", + "$.masked_encrypted_extra", + ]: + jsonpath_expr = parse(redacted_field) + for match in jsonpath_expr.find(logged_config): + assert match.value == PASSWORD_MASK
