This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-4-test by this push:
new 20ff45d43a2 [v3-4-test] Redact Connection extra fields that providers
declare as secrets (#73797) (#76815)
20ff45d43a2 is described below
commit 20ff45d43a24a1ca6314c0d831ed94c70182ac68
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Nov 10 12:32:04 2026 +0100
[v3-4-test] Redact Connection extra fields that providers declare as
secrets (#73797) (#76815)
Providers declare these connection extra fields as secrets, but their names
were missing from the redaction allowlist, so their values were not redacted
like other secret fields. They are matched as whole names because, as
substrings, they would also hide non-secret fields such as kube_config_path
and credential_config_file.
(cherry picked from commit eeab7a877df75b5132702821abf380e10fb87fc6)
Co-authored-by: JelyFishhhhhh
<[email protected]>
Co-authored-by: JelyF1shhhhhh <[email protected]>
---
.../tests/unit/always/test_providers_manager.py | 32 ++++++++++++++++++++++
.../secrets_masker/secrets_masker.py | 22 +++++++++++++++
.../tests/secrets_masker/test_secrets_masker.py | 29 ++++++++++++++++++++
3 files changed, 83 insertions(+)
diff --git a/airflow-core/tests/unit/always/test_providers_manager.py
b/airflow-core/tests/unit/always/test_providers_manager.py
index 424a629ba1f..b82ff4c6228 100644
--- a/airflow-core/tests/unit/always/test_providers_manager.py
+++ b/airflow-core/tests/unit/always/test_providers_manager.py
@@ -36,6 +36,7 @@ from unittest.mock import Mock, patch
import pytest
+from airflow._shared.secrets_masker import DEFAULT_SENSITIVE_FIELDS,
SecretsMasker
from airflow.providers_manager import (
DialectInfo,
LazyDictWithCache,
@@ -269,6 +270,37 @@ class TestProviderManager:
# widgets loaded from YAML metadata only
assert len(connections_form_widgets) == yaml_widgets
+ def test_sensitive_connection_fields_are_redactable(self):
+ """
+ Every connection field a provider declares as sensitive must be in the
redaction allowlist.
+
+ Providers already state which fields are secret; this asserts the
allowlist never drifts
+ apart from them.
+ """
+ # Check the shipped defaults rather than the process-wide masker, whose
+ # sensitive_variables_fields is only populated once settings have been
configured.
+ masker = SecretsMasker()
+ masker.sensitive_variables_fields = list(DEFAULT_SENSITIVE_FIELDS)
+
+ # "config" is too generic a field name for the allowlist. apprise's
"config" is masked in
+ # API responses because its provider declares it sensitive, but it is
not masked in task logs.
+ allowed_unredacted = {("config", "apache-airflow-providers-apprise")}
+
+ provider_manager = ProvidersManager()
+ missing = sorted(
+ {
+ (widget.field_name, widget.package_name)
+ for widget in
provider_manager._connection_form_widgets_from_metadata.values()
+ if widget.is_sensitive and not
masker.should_hide_value_for_key(widget.field_name)
+ }
+ - allowed_unredacted
+ )
+ assert not missing, (
+ "These connection fields are declared sensitive by their provider
but are not "
+ "redacted. Add each name to DEFAULT_SENSITIVE_FIELD_NAMES in "
+ f"shared/secrets_masker: {missing}"
+ )
+
def test_field_behaviours(self, yaml_ui_metadata_counts):
_, yaml_behaviours = yaml_ui_metadata_counts
provider_manager = ProvidersManager()
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 d802ea6bcb6..7137e526010 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
@@ -76,6 +76,26 @@ DEFAULT_SENSITIVE_FIELDS = frozenset(
)
"""Names of fields (Connection extra, Variable key name etc.) that are deemed
sensitive"""
+DEFAULT_SENSITIVE_FIELD_NAMES = frozenset(
+ {
+ "app_key",
+ "certificate",
+ "credential",
+ "keytab",
+ "kube_config",
+ "oauth",
+ "routing_key",
+ "thumbprint",
+ "write_key",
+ }
+)
+"""
+Names of fields (Connection extra, Variable key name etc.) that are sensitive
only as a whole name.
+
+Matching these as substrings would also hide non-secret fields such as
``kube_config_path`` or
+``credential_config_file``. The ``extra__<conn_type>__<name>`` form of each
name matches too.
+"""
+
SECRETS_TO_SKIP_MASKING = {"airflow"}
"""Common terms that should be excluded from masking in both production and
tests"""
@@ -589,6 +609,8 @@ class SecretsMasker(logging.Filter):
normalized = _NON_WORD_RE.sub("_", name.lower())
split = _CAMEL_ACRONYM_RE.sub(r"\1_\2",
_CAMEL_LOWER_UPPER_RE.sub(r"\1_\2", name))
split = _NON_WORD_RE.sub("_", split.lower())
+ if any(form.rsplit("__", 1)[-1] in DEFAULT_SENSITIVE_FIELD_NAMES
for form in (normalized, split)):
+ return True
# Check both forms so fields configured without separators (e.g.
``mysensitivekey``)
# keep matching camelCase keys such as ``mySensitiveKey``.
return any(s in normalized or s in split for s in
self.sensitive_variables_fields)
diff --git a/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
b/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
index f1b415bde2c..0a128c1114e 100644
--- a/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
+++ b/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
@@ -893,6 +893,35 @@ class TestShouldHideValueForKey:
configure_secrets_masker_for_test(masker)
assert expected_result == masker.should_hide_value_for_key(key)
+ @pytest.mark.parametrize(
+ ("key", "expected_result"),
+ [
+ ("app_key", True),
+ ("certificate", True),
+ ("credential", True),
+ ("keytab", True),
+ ("kube_config", True),
+ ("KUBE_CONFIG", True),
+ ("oauth", True),
+ ("routing_key", True),
+ ("thumbprint", True),
+ ("write_key", True),
+ ("extra__kubernetes__kube_config", True),
+ ("kubeConfig", True),
+ ("routingKey", True),
+ ("kube_config_path", False),
+ ("kubeConfigPath", False),
+ ("extra__kubernetes__kube_config_path", False),
+ ("credential_config_file", False),
+ ("certificate_path", False),
+ ("service_principal_oauth", False),
+ ],
+ )
+ def test_hiding_whole_field_names(self, key, expected_result):
+ masker = SecretsMasker()
+ configure_secrets_masker_for_test(masker)
+ assert expected_result == masker.should_hide_value_for_key(key)
+
@pytest.mark.parametrize(
("sensitive_variable_fields", "key", "expected_result"),
[