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"}