Resolve backfill_id in the access dependency with the type the routes declare - #70889
Conversation
amoghrajesh
left a comment
There was a problem hiding this comment.
I guess we can do better here, maybe @pierrejeambrun has an opinion too.
… declare The backfill routes declare `backfill_id: NonNegativeInt`, but `requires_access_backfill` parsed the raw path value with `int()` and swallowed the failure. The two parsers do not agree: pydantic's lax mode validates "1.0" and "1.00" to 1, while `int()` rejects both. Dependencies resolve before the endpoint's own parameter validation, so for those spellings the dependency left the Dag unresolved on a request the handler then served against backfill 1 -- the two disagreed about which Dag the request concerned. Parse with the same TypeAdapter the routes declare so they cannot diverge.
An unspecced Mock accepts any attribute, so the test would keep passing if the dependency started reading something the real Request, Session or Backfill does not have.
00f6305 to
8f271a5
Compare
|
cc: @pierrejeambrun ? |
|
Both threads addressed and resolved. Mocks now carry I did not move Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting |
amoghrajesh
left a comment
There was a problem hiding this comment.
I see, seems like a valid issue to me. Can someone with better backfill API knowledge look at this one to prevent any ripple effects?
A backfill_id that parses but matches no row falls through to the body's dag_id, so an unknown backfill answers 404 where an unauthorized one answers 403 and a caller can tell which ids exist. That is a separate fix from the parser divergence this change closes, and it has to keep the three body-authorized routes working, so it is tracked rather than folded in here. The comment above the adapter also loses the history that led to it; what matters going forward is the rule it states.
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.
|
…e the routes declare (#70889) (#71090) * Resolve backfill_id in the access dependency with the type the routes declare The backfill routes declare `backfill_id: NonNegativeInt`, but `requires_access_backfill` parsed the raw path value with `int()` and swallowed the failure. The two parsers do not agree: pydantic's lax mode validates "1.0" and "1.00" to 1, while `int()` rejects both. Dependencies resolve before the endpoint's own parameter validation, so for those spellings the dependency left the Dag unresolved on a request the handler then served against backfill 1 -- the two disagreed about which Dag the request concerned. Parse with the same TypeAdapter the routes declare so they cannot diverge. * Use spec'd mocks in the backfill authorization dependency test An unspecced Mock accepts any attribute, so the test would keep passing if the dependency started reading something the real Request, Session or Backfill does not have. * Point at the tracking issue for the unknown-backfill fallback A backfill_id that parses but matches no row falls through to the body's dag_id, so an unknown backfill answers 404 where an unauthorized one answers 403 and a caller can tell which ids exist. That is a separate fix from the parser divergence this change closes, and it has to keep the three body-authorized routes working, so it is tracked rather than folded in here. The comment above the adapter also loses the history that led to it; what matters going forward is the rule it states. (cherry picked from commit a6265b7) Co-authored-by: Jarek Potiuk <jarek@potiuk.com> Co-authored-by: Rahul Vats <43964496+vatsrahul1001@users.noreply.github.com>
The backfill routes declare
backfill_id: NonNegativeInt, butrequires_access_backfillparsed the raw path value withint()and swallowedthe failure. Those two parsers do not agree.
The divergence
backfill_idNonNegativeInt(the handler)int()(the dependency)"42""42.0""42.00""42.","42e0","42.5","0x2a"pydantic's lax mode accepts the two decimal spellings and coerces them to an
int;
int()rejects them.FastAPI resolves route dependencies before the endpoint's own parameter
validation, so for those spellings the dependency's parse failed, it left the
Dag unresolved, and the request went on to be served by the handler against
backfill 42. The dependency and the handler disagreed about which backfill --
and therefore which Dag -- the request concerned.
Approach
Parse with the same
TypeAdapter(NonNegativeInt)the routes declare, so thetwo cannot diverge by construction. The adapter is module-level and named, so
the coupling to the route signature is explicit rather than re-derived.
This deliberately does not introduce a stricter parser. An earlier revision
used
int()with an explicit 400, which changed the public API contract: amalformed path value such as
/backfills/invalid_idpreviously producedFastAPI's structured 422 (
loc: ["path", "backfill_id"]), and a 400 fromthe dependency preempted it. Five existing route tests caught that. Matching
the declared type keeps every existing status code and body shape intact --
values both parsers reject still yield the handler's 422, and an unknown
backfill still yields each route's own 404 wording.
Test plan
test_requires_access_backfill_authorizes_the_backfill_the_handler_will_act_on-- parametrized over
42,42.0,42.00; asserts the Dag comes from theresolved backfill and not from a value supplied on the request
42.0and42.00fail,42passes --so the test pins the divergence and not the plumbing
airflow-core/tests/unit/api_fastapi/core_api/test_security.py-- 111 passedairflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py-- 63 passed, including the four
test_invalid_id422 cases andtest_no_existruff check/ruff formatcleanWas 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