Only resolve a team namespaced environment secret for its own team - #70736
Merged
potiuk merged 2 commits intoAug 1, 2026
Merged
Conversation
A team specific Connection or Variable lives in the `_<TEAM_NAME>___<SECRET_ID>` namespace of the environment. An id that already spells such a namespace out therefore reaches that variable through the team agnostic lookup, which is only correct when the namespace is the one the lookup is made for. The check guarding that had two gaps. Its pattern, `_[^_]+___.+`, could not span an underscore in the team name, and team names may contain underscores. And it only applied when no team was in scope, so with a team in scope it did nothing: the team scoped probe only returns on a hit, and a miss fell through to the team agnostic name, which is byte identical to the other team's variable. Recognise a namespaced id without assuming the team name has no underscores, deny it outright when no team is in scope, and otherwise allow it only when it begins with the namespace prefix that the team in scope itself builds. The stored id is deliberately not parsed: a team name may contain underscores, so `_a___b___c` is both team `a` with id `b___c` and team `a___b` with id `c`, and no pattern separates them. Comparing against the prefix the caller builds needs no such reading. Both lookups share the helper, so this covers Connections and Variables. 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
potiuk
requested review from
vincbeck
and removed request for
ashb and
dstandish
July 30, 2026 09:41
vincbeck
approved these changes
Jul 30, 2026
The previous guard compared the supplied id against the namespace prefix the caller's own team builds, and treated a match as proof the id was the caller's own. It is not. A team name may itself contain the `___` separator, so one team's namespace can start with another's: for a caller in team `a`, the id `_a___b___c` starts with `_A___`, but the variable it resolves, `AIRFLOW_CONN__A___B___C`, belongs to team `a___b`. The prefix cleared the guard, the team scoped lookup missed, and the team agnostic lookup returned the other team's secret -- the same cross-team read the guard exists to stop, for every team whose name extends the caller's. Drop the attribution attempt. The team scoped lookup runs first and is safe by construction, since it can only ever build the caller's own namespace. After it misses, an id that spells out any team namespace is refused, because the team agnostic lookup would land inside one. The id is never parsed to decide which team it belongs to -- that question has no answer. A caller reaching its own team's secret through the namespaced spelling rather than the bare id plus its team scope is no longer resolved. That spelling is what made a prefix match look like ownership.
Contributor
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.
|
github-actions Bot
pushed a commit
to aws-mwaa/upstream-to-airflow
that referenced
this pull request
Aug 1, 2026
… own team (apache#70736) * Only resolve a team namespaced environment secret for its own team A team specific Connection or Variable lives in the `_<TEAM_NAME>___<SECRET_ID>` namespace of the environment. An id that already spells such a namespace out therefore reaches that variable through the team agnostic lookup, which is only correct when the namespace is the one the lookup is made for. The check guarding that had two gaps. Its pattern, `_[^_]+___.+`, could not span an underscore in the team name, and team names may contain underscores. And it only applied when no team was in scope, so with a team in scope it did nothing: the team scoped probe only returns on a hit, and a miss fell through to the team agnostic name, which is byte identical to the other team's variable. Recognise a namespaced id without assuming the team name has no underscores, deny it outright when no team is in scope, and otherwise allow it only when it begins with the namespace prefix that the team in scope itself builds. The stored id is deliberately not parsed: a team name may contain underscores, so `_a___b___c` is both team `a` with id `b___c` and team `a___b` with id `c`, and no pattern separates them. Comparing against the prefix the caller builds needs no such reading. Both lookups share the helper, so this covers Connections and Variables. 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 * Refuse a team namespaced id for the team agnostic lookup outright The previous guard compared the supplied id against the namespace prefix the caller's own team builds, and treated a match as proof the id was the caller's own. It is not. A team name may itself contain the `___` separator, so one team's namespace can start with another's: for a caller in team `a`, the id `_a___b___c` starts with `_A___`, but the variable it resolves, `AIRFLOW_CONN__A___B___C`, belongs to team `a___b`. The prefix cleared the guard, the team scoped lookup missed, and the team agnostic lookup returned the other team's secret -- the same cross-team read the guard exists to stop, for every team whose name extends the caller's. Drop the attribution attempt. The team scoped lookup runs first and is safe by construction, since it can only ever build the caller's own namespace. After it misses, an id that spells out any team namespace is refused, because the team agnostic lookup would land inside one. The id is never parsed to decide which team it belongs to -- that question has no answer. A caller reaching its own team's secret through the namespaced spelling rather than the bare id plus its team scope is no longer resolved. That spelling is what made a prefix match look like ownership. (cherry picked from commit e0cac1f) Co-authored-by: Jarek Potiuk <jarek@potiuk.com>
7 tasks
potiuk
added a commit
that referenced
this pull request
Aug 1, 2026
… own team (#70736) (#70882) * Only resolve a team namespaced environment secret for its own team A team specific Connection or Variable lives in the `_<TEAM_NAME>___<SECRET_ID>` namespace of the environment. An id that already spells such a namespace out therefore reaches that variable through the team agnostic lookup, which is only correct when the namespace is the one the lookup is made for. The check guarding that had two gaps. Its pattern, `_[^_]+___.+`, could not span an underscore in the team name, and team names may contain underscores. And it only applied when no team was in scope, so with a team in scope it did nothing: the team scoped probe only returns on a hit, and a miss fell through to the team agnostic name, which is byte identical to the other team's variable. Recognise a namespaced id without assuming the team name has no underscores, deny it outright when no team is in scope, and otherwise allow it only when it begins with the namespace prefix that the team in scope itself builds. The stored id is deliberately not parsed: a team name may contain underscores, so `_a___b___c` is both team `a` with id `b___c` and team `a___b` with id `c`, and no pattern separates them. Comparing against the prefix the caller builds needs no such reading. Both lookups share the helper, so this covers Connections and Variables. 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 * Refuse a team namespaced id for the team agnostic lookup outright The previous guard compared the supplied id against the namespace prefix the caller's own team builds, and treated a match as proof the id was the caller's own. It is not. A team name may itself contain the `___` separator, so one team's namespace can start with another's: for a caller in team `a`, the id `_a___b___c` starts with `_A___`, but the variable it resolves, `AIRFLOW_CONN__A___B___C`, belongs to team `a___b`. The prefix cleared the guard, the team scoped lookup missed, and the team agnostic lookup returned the other team's secret -- the same cross-team read the guard exists to stop, for every team whose name extends the caller's. Drop the attribution attempt. The team scoped lookup runs first and is safe by construction, since it can only ever build the caller's own namespace. After it misses, an id that spells out any team namespace is refused, because the team agnostic lookup would land inside one. The id is never parsed to decide which team it belongs to -- that question has no answer. A caller reaching its own team's secret through the namespaced spelling rather than the bare id plus its team scope is no longer resolved. That spelling is what made a prefix match look like ownership. (cherry picked from commit e0cac1f) Co-authored-by: Jarek Potiuk <jarek@potiuk.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A team specific Connection or Variable lives in the
_<TEAM_NAME>___<SECRET_ID>namespace of the environment. An id that already spells such a namespace out
therefore reaches that variable through the team agnostic lookup, which is only
correct when the namespace is the one the lookup is made for.
_is_team_specific_accessed_as_globalguarded that, and had two gaps:_[^_]+___.+could not span an underscore in the team name, andteam names may contain underscores (
^[a-zA-Z0-9_-]{3,50}$);team_name is None), so with a teamin scope it did nothing. The team scoped probe above it only returns on a hit,
so a miss fell through to
os.environ.get(PREFIX + id.upper())— byte identicalto another team's variable name.
Approach
The lookup order is inverted, and the guard stops trying to attribute an id to a
team:
ever build
PREFIX + "_" + <caller's team> + "___" + id, the caller's ownnamespace;
_.+___.+) isrefused, because the team agnostic lookup would land inside one;
The id is never parsed to decide which team it belongs to, because that question
has no answer. A team name may itself contain the
___separator, so_a___b___cis simultaneously teamawith idb___cand teama___bwith idc, and nothing in the string distinguishes them.An earlier revision of this PR did try to answer it, by comparing the id against
the namespace prefix the caller's own team builds and treating a match as proof of
ownership. That is not sound, and it left the cross-team read open for a whole
class of team names: for a caller in team
a, the id_a___b___cstarts with_A___, so the guard cleared it — but the variable it resolves,AIRFLOW_CONN__A___B___C, is teama___b's. The prefix matched, the team scopedlookup missed, and the team agnostic lookup returned the other team's secret. Any
team whose name extends the caller's was readable. The regression test
test_team_whose_name_extends_the_callers_is_not_readablecovers exactly thatshape.
Both
get_conn_valueandget_variableshare the helper, so this coversConnections and Variables together.
Behaviour change
Ids of the shape
_<something>___<something>are no longer resolvable through theteam agnostic environment variable name from any scope — including the team
that owns the namespace, which previously could reach its own secret through the
namespaced spelling. That spelling is exactly what made a prefix match look like
ownership. Callers reach their own team's secrets the normal way, with the bare id
plus their team scope, which is unaffected.
Practically: a deployment that stored a team agnostic connection or variable
whose id begins with
_and contains___loses that lookup. Legitimate globalids and same team lookups are unaffected.
Test plan
airflow-core/tests/unit/always/test_secrets_environment_variables.py, 34 cases —every test parametrized over both lookups and over an underscore-bearing (
team_a)and underscore-free (
teama) team nameresolved through the namespaced spelling by any caller including the owning team
test_team_whose_name_extends_the_callers_is_not_readable— callerteam_a,target
team_a___prod, id_team_a___prod___dbconn; asserts the read is refusedand that
team_a___prodstill reaches its own secret with the bare idagnostic still resolves for any team scope
collision-prone
team_a/team_a___prodanda/a___b), both lookups, everycaller scope including none, and both bare and namespaced ids: 0 cross-team
resolutions with this change, 4 with the previous revision
ruff check/ruff format --checkclean on both filesWas 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