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

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


The following commit(s) were added to refs/heads/v3-4-test by this push:
     new d22b2fb0e8c [v3-4-test] Scope asset-wide queued event deletion to 
editable Dags (#74839) (#75089)
d22b2fb0e8c is described below

commit d22b2fb0e8cf3693f5b444b71d2dc5d64e9db1ce
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Oct 19 18:44:19 2026 +0530

    [v3-4-test] Scope asset-wide queued event deletion to editable Dags 
(#74839) (#75089)
    
    * Scope asset-wide queued event deletion to editable Dags
    
    DELETE /assets/{asset_id}/queuedEvents filtered the rows it removed by the
    Dags the user can read, and checked Dag edit against an optional ?dag_id=
    query parameter unrelated to those rows. The delete is now limited to the
    queued events of Dags the user may edit, and the route no longer makes a
    Dag-level check against a request-supplied dag_id.
    
    Generated-by: Claude Opus 5 following the guidelines at 
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
    
    * Rename newsfragment to the PR number
    
    Generated-by: Claude Opus 5 following the guidelines at 
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
    
    * Regenerate API permission docs
    (cherry picked from commit 8942dd498313e79b7ff3d6766f5ec253e3dff926)
    
    
    Generated-by: Claude Opus 5 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]>
---
 airflow-core/docs/security/api_permissions_ref.rst |  4 --
 airflow-core/newsfragments/74839.bugfix.rst        |  1 +
 .../api_fastapi/core_api/routes/public/assets.py   |  8 ++-
 .../core_api/routes/public/test_assets.py          | 69 +++++++++++++++++++++-
 providers/fab/docs/auth-manager/access-control.rst |  4 --
 5 files changed, 74 insertions(+), 12 deletions(-)

diff --git a/airflow-core/docs/security/api_permissions_ref.rst 
b/airflow-core/docs/security/api_permissions_ref.rst
index 0e9af0696c5..cf5e188ff15 100644
--- a/airflow-core/docs/security/api_permissions_ref.rst
+++ b/airflow-core/docs/security/api_permissions_ref.rst
@@ -86,10 +86,6 @@ source code so it stays up to date as endpoints are added or 
changed.
      - ``/api/v2/assets/{asset_id}/queuedEvents``
      - ``Asset``
      - ``DELETE``
-   * - ``DELETE``
-     - ``/api/v2/assets/{asset_id}/queuedEvents``
-     - ``DAG``
-     - ``PUT``
    * - ``GET``
      - ``/api/v2/assets/{asset_id}/queuedEvents``
      - ``Asset``
diff --git a/airflow-core/newsfragments/74839.bugfix.rst 
b/airflow-core/newsfragments/74839.bugfix.rst
new file mode 100644
index 00000000000..8c20449bae6
--- /dev/null
+++ b/airflow-core/newsfragments/74839.bugfix.rst
@@ -0,0 +1 @@
+Fix ``DELETE /assets/{asset_id}/queuedEvents`` deleting queued asset events of 
Dags the user can only read; it now deletes only the queued events of Dags the 
user may edit.
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 abd1d1ae70f..989b57ca1f2 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
@@ -70,6 +70,7 @@ from airflow.api_fastapi.core_api.datamodels.assets import (
 from airflow.api_fastapi.core_api.datamodels.dag_run import DAGRunResponse
 from airflow.api_fastapi.core_api.openapi.exceptions import 
create_openapi_http_exception_doc
 from airflow.api_fastapi.core_api.security import (
+    EditableDagsFilterDep,
     GetUserDep,
     ReadableAssetEventsByAssetFilterDep,
     ReadableAssetEventsFilterDep,
@@ -719,19 +720,20 @@ def get_dag_asset_queued_event(
     responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]),
     dependencies=[
         Depends(requires_access_asset(method="DELETE")),
-        Depends(requires_access_dag(method="PUT")),
         Depends(action_logging()),
     ],
 )
 def delete_asset_queued_events(
     asset_id: int,
-    readable_dags_filter: ReadableDagsFilterDep,
+    editable_dags_filter: EditableDagsFilterDep,
     session: SessionDep,
     before: OptionalDateTimeQuery = None,
 ):
     """Delete queued asset events for an asset."""
+    # This route is not tied to a single Dag, so the Dag axis is enforced per 
row: only the
+    # queued events of Dags the user may edit are deleted, and the others are 
left in place.
     where_clause = _generate_queued_event_where_clause(
-        asset_id=asset_id, before=before, 
permitted_dag_ids=readable_dags_filter.value
+        asset_id=asset_id, before=before, 
permitted_dag_ids=editable_dags_filter.value
     )
     delete_stmt = delete(AssetDagRunQueue).where(*where_clause)
     result = cast("CursorResult", session.execute(delete_stmt))
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 ac902c693af..5cf95214368 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
@@ -1985,7 +1985,6 @@ class TestQueuedEventsDagAxisAuthorization:
     @pytest.mark.parametrize(
         "path",
         [
-            "/assets/{asset_id}/queuedEvents",
             "/dags/{dag_id}/assets/queuedEvents",
             "/dags/{dag_id}/assets/{asset_id}/queuedEvents",
         ],
@@ -2013,6 +2012,24 @@ class TestQueuedEventsDagAxisAuthorization:
         for route in matches:
             assert self._dag_axis_methods(route) == ["GET"]
 
+    @pytest.mark.parametrize("method", ["GET", "DELETE"])
+    def test_asset_wide_queued_events_do_not_gate_on_a_request_dag_id(self, 
routes_by_path, method):
+        """The asset-wide routes have no Dag in the path; they scope rows to 
permitted Dags instead.
+
+        A Dag check there would be made against whatever ``?dag_id=`` the 
caller sends, which says
+        nothing about the Dags whose rows the route returns or deletes.
+        """
+        path = "/assets/{asset_id}/queuedEvents"
+        # Exclude the per-Dag routes, whose paths end with the same suffix.
+        matches = [
+            r
+            for (p, methods), r in routes_by_path.items()
+            if p.endswith(path) and "/dags/" not in p and method in methods
+        ]
+        assert matches, f"no {method} route registered for {path}"
+        for route in matches:
+            assert self._dag_axis_methods(route) == []
+
 
 class TestDeleteDagDatasetQueuedEvents(TestQueuedEventEndpoint):
     @pytest.mark.usefixtures("time_freezer")
@@ -2816,6 +2833,56 @@ class 
TestDeleteAssetQueuedEvents(TestQueuedEventEndpoint):
         assert response.status_code == 404
         assert response.json()["detail"] == "Queue event with asset_id: `1` 
was not found"
 
+    def _create_queued_events_for_dags(self, session, dag_ids: list[str]) -> 
AssetModel:
+        (asset,) = self.create_assets(session=session, num=1)
+        _ensure_dags(session, *dag_ids)
+        for dag_id in dag_ids:
+            event = AssetEvent(asset_id=asset.id, timestamp=timezone.utcnow())
+            session.add(event)
+            session.flush()
+            session.add(AssetDagRunQueue(target_dag_id=dag_id, 
asset_id=asset.id, asset_event_id=event.id))
+        session.commit()
+        return asset
+
+    @staticmethod
+    def _authorized_dag_ids_by_method(*, readable: set[str], editable: 
set[str]):
+        def get_authorized_dag_ids(*, user, method="GET", **kwargs):
+            return set(editable) if method == "PUT" else set(readable)
+
+        return get_authorized_dag_ids
+
+    @pytest.mark.parametrize("query_string", ["", "?dag_id=dag_a"], 
ids=["no-dag-id", "editable-dag-id"])
+    def test_deletes_only_queued_events_of_editable_dags(self, test_client, 
session, query_string):
+        """A user who may edit dag_a but only read dag_b must leave dag_b's 
queued events in place."""
+        asset = self._create_queued_events_for_dags(session, ["dag_a", 
"dag_b"])
+
+        with mock.patch.object(
+            BaseAuthManager,
+            "get_authorized_dag_ids",
+            side_effect=self._authorized_dag_ids_by_method(readable={"dag_a", 
"dag_b"}, editable={"dag_a"}),
+        ):
+            response = 
test_client.delete(f"/assets/{asset.id}/queuedEvents{query_string}")
+
+        assert response.status_code == 204
+        session.expire_all()
+        remaining = 
session.scalars(select(AssetDagRunQueue.target_dag_id)).all()
+        assert remaining == ["dag_b"]
+
+    def test_responds_404_when_only_readable_dags_have_queued_events(self, 
test_client, session):
+        """Naming an editable Dag in the query string does not widen the 
delete to readable Dags."""
+        asset = self._create_queued_events_for_dags(session, ["dag_b"])
+
+        with mock.patch.object(
+            BaseAuthManager,
+            "get_authorized_dag_ids",
+            side_effect=self._authorized_dag_ids_by_method(readable={"dag_a", 
"dag_b"}, editable={"dag_a"}),
+        ):
+            response = 
test_client.delete(f"/assets/{asset.id}/queuedEvents?dag_id=dag_a")
+
+        assert response.status_code == 404
+        session.expire_all()
+        assert session.scalars(select(AssetDagRunQueue.target_dag_id)).all() 
== ["dag_b"]
+
     def test_delete_does_not_read_back_deleted_row_keys(self, test_client, 
session, create_dummy_dag):
         from sqlalchemy import event
 
diff --git a/providers/fab/docs/auth-manager/access-control.rst 
b/providers/fab/docs/auth-manager/access-control.rst
index 8d73e648bb9..87567352400 100644
--- a/providers/fab/docs/auth-manager/access-control.rst
+++ b/providers/fab/docs/auth-manager/access-control.rst
@@ -260,10 +260,6 @@ Stable API Permissions
      - DELETE
      - Assets.can_delete
      - Op
-   * - ``/api/v2/assets/{asset_id}/queuedEvents``
-     - DELETE
-     - DAGs.can_edit
-     - User
    * - ``/api/v2/assets/{asset_id}/queuedEvents``
      - GET
      - Assets.can_read

Reply via email to