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

vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new c654696c2fb Resolve the Dag's team when authorizing a Dag found by 
lookup (#70893) (#71107)
c654696c2fb is described below

commit c654696c2fb7b07dfbd759b6e9ff4164267eb711
Author: Rahul Vats <[email protected]>
AuthorDate: Wed Aug 5 07:34:32 2026 +0530

    Resolve the Dag's team when authorizing a Dag found by lookup (#70893) 
(#71107)
    
    * 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
    
    
    
    * Update 
airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
    
    
    
    * Fix docstring indentation from the applied review suggestions
    
    ---------
    
    
    (cherry picked from commit f81459835adb111e57b2a87f586d856395660f8a)
    
    Co-authored-by: Jarek Potiuk <[email protected]>
    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 a19f4c7da77..9db8e1bffbd 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
@@ -82,6 +82,7 @@ from airflow.models.asset import (
     AssetWatcherModel,
     TaskOutletAssetReference,
 )
+from airflow.models.dag import DagModel
 from airflow.typing_compat import Unpack
 from airflow.utils.state import DagRunState
 from airflow.utils.types import DagRunTriggeredByType, DagRunType
@@ -435,7 +436,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 be2a247f748..19bffb1015c 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
@@ -834,7 +834,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 f28915973af..b4a4e697f62 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 (
@@ -1742,6 +1743,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 7b672ab8134..57fb84cd6b2 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
@@ -4085,6 +4085,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