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

vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new b080248faf2 [v3-3-test] Mask list-shaped Variable values on 
deserialization (#70891) (#71069)
b080248faf2 is described below

commit b080248faf29dedd190721bacdd7af871de6ff1f
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Aug 4 19:40:04 2026 +0530

    [v3-3-test] Mask list-shaped Variable values on deserialization (#70891) 
(#71069)
    
    * Mask list-shaped Variable values on deserialization
    
    _mask_and_deserialize_variable dispatched on the top-level type of the
    deserialized value and handled only str and dict, so a Variable whose JSON 
is a
    list was returned with no masking applied to anything inside it. add_mask 
walks
    iterables itself, so the same list nested one level inside a dict was 
already
    masked -- only a top-level one was skipped.
    
    The list is passed under the variable's key rather than anonymously: 
elements
    have no key names of their own, so they follow the variable key's 
sensitivity,
    and a list of ordinary values such as region names is not added to the 
global
    pattern set. Dicts inside the list are still masked by their own key names.
    
    * Tighten the comments around Variable masking dispatch
    
    The rationale was spread across three sites saying the same thing; keep it 
at
    the branch where the decision is made.
    (cherry picked from commit b968192cd3aeb55fad3bb5dc0d88917b4b29407f)
    
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 task-sdk/src/airflow/sdk/execution_time/context.py |  5 ++++
 .../tests/task_sdk/execution_time/test_context.py  | 35 +++++++++++++++++++++-
 2 files changed, 39 insertions(+), 1 deletion(-)

diff --git a/task-sdk/src/airflow/sdk/execution_time/context.py 
b/task-sdk/src/airflow/sdk/execution_time/context.py
index c3356f79614..e9deba57d67 100644
--- a/task-sdk/src/airflow/sdk/execution_time/context.py
+++ b/task-sdk/src/airflow/sdk/execution_time/context.py
@@ -296,7 +296,12 @@ def _mask_and_deserialize_variable(raw: str, key: str, 
deserialize_json: bool) -
     if isinstance(val, str):
         mask_secret(val, key)
     elif isinstance(val, dict):
+        # Masked by the dict's own inner key names, which is what ``add_mask`` 
uses.
         mask_secret(val)
+    elif isinstance(val, list):
+        # Pass the Variable's key so list elements inherit the Variable's 
sensitivity
+        # instead of being added to the global mask patterns.
+        mask_secret(val, key)
     return val
 
 
diff --git a/task-sdk/tests/task_sdk/execution_time/test_context.py 
b/task-sdk/tests/task_sdk/execution_time/test_context.py
index e34008ef9c7..ba1ebb3694f 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_context.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_context.py
@@ -428,6 +428,36 @@ class TestVariableAccessor:
         # Second call: deserialized dict so internal sensitive fields like 
"password" get masked
         mock_mask_secret.assert_any_call({"password": "s3cr3t", "host": 
"db.example.com"})
 
+    @mock.patch("airflow.sdk.execution_time.context.mask_secret")
+    def test_var_json_masks_list_values(self, mock_mask_secret, 
mock_supervisor_comms):
+        """A JSON list is handed to the masker whole, exactly as a dict is."""
+        accessor = VariableAccessor(deserialize_json=True)
+        raw_json = '[{"password": "s3cr3t"}, {"password": "s3cr3t2"}]'
+        mock_supervisor_comms.send.return_value = 
VariableResult(key="db_configs", value=raw_json)
+
+        val = accessor.db_configs
+
+        assert val == [{"password": "s3cr3t"}, {"password": "s3cr3t2"}]
+        mock_mask_secret.assert_any_call(raw_json, "db_configs")
+        # under the variable's key; dicts inside are still masked by their own 
key names
+        mock_mask_secret.assert_any_call([{"password": "s3cr3t"}, {"password": 
"s3cr3t2"}], "db_configs")
+
+    @pytest.mark.parametrize(
+        ("raw", "expected"),
+        [
+            pytest.param("12345", 12345, id="int"),
+            pytest.param("true", True, id="bool"),
+            pytest.param("null", None, id="null"),
+            pytest.param("1.5", 1.5, id="float"),
+        ],
+    )
+    def test_var_json_scalar_values_pass_through(self, raw, expected, 
mock_supervisor_comms):
+        """Handing a scalar to the masker is a no-op and must not change the 
value returned."""
+        accessor = VariableAccessor(deserialize_json=True)
+        mock_supervisor_comms.send.return_value = 
VariableResult(key="some_number", value=raw)
+
+        assert accessor.some_number == expected
+
     @mock.patch("airflow.sdk.execution_time.context.mask_secret")
     def test_var_json_sensitive_key_masks_raw_json(self, mock_mask_secret, 
mock_supervisor_comms):
         """var.json.<sensitive_key> masks the entire raw JSON string because 
the variable key is sensitive."""
@@ -464,7 +494,10 @@ class TestVariableAccessor:
         val = accessor.aws_regions
 
         assert val == ["us-east-1", "eu-west-1"]
-        mock_mask_secret.assert_called_once_with(raw_json, "aws_regions")
+        mock_mask_secret.assert_any_call(raw_json, "aws_regions")
+        mock_mask_secret.assert_any_call(["us-east-1", "eu-west-1"], 
"aws_regions")
+        # never anonymously -- that is what would mask the elements globally
+        assert mock.call(["us-east-1", "eu-west-1"]) not in 
mock_mask_secret.call_args_list
 
     @mock.patch("airflow.sdk.execution_time.context.mask_secret")
     def test_var_json_invalid_json_raises(self, mock_mask_secret):

Reply via email to