This is an automated email from the ASF dual-hosted git repository.

vavila pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 228b5984096 feat: Labels for encrypted fields (#38075)
228b5984096 is described below

commit 228b5984096193a84b4ae932c17f2344936a466a
Author: Vitor Avila <[email protected]>
AuthorDate: Mon Feb 23 13:23:33 2026 -0300

    feat: Labels for encrypted fields (#38075)
---
 superset/db_engine_specs/base.py              | 29 ++++++++++++++---
 superset/db_engine_specs/bigquery.py          |  4 ++-
 superset/db_engine_specs/gsheets.py           |  4 +--
 superset/db_engine_specs/mysql.py             |  4 +--
 superset/db_engine_specs/postgres.py          |  4 +--
 superset/db_engine_specs/redshift.py          |  4 +--
 superset/db_engine_specs/snowflake.py         |  4 +--
 superset/db_engine_specs/ydb.py               |  5 ++-
 tests/unit_tests/db_engine_specs/test_base.py | 45 +++++++++++++++++++++++++++
 9 files changed, 86 insertions(+), 17 deletions(-)

diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py
index 51a6a8778ba..fb0e26e77e7 100644
--- a/superset/db_engine_specs/base.py
+++ b/superset/db_engine_specs/base.py
@@ -532,10 +532,13 @@ class BaseEngineSpec:  # pylint: 
disable=too-many-public-methods
         Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]
     ] = {}
 
-    # List of JSON path to fields in `encrypted_extra` that should be masked 
when the
-    # database is edited. By default everything is masked.
+    # JSONPath fields in `encrypted_extra` that should be masked when the 
database is
+    # edited. Can be a set of paths (labels will default to the path) or a 
dict mapping
+    # paths to human-readable labels for import validation error messages.
     # pylint: disable=invalid-name
-    encrypted_extra_sensitive_fields: set[str] = {"$.*"}
+    encrypted_extra_sensitive_fields: set[str] | dict[str, str] = {
+        "$.*": "Encrypted Extra",
+    }
 
     # Whether the engine supports file uploads
     # if True, database will be listed as option in the upload file form
@@ -580,6 +583,22 @@ class BaseEngineSpec:  # pylint: 
disable=too-many-public-methods
     # the `cancel_query` value in the `extra` field of the `query` object
     has_query_id_before_execute = True
 
+    @classmethod
+    def encrypted_extra_sensitive_field_paths(cls) -> set[str]:
+        """
+        Returns a set of paths for fields that should be masked in the
+        ``masked_encrypted_extra`` JSON.
+
+        :param cls: Description
+        :return: Description
+        :rtype: set[str]
+        """
+        return (
+            set(cls.encrypted_extra_sensitive_fields)
+            if isinstance(cls.encrypted_extra_sensitive_fields, dict)
+            else cls.encrypted_extra_sensitive_fields
+        )
+
     @classmethod
     def get_rls_method(cls) -> RLSMethod:
         """
@@ -2443,7 +2462,7 @@ class BaseEngineSpec:  # pylint: 
disable=too-many-public-methods
 
         masked_encrypted_extra = redact_sensitive(
             config,
-            cls.encrypted_extra_sensitive_fields,
+            cls.encrypted_extra_sensitive_field_paths(),
         )
 
         return json.dumps(masked_encrypted_extra)
@@ -2469,7 +2488,7 @@ class BaseEngineSpec:  # pylint: 
disable=too-many-public-methods
         new_config = reveal_sensitive(
             old_config,
             new_config,
-            cls.encrypted_extra_sensitive_fields,
+            cls.encrypted_extra_sensitive_field_paths(),
         )
 
         return json.dumps(new_config)
diff --git a/superset/db_engine_specs/bigquery.py 
b/superset/db_engine_specs/bigquery.py
index 1284464ab7c..8ba5e99b389 100644
--- a/superset/db_engine_specs/bigquery.py
+++ b/superset/db_engine_specs/bigquery.py
@@ -191,7 +191,9 @@ class BigQueryEngineSpec(BaseEngineSpec):  # pylint: 
disable=too-many-public-met
 
     # when editing the database, mask this field in `encrypted_extra`
     # pylint: disable=invalid-name
-    encrypted_extra_sensitive_fields = {"$.credentials_info.private_key"}
+    encrypted_extra_sensitive_fields = {
+        "$.credentials_info.private_key": "Service Account Private Key",
+    }
 
     """
     https://www.python.org/dev/peps/pep-0249/#arraysize
diff --git a/superset/db_engine_specs/gsheets.py 
b/superset/db_engine_specs/gsheets.py
index 9692db9d9a6..3bcb3a8c871 100644
--- a/superset/db_engine_specs/gsheets.py
+++ b/superset/db_engine_specs/gsheets.py
@@ -130,8 +130,8 @@ class GSheetsEngineSpec(ShillelaghEngineSpec):
     # when editing the database, mask this field in `encrypted_extra`
     # pylint: disable=invalid-name
     encrypted_extra_sensitive_fields = {
-        "$.service_account_info.private_key",
-        "$.oauth2_client_info.secret",
+        "$.service_account_info.private_key": "Service Account Private Key",
+        "$.oauth2_client_info.secret": "OAuth2 Client Secret",
     }
 
     custom_errors: dict[Pattern[str], tuple[str, SupersetErrorType, dict[str, 
Any]]] = {
diff --git a/superset/db_engine_specs/mysql.py 
b/superset/db_engine_specs/mysql.py
index b6cba3906a6..99bf203f9a4 100644
--- a/superset/db_engine_specs/mysql.py
+++ b/superset/db_engine_specs/mysql.py
@@ -307,8 +307,8 @@ class MySQLEngineSpec(BasicParametersMixin, BaseEngineSpec):
     # This follows the pattern used by other engine specs (bigquery, 
snowflake, etc.)
     # that specify exact paths rather than using the base class's catch-all 
"$.*".
     encrypted_extra_sensitive_fields = {
-        "$.aws_iam.external_id",
-        "$.aws_iam.role_arn",
+        "$.aws_iam.external_id": "AWS IAM External ID",
+        "$.aws_iam.role_arn": "AWS IAM Role ARN",
     }
 
     @staticmethod
diff --git a/superset/db_engine_specs/postgres.py 
b/superset/db_engine_specs/postgres.py
index c407e3d7fb1..b744b7b440b 100644
--- a/superset/db_engine_specs/postgres.py
+++ b/superset/db_engine_specs/postgres.py
@@ -464,8 +464,8 @@ class PostgresEngineSpec(BasicParametersMixin, 
PostgresBaseEngineSpec):
     # This follows the pattern used by other engine specs (bigquery, 
snowflake, etc.)
     # that specify exact paths rather than using the base class's catch-all 
"$.*".
     encrypted_extra_sensitive_fields = {
-        "$.aws_iam.external_id",
-        "$.aws_iam.role_arn",
+        "$.aws_iam.external_id": "AWS IAM External ID",
+        "$.aws_iam.role_arn": "AWS IAM Role ARN",
     }
 
     column_type_mappings = (
diff --git a/superset/db_engine_specs/redshift.py 
b/superset/db_engine_specs/redshift.py
index fcdfab16967..8621873e75b 100644
--- a/superset/db_engine_specs/redshift.py
+++ b/superset/db_engine_specs/redshift.py
@@ -206,8 +206,8 @@ class RedshiftEngineSpec(BasicParametersMixin, 
PostgresBaseEngineSpec):
     # This follows the pattern used by other engine specs (bigquery, 
snowflake, etc.)
     # that specify exact paths rather than using the base class's catch-all 
"$.*".
     encrypted_extra_sensitive_fields = {
-        "$.aws_iam.external_id",
-        "$.aws_iam.role_arn",
+        "$.aws_iam.external_id": "AWS IAM External ID",
+        "$.aws_iam.role_arn": "AWS IAM Role ARN",
     }
 
     @staticmethod
diff --git a/superset/db_engine_specs/snowflake.py 
b/superset/db_engine_specs/snowflake.py
index 3f541699f05..7008b8fa028 100644
--- a/superset/db_engine_specs/snowflake.py
+++ b/superset/db_engine_specs/snowflake.py
@@ -151,8 +151,8 @@ class SnowflakeEngineSpec(PostgresBaseEngineSpec):
 
     # pylint: disable=invalid-name
     encrypted_extra_sensitive_fields = {
-        "$.auth_params.privatekey_body",
-        "$.auth_params.privatekey_pass",
+        "$.auth_params.privatekey_body": "Private Key Body",
+        "$.auth_params.privatekey_pass": "Private Key Password",
     }
 
     _time_grain_expressions = {
diff --git a/superset/db_engine_specs/ydb.py b/superset/db_engine_specs/ydb.py
index 07c0b3d5571..9ea1b5dd195 100755
--- a/superset/db_engine_specs/ydb.py
+++ b/superset/db_engine_specs/ydb.py
@@ -43,7 +43,10 @@ class YDBEngineSpec(BaseEngineSpec):
     sqlalchemy_uri_placeholder = "ydb://{host}:{port}/{database_name}"
 
     # pylint: disable=invalid-name
-    encrypted_extra_sensitive_fields = {"$.connect_args.credentials", 
"$.credentials"}
+    encrypted_extra_sensitive_fields = {
+        "$.connect_args.credentials": "Connection Credentials",
+        "$.credentials": "Credentials",
+    }
 
     disable_ssh_tunneling = False
 
diff --git a/tests/unit_tests/db_engine_specs/test_base.py 
b/tests/unit_tests/db_engine_specs/test_base.py
index 4f4409c1b99..6c6b98e0593 100644
--- a/tests/unit_tests/db_engine_specs/test_base.py
+++ b/tests/unit_tests/db_engine_specs/test_base.py
@@ -360,6 +360,51 @@ def test_unmask_encrypted_extra() -> None:
     )
 
 
[email protected](
+    "masked_encrypted_extra,expected_result",
+    [
+        (
+            {
+                "$.credentials_info.private_key": "Private Key",
+                "$.access_token": "Access Token",
+            },
+            {
+                "$.credentials_info.private_key",
+                "$.access_token",
+            },
+        ),
+        (
+            {
+                "$.credentials_info.private_key",
+                "$.access_token",
+            },
+            {
+                "$.credentials_info.private_key",
+                "$.access_token",
+            },
+        ),
+        (
+            None,
+            {"$.*"},
+        ),
+    ],
+)
+def test_encrypted_extra_sensitive_field_paths_from_dict(
+    masked_encrypted_extra: set[str] | dict[str, str] | None,
+    expected_result: set[str],
+) -> None:
+    """
+    Test that `encrypted_extra_sensitive_field_paths` extracts the keys
+    when `encrypted_extra_sensitive_fields` is a dict.
+    """
+
+    class DictFieldsSpec(BaseEngineSpec):
+        if masked_encrypted_extra:
+            encrypted_extra_sensitive_fields = masked_encrypted_extra
+
+    assert DictFieldsSpec.encrypted_extra_sensitive_field_paths() == 
expected_result
+
+
 def test_impersonate_user_backwards_compatible(mocker: MockerFixture) -> None:
     """
     Test that the `impersonate_user` method calls the original methods it 
replaced.

Reply via email to