Pushkal-Gupta opened a new pull request, #74871:
URL: https://github.com/apache/airflow/pull/74871

   Depends on #74654. That one is approved and green and needs nothing more 
from me, it just needs a committer. This branch is stacked on it and touches 
the same two scripts, so until it lands the diff here carries its commits too. 
I'll rebase once it merges.
   
   The generated API permission reference lists `PUT /api/v2/backfills`, which 
is not a real endpoint, and leaves out six endpoints that are. It also prints 
five paths with a `:path` suffix that no client ever calls.
   
   ### What's wrong
   
   `airflow-core/docs/security/api_permissions_ref.rst` is generated by 
`scripts/ci/prek/extract_permissions.py`, which statically reads the route 
decorators under `api_fastapi/core_api/routes/public/`. I compared what it 
produces against FastAPI's own `v2-rest-api-generated.yaml`, which covers the 
same routers, so the two should agree exactly. The spec has 134 operations. 22 
of them are missing from the doc, and 17 rows in the doc have no matching 
operation.
   
   Six of the missing ones are backfill endpoints:
   
   ```
   POST   /api/v2/backfills/dry_run
   GET    /api/v2/backfills/{backfill_id}
   GET    /api/v2/backfills/{backfill_id}/dag_runs
   PUT    /api/v2/backfills/{backfill_id}/cancel
   PUT    /api/v2/backfills/{backfill_id}/pause
   PUT    /api/v2/backfills/{backfill_id}/unpause
   ```
   
   `backfills.py` is the one route file that passes the path as a keyword:
   
   ```python
   @backfills_router.put(
       path="/{backfill_id}/pause",
       dependencies=[Depends(action_logging()), 
Depends(requires_access_backfill(method="PUT"))],
   )
   ```
   
   The extractor only read `decorator.args[0]`, so for all eight routes in that 
file the path came out empty and `full_path` ended up as just the router 
prefix, `/api/v2/backfills`. Dedup then folded eight operations into three 
rows, and one of those three is `PUT /api/v2/backfills`, which doesn't exist, 
since PUT only appears on the sub-paths. 126 routes in the repo pass the path 
positionally and 8 pass it as `path=`, which is probably why nobody hit this.
   
   The other 16 rows are a smaller thing. Routes declared as 
`/{pool_name:path}` get printed with the converter still attached. That's 
Starlette's matching syntax rather than part of the URL, and the OpenAPI spec 
and the real request path are both `/{pool_name}`. It affects pools, variables, 
xcoms and the two state-store paths.
   
   The FAB page comes out of the same extractor, so it has the same holes. 
There it also means an operator looking up any of the six backfill endpoints 
finds no row telling them which FAB permissions and minimum role it needs.
   
   ### The fix
   
   Read the path whichever way the decorator passes it, and drop the converter 
so the documented path is the one clients call. If a decorator has no path at 
all, raise instead of quietly falling back to the prefix, since that fallback 
is what turned a parsing gap into wrong documentation.
   
   After the change the doc covers all 134 operations with nothing left over: 
both the 22 missing and the 17 phantom rows go to 0, and the entry count goes 
from 137 to 142.
   
   I made the same two changes in `fab_permissions_doc.py`, and also let it 
walk `async def` handlers the way `extract_permissions.py` already does. 
Nothing there is broken today, since every FAB route is a sync `def` with a 
positional path, but it would drop rows silently rather than fail if that 
changed. I left its tolerant `continue` for a non-constant path alone rather 
than making that raise too, since that part isn't what this PR is about.
   
   ### Tests
   
   Unit tests for both helpers, including the `path=` form taken from 
`backfills.py` and a decorator with no path at all, plus two end-to-end cases 
on synthetic route files for the keyword path and the converter.
   
   The guard I most wanted is an invariant rather than a snapshot: every route 
decorator in the source has to produce its own row. Against the current code it 
fails with `backfills.py declares 8 routes but the table holds 3 distinct 
operations`, so it would have caught this when `backfills.py` was written. 
There's a second check that no documented path contains a converter.
   
   All four of those fail without the change. `scripts/tests/` goes from 1684 
to 1698 passed, 1 skipped. ruff, mypy and the 43 prek checks that apply to 
these five files are clean, and both hooks pass leaving the docs byte-identical 
on a second run.
   
   No newsfragment, since this is a generated doc and the tooling behind it, 
with no user-facing code change.
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   - [X] Yes (please specify the tool below)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to