Repository navigation
Reject blank pool names and variable keys in the REST API - #74896
Conversation
fat-catTW
left a comment
There was a problem hiding this comment.
Thanks for contribution!
Could we add tests for the PATCH and bulk endpoints that also use PoolBody/VariableBody?
|
Nice catch, thanks. The bulk endpoints were affected and untested, and chasing it turned up a third route I had missed. Added 14 tests:
On
|
fat-catTW
left a comment
There was a problem hiding this comment.
Thanks for the change!
Look great!
Backport successfully created: v3-4-testNote: As of Merging PRs targeted for Airflow 3.X In matter of doubt please ask in #release-management Slack channel.
|
POST /api/v2/poolsandPOST /api/v2/variablesaccept a blank name, so you can create a pool called""or a variable keyed"". Worse,PATCH /api/v2/pools/{pool_name}with{"name": ""}renames an existing pool to the empty string and answers 200.What's wrong
Neither request model requires a character in the name, only a maximum length:
So
""," ","\t"and"\n"all pass validation and reach the database. You can address the row afterwards because these routes are declared/{pool_name:path}and/{variable_key:path}, and Starlette compiles a:pathconverter to(?P<name>.*), which matches an empty segment. A plain/{name}compiles to[^/]+and does not, which is why only these two resources show it.The rename is the part that bothers me more.
patch_poolalready tries to stop a body name that disagrees with the URI:It tests truthiness, so
""is read as "no name supplied", skips the comparison and carries on into the update.PATCH /api/v2/pools/realpoolwith{"name": "", "slots": 2, "include_deferred": false}returns 200 today and leaves the pool named"".patch_variablespells the same guardpatch_body.key != variable_key, so variables were never affected by it. The two routes just wrote the check differently.The fix
Require one non-whitespace character in the two request bodies, and compare the pool name against
Noneinstead of truthiness.The pattern is the one already used on asset partition keys, added in #69453 for the same reason:
\Sand not something stricter because names here really do contain slashes, which is why the routes use:pathat all and what the existingPOOL3_NAME = "pool3/with_slashes"covers.ConnectionBody.connection_iduses^[\w.-]+$, which would reject those. Andpatternrather thanstrip_whitespaceso the stored name stays exactly what was sent.Blank names now get 422, and the blank rename gets the 400 the guard always intended. Reads are untouched, so a deployment that already picked up a blank-named pool or variable through this gap can still list and delete it.
I left
PoolPatchBodyalone deliberately. Constraining an optional field makes datamodel-codegen lift it into aRootModel, andairflowctl'sPoolsOperations.updatebuilds its URL asf"pools/{pool_body.pool}", which then comes out aspools/root='pool_name'and fails two of its tests. Fixing the route guard deals with the rename directly and leaves the generated client alone. That f-string is fragile for the same reason, andSlotsis already aRootModel, so something will run into it eventually, but that is a separate change.Four of the eight files are regenerated rather than hand-edited: the OpenAPI spec, the UI
schemas.gen.tsand the airflowctl datamodels each pick up the same twopatternentries from their hooks.types.gen.tsdoes not change, since the TypeScript types never carried the existingmaxLengtheither.Tests
Eleven cases:
""," "," ","\t"and"\n"against both POST routes, plus one for the rename that checks both the 400 and that the pool still has its name. All eleven fail without this change, and the rename one fails asassert 200 == 400with the pool left called"".test_pools.pyandtest_variables.pygo from 177 to 188 passed,airflow-ctlstays at 439, ruff and mypy are clean, and the three generator hooks are stable on a second run.Was generative AI tooling used to co-author this PR?