This is an automated email from the ASF dual-hosted git repository.
rahulvats 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 a9dea6d0b81 Prevent client secrets and proxy credentials from being
logged in Microsoft Graph hook logs (#59688)
a9dea6d0b81 is described below
commit a9dea6d0b81d67843c2507bd85af772fde9dd738
Author: Ankit Chaurasia <[email protected]>
AuthorDate: Wed Dec 24 15:56:51 2025 +0545
Prevent client secrets and proxy credentials from being logged in Microsoft
Graph hook logs (#59688)
Prevent client secrets and proxy credentials from being logged in Microsoft
Graph hook logs (#59688)
---
airflow-core/newsfragments/59688.improvement.rst | 1 +
.../src/airflow/providers/common/compat/sdk.py | 12 +++++++++++-
.../providers/microsoft/azure/hooks/msgraph.py | 9 +++++----
.../unit/microsoft/azure/hooks/test_msgraph.py | 22 +++++++++++++++++++++-
.../secrets_masker/secrets_masker.py | 2 ++
task-sdk/src/airflow/sdk/log.py | 4 +---
6 files changed, 41 insertions(+), 9 deletions(-)
diff --git a/airflow-core/newsfragments/59688.improvement.rst
b/airflow-core/newsfragments/59688.improvement.rst
new file mode 100644
index 00000000000..d3c592cfcc3
--- /dev/null
+++ b/airflow-core/newsfragments/59688.improvement.rst
@@ -0,0 +1 @@
+Add ``proxy`` and ``proxies`` to ``DEFAULT_SENSITIVE_FIELDS`` in
secrets_masker to treat proxy configurations as sensitive by default
diff --git a/providers/common/compat/src/airflow/providers/common/compat/sdk.py
b/providers/common/compat/src/airflow/providers/common/compat/sdk.py
index f08c358133a..2d6edd2f43f 100644
--- a/providers/common/compat/src/airflow/providers/common/compat/sdk.py
+++ b/providers/common/compat/src/airflow/providers/common/compat/sdk.py
@@ -90,7 +90,8 @@ if TYPE_CHECKING:
TaskDeferred as TaskDeferred,
XComNotFound as XComNotFound,
)
- from airflow.sdk.observability.stats import Stats # noqa: F401
+ from airflow.sdk.log import redact as redact
+ from airflow.sdk.observability.stats import Stats as Stats
# Airflow 3-only exceptions (conditionally imported)
if AIRFLOW_V_3_0_PLUS:
@@ -239,6 +240,15 @@ _IMPORT_MAP: dict[str, str | tuple[str, ...]] = {
# Observability
#
============================================================================
"Stats": ("airflow.sdk.observability.stats", "airflow.stats"),
+ #
============================================================================
+ # Secrets Masking
+ #
============================================================================
+ "redact": (
+ "airflow.sdk.log",
+ "airflow.sdk._shared.secrets_masker",
+ "airflow.sdk.execution_time.secrets_masker",
+ "airflow.utils.log.secrets_masker",
+ ),
}
# Airflow 3-only exceptions (not available in Airflow 2)
diff --git
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
index 84698c3d6a4..6b62fac0b78 100644
---
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
+++
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
@@ -59,6 +59,7 @@ if TYPE_CHECKING:
from airflow.providers.common.compat.sdk import Connection
+from airflow.providers.common.compat.sdk import redact
PaginationCallable = Callable[..., tuple[str, dict[str, Any] | None]]
@@ -303,7 +304,7 @@ class KiotaRequestAdapterHook(BaseHook):
self.log.info("Host: %s", host)
self.log.info("Base URL: %s", base_url)
self.log.info("Client id: %s", client_id)
- self.log.info("Client secret: %s", client_secret)
+ self.log.info("Client secret: %s", redact(client_secret,
name="client_secret"))
self.log.info("API version: %s", api_version)
self.log.info("Scope: %s", scopes)
self.log.info("Verify: %s", verify)
@@ -311,8 +312,8 @@ class KiotaRequestAdapterHook(BaseHook):
self.log.info("Trust env: %s", trust_env)
self.log.info("Authority: %s", authority)
self.log.info("Allowed hosts: %s", allowed_hosts)
- self.log.info("Proxies: %s", proxies)
- self.log.info("HTTPX Proxies: %s", httpx_proxies)
+ self.log.info("Proxies: %s", redact(proxies, name="proxies"))
+ self.log.info("HTTPX Proxies: %s", redact(httpx_proxies,
name="proxies"))
credentials = self.get_credentials(
login=connection.login,
password=connection.password,
@@ -433,7 +434,7 @@ class KiotaRequestAdapterHook(BaseHook):
self.log.info("Certificate data: %s", certificate_data is not None)
self.log.info("Authority: %s", authority)
self.log.info("Disable instance discovery: %s",
disable_instance_discovery)
- self.log.info("MSAL Proxies: %s", msal_proxies)
+ self.log.info("MSAL Proxies: %s", redact(msal_proxies, name="proxies"))
if certificate_path or certificate_data:
return CertificateCredential(
tenant_id=tenant_id,
diff --git
a/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_msgraph.py
b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_msgraph.py
index f4d8bba7d8f..6f6216d8fa9 100644
--- a/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_msgraph.py
+++ b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_msgraph.py
@@ -21,7 +21,7 @@ import inspect
from json import JSONDecodeError
from os.path import dirname
from typing import TYPE_CHECKING, cast
-from unittest.mock import Mock
+from unittest.mock import Mock, patch
import pytest
from httpx import Response
@@ -412,6 +412,26 @@ class TestKiotaRequestAdapterHook:
assert isinstance(actual, list)
assert actual == [users, next_users]
+ @pytest.mark.asyncio
+ async def test_build_request_adapter_masks_secrets(self):
+ """Test that sensitive data is masked when building request adapter."""
+ with patch_hook(
+ side_effect=lambda conn_id: get_airflow_connection(
+ conn_id=conn_id,
+ password="my_secret_password",
+ proxies={"http": "http://user:pass@proxy:3128"},
+ )
+ ):
+ with
patch("airflow.providers.microsoft.azure.hooks.msgraph.redact") as mock_redact:
+ mock_redact.side_effect = lambda x, name=None: "***" if x else
x
+
+ hook = KiotaRequestAdapterHook(conn_id="msgraph_api")
+ await hook.get_async_conn()
+
+ assert mock_redact.call_count >= 3
+ mock_redact.assert_any_call({"http":
"http://user:pass@proxy:3128"}, name="proxies")
+ mock_redact.assert_any_call("my_secret_password",
name="client_secret")
+
class TestResponseHandler:
def test_default_response_handler_when_json(self):
diff --git
a/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
b/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
index bf3fd91ebd7..e9d439e2e74 100644
--- a/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
+++ b/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
@@ -58,6 +58,8 @@ DEFAULT_SENSITIVE_FIELDS = frozenset(
"passwd",
"password",
"private_key",
+ "proxy",
+ "proxies",
"secret",
"token",
"keyfile_dict",
diff --git a/task-sdk/src/airflow/sdk/log.py b/task-sdk/src/airflow/sdk/log.py
index 198b156a1d6..9e0835a1efb 100644
--- a/task-sdk/src/airflow/sdk/log.py
+++ b/task-sdk/src/airflow/sdk/log.py
@@ -37,7 +37,7 @@ if TYPE_CHECKING:
from airflow.sdk.types import Logger, RuntimeTaskInstanceProtocol as
RuntimeTI
-__all__ = ["configure_logging", "reset_logging", "mask_secret"]
+from airflow.sdk._shared.secrets_masker import redact
class _WarningsInterceptor:
@@ -64,8 +64,6 @@ class _WarningsInterceptor:
def mask_logs(logger: Any, method_name: str, event_dict: EventDict) ->
EventDict:
- from airflow.sdk._shared.secrets_masker import redact
-
event_dict = redact(event_dict) # type: ignore[assignment]
return event_dict