This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-2-test by this push:
new 1d2529b0e5e [v3-2-test] Filter external dependency nodes by readable
DAGs in structure_data endpoint (#65342) (#65534)
1d2529b0e5e is described below
commit 1d2529b0e5ea176ac5317454ae014f7c30a7a825
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Apr 21 11:53:22 2026 +0200
[v3-2-test] Filter external dependency nodes by readable DAGs in
structure_data endpoint (#65342) (#65534)
The structure_data endpoint returned external dependency nodes for
linked DAGs without checking whether the caller had read permission
on those DAGs. Add the ReadableDagsFilterDep and skip dependency
entries that reference DAGs outside the caller's readable set.
(cherry picked from commit 01888df3ae9b437e9086d296d29d2fee147d6312)
Generated-by: Claude Opus 4.6 (1M context) following the guidelines at
https:
//github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
Co-authored-by: Jarek Potiuk <[email protected]>
---
.../api_fastapi/core_api/routes/ui/structure.py | 14 ++++++++++-
.../core_api/routes/ui/test_structure.py | 29 ++++++++++++++++++----
2 files changed, 37 insertions(+), 6 deletions(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py
index 8fdb19f34a8..bc7efc0a75a 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py
@@ -26,7 +26,7 @@ from airflow.api_fastapi.common.parameters import
QueryIncludeDownstream, QueryI
from airflow.api_fastapi.common.router import AirflowRouter
from airflow.api_fastapi.core_api.datamodels.ui.structure import
StructureDataResponse
from airflow.api_fastapi.core_api.openapi.exceptions import
create_openapi_http_exception_doc
-from airflow.api_fastapi.core_api.security import requires_access_dag
+from airflow.api_fastapi.core_api.security import ReadableDagsFilterDep,
requires_access_dag
from airflow.api_fastapi.core_api.services.ui.structure import (
bind_output_assets_to_tasks,
get_upstream_assets,
@@ -52,6 +52,7 @@ structure_router = AirflowRouter(tags=["Structure"],
prefix="/structure")
def structure_data(
session: SessionDep,
dag_id: str,
+ readable_dags_filter: ReadableDagsFilterDep,
include_upstream: QueryIncludeUpstream = False,
include_downstream: QueryIncludeDownstream = False,
depth: int | None = None,
@@ -105,11 +106,22 @@ def structure_data(
start_edges: list[dict] = []
end_edges: list[dict] = []
+ readable_dag_ids = readable_dags_filter.value
for dependency_dag_id, dependencies in
sorted(SerializedDagModel.get_dag_dependencies().items()):
+ if readable_dag_ids is not None and dependency_dag_id not in
readable_dag_ids:
+ continue
for dependency in dependencies:
# Dependencies not related to `dag_id` are ignored
if dependency_dag_id != dag_id and dependency.target != dag_id:
continue
+ # When target is a real DAG ID (not a type label), hide it
+ # if the caller cannot read that DAG.
+ if (
+ readable_dag_ids is not None
+ and dependency.target != dependency.dependency_type
+ and dependency.target not in readable_dag_ids
+ ):
+ continue
# upstream assets are handled by the `get_upstream_assets`
function.
if dependency.target != dependency.dependency_type and
dependency.dependency_type in [
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_structure.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_structure.py
index f4a56db93e4..db436ca3cf9 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_structure.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_structure.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import copy
+from unittest import mock
import pendulum
import pytest
@@ -303,7 +304,7 @@ class TestStructureDataEndpoint:
},
],
},
- 6,
+ 7,
),
(
{
@@ -311,7 +312,7 @@ class TestStructureDataEndpoint:
"root": "unknown_task",
},
{"edges": [], "nodes": []},
- 6,
+ 7,
),
(
{
@@ -336,7 +337,7 @@ class TestStructureDataEndpoint:
},
],
},
- 6,
+ 7,
),
(
{"dag_id": DAG_ID_EXTERNAL_TRIGGER, "external_dependencies":
True},
@@ -375,7 +376,7 @@ class TestStructureDataEndpoint:
},
],
},
- 13,
+ 14,
),
],
)
@@ -572,7 +573,7 @@ class TestStructureDataEndpoint:
],
}
- with assert_queries_count(13):
+ with assert_queries_count(14):
response = test_client.get("/structure/structure_data",
params=params)
assert response.status_code == 200
assert response.json() == expected
@@ -685,6 +686,24 @@ class TestStructureDataEndpoint:
response = unauthorized_test_client.get("/structure/structure_data",
params={"dag_id": DAG_ID})
assert response.status_code == 403
+ @mock.patch(
+
"airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager.get_authorized_dag_ids",
+ return_value={DAG_ID_EXTERNAL_TRIGGER},
+ )
+ @pytest.mark.usefixtures("make_dags")
+ def test_external_deps_filters_unreadable_dags(self, _, test_client):
+ response = test_client.get(
+ "/structure/structure_data",
+ params={"dag_id": DAG_ID_EXTERNAL_TRIGGER,
"external_dependencies": True},
+ )
+ assert response.status_code == 200
+ result = response.json()
+ node_ids = {node["id"] for node in result["nodes"]}
+ assert "trigger_dag_run_operator" in node_ids
+ assert not any(DAG_ID in nid for nid in node_ids if nid !=
"trigger_dag_run_operator")
+ edge_targets = {edge["target_id"] for edge in result["edges"]}
+ assert not any(DAG_ID in tid for tid in edge_targets)
+
def test_should_return_404(self, test_client):
response = test_client.get("/structure/structure_data",
params={"dag_id": "not_existing"})
assert response.status_code == 404