This is an automated email from the ASF dual-hosted git repository.
amoghdesai pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-1-test by this push:
new 2f83cb9a6bf [v3-1-test] Redact secrets in rendered templates properly
to not expose them on UI (#58767) (#58772)
2f83cb9a6bf is described below
commit 2f83cb9a6bf8abf9a64613cfb5d34286abfd1c27
Author: Amogh Desai <[email protected]>
AuthorDate: Thu Nov 27 21:41:02 2025 +0530
[v3-1-test] Redact secrets in rendered templates properly to not expose
them on UI (#58767) (#58772)
---
.../src/airflow/sdk/execution_time/task_runner.py | 12 ++++-
.../task_sdk/execution_time/test_task_runner.py | 52 ++++++++++++++++++++++
2 files changed, 63 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 461e770c425..e4fd3ace38c 100644
--- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py
+++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py
@@ -767,9 +767,19 @@ def _serialize_rendered_fields(task: AbstractOperator) ->
dict[str, JsonValue]:
# TODO: Port one of the following to Task SDK
# airflow.serialization.helpers.serialize_template_field or
# airflow.models.renderedtifields.get_serialized_template_fields
+ from airflow.sdk._shared.secrets_masker import redact
from airflow.serialization.helpers import serialize_template_field
- return {field: serialize_template_field(getattr(task, field), field) for
field in task.template_fields}
+ rendered_fields = {}
+ for field in task.template_fields:
+ value = getattr(task, field)
+ serialized = serialize_template_field(value, field)
+ # Redact secrets in the task process itself before sending to API
server
+ # This ensures that the secrets those are registered via mask_secret()
on workers / dag processor are properly masked
+ # on the UI.
+ rendered_fields[field] = redact(serialized, field)
+
+ return rendered_fields # type: ignore[return-value] # Convince mypy that
this is OK since we pass JsonValue to redact, so it will return the same
def _build_asset_profiles(lineage_objects: list) -> Iterator[AssetProfile]:
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 4adf3c23723..9728325cc23 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
@@ -87,6 +87,7 @@ from airflow.sdk.execution_time.comms import (
GetVariable,
GetXCom,
GetXComSequenceSlice,
+ MaskSecret,
OKResponse,
PreviousDagRunResult,
PrevSuccessfulDagRunResult,
@@ -2364,6 +2365,57 @@ class TestXComAfterTaskExecution:
),
)
+ @pytest.mark.enable_redact
+ def test_rendered_templates_mask_secrets(self, create_runtime_ti,
mock_supervisor_comms):
+ """Test that secrets registered with mask_secret() are redacted in
rendered template fields."""
+ from unittest.mock import call
+
+ from airflow.sdk._shared.secrets_masker import _secrets_masker
+ from airflow.sdk.log import mask_secret
+
+ _secrets_masker().add_mask("admin_user_12345", None)
+
+ class CustomOperator(BaseOperator):
+ template_fields = ("username", "region")
+
+ def __init__(self, username, region, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.username = username
+ self.region = region
+
+ def execute(self, context):
+ # Only mask username
+ mask_secret(self.username)
+
+ task = CustomOperator(
+ task_id="test_masking",
+ username="admin_user_12345",
+ region="us-west-2",
+ )
+
+ runtime_ti = create_runtime_ti(task=task,
dag_id="test_secrets_in_rtif")
+ run(runtime_ti, context=runtime_ti.get_template_context(),
log=mock.MagicMock())
+
+ assert (
+ call(MaskSecret(value="admin_user_12345", name=None,
type="MaskSecret"))
+ in mock_supervisor_comms.send.mock_calls
+ )
+ # Region should not be masked
+ assert (
+ call(MaskSecret(value="us-west-2", name=None, type="MaskSecret"))
+ not in mock_supervisor_comms.send.mock_calls
+ )
+
+ assert (
+ call(
+ msg=SetRenderedFields(
+ rendered_fields={"username": "***", "region": "us-west-2"},
+ type="SetRenderedFields",
+ )
+ )
+ in mock_supervisor_comms.send.mock_calls
+ )
+
class TestDagParamRuntime:
DEFAULT_ARGS = {