Skip to content

Mask list-shaped Variable values on deserialization - #70891

Open
potiuk wants to merge 1 commit into
apache:mainfrom
potiuk:mask-list-shaped-variable-values
Open

Mask list-shaped Variable values on deserialization#70891
potiuk wants to merge 1 commit into
apache:mainfrom
potiuk:mask-list-shaped-variable-values

Conversation

@potiuk

@potiuk potiuk commented Aug 1, 2026

Copy link
Copy Markdown
Member

_mask_and_deserialize_variable dispatched on the top-level type of the
deserialized value and handled only str and dict. A Variable whose JSON is a
list therefore matched no branch and was returned with nothing inside it masked.

add_mask walks dicts and iterables itself, so the same list nested one level
inside a dict was already masked -- the top-level dispatch was the only thing
deciding whether the nested values were reached at all.

Approach

Add the list branch, passing the value under the variable's key rather
than anonymously.

That distinction is the whole design of the change. Elements of a list have no
key names of their own, so they follow the variable key's sensitivity -- the same
rule already applied to a plain string value. Passing the list anonymously would
mask every element unconditionally and add each to the global pattern set, so a
Variable holding ["us-east-1", "eu-west-1"] would cause us-east-1 to be
redacted everywhere it appeared in any log. test_var_json_list_value_does_not_over_mask
already guarded that, and this keeps it true. Dicts inside the list are still
masked by their own key names.

Verified against the real masker:

variable key result
[{"password": "…"}] db_configs [{"password": "***"}]
["us-east-1", "eu-west-1"] aws_regions unchanged
["…"] my_password ***
{"password": "…"} db_config {"password": "***"} (unchanged path)

test_var_json_list_value_does_not_over_mask is updated: it asserted
mask_secret was called exactly once, and there are now two calls. The property
it guards is unchanged and the assertion is now stated directly -- the list is
passed under the variable's key, and never anonymously.

Test plan

  • test_var_json_masks_list_values -- list handed to the masker under the
    variable's key
  • test_var_json_list_value_does_not_over_mask -- updated to assert the
    no-over-masking property rather than the call count
  • test_var_json_scalar_values_pass_through -- int/bool/null/float returned
    unchanged
  • Verified against unmodified code: both list tests fail
  • test_context.py -- 170 passed
  • ruff check / ruff format clean
Was generative AI tooling used to co-author this PR?
  • Yes — Claude Opus 5 (1M context)

Generated-by: Claude Opus 5 (1M context) following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

_mask_and_deserialize_variable dispatched on the top-level type of the
deserialized value and handled only str and dict, so a Variable whose JSON is a
list was returned with no masking applied to anything inside it. add_mask walks
iterables itself, so the same list nested one level inside a dict was already
masked -- only a top-level one was skipped.

The list is passed under the variable's key rather than anonymously: elements
have no key names of their own, so they follow the variable key's sensitivity,
and a list of ordinary values such as region names is not added to the global
pattern set. Dicts inside the list are still masked by their own key names.
@potiuk potiuk added this to the Airflow 3.3.1 milestone Aug 1, 2026
@potiuk potiuk added the backport-to-v3-3-test Backport to v3-3-test label Aug 1, 2026

@SameerMesiah97 SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Just a few stylistic nits (for e.g. I found one comment a bit hard to understand).

# its own, so bare values inside follow the same rule as the plain-string case above.
# Masking them unconditionally would add every element to the global pattern set, so a
# list of ordinary values such as region names would be redacted everywhere it appeared.
# Dicts *inside* the list are still masked by their own key names.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to read this a few times to understand what it was trying to convey. I think the comment can be more concise and clear. Please see below:

# Pass the Variable's key so list elements inherit the Variable's sensitivity
# instead of being added to the global mask patterns.

@mock.patch("airflow.sdk.execution_time.context.mask_secret")
def test_var_json_masks_list_values(self, mock_mask_secret, mock_supervisor_comms):
"""A JSON list is handed to the masker whole, exactly as a dict is.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep the first line for the docstring.

this: bare values inside a list have no key names of their own, so they follow the
variable key's sensitivity. Passing the list anonymously instead would add every element
to the global pattern set, and an ordinary value such as a region name would then be
redacted everywhere it appeared in the logs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. The first line of the docstring can be kept and extra context can be retained in the comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants