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."
+        )

Reply via email to