Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,11 +49,16 @@
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]),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,8 +34,11 @@
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)
Expand Down
30 changes: 30 additions & 0 deletions airflow-core/tests/unit/api_fastapi/core_api/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,33 @@ def test_gzip_middleware_should_not_be_chunked(self, test_client) -> None:

# 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):
from airflow.api_fastapi.core_api.routes.public import authenticated_router
from airflow.api_fastapi.core_api.security import get_user

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):
from airflow.api_fastapi.core_api.routes.ui import ui_router
from airflow.api_fastapi.core_api.security import get_user

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