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

Reply via email to