Hide team scoped values of options registered as sensitive - #70755
Conversation
amoghrajesh
left a comment
There was a problem hiding this comment.
Separate issue found while reviewing: https://github.com/apache/airflow/blob/main/airflow-core/src/airflow/api_fastapi/core_api/services/public/config.py#L40-L59 only matches literal secrets/workers section names, not their team scoped spelling. Same root cause, different mechanism, untouched here. _get_custom_secret_backend is not team aware either so nothing real leaks today but worth a follow up (cc: @vincbeck / @o-nikolas)
Although I am ok with this PR, would like either @vincbeck or @o-nikolas to review as well.
Options are registered as sensitive under their base section, but a team scoped override lives in a `[<team>=<section>]` config file section, or in an `AIRFLOW__<TEAM>___<SECTION>__<KEY>` environment variable. Every sensitivity decision was a direct membership test against the registered base pairs, and the masking pass iterated those pairs and looked each section up verbatim, so a team scoped section was never visited and its value was returned in clear. Resolve the team scoped spelling back to the base option before deciding whether a value is sensitive. `team_section_name` now builds the config file section name at both construction sites so the two representations cannot drift, `base_section_name` recovers the base section from it, and `is_sensitive_option` tests a pair directly, then via the base section, then via the tail an environment variable contributes -- so it can only ever recognise more options as sensitive, never fewer. The team name is not parsed out of an environment variable name, because a team name may contain underscores; the name is matched against the tail each registered option contributes instead. The config file section name is split on the last separator, so the base section is recovered even for a team name that contains the separator itself. Four call sites use the predicate: the masking pass, the environment collection, `write`, and the single option config route, which does not go through `as_dict` and so needed its own change. Team scoped `_cmd` and `_secret` entries are hidden in place rather than resolved into their value, because resolving them is not supported for a team. 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
…touched gap The per-key secrets-backend options are matched by literal section name and stay unaware of a team scoped spelling. Nothing leaks while the secrets backend itself is not team aware, so it is recorded rather than fixed here.
d782298 to
9429d55
Compare
|
Both threads addressed and resolved — duplicated rationale trimmed to one site in each case. The separate gap you spotted in Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting |
amoghrajesh
left a comment
There was a problem hiding this comment.
I have a qn and few nits.
Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>
…t_config.py Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>
Backport successfully created: v3-3-testNote: As of Merging PRs targeted for Airflow 3.X In matter of doubt please ask in #release-management Slack channel.
|
apache#70755) * Hide team scoped values of options registered as sensitive Options are registered as sensitive under their base section, but a team scoped override lives in a `[<team>=<section>]` config file section, or in an `AIRFLOW__<TEAM>___<SECTION>__<KEY>` environment variable. Every sensitivity decision was a direct membership test against the registered base pairs, and the masking pass iterated those pairs and looked each section up verbatim, so a team scoped section was never visited and its value was returned in clear. Resolve the team scoped spelling back to the base option before deciding whether a value is sensitive. `team_section_name` now builds the config file section name at both construction sites so the two representations cannot drift, `base_section_name` recovers the base section from it, and `is_sensitive_option` tests a pair directly, then via the base section, then via the tail an environment variable contributes -- so it can only ever recognise more options as sensitive, never fewer. The team name is not parsed out of an environment variable name, because a team name may contain underscores; the name is matched against the tail each registered option contributes instead. The config file section name is split on the last separator, so the base section is recovered even for a team name that contains the separator itself. Four call sites use the predicate: the masking pass, the environment collection, `write`, and the single option config route, which does not go through `as_dict` and so needed its own change. Team scoped `_cmd` and `_secret` entries are hidden in place rather than resolved into their value, because resolving them is not supported for a team. 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 * Add newsfragment for the team scoped sensitive option masking change * Keep one copy of the team scoped masking rationale, and record the untouched gap The per-key secrets-backend options are matched by literal section name and stay unaware of a team scoped spelling. Nothing leaks while the secrets backend itself is not team aware, so it is recorded rather than fixed here. * Update shared/configuration/tests/configuration/test_parser.py Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com> * Update airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com> --------- (cherry picked from commit d41ac7b) Co-authored-by: Jarek Potiuk <jarek@potiuk.com> Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>
apache#70755) * Hide team scoped values of options registered as sensitive Options are registered as sensitive under their base section, but a team scoped override lives in a `[<team>=<section>]` config file section, or in an `AIRFLOW__<TEAM>___<SECTION>__<KEY>` environment variable. Every sensitivity decision was a direct membership test against the registered base pairs, and the masking pass iterated those pairs and looked each section up verbatim, so a team scoped section was never visited and its value was returned in clear. Resolve the team scoped spelling back to the base option before deciding whether a value is sensitive. `team_section_name` now builds the config file section name at both construction sites so the two representations cannot drift, `base_section_name` recovers the base section from it, and `is_sensitive_option` tests a pair directly, then via the base section, then via the tail an environment variable contributes -- so it can only ever recognise more options as sensitive, never fewer. The team name is not parsed out of an environment variable name, because a team name may contain underscores; the name is matched against the tail each registered option contributes instead. The config file section name is split on the last separator, so the base section is recovered even for a team name that contains the separator itself. Four call sites use the predicate: the masking pass, the environment collection, `write`, and the single option config route, which does not go through `as_dict` and so needed its own change. Team scoped `_cmd` and `_secret` entries are hidden in place rather than resolved into their value, because resolving them is not supported for a team. 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 * Add newsfragment for the team scoped sensitive option masking change * Keep one copy of the team scoped masking rationale, and record the untouched gap The per-key secrets-backend options are matched by literal section name and stay unaware of a team scoped spelling. Nothing leaks while the secrets backend itself is not team aware, so it is recorded rather than fixed here. * Update shared/configuration/tests/configuration/test_parser.py Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com> * Update airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com> --------- (cherry picked from commit d41ac7b) Co-authored-by: Jarek Potiuk <jarek@potiuk.com> Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>
#70755) (#71099) * Hide team scoped values of options registered as sensitive Options are registered as sensitive under their base section, but a team scoped override lives in a `[<team>=<section>]` config file section, or in an `AIRFLOW__<TEAM>___<SECTION>__<KEY>` environment variable. Every sensitivity decision was a direct membership test against the registered base pairs, and the masking pass iterated those pairs and looked each section up verbatim, so a team scoped section was never visited and its value was returned in clear. Resolve the team scoped spelling back to the base option before deciding whether a value is sensitive. `team_section_name` now builds the config file section name at both construction sites so the two representations cannot drift, `base_section_name` recovers the base section from it, and `is_sensitive_option` tests a pair directly, then via the base section, then via the tail an environment variable contributes -- so it can only ever recognise more options as sensitive, never fewer. The team name is not parsed out of an environment variable name, because a team name may contain underscores; the name is matched against the tail each registered option contributes instead. The config file section name is split on the last separator, so the base section is recovered even for a team name that contains the separator itself. Four call sites use the predicate: the masking pass, the environment collection, `write`, and the single option config route, which does not go through `as_dict` and so needed its own change. Team scoped `_cmd` and `_secret` entries are hidden in place rather than resolved into their value, because resolving them is not supported for a team. 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 * Add newsfragment for the team scoped sensitive option masking change * Keep one copy of the team scoped masking rationale, and record the untouched gap The per-key secrets-backend options are matched by literal section name and stay unaware of a team scoped spelling. Nothing leaks while the secrets backend itself is not team aware, so it is recorded rather than fixed here. * Update shared/configuration/tests/configuration/test_parser.py * Update airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py --------- (cherry picked from commit d41ac7b) Co-authored-by: Jarek Potiuk <jarek@potiuk.com> Co-authored-by: Amogh Desai <amoghrajesh1999@gmail.com>
… as sensitive
_is_per_key_sensitive_option and _mask_per_key_sensitive_options matched
literal section names ('secrets', 'workers') only, so a team scoped
spelling of the same option -- the [<team>=secrets] config-file section,
or the AIRFLOW__<TEAM>___SECRETS__BACKEND_KWARG__* env var, both of which
are reported under a section named after the team -- was never recognised
as sensitive.
Resolve the section to its base section via base_section_name (the same
helper AirflowConfigParser.is_sensitive_option uses for registered options,
introduced in apache#70755) before deciding sensitivity, in both
_is_per_key_sensitive_option and _mask_per_key_sensitive_options.
closes apache#71037
Options are registered as sensitive under their base section, but a team scoped
override lives elsewhere:
[<team>=<section>]sectionAIRFLOW__<TEAM>___<SECTION>__<KEY>Every sensitivity decision was a direct membership test against the registered base
pairs, and the masking pass iterated those pairs and looked each section up
verbatim. A team scoped section was therefore never visited, and its value was
returned in clear text while the same option in the base section was correctly
hidden.
The environment spelling misses by a further step:
_include_envssplitsAIRFLOW__TEAM_A___CELERY__BROKER_URLon__withmaxsplit=2, yielding sectionteam_aand key_celery__broker_url— a pair that is neither the base nor theteam scoped section name, so no base keyed loop could reach it either.
Approach
Sensitivity decisions now resolve the team scoped spelling back to the base option.
team_section_name()builds the config file section name, and is used at bothconstruction sites so the two representations cannot drift
base_section_name()recovers the base section from itis_sensitive_option()tests a pair directly, then via the base section, then viathe tail an environment variable contributes — so it can only ever recognise
more options as sensitive, never fewer
Two deliberate choices:
The team name is never parsed out of an environment variable name. A team name
may contain underscores, so splitting
AIRFLOW__<TEAM>___<SECTION>__<KEY>isambiguous. The name is matched against the tail each registered sensitive option
contributes instead. No section ends with
_or.and no key starts with_anywhere in core or providers, so this cannot collide with a global variable.
The config file section is split on the last separator. Base section names
never contain
=(checked across all sections inconfig.ymland everyconfig:block in every
provider.yaml), so the base section is recovered even for a teamname that contains the separator itself.
airflow teams synccreates teams fromdag bundle config, whose
team_namefield is not validated against the CLI'scharset, so such a name is conceivable.
routes/public/config.pyneeded its own two line change:get_config_valuenevergoes through
as_dict, so the parser change cannot reach it._get_cmd_option_from_config_sourcesand_get_secret_option_from_config_sourcesare intentionally left alone. They are only called with base pairs, and making them
team aware would mean executing team scoped commands and fetching team secrets,
which the parser deliberately refuses. Those fallbacks are hidden instead.
Behaviour changes
< hidden >inas_dict,GET /config,GET /config/section/{section}/option/{option}andwrite(hide_sensitive=True)— symmetric with how the base option alreadybehaved. Anyone using the API to read a team's real value will now get the mask.
_cmd/_secretentries are replaced with< hidden >in placerather than resolved and deleted as in a base section, since resolution is not
supported for a team. The command string itself is no longer shown.
write()masking now covers team sections, soairflow config listis included.display_sensitive=Truestill returnsreal values.
as_dictcost 0.62 ms versus 0.48 ms baseline at 750 options with 40 sensitivevalues.
Test plan
shared/configuration/tests/configuration/test_parser.py— 6 tests:base_section_nameparametrized including a separator inside the team name;is_sensitive_optionasserting spoofing fails in both directions;as_dictfor a file backed team section, withdisplay_sourceanddisplay_sensitive=Truevariants; team_cmd/_secretfallbacks; team environment variable;write()airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_config.py—TestTeamScopedOptionMasking, covering the single option path masked and a non sensitive team scoped option still readableruffcheck and format clean;mypyreports the same single pre-existing error as baselineDeferred
The synthetic per-key secrets-backend options in
api_fastapi/core_api/services/public/config.pymatch literal section names only, so they stay unaware of a team scoped spelling. Nothing leaks today because_get_custom_secret_backendis not team aware; tracked in #71037, and linked from a comment at that site.Was generative AI tooling used to co-author this PR?
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