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 43b674c1309 [v3-4-test] Mask registered secrets at any nesting depth
(#73793) (#75112)
43b674c1309 is described below
commit 43b674c13094d8c62c76a7368e5719dc1c5d7c8b
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Oct 19 18:26:40 2026 +0200
[v3-4-test] Mask registered secrets at any nesting depth (#73793) (#75112)
* Mask registered secrets at any nesting depth
Pattern-based masking of registered secrets stopped at max_depth: a
string nested deeper than that was returned without the replacer being
applied, while key-name masking already worked at any depth. Apply the
replacer to string leaves wherever they sit. max_depth still bounds the
fail-closed walk of a value already known to be sensitive.
Generated-by: Claude Opus 5
* Add newsfragment
(cherry picked from commit bfa2390919046ddcee5e568362adff7725672886)
Generated-by: Claude Opus 5
Co-authored-by: Jarek Potiuk <[email protected]>
---
airflow-core/newsfragments/73793.improvement.rst | 1 +
.../src/airflow_shared/secrets_masker/secrets_masker.py | 15 ++++++---------
.../tests/secrets_masker/test_secrets_masker.py | 11 ++++++++---
3 files changed, 15 insertions(+), 12 deletions(-)
diff --git a/airflow-core/newsfragments/73793.improvement.rst
b/airflow-core/newsfragments/73793.improvement.rst
new file mode 100644
index 00000000000..fa43fd229c7
--- /dev/null
+++ b/airflow-core/newsfragments/73793.improvement.rst
@@ -0,0 +1 @@
+Mask registered secrets at any nesting depth. Secret values registered with
the masker are now replaced wherever they appear in a nested structure, instead
of only up to the masker's maximum depth.
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 6de7a125534..1286e296f12 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
@@ -363,10 +363,12 @@ class SecretsMasker(logging.Filter):
self, item: Redactable, name: str | None, depth: int, max_depth: int,
replacement: str = "***"
) -> Redacted:
try:
- # Key-name-based redaction is unbounded by depth — sensitive keys
- # must fail closed at any nesting level. The depth cutoff below is
- # only used to bound the work of pattern-based string masking and
- # to terminate recursion through self-referential iterables.
+ # Both key-name-based and pattern-based redaction apply at any
+ # nesting depth: a registered secret or a sensitive key must fail
+ # closed wherever it sits in the structure. ``max_depth`` only
bounds
+ # the fail-closed walk of a value already known to be sensitive
+ # (``_redact_all``); self-referential structures are stopped by
+ # Python's recursion limit and the except clause below.
if name and self.should_hide_value_for_key(name):
return self._redact_all(item, depth, max_depth,
replacement=replacement)
# Always walk dicts so deeper sensitive keys are still caught;
@@ -402,11 +404,6 @@ class SecretsMasker(logging.Filter):
)
for subval in item
]
- # The depth cutoff only bounds the work of pattern-based string
- # masking below — key-name redaction (dicts and iterables above) is
- # unbounded so sensitive keys fail closed at any depth.
- if depth > max_depth:
- return item
if isinstance(item, Enum):
return self._redact(
item=item.value, name=name, depth=depth,
max_depth=max_depth, replacement=replacement
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 f9f880891c7..7bd72c101c1 100644
--- a/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
+++ b/shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py
@@ -675,9 +675,14 @@ class TestSecretsMasker:
(["abcdef"], ["***"], 1),
([[[["abcdef"]]]], [[[["***"]]]], None),
([[[[["abcdef"]]]]], [[[[["***"]]]]], None),
- # Items below max depth aren't redacted
- ([[[[[["abcdef"]]]]]], [[[[[["abcdef"]]]]]], None),
- ([["abcdef"]], [["abcdef"]], 1),
+ # Registered secrets are redacted at any depth, whatever max_depth
says
+ ([[[[[["abcdef"]]]]]], [[[[[["***"]]]]]], None),
+ ([["abcdef"]], [["***"]], 1),
+ (
+ {"a": {"b": {"c": {"d": {"e": {"f": {"g":
"prefix-abcdef-suffix"}}}}}}},
+ {"a": {"b": {"c": {"d": {"e": {"f": {"g":
"prefix-***-suffix"}}}}}}},
+ None,
+ ),
],
)
def test_redact_max_depth(self, val, expected, max_depth):