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

potiuk 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 5c40e732444 Prevent Dag-existence disclosure on partitioned dag runs 
listing (#72640)
5c40e732444 is described below

commit 5c40e732444ed67c2b3f76c699aace93d520c6a6
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Mon Sep 7 20:07:01 2026 +0200

    Prevent Dag-existence disclosure on partitioned dag runs listing (#72640)
    
    * Prevent Dag-existence disclosure on partitioned dag runs listing
    
    The empty-results branch of the /ui/partitioned_dag_runs listing raised 404
    when a Dag did not exist, but returned 200-empty when a Dag existed but was
    outside the caller's permitted set (the readable-dags row filter had already
    dropped its rows). A caller could therefore probe by dag_id and learn which
    identifiers exist beyond what they are allowed to read.
    
    Scope the existence probe to readable Dags so the two cases collapse to the
    same 404, matching the "so their existence does not leak either" idiom used
    by PermittedEventLogFilter / PermittedAssetEventFilter. Behavior is 
unchanged
    for callers with full Dag access.
    
    * Simplify existence check and strengthen leak-regression test
    
    readable_dag_ids already comes from DagModel in both in-tree auth managers,
    so membership in the set proves existence — the extra DagModel round-trip
    and IN list is only useful for the admin path, where no filter is active.
    
    Add an AssetPartitionDagRun for the unreadable Dag in the regression test
    so the readable-dags row filter is what drops the rows, matching the
    scenario the docstring describes.
    
    * Declare 404 response on the partitioned dag runs listing
    
    The listing raises HTTPException(404) when a dag_id filter targets a Dag the
    caller cannot see, but the route did not declare 404 in its OpenAPI
    responses. Clients generated from the spec therefore treated 404 as an
    unexpected transport error rather than a documented outcome, and the API
    docs page did not surface it.
    
    * Tighten comment on the existence-check branch
---
 .../api_fastapi/core_api/openapi/_private_ui.yaml  |  6 +++++
 .../core_api/routes/ui/partitioned_dag_runs.py     | 12 ++++++++--
 .../ui/openapi-gen/requests/services.gen.ts        |  1 +
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  4 ++++
 .../routes/ui/test_partitioned_dag_runs.py         | 27 ++++++++++++++++++++++
 5 files changed, 48 insertions(+), 2 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index 6ca0e3c24b4..f713c4137dc 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -403,6 +403,12 @@ paths:
             application/json:
               schema:
                 $ref: 
'#/components/schemas/PartitionedDagRunCollectionResponse'
+        '404':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPExceptionResponse'
+          description: Not Found
         '422':
           description: Validation Error
           content:
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py
 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py
index 60c98f39cd3..d2141ae5b0d 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py
@@ -40,6 +40,7 @@ from 
airflow.api_fastapi.core_api.datamodels.ui.partitioned_dag_runs import (
     PartitionedDagRunDetailResponse,
     PartitionedDagRunResponse,
 )
+from airflow.api_fastapi.core_api.openapi.exceptions import 
create_openapi_http_exception_doc
 from airflow.api_fastapi.core_api.security import (
     ReadableDagsFilterDep,
     requires_access_asset,
@@ -242,6 +243,7 @@ def _build_response(row, required_count: int, 
received_count: int) -> Partitione
 
 @partitioned_dag_runs_router.get(
     "/partitioned_dag_runs",
+    responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]),
     dependencies=[Depends(requires_access_asset(method="GET"))],
 )
 def get_partitioned_dag_runs(
@@ -283,8 +285,14 @@ def get_partitioned_dag_runs(
 
     if not (rows := session.execute(query).all()):
         if dag_id.value is not None and total_entries == 0:
-            dag_exists = 
session.scalar(select(DagModel.dag_id).where(DagModel.dag_id == dag_id.value))
-            if dag_exists is None:
+            # An unreadable-but-existing Dag must return 404 too — otherwise 
the caller
+            # can probe by dag_id and learn which Dags exist outside their 
permitted set.
+            if readable_dag_ids is not None:
+                if dag_id.value not in readable_dag_ids:
+                    raise HTTPException(
+                        status.HTTP_404_NOT_FOUND, f"Dag with id 
{dag_id.value} was not found"
+                    )
+            elif session.scalar(select(DagModel.dag_id).where(DagModel.dag_id 
== dag_id.value)) is None:
                 raise HTTPException(status.HTTP_404_NOT_FOUND, f"Dag with id 
{dag_id.value} was not found")
         return PartitionedDagRunCollectionResponse(partitioned_dag_runs=[], 
total=total_entries)
 
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index f5a53a28d22..0564552f6eb 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -4771,6 +4771,7 @@ export class PartitionedDagRunService {
                 has_created_dag_run_id: data.hasCreatedDagRunId
             },
             errors: {
+                404: 'Not Found',
                 422: 'Validation Error'
             }
         });
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index ae6b2c2074b..da6f155d7c5 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -8591,6 +8591,10 @@ export type $OpenApiTs = {
                  * Successful Response
                  */
                 200: PartitionedDagRunCollectionResponse;
+                /**
+                 * Not Found
+                 */
+                404: HTTPExceptionResponse;
                 /**
                  * Validation Error
                  */
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_partitioned_dag_runs.py
 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_partitioned_dag_runs.py
index 247d38a4c44..7016db222dd 100644
--- 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_partitioned_dag_runs.py
+++ 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_partitioned_dag_runs.py
@@ -301,6 +301,33 @@ class TestGetPartitionedDagRuns:
         dag_ids = {r["dag_id"] for r in body["partitioned_dag_runs"]}
         assert "restricted_dag" not in dag_ids
 
+    @mock.patch(
+        
"airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager.get_authorized_dag_ids",
+        return_value={"other_dag"},
+    )
+    def test_dag_id_filter_does_not_disclose_unreadable_dag_existence(
+        self, _, test_client, dag_maker, session
+    ):
+        """
+        An unreadable-but-existing Dag must not be distinguishable from a 
nonexistent
+        Dag via the ``dag_id`` filter: both return 404. Without the scoping, 
the
+        readable-dags row filter drops the APDR rows, ``total_entries`` 
collapses to
+        0, and the DagModel probe still finds the Dag — returning 200-empty and
+        giving the caller an oracle for Dag ids outside their permitted set.
+        """
+        schedule = PartitionedAssetTimetable(assets=Asset(uri="s3://bucket/a", 
name="a"))
+        with dag_maker(dag_id="restricted_dag", schedule=schedule, 
serialized=True):
+            EmptyOperator(task_id="t")
+        dag_maker.sync_dagbag_to_db()
+        # An APDR row exists but the readable-dags filter drops it — this is 
the
+        # branch where the pre-fix DagModel probe leaked existence.
+        session.add(AssetPartitionDagRun(target_dag_id="restricted_dag", 
partition_key="2024-06-01"))
+        session.commit()
+
+        existing_unreadable = 
test_client.get("/partitioned_dag_runs?dag_id=restricted_dag")
+        nonexistent = 
test_client.get("/partitioned_dag_runs?dag_id=no_such_dag")
+        assert existing_unreadable.status_code == nonexistent.status_code == 
404
+
     def test_duplicate_events_count_as_one(self, test_client, dag_maker, 
session):
         """Multiple log entries for the same asset count as 1 received, not 
N."""
         asset_def = Asset(uri="s3://bucket/dup0", name="dup0")

Reply via email to