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

vatsrahul1001 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 f81459835ad Resolve the Dag's team when authorizing a Dag found by 
lookup (#70893)
f81459835ad is described below

commit f81459835adb111e57b2a87f586d856395660f8a
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue Aug 4 14:23:50 2026 +0200

    Resolve the Dag's team when authorizing a Dag found by lookup (#70893)
    
    * Resolve the Dag's team when authorizing a Dag found by lookup
    
    Two authorization checks build DagDetails(id=dag_id) without team_name:
    materialize_asset, where the Dag is resolved from the asset, and the
    XCom-specific check in wait_dag_run_until_finished. Every other call site
    passes the team, resolved with DagModel.get_team_name.
    
    A team-aware auth manager distinguishes a team-scoped Dag from a global one 
by
    that field, so omitting it asks about a differently-scoped resource than the
    one being acted on. In wait_dag_run_until_finished the route dependency 
already
    resolves the team for its RUN check, so the two checks in the same handler
    disagreed.
    
    Resolve the team at both sites, reusing the request session.
    
    * Cover the XCom authorization check with a team-scoped Dag
    
    The existing wait-endpoint test uses a Dag with no team, where the resolved 
and
    unresolved forms are indistinguishable, so nothing caught the second check 
asking
    about a differently-scoped resource than the route dependency did.
    
    * Update 
airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
    
    Co-authored-by: Amogh Desai <[email protected]>
    
    * Update 
airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
    
    Co-authored-by: Amogh Desai <[email protected]>
    
    * Fix docstring indentation from the applied review suggestions
    
    ---------
    
    Co-authored-by: Amogh Desai <[email protected]>
---
 .../api_fastapi/core_api/routes/public/assets.py   |  6 ++++-
 .../api_fastapi/core_api/routes/public/dag_run.py  |  5 +++-
 .../core_api/routes/public/test_assets.py          | 28 ++++++++++++++++++++++
 .../core_api/routes/public/test_dag_run.py         | 26 ++++++++++++++++++++
 4 files changed, 63 insertions(+), 2 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py
index 0a6818648cd..830d4edaeed 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/assets.py
@@ -86,6 +86,7 @@ from airflow.models.asset import (
     AssetWatcherModel,
     TaskOutletAssetReference,
 )
+from airflow.models.dag import DagModel
 from airflow.models.dag_version import DagVersion
 from airflow.typing_compat import Unpack
 from airflow.utils.state import DagRunState
@@ -459,7 +460,10 @@ def materialize_asset(
     if not get_auth_manager().is_authorized_dag(
         method="POST",
         access_entity=DagAccessEntity.RUN,
-        details=DagDetails(id=dag_id),
+        # The Dag is resolved from the asset here rather than named by the 
caller, so its team has
+        # to be looked up too. A team-aware auth manager distinguishes a 
team-scoped Dag from a
+        # global one by this field, so leaving it None asks the wrong question.
+        details=DagDetails(id=dag_id, team_name=DagModel.get_team_name(dag_id, 
session=session)),
         user=user,
     ):
         raise HTTPException(
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
index 1b857df06ce..e5d06587cb5 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
@@ -869,7 +869,10 @@ def wait_dag_run_until_finished(
     if not get_auth_manager().is_authorized_dag(
         method="GET",
         access_entity=DagAccessEntity.XCOM,
-        details=DagDetails(id=dag_id),
+        # The route dependency above already authorizes RUN access with the 
Dag's team resolved;
+        # this second, XCom-specific check has to resolve it the same way, or 
the two checks ask
+        # a team-aware auth manager about differently-scoped resources.
+        details=DagDetails(id=dag_id, team_name=DagModel.get_team_name(dag_id, 
session=session)),
         user=user,
     ):
         if result_task_ids:
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
index fcf41162872..39054a28d70 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
@@ -25,6 +25,7 @@ import time_machine
 from sqlalchemy import delete, func, select, update
 
 from airflow._shared.timezones import timezone
+from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager
 from airflow.api_fastapi.auth.managers.models.resource_details import 
DagAccessEntity, DagDetails
 from airflow.models import DagModel
 from airflow.models.asset import (
@@ -2151,6 +2152,33 @@ class TestPostAssetMaterialize(TestAssets):
         assert dag_run.partition_key == "2025-06-01T00:00:00"
         assert dag_run.partition_date == timezone.datetime(2025, 6, 1)
 
+    @pytest.mark.parametrize("team_name", ["team_b", None])
+    def test_authorizes_against_the_dags_team(self, test_client, session, 
team_name):
+        """The Dag is resolved from the asset, so its team must be resolved 
and passed too — see the
+        call site's comment for why an unresolved team asks about the wrong 
resource."""
+        recorded = []
+
+        auth_manager = mock.Mock(spec=BaseAuthManager)
+        auth_manager.is_authorized_dag.side_effect = lambda **kw: 
recorded.append(kw) or True
+
+        with (
+            mock.patch(
+                
"airflow.api_fastapi.core_api.routes.public.assets.get_auth_manager",
+                return_value=auth_manager,
+            ),
+            mock.patch.object(
+                DagModel, "get_team_name", return_value=team_name, 
autospec=True
+            ) as mock_get_team_name,
+        ):
+            test_client.post("/assets/1/materialize")
+
+        assert len(recorded) == 1, "expected exactly one authorization check"
+        details = recorded[0]["details"]
+        assert details.id == self.DAG_ASSET1_ID
+        assert details.team_name == team_name
+        # resolved for the Dag the asset led to, not for some other Dag
+        mock_get_team_name.assert_called_once_with(self.DAG_ASSET1_ID, 
session=mock.ANY)
+
 
 class TestGetAssetQueuedEvents(TestQueuedEventEndpoint):
     @pytest.mark.usefixtures("time_freezer")
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
index 97a54d1ef57..0a14f69bcbe 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py
@@ -4394,6 +4394,32 @@ class TestWaitDagRun:
                 user=mock.ANY,
             )
 
+    @pytest.mark.parametrize("team_name", ["team_b", None])
+    def test_authorizes_xcom_against_the_dags_team(self, test_client, 
team_name):
+        """The XCom check must carry the Dag's team, matching what the route 
dependency above already
+        resolves — see the call site's comment for why."""
+        with (
+            mock.patch(
+                
"airflow.api_fastapi.core_api.routes.public.dag_run.get_auth_manager",
+                autospec=True,
+            ) as mock_get_auth_manager,
+            mock.patch.object(DagModel, "get_team_name", 
return_value=team_name, autospec=True),
+        ):
+            mock_get_auth_manager.return_value.is_authorized_dag.return_value 
= True
+
+            response = test_client.get(
+                f"/dags/{DAG1_ID}/dagRuns/{DAG1_RUN1_ID}/wait",
+                params={"interval": "1", "result": "task_1"},
+            )
+
+            assert response.status_code == 200
+            
mock_get_auth_manager.return_value.is_authorized_dag.assert_called_once_with(
+                method="GET",
+                access_entity=DagAccessEntity.XCOM,
+                details=DagDetails(id=DAG1_ID, team_name=team_name),
+                user=mock.ANY,
+            )
+
     def 
test_should_respond_200_without_result_when_user_lacks_xcom_permission(self, 
test_client):
         """Waiting without result parameter should not require XCom 
permissions."""
         with mock.patch(

Reply via email to