This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new aa8542f69ad Default-deny auth at the API and UI router level (#66505)
aa8542f69ad is described below
commit aa8542f69ad936906e39d0e28b677a676e74142f
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue May 19 13:19:42 2026 +0200
Default-deny auth at the API and UI router level (#66505)
* Default-deny auth at the API and UI router level
Add `dependencies=[Depends(get_user)]` to `authenticated_router`
(parent of every route under `/api/v2` except the explicit no-auth
carve-outs `monitor_router`, `version_router`, and the public
`auth_router`) and to `ui_router` (every route under `/ui`).
Today every authenticated route already declares `GetUserDep` or a
`requires_access_*` dependency that itself depends on `get_user`, so
this is purely additive — FastAPI deduplicates the dependency via
its per-request cache, so each request still resolves `get_user`
once. The value is preventing a future route from being added under
either router without an auth check: the router-level dependency
catches the regression at registration time rather than at audit
time.
Add a structural test that asserts both routers carry the
router-level `Depends(get_user)`, so a future refactor that drops
the dependency without considering its purpose fails the test
rather than silently widening the unauthenticated surface.
* Move test imports to top of file
Address review feedback from @Lee-W on PR #66505.
---
.../api_fastapi/core_api/routes/public/__init__.py | 9 ++++++--
.../api_fastapi/core_api/routes/ui/__init__.py | 7 +++++-
.../tests/unit/api_fastapi/core_api/test_app.py | 27 ++++++++++++++++++++++
3 files changed, 40 insertions(+), 3 deletions(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/__init__.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/__init__.py
index 0b501b4f99f..b590424de4e 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/__init__.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/__init__.py
@@ -17,7 +17,7 @@
from __future__ import annotations
-from fastapi import status
+from fastapi import Depends, status
from airflow.api_fastapi.common.router import AirflowRouter
from airflow.api_fastapi.core_api.openapi.exceptions import
create_openapi_http_exception_doc
@@ -49,11 +49,16 @@ from airflow.api_fastapi.core_api.routes.public.tasks
import tasks_router
from airflow.api_fastapi.core_api.routes.public.variables import
variables_router
from airflow.api_fastapi.core_api.routes.public.version import version_router
from airflow.api_fastapi.core_api.routes.public.xcom import xcom_router
+from airflow.api_fastapi.core_api.security import get_user
public_router = AirflowRouter(prefix="/api/v2")
-# Router with common attributes for all routes
+# Router-level Depends(get_user) makes authentication the default for every
route below.
+# Individual routes still declare their own GetUserDep / requires_access_*
dependencies for
+# fine-grained authorization; the router-level dependency is the
defense-in-depth backstop
+# that prevents a future route from accidentally being added without an auth
check.
authenticated_router = AirflowRouter(
+ dependencies=[Depends(get_user)],
responses=create_openapi_http_exception_doc([status.HTTP_401_UNAUTHORIZED,
status.HTTP_403_FORBIDDEN]),
)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/__init__.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/__init__.py
index f1780bfffb9..dcade9c88b5 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/__init__.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/__init__.py
@@ -16,6 +16,8 @@
# under the License.
from __future__ import annotations
+from fastapi import Depends
+
from airflow.api_fastapi.common.router import AirflowRouter
from airflow.api_fastapi.core_api.routes.ui.assets import assets_router
from airflow.api_fastapi.core_api.routes.ui.auth import auth_router
@@ -33,8 +35,11 @@ from airflow.api_fastapi.core_api.routes.ui.grid import
grid_router
from airflow.api_fastapi.core_api.routes.ui.partitioned_dag_runs import
partitioned_dag_runs_router
from airflow.api_fastapi.core_api.routes.ui.structure import structure_router
from airflow.api_fastapi.core_api.routes.ui.teams import teams_router
+from airflow.api_fastapi.core_api.security import get_user
-ui_router = AirflowRouter(prefix="/ui", include_in_schema=False)
+# Every UI route requires an authenticated user; the router-level dependency
makes that
+# the default so future routes added here cannot accidentally skip
authentication.
+ui_router = AirflowRouter(prefix="/ui", include_in_schema=False,
dependencies=[Depends(get_user)])
ui_router.include_router(auth_router)
ui_router.include_router(assets_router)
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/test_app.py
b/airflow-core/tests/unit/api_fastapi/core_api/test_app.py
index 4aadcdd005a..061ac788d8d 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/test_app.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/test_app.py
@@ -26,6 +26,9 @@ from fastapi.responses import StreamingResponse
from starlette.routing import Mount
from airflow.api_fastapi.app import create_app
+from airflow.api_fastapi.core_api.routes.public import authenticated_router
+from airflow.api_fastapi.core_api.routes.ui import ui_router
+from airflow.api_fastapi.core_api.security import get_user
from tests_common.test_utils.db import clear_db_jobs
@@ -116,3 +119,27 @@ class TestGzipMiddleware:
# Ensure we do not reintroduce Transfer-Encoding: chunked
assert "transfer-encoding" not in headers
+
+
+class TestRouterLevelDefaultDeny:
+ """
+ Authentication is enforced as a router-level default on the routers that
+ serve user-facing endpoints. A future route added under one of these
+ routers cannot accidentally be added without an auth dependency — the
+ router-level Depends(get_user) is the defense-in-depth backstop.
+ """
+
+ def test_authenticated_router_carries_get_user_dependency(self):
+ assert any(
+ getattr(dep, "dependency", None) is get_user for dep in
authenticated_router.dependencies
+ ), (
+ "authenticated_router must declare Depends(get_user) at the router
level so every "
+ "route below /api/v2 (other than the explicit no-auth carve-outs
in public_router) "
+ "default-denies unauthenticated requests."
+ )
+
+ def test_ui_router_carries_get_user_dependency(self):
+ assert any(getattr(dep, "dependency", None) is get_user for dep in
ui_router.dependencies), (
+ "ui_router must declare Depends(get_user) at the router level so
every UI endpoint "
+ "default-denies unauthenticated requests."
+ )