This is an automated email from the ASF dual-hosted git repository.

vincbeck 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 211b96ce63c Filter backfills list by readable DAGs (#63003)
211b96ce63c is described below

commit 211b96ce63cd8884e4db912e9c36e46e6e48852d
Author: Henry Chen <[email protected]>
AuthorDate: Tue Mar 10 22:10:20 2026 +0800

    Filter backfills list by readable DAGs (#63003)
---
 .../api_fastapi/core_api/routes/ui/backfills.py    |  5 +++--
 .../src/airflow/api_fastapi/core_api/security.py   | 10 +++++++++
 .../core_api/routes/ui/test_backfills.py           | 26 +++++++++++++++++++++-
 3 files changed, 38 insertions(+), 3 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py
index 32b2891b954..02583a8355b 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py
@@ -36,7 +36,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
+from airflow.api_fastapi.core_api.security import ReadableBackfillsFilterDep, 
requires_access_backfill
 from airflow.models.backfill import Backfill
 
 backfills_router = AirflowRouter(tags=["Backfill"], prefix="/backfills")
@@ -56,6 +56,7 @@ def list_backfills_ui(
         SortParam,
         Depends(SortParam(["id"], Backfill).dynamic_depends()),
     ],
+    readable_backfills_filter: ReadableBackfillsFilterDep,
     session: SessionDep,
     dag_id: Annotated[FilterParam[str | None], 
Depends(filter_param_factory(Backfill.dag_id, str | None))],
     active: Annotated[
@@ -65,7 +66,7 @@ def list_backfills_ui(
 ) -> BackfillCollectionResponse:
     select_stmt, total_entries = paginated_select(
         statement=select(Backfill).options(joinedload(Backfill.dag_model)),
-        filters=[dag_id, active],
+        filters=[dag_id, active, readable_backfills_filter],
         order_by=order_by,
         offset=offset,
         limit=limit,
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/security.py 
b/airflow-core/src/airflow/api_fastapi/core_api/security.py
index 0e59fb3550c..774cfa9ed6a 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/security.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/security.py
@@ -238,6 +238,13 @@ class PermittedDagVersionFilter(PermittedDagFilter):
         return select.where(DagVersion.dag_id.in_(self.value or set()))
 
 
+class PermittedBackfillFilter(PermittedDagFilter):
+    """A parameter that filters the permitted backfills for the user."""
+
+    def to_orm(self, select: Select) -> Select:
+        return select.where(Backfill.dag_id.in_(self.value or set()))
+
+
 def permitted_dag_filter_factory(
     method: ResourceMethod, filter_class=PermittedDagFilter
 ) -> Callable[[BaseUser, BaseAuthManager], PermittedDagFilter]:
@@ -282,6 +289,9 @@ ReadableTagsFilterDep = Annotated[
 ReadableDagVersionsFilterDep = Annotated[
     PermittedDagVersionFilter, Depends(permitted_dag_filter_factory("GET", 
PermittedDagVersionFilter))
 ]
+ReadableBackfillsFilterDep = Annotated[
+    PermittedBackfillFilter, Depends(permitted_dag_filter_factory("GET", 
PermittedBackfillFilter))
+]
 
 
 def requires_access_backfill(
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py
index b14ef9cf1a0..21ae10d23b3 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py
@@ -153,7 +153,7 @@ class TestListBackfills(TestBackfillEndpoint):
         expected_response = []
         for backfill in response_params:
             expected_response.append(backfill_responses[backfill])
-        with assert_queries_count(2 if test_params.get("dag_id") is None else 
3):
+        with assert_queries_count(3 if test_params.get("dag_id") is None else 
4):
             response = test_client.get("/backfills", params=test_params)
         assert response.status_code == 200
         assert response.json() == {
@@ -168,3 +168,27 @@ class TestListBackfills(TestBackfillEndpoint):
     def test_should_response_403(self, unauthorized_test_client):
         response = unauthorized_test_client.get("/backfills", params={})
         assert response.status_code == 403
+
+    
@mock.patch("airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager.get_authorized_dag_ids")
+    def test_should_only_return_authorized_dag_backfills(
+        self, mock_get_authorized_dag_ids, test_client, session, 
testing_dag_bundle
+    ):
+        dags = self._create_dag_models()
+        from_date = timezone.utcnow()
+        to_date = timezone.utcnow()
+        backfills = [
+            Backfill(dag_id=dags[0].dag_id, from_date=from_date, 
to_date=to_date),
+            Backfill(dag_id=dags[1].dag_id, from_date=from_date, 
to_date=to_date),
+            Backfill(dag_id=dags[2].dag_id, from_date=from_date, 
to_date=to_date),
+        ]
+        session.add_all(backfills)
+        session.commit()
+
+        mock_get_authorized_dag_ids.return_value = {"TEST_DAG_2", "TEST_DAG_3"}
+        response = test_client.get("/backfills")
+
+        mock_get_authorized_dag_ids.assert_called_once_with(user=mock.ANY, 
method="GET")
+        assert response.status_code == 200
+        body = response.json()
+        assert body["total_entries"] == 2
+        assert {b["dag_id"] for b in body["backfills"]} == {"TEST_DAG_2", 
"TEST_DAG_3"}

Reply via email to