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

amoghdesai pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 490eb31ac32 Fix secrets masking in Rendered Templates for complex 
objects (#61394)
490eb31ac32 is described below

commit 490eb31ac3275e901fd60c45c632a0032d716074
Author: Amogh Desai <[email protected]>
AuthorDate: Wed Feb 11 12:25:21 2026 +0530

    Fix secrets masking in Rendered Templates for complex objects (#61394)
---
 .../src/airflow/sdk/execution_time/task_runner.py  | 13 ++++++-
 .../task_sdk/execution_time/test_task_runner.py    | 45 ++++++++++++++++++++++
 2 files changed, 57 insertions(+), 1 deletion(-)

diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py 
b/task-sdk/src/airflow/sdk/execution_time/task_runner.py
index 9fa13a08d36..2b3933c1071 100644
--- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py
+++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py
@@ -957,13 +957,24 @@ def _serialize_template_field(template_field: Any, name: 
str) -> str | dict | li
             return tuple(sort_dict_recursively(item) for item in obj)
         return obj
 
+    def _fallback_serialization(obj):
+        """Serialize objects with to_dict() method (eg: k8s objects) for 
json.dumps() default parameter."""
+        if hasattr(obj, "to_dict"):
+            return obj.to_dict()
+        raise TypeError(f"cannot serialize {obj}")
+
     max_length = conf.getint("core", "max_templated_field_length")
 
     if not is_jsonable(template_field):
         try:
             serialized = template_field.serialize()
         except AttributeError:
-            serialized = str(template_field)
+            # check if these objects can be converted to JSON serializable 
types
+            try:
+                serialized = json.dumps(template_field, 
default=_fallback_serialization)
+            except (TypeError, ValueError):
+                # fall back to string representation if not
+                serialized = str(template_field)
         if len(serialized) > max_length:
             rendered = redact(serialized, name)
             return (
diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py 
b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
index fc832e3195d..8724877853f 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py
@@ -2657,6 +2657,51 @@ class TestRuntimeTaskInstance:
             in mock_supervisor_comms.send.mock_calls
         )
 
+    @pytest.mark.enable_redact
+    def test_rendered_templates_masks_secrets_in_complex_objects(
+        self, create_runtime_ti, mock_supervisor_comms
+    ):
+        """Test that secrets in complex objects like V1EnvVar are masked 
well."""
+        from kubernetes import client as k8s
+
+        from airflow.sdk._shared.secrets_masker import _secrets_masker
+
+        secret1 = "This is a longer test phrase. We are checking if this 
handles regular sentences."
+        secret2 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad 
minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 
commodo consequat."
+        _secrets_masker().add_mask(secret1, None)
+        _secrets_masker().add_mask(secret2, None)
+
+        class CustomOperator(BaseOperator):
+            template_fields = ("env_vars",)
+
+            def __init__(self, env_vars, *args, **kwargs):
+                super().__init__(*args, **kwargs)
+                self.env_vars = env_vars
+
+            def execute(self, context):
+                pass
+
+        env_vars = [
+            k8s.V1EnvVar(name="var1", value="This is a test phrase."),
+            k8s.V1EnvVar(name="var2", value=secret1),
+            k8s.V1EnvVar(name="var3", value=secret2),
+        ]
+
+        task = CustomOperator(
+            task_id="test_complex_object_masking",
+            env_vars=env_vars,
+        )
+
+        runtime_ti = create_runtime_ti(task=task, 
dag_id="test_complex_object_dag")
+        run(runtime_ti, context=runtime_ti.get_template_context(), 
log=mock.MagicMock())
+
+        rendered_fields = 
mock_supervisor_comms.send.mock_calls[0].kwargs["msg"].rendered_fields
+        assert rendered_fields is not None
+        assert (
+            rendered_fields["env_vars"]
+            == '[{"name": "var1", "value": "This is a test phrase.", 
"value_from": null}, {"name": "var2", "value": "***", "value_from": null}, 
{"name": "var3", "value": "***", "value_from": null}]'
+        )
+
 
 class TestXComAfterTaskExecution:
     @pytest.mark.parametrize(

Reply via email to