From c1a15a9d6fe325e79626f9284da49ffd7c195c5c Mon Sep 17 00:00:00 2001 From: kalyanr Date: Wed, 23 Apr 2025 09:11:11 +0530 Subject: [PATCH 01/13] add SimpleAllAdminMiddleware --- .../auth/managers/simple/middleware.py | 34 +++++++++++++++++++ .../src/airflow/api_fastapi/core_api/app.py | 6 ++++ 2 files changed, 40 insertions(+) create mode 100644 airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py new file mode 100644 index 0000000000000..004f29671075f --- /dev/null +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py @@ -0,0 +1,34 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +from fastapi import Request +from starlette.middleware.base import BaseHTTPMiddleware + +from airflow.api_fastapi.auth.managers.simple.services.login import SimpleAuthManagerLogin + + +class SimpleAllAdminMiddleware(BaseHTTPMiddleware): + """Middleware that automatically generates and includes auth header for simple auth manager.""" + + async def dispatch(self, request: Request, call_next): + # Starlette Request is expected to be immutable, but we monkey-patch it to add the auth header + # https://github.com/fastapi/fastapi/issues/2727#issuecomment-770202019 + token = SimpleAuthManagerLogin.create_token_all_admins() + request.headers.__dict__["_list"].append((b"authorization", f"Bearer {token}".encode())) + return await call_next(request) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/app.py b/airflow-core/src/airflow/api_fastapi/core_api/app.py index 49f522994313a..90327ecc26677 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/app.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/app.py @@ -167,4 +167,10 @@ def init_error_handlers(app: FastAPI) -> None: def init_middlewares(app: FastAPI) -> None: + from airflow.configuration import conf + app.add_middleware(FlaskExceptionsMiddleware) + if conf.getboolean("core", "simple_auth_manager_all_admins"): + from airflow.api_fastapi.auth.managers.simple.middleware import SimpleAllAdminMiddleware + + app.add_middleware(SimpleAllAdminMiddleware) From 2059b929f5d963c983caf373be2fb27e805e700a Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 00:06:41 +0530 Subject: [PATCH 02/13] add tests --- .../auth/managers/simple/test_middleware.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py diff --git a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py new file mode 100644 index 0000000000000..2e7479d92412d --- /dev/null +++ b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py @@ -0,0 +1,51 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +from fastapi.testclient import TestClient + +from airflow.api_fastapi.app import create_app + +from tests_common.test_utils.config import conf_vars + + +def test_invoking_without_auth_header(): + with conf_vars( + { + ( + "core", + "simple_auth_manager_all_admins", + ): "true", + ( + "webserver", + "expose_config", + ): "true", + } + ): + app = create_app() + client = TestClient(app) + + # Fetch all routes from the FastAPI app + for route in app.routes: + if hasattr(route, "path") and hasattr(route, "methods"): + for method in route.methods: + if method in {"GET", "POST", "PUT", "DELETE", "PATCH"}: # Common HTTP methods + response = client.request(method, route.path) + assert response.status_code not in {401, 403}, ( + f"Unexpected status code {response.status_code} for {method} {route.path}" + ) From 754b6a730a924a1e29657837eaa08f506f14feac Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 00:12:46 +0530 Subject: [PATCH 03/13] update test --- .../auth/managers/simple/test_middleware.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py index 2e7479d92412d..1af5384778aaa 100644 --- a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py +++ b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py @@ -40,12 +40,10 @@ def test_invoking_without_auth_header(): app = create_app() client = TestClient(app) - # Fetch all routes from the FastAPI app for route in app.routes: if hasattr(route, "path") and hasattr(route, "methods"): for method in route.methods: - if method in {"GET", "POST", "PUT", "DELETE", "PATCH"}: # Common HTTP methods - response = client.request(method, route.path) - assert response.status_code not in {401, 403}, ( - f"Unexpected status code {response.status_code} for {method} {route.path}" - ) + response = client.request(method, route.path) + assert response.status_code not in {401, 403}, ( + f"Unexpected status code {response.status_code} for {method} {route.path}" + ) From cd1bb3501bff630c18bb71d2c746cd1e836f6b41 Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 00:15:20 +0530 Subject: [PATCH 04/13] update test name --- .../unit/api_fastapi/auth/managers/simple/test_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py index 1af5384778aaa..0d2fa054176bd 100644 --- a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py +++ b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py @@ -24,7 +24,7 @@ from tests_common.test_utils.config import conf_vars -def test_invoking_without_auth_header(): +def test_invoke_api_without_auth_header(): with conf_vars( { ( From 4181bb19b8ab6acff99496bcd6200f9adae1f905 Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 08:40:23 +0530 Subject: [PATCH 05/13] refactor test --- .../auth/managers/simple/test_middleware.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py index 0d2fa054176bd..48d35d69fc521 100644 --- a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py +++ b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py @@ -17,6 +17,7 @@ from __future__ import annotations +import pytest from fastapi.testclient import TestClient from airflow.api_fastapi.app import create_app @@ -24,26 +25,29 @@ from tests_common.test_utils.config import conf_vars -def test_invoke_api_without_auth_header(): +@pytest.fixture +def all_access_test_client(): with conf_vars( { - ( - "core", - "simple_auth_manager_all_admins", - ): "true", - ( - "webserver", - "expose_config", - ): "true", + ("core", "simple_auth_manager_all_admins"): "true", + ("webserver", "expose_config"): "true", } ): app = create_app() - client = TestClient(app) - - for route in app.routes: - if hasattr(route, "path") and hasattr(route, "methods"): - for method in route.methods: - response = client.request(method, route.path) - assert response.status_code not in {401, 403}, ( - f"Unexpected status code {response.status_code} for {method} {route.path}" - ) + yield TestClient(app) + + +@pytest.mark.parametrize( + "method, path", + [ + (method, route.path) + for route in create_app().routes + if hasattr(route, "path") and hasattr(route, "methods") + for method in route.methods + ], +) +def test_all_endpoints_without_auth_header(all_access_test_client, method, path): + response = all_access_test_client.request(method, path) + assert response.status_code not in {401, 403}, ( + f"Unexpected status code {response.status_code} for {method} {path}" + ) From c503df8acd871305a257f98da42ff62b8c374af1 Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 09:02:20 +0530 Subject: [PATCH 06/13] sort test order --- .../unit/api_fastapi/auth/managers/simple/test_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py index 48d35d69fc521..0cca31440baf5 100644 --- a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py +++ b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py @@ -41,7 +41,7 @@ def all_access_test_client(): "method, path", [ (method, route.path) - for route in create_app().routes + for route in sorted(create_app().routes, key=lambda r: r.path) # type: ignore[attr-defined] if hasattr(route, "path") and hasattr(route, "methods") for method in route.methods ], From de0e3766d720fd3dd7402cf61f76116c609f92d1 Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 09:33:51 +0530 Subject: [PATCH 07/13] hardcode endpoints --- .../auth/managers/simple/test_middleware.py | 125 +++++++++++++++++- 1 file changed, 121 insertions(+), 4 deletions(-) diff --git a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py index 0cca31440baf5..83e316f6040c6 100644 --- a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py +++ b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py @@ -40,10 +40,127 @@ def all_access_test_client(): @pytest.mark.parametrize( "method, path", [ - (method, route.path) - for route in sorted(create_app().routes, key=lambda r: r.path) # type: ignore[attr-defined] - if hasattr(route, "path") and hasattr(route, "methods") - for method in route.methods + ("GET", "/api/v1/{_:path}"), + ("GET", "/api/v2/assets"), + ("GET", "/api/v2/assets/aliases"), + ("GET", "/api/v2/assets/aliases/{asset_alias_id}"), + ("GET", "/api/v2/assets/events"), + ("POST", "/api/v2/assets/events"), + ("GET", "/api/v2/assets/{asset_id}"), + ("POST", "/api/v2/assets/{asset_id}/materialize"), + ("GET", "/api/v2/assets/{asset_id}/queuedEvents"), + ("DELETE", "/api/v2/assets/{asset_id}/queuedEvents"), + ("GET", "/api/v2/backfills"), + ("POST", "/api/v2/backfills"), + ("POST", "/api/v2/backfills/dry_run"), + ("GET", "/api/v2/backfills/{backfill_id}"), + ("PUT", "/api/v2/backfills/{backfill_id}/cancel"), + ("PUT", "/api/v2/backfills/{backfill_id}/pause"), + ("PUT", "/api/v2/backfills/{backfill_id}/unpause"), + ("GET", "/api/v2/config"), + ("GET", "/api/v2/config/section/{section}/option/{option}"), + ("GET", "/api/v2/connections"), + ("POST", "/api/v2/connections"), + ("PATCH", "/api/v2/connections"), + ("POST", "/api/v2/connections/defaults"), + ("POST", "/api/v2/connections/test"), + ("DELETE", "/api/v2/connections/{connection_id}"), + ("GET", "/api/v2/connections/{connection_id}"), + ("PATCH", "/api/v2/connections/{connection_id}"), + ("GET", "/api/v2/dagReports"), + ("GET", "/api/v2/dagSources/{dag_id}"), + ("GET", "/api/v2/dagStats"), + ("GET", "/api/v2/dagTags"), + ("GET", "/api/v2/dagWarnings"), + ("GET", "/api/v2/dags"), + ("PATCH", "/api/v2/dags"), + ("GET", "/api/v2/dags/{dag_id}"), + ("PATCH", "/api/v2/dags/{dag_id}"), + ("DELETE", "/api/v2/dags/{dag_id}"), + ("GET", "/api/v2/dags/{dag_id}/assets/queuedEvents"), + ("DELETE", "/api/v2/dags/{dag_id}/assets/queuedEvents"), + ("GET", "/api/v2/dags/{dag_id}/assets/{asset_id}/queuedEvents"), + ("DELETE", "/api/v2/dags/{dag_id}/assets/{asset_id}/queuedEvents"), + ("POST", "/api/v2/dags/{dag_id}/clearTaskInstances"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns"), + ("POST", "/api/v2/dags/{dag_id}/dagRuns"), + ("POST", "/api/v2/dags/{dag_id}/dagRuns/list"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}"), + ("DELETE", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}"), + ("PATCH", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}"), + ("POST", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/clear"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances"), + ("POST", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/list"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}"), + ("PATCH", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/dependencies"), + ("PATCH", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/dry_run"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/links"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/listMapped"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/logs/{try_number}"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/tries"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/tries/{task_try_number}"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries"), + ("POST", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries/{xcom_key}"), + ( + "PATCH", + "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries/{xcom_key}", + ), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}"), + ("PATCH", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}"), + ( + "GET", + "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/dependencies", + ), + ("PATCH", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/dry_run"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/tries"), + ( + "GET", + "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/tries/{task_try_number}", + ), + ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/upstreamAssetEvents"), + ("GET", "/api/v2/dags/{dag_id}/dagVersions"), + ("GET", "/api/v2/dags/{dag_id}/dagVersions/{version_number}"), + ("GET", "/api/v2/dags/{dag_id}/details"), + ("GET", "/api/v2/dags/{dag_id}/tasks"), + ("GET", "/api/v2/dags/{dag_id}/tasks/{task_id}"), + ("GET", "/api/v2/eventLogs"), + ("GET", "/api/v2/eventLogs/{event_log_id}"), + ("GET", "/api/v2/importErrors"), + ("GET", "/api/v2/importErrors/{import_error_id}"), + ("GET", "/api/v2/jobs"), + ("GET", "/api/v2/monitor/health"), + ("PUT", "/api/v2/parseDagFile/{file_token}"), + ("GET", "/api/v2/plugins"), + ("GET", "/api/v2/plugins/importErrors"), + ("GET", "/api/v2/pools"), + ("POST", "/api/v2/pools"), + ("PATCH", "/api/v2/pools"), + ("DELETE", "/api/v2/pools/{pool_name}"), + ("GET", "/api/v2/pools/{pool_name}"), + ("PATCH", "/api/v2/pools/{pool_name}"), + ("GET", "/api/v2/providers"), + ("GET", "/api/v2/variables"), + ("POST", "/api/v2/variables"), + ("PATCH", "/api/v2/variables"), + ("DELETE", "/api/v2/variables/{variable_key}"), + ("GET", "/api/v2/variables/{variable_key}"), + ("PATCH", "/api/v2/variables/{variable_key}"), + ("GET", "/api/v2/version"), + ("GET", "/api/{_:path}"), + ("HEAD", "/docs/oauth2-redirect"), + ("GET", "/docs/oauth2-redirect"), + ("GET", "/ui/auth/menus"), + ("GET", "/ui/backfills"), + ("GET", "/ui/config"), + ("GET", "/ui/connections/hook_meta"), + ("GET", "/ui/dags/recent_dag_runs"), + ("GET", "/ui/dashboard/historical_metrics_data"), + ("GET", "/ui/dependencies"), + ("GET", "/ui/grid/{dag_id}"), + ("GET", "/ui/next_run_assets/{dag_id}"), + ("GET", "/ui/structure/structure_data"), ], ) def test_all_endpoints_without_auth_header(all_access_test_client, method, path): From 66f4f295a25353df30f4dbd96f76c08364575271 Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 10:11:19 +0530 Subject: [PATCH 08/13] use session --- .../unit/api_fastapi/auth/managers/simple/test_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py index 83e316f6040c6..09834ba9e6e26 100644 --- a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py +++ b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py @@ -163,7 +163,7 @@ def all_access_test_client(): ("GET", "/ui/structure/structure_data"), ], ) -def test_all_endpoints_without_auth_header(all_access_test_client, method, path): +def test_all_endpoints_without_auth_header(all_access_test_client, method, path, session): response = all_access_test_client.request(method, path) assert response.status_code not in {401, 403}, ( f"Unexpected status code {response.status_code} for {method} {path}" From 94faef632e514cd1db5f4a369deb3463f906429e Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 10:58:24 +0530 Subject: [PATCH 09/13] db_test mark --- .../unit/api_fastapi/auth/managers/simple/test_middleware.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py index 09834ba9e6e26..867a40acb9984 100644 --- a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py +++ b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py @@ -24,6 +24,8 @@ from tests_common.test_utils.config import conf_vars +pytestmark = pytest.mark.db_test + @pytest.fixture def all_access_test_client(): @@ -163,7 +165,7 @@ def all_access_test_client(): ("GET", "/ui/structure/structure_data"), ], ) -def test_all_endpoints_without_auth_header(all_access_test_client, method, path, session): +def test_all_endpoints_without_auth_header(all_access_test_client, method, path): response = all_access_test_client.request(method, path) assert response.status_code not in {401, 403}, ( f"Unexpected status code {response.status_code} for {method} {path}" From f4e44e3b558cdf52f91d5bfb79a0ac02371bd6e5 Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 18:29:12 +0530 Subject: [PATCH 10/13] iterate over app.routes --- .../auth/managers/simple/test_middleware.py | 155 ++---------------- 1 file changed, 17 insertions(+), 138 deletions(-) diff --git a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py index 867a40acb9984..fd13c4f6aaa45 100644 --- a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py +++ b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py @@ -27,146 +27,25 @@ pytestmark = pytest.mark.db_test -@pytest.fixture -def all_access_test_client(): +def test_invoke_api_without_auth_header(): with conf_vars( { - ("core", "simple_auth_manager_all_admins"): "true", - ("webserver", "expose_config"): "true", + ( + "core", + "simple_auth_manager_all_admins", + ): "true", + ( + "webserver", + "expose_config", + ): "true", } ): app = create_app() - yield TestClient(app) - - -@pytest.mark.parametrize( - "method, path", - [ - ("GET", "/api/v1/{_:path}"), - ("GET", "/api/v2/assets"), - ("GET", "/api/v2/assets/aliases"), - ("GET", "/api/v2/assets/aliases/{asset_alias_id}"), - ("GET", "/api/v2/assets/events"), - ("POST", "/api/v2/assets/events"), - ("GET", "/api/v2/assets/{asset_id}"), - ("POST", "/api/v2/assets/{asset_id}/materialize"), - ("GET", "/api/v2/assets/{asset_id}/queuedEvents"), - ("DELETE", "/api/v2/assets/{asset_id}/queuedEvents"), - ("GET", "/api/v2/backfills"), - ("POST", "/api/v2/backfills"), - ("POST", "/api/v2/backfills/dry_run"), - ("GET", "/api/v2/backfills/{backfill_id}"), - ("PUT", "/api/v2/backfills/{backfill_id}/cancel"), - ("PUT", "/api/v2/backfills/{backfill_id}/pause"), - ("PUT", "/api/v2/backfills/{backfill_id}/unpause"), - ("GET", "/api/v2/config"), - ("GET", "/api/v2/config/section/{section}/option/{option}"), - ("GET", "/api/v2/connections"), - ("POST", "/api/v2/connections"), - ("PATCH", "/api/v2/connections"), - ("POST", "/api/v2/connections/defaults"), - ("POST", "/api/v2/connections/test"), - ("DELETE", "/api/v2/connections/{connection_id}"), - ("GET", "/api/v2/connections/{connection_id}"), - ("PATCH", "/api/v2/connections/{connection_id}"), - ("GET", "/api/v2/dagReports"), - ("GET", "/api/v2/dagSources/{dag_id}"), - ("GET", "/api/v2/dagStats"), - ("GET", "/api/v2/dagTags"), - ("GET", "/api/v2/dagWarnings"), - ("GET", "/api/v2/dags"), - ("PATCH", "/api/v2/dags"), - ("GET", "/api/v2/dags/{dag_id}"), - ("PATCH", "/api/v2/dags/{dag_id}"), - ("DELETE", "/api/v2/dags/{dag_id}"), - ("GET", "/api/v2/dags/{dag_id}/assets/queuedEvents"), - ("DELETE", "/api/v2/dags/{dag_id}/assets/queuedEvents"), - ("GET", "/api/v2/dags/{dag_id}/assets/{asset_id}/queuedEvents"), - ("DELETE", "/api/v2/dags/{dag_id}/assets/{asset_id}/queuedEvents"), - ("POST", "/api/v2/dags/{dag_id}/clearTaskInstances"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns"), - ("POST", "/api/v2/dags/{dag_id}/dagRuns"), - ("POST", "/api/v2/dags/{dag_id}/dagRuns/list"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}"), - ("DELETE", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}"), - ("PATCH", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}"), - ("POST", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/clear"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances"), - ("POST", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/list"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}"), - ("PATCH", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/dependencies"), - ("PATCH", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/dry_run"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/links"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/listMapped"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/logs/{try_number}"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/tries"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/tries/{task_try_number}"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries"), - ("POST", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries/{xcom_key}"), - ( - "PATCH", - "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries/{xcom_key}", - ), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}"), - ("PATCH", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}"), - ( - "GET", - "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/dependencies", - ), - ("PATCH", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/dry_run"), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/tries"), - ( - "GET", - "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/{map_index}/tries/{task_try_number}", - ), - ("GET", "/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/upstreamAssetEvents"), - ("GET", "/api/v2/dags/{dag_id}/dagVersions"), - ("GET", "/api/v2/dags/{dag_id}/dagVersions/{version_number}"), - ("GET", "/api/v2/dags/{dag_id}/details"), - ("GET", "/api/v2/dags/{dag_id}/tasks"), - ("GET", "/api/v2/dags/{dag_id}/tasks/{task_id}"), - ("GET", "/api/v2/eventLogs"), - ("GET", "/api/v2/eventLogs/{event_log_id}"), - ("GET", "/api/v2/importErrors"), - ("GET", "/api/v2/importErrors/{import_error_id}"), - ("GET", "/api/v2/jobs"), - ("GET", "/api/v2/monitor/health"), - ("PUT", "/api/v2/parseDagFile/{file_token}"), - ("GET", "/api/v2/plugins"), - ("GET", "/api/v2/plugins/importErrors"), - ("GET", "/api/v2/pools"), - ("POST", "/api/v2/pools"), - ("PATCH", "/api/v2/pools"), - ("DELETE", "/api/v2/pools/{pool_name}"), - ("GET", "/api/v2/pools/{pool_name}"), - ("PATCH", "/api/v2/pools/{pool_name}"), - ("GET", "/api/v2/providers"), - ("GET", "/api/v2/variables"), - ("POST", "/api/v2/variables"), - ("PATCH", "/api/v2/variables"), - ("DELETE", "/api/v2/variables/{variable_key}"), - ("GET", "/api/v2/variables/{variable_key}"), - ("PATCH", "/api/v2/variables/{variable_key}"), - ("GET", "/api/v2/version"), - ("GET", "/api/{_:path}"), - ("HEAD", "/docs/oauth2-redirect"), - ("GET", "/docs/oauth2-redirect"), - ("GET", "/ui/auth/menus"), - ("GET", "/ui/backfills"), - ("GET", "/ui/config"), - ("GET", "/ui/connections/hook_meta"), - ("GET", "/ui/dags/recent_dag_runs"), - ("GET", "/ui/dashboard/historical_metrics_data"), - ("GET", "/ui/dependencies"), - ("GET", "/ui/grid/{dag_id}"), - ("GET", "/ui/next_run_assets/{dag_id}"), - ("GET", "/ui/structure/structure_data"), - ], -) -def test_all_endpoints_without_auth_header(all_access_test_client, method, path): - response = all_access_test_client.request(method, path) - assert response.status_code not in {401, 403}, ( - f"Unexpected status code {response.status_code} for {method} {path}" - ) + client = TestClient(app) + for route in app.routes: + if hasattr(route, "path") and hasattr(route, "methods"): + for method in route.methods: + response = client.request(method, route.path) + assert response.status_code not in {401, 403}, ( + f"Unexpected status code {response.status_code} for {method} {route.path}" + ) From 019296c21f830ae092f556cfc62f700dc30c22e4 Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 19:32:09 +0530 Subject: [PATCH 11/13] use fewer endpoints --- .../auth/managers/simple/test_middleware.py | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py index fd13c4f6aaa45..dcd39ef19ed62 100644 --- a/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py +++ b/airflow-core/tests/unit/api_fastapi/auth/managers/simple/test_middleware.py @@ -27,25 +27,35 @@ pytestmark = pytest.mark.db_test -def test_invoke_api_without_auth_header(): +@pytest.fixture +def all_access_test_client(): with conf_vars( { - ( - "core", - "simple_auth_manager_all_admins", - ): "true", - ( - "webserver", - "expose_config", - ): "true", + ("core", "simple_auth_manager_all_admins"): "true", + ("webserver", "expose_config"): "true", } ): app = create_app() - client = TestClient(app) - for route in app.routes: - if hasattr(route, "path") and hasattr(route, "methods"): - for method in route.methods: - response = client.request(method, route.path) - assert response.status_code not in {401, 403}, ( - f"Unexpected status code {response.status_code} for {method} {route.path}" - ) + yield TestClient(app) + + +@pytest.mark.parametrize( + "method, path", + [ + ("GET", "/api/v2/assets"), + ("POST", "/api/v2/backfills"), + ("GET", "/api/v2/config"), + ("GET", "/api/v2/dags"), + ("POST", "/api/v2/dags/{dag_id}/clearTaskInstances"), + ("GET", "/api/v2/dags/{dag_id}/dagRuns"), + ("GET", "/api/v2/eventLogs"), + ("GET", "/api/v2/jobs"), + ("GET", "/api/v2/variables"), + ("GET", "/api/v2/version"), + ], +) +def test_all_endpoints_without_auth_header(all_access_test_client, method, path): + response = all_access_test_client.request(method, path) + assert response.status_code not in {401, 403}, ( + f"Unexpected status code {response.status_code} for {method} {path}" + ) From 6f18eb352feee606750492528ef580f73db0dbcb Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 19:33:44 +0530 Subject: [PATCH 12/13] update comment --- .../src/airflow/api_fastapi/auth/managers/simple/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py index 004f29671075f..c3dbb9663dfcd 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py @@ -27,7 +27,7 @@ class SimpleAllAdminMiddleware(BaseHTTPMiddleware): """Middleware that automatically generates and includes auth header for simple auth manager.""" async def dispatch(self, request: Request, call_next): - # Starlette Request is expected to be immutable, but we monkey-patch it to add the auth header + # Starlette Request is expected to be immutable, but we modify it to add the auth header # https://github.com/fastapi/fastapi/issues/2727#issuecomment-770202019 token = SimpleAuthManagerLogin.create_token_all_admins() request.headers.__dict__["_list"].append((b"authorization", f"Bearer {token}".encode())) From fe71f6d1cdf646679dd7f5867e91024efb21b239 Mon Sep 17 00:00:00 2001 From: kalyanr Date: Thu, 24 Apr 2025 21:50:30 +0530 Subject: [PATCH 13/13] update scope --- .../src/airflow/api_fastapi/auth/managers/simple/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py index c3dbb9663dfcd..6c73cd015fa9f 100644 --- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py +++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/middleware.py @@ -30,5 +30,5 @@ async def dispatch(self, request: Request, call_next): # Starlette Request is expected to be immutable, but we modify it to add the auth header # https://github.com/fastapi/fastapi/issues/2727#issuecomment-770202019 token = SimpleAuthManagerLogin.create_token_all_admins() - request.headers.__dict__["_list"].append((b"authorization", f"Bearer {token}".encode())) + request.scope["headers"].append((b"authorization", f"Bearer {token}".encode())) return await call_next(request)