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):