This is an automated email from the ASF dual-hosted git repository. weilee 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 5f004b112f4 feat(AIP-84): add auth to /ui/backfills (#47657) 5f004b112f4 is described below commit 5f004b112f4a4ea2000026762d6641084aa85b3e Author: Wei Lee <weilee...@gmail.com> AuthorDate: Fri Mar 14 08:28:19 2025 +0800 feat(AIP-84): add auth to /ui/backfills (#47657) * add auth to backfills endpoints * feat(security): add is_authorized_backfill * feat(api_fastapi): add required permission for backfills * feat(AIP-84): add auth to /ui/backfills --------- Co-authored-by: vatsrahul1001 <rah.sharm...@gmail.com> --- .../api_fastapi/core_api/openapi/v1-generated.yaml | 2 + .../core_api/routes/public/backfills.py | 10 ++++- .../api_fastapi/core_api/routes/ui/backfills.py | 5 +++ .../providers/fab/auth_manager/fab_auth_manager.py | 45 +++++++++++++++++----- .../fab/auth_manager/security_manager/override.py | 3 +- .../core_api/routes/ui/test_backfills.py | 10 ++++- 6 files changed, 61 insertions(+), 14 deletions(-) diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml index 56e4f119e40..a437510b453 100644 --- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml +++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml @@ -373,6 +373,8 @@ paths: - Backfill summary: List Backfills operationId: list_backfills + security: + - OAuth2PasswordBearer: [] parameters: - name: limit in: query diff --git a/airflow/api_fastapi/core_api/routes/public/backfills.py b/airflow/api_fastapi/core_api/routes/public/backfills.py index 31638e55503..ea882f9dc8b 100644 --- a/airflow/api_fastapi/core_api/routes/public/backfills.py +++ b/airflow/api_fastapi/core_api/routes/public/backfills.py @@ -116,7 +116,10 @@ def get_backfill( status.HTTP_409_CONFLICT, ] ), - dependencies=[Depends(requires_access_backfill(method="PUT"))], + dependencies=[ + Depends(requires_access_backfill(method="PUT")), + Depends(requires_access_dag(method="PUT", access_entity=DagAccessEntity.RUN)), + ], ) def pause_backfill(backfill_id, session: SessionDep) -> BackfillResponse: b = session.get(Backfill, backfill_id) @@ -138,7 +141,10 @@ def pause_backfill(backfill_id, session: SessionDep) -> BackfillResponse: status.HTTP_409_CONFLICT, ] ), - dependencies=[Depends(requires_access_backfill(method="PUT"))], + dependencies=[ + Depends(requires_access_backfill(method="PUT")), + Depends(requires_access_dag(method="PUT", access_entity=DagAccessEntity.RUN)), + ], ) def unpause_backfill(backfill_id, session: SessionDep) -> BackfillResponse: b = session.get(Backfill, backfill_id) diff --git a/airflow/api_fastapi/core_api/routes/ui/backfills.py b/airflow/api_fastapi/core_api/routes/ui/backfills.py index a749cdd6cfc..add5c536e76 100644 --- a/airflow/api_fastapi/core_api/routes/ui/backfills.py +++ b/airflow/api_fastapi/core_api/routes/ui/backfills.py @@ -35,6 +35,7 @@ from airflow.api_fastapi.core_api.datamodels.backfills import BackfillCollection from airflow.api_fastapi.core_api.openapi.exceptions import ( create_openapi_http_exception_doc, ) +from airflow.api_fastapi.core_api.security import requires_access_backfill, requires_access_dag from airflow.models.backfill import Backfill backfills_router = AirflowRouter(tags=["Backfill"], prefix="/backfills") @@ -43,6 +44,10 @@ backfills_router = AirflowRouter(tags=["Backfill"], prefix="/backfills") @backfills_router.get( path="", responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]), + dependencies=[ + Depends(requires_access_backfill(method="GET")), + Depends(requires_access_dag(method="GET")), + ], ) def list_backfills( limit: QueryLimit, diff --git a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py index 9238aecf5af..331c92f8a5f 100644 --- a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py +++ b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py @@ -62,7 +62,10 @@ from airflow.providers.fab.auth_manager.models import Permission, Role, User from airflow.providers.fab.auth_manager.models.anonymous_user import AnonymousUser from airflow.providers.fab.www.app import create_app from airflow.providers.fab.www.constants import SWAGGER_BUNDLE, SWAGGER_ENABLED -from airflow.providers.fab.www.extensions.init_views import _CustomErrorRequestBodyValidator, _LazyResolver +from airflow.providers.fab.www.extensions.init_views import ( + _CustomErrorRequestBodyValidator, + _LazyResolver, +) from airflow.providers.fab.www.security import permissions from airflow.providers.fab.www.security.permissions import ( RESOURCE_AUDIT_LOG, @@ -89,7 +92,10 @@ from airflow.providers.fab.www.security.permissions import ( RESOURCE_WEBSITE, RESOURCE_XCOM, ) -from airflow.providers.fab.www.utils import get_fab_action_from_method_map, get_method_from_fab_action_map +from airflow.providers.fab.www.utils import ( + get_fab_action_from_method_map, + get_method_from_fab_action_map, +) from airflow.security.permissions import RESOURCE_BACKFILL from airflow.utils.session import NEW_SESSION, create_session, provide_session from airflow.utils.yaml import safe_load @@ -100,7 +106,9 @@ if TYPE_CHECKING: CLICommand, ) from airflow.providers.common.compat.assets import AssetAliasDetails, AssetDetails - from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride + from airflow.providers.fab.auth_manager.security_manager.override import ( + FabAirflowSecurityManagerOverride, + ) from airflow.providers.fab.www.extensions.init_appbuilder import AirflowAppBuilder from airflow.providers.fab.www.security.permissions import ( RESOURCE_ASSET, @@ -200,7 +208,9 @@ class FabAuthManager(BaseAuthManager[User]): def get_fastapi_app(self) -> FastAPI | None: """Get the FastAPI app.""" - from airflow.providers.fab.auth_manager.api_fastapi.routes.login import login_router + from airflow.providers.fab.auth_manager.api_fastapi.routes.login import ( + login_router, + ) flask_app = create_app(enable_plugins=False) @@ -229,7 +239,10 @@ class FabAuthManager(BaseAuthManager[User]): specification=specification, resolver=_LazyResolver(), base_path="/fab/v1", - options={"swagger_ui": SWAGGER_ENABLED, "swagger_path": SWAGGER_BUNDLE.__fspath__()}, + options={ + "swagger_ui": SWAGGER_ENABLED, + "swagger_path": SWAGGER_BUNDLE.__fspath__(), + }, strict_validation=True, validate_responses=True, validator_map={"body": _CustomErrorRequestBodyValidator}, @@ -336,7 +349,11 @@ class FabAuthManager(BaseAuthManager[User]): ) def is_authorized_backfill( - self, *, method: ResourceMethod, user: User, details: BackfillDetails | None = None + self, + *, + method: ResourceMethod, + user: User, + details: BackfillDetails | None = None, ) -> bool: return self._is_authorized(method=method, resource_type=RESOURCE_BACKFILL, user=user) @@ -346,7 +363,11 @@ class FabAuthManager(BaseAuthManager[User]): return self._is_authorized(method=method, resource_type=RESOURCE_ASSET, user=user) def is_authorized_asset_alias( - self, *, method: ResourceMethod, user: User, details: AssetAliasDetails | None = None + self, + *, + method: ResourceMethod, + user: User, + details: AssetAliasDetails | None = None, ) -> bool: return self._is_authorized(method=method, resource_type=RESOURCE_ASSET_ALIAS, user=user) @@ -356,7 +377,11 @@ class FabAuthManager(BaseAuthManager[User]): return self._is_authorized(method=method, resource_type=RESOURCE_POOL, user=user) def is_authorized_variable( - self, *, method: ResourceMethod, user: User, details: VariableDetails | None = None + self, + *, + method: ResourceMethod, + user: User, + details: VariableDetails | None = None, ) -> bool: return self._is_authorized(method=method, resource_type=RESOURCE_VARIABLE, user=user) @@ -364,7 +389,9 @@ class FabAuthManager(BaseAuthManager[User]): # "Docs" are only links in the menu, there is no page associated method: ResourceMethod = "MENU" if access_view == AccessView.DOCS else "GET" return self._is_authorized( - method=method, resource_type=_MAP_ACCESS_VIEW_TO_FAB_RESOURCE_TYPE[access_view], user=user + method=method, + resource_type=_MAP_ACCESS_VIEW_TO_FAB_RESOURCE_TYPE[access_view], + user=user, ) def is_authorized_custom_view( diff --git a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py index bf8ca649900..37929d3cb24 100644 --- a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py +++ b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py @@ -106,18 +106,17 @@ from airflow.providers.fab.www.session import ( AirflowDatabaseSessionInterface, AirflowDatabaseSessionInterface as FabAirflowDatabaseSessionInterface, ) +from airflow.security.permissions import RESOURCE_BACKFILL if TYPE_CHECKING: from airflow.providers.fab.www.security.permissions import ( RESOURCE_ASSET, RESOURCE_ASSET_ALIAS, - RESOURCE_BACKFILL, ) else: from airflow.providers.common.compat.security.permissions import ( RESOURCE_ASSET, RESOURCE_ASSET_ALIAS, - RESOURCE_BACKFILL, ) log = logging.getLogger(__name__) diff --git a/tests/api_fastapi/core_api/routes/ui/test_backfills.py b/tests/api_fastapi/core_api/routes/ui/test_backfills.py index bf405b087a2..0c5e2a7cda7 100644 --- a/tests/api_fastapi/core_api/routes/ui/test_backfills.py +++ b/tests/api_fastapi/core_api/routes/ui/test_backfills.py @@ -87,7 +87,7 @@ class TestListBackfills(TestBackfillEndpoint): ({"dag_id": "TEST_DAG_1"}, ["backfill1"], 1), ], ) - def test_list_backfill(self, test_params, response_params, total_entries, test_client, session): + def test_should_response_200(self, test_params, response_params, total_entries, test_client, session): dags = self._create_dag_models() from_date = timezone.utcnow() to_date = timezone.utcnow() @@ -150,3 +150,11 @@ class TestListBackfills(TestBackfillEndpoint): "backfills": expected_response, "total_entries": total_entries, } + + def test_should_response_401(self, unauthenticated_test_client): + response = unauthenticated_test_client.get("/ui/backfills", params={}) + assert response.status_code == 401 + + def test_should_response_403(self, unauthorized_test_client): + response = unauthorized_test_client.get("/ui/backfills", params={}) + assert response.status_code == 403