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

vincbeck 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 aef881370ea Sync Dag permissions for CustomAuthManager (#74022)
aef881370ea is described below

commit aef881370eaa836609b8ce6ca8c5094a56e33f6e
Author: AutomationDev85 <[email protected]>
AuthorDate: Fri Oct 9 20:16:28 2026 +0200

    Sync Dag permissions for CustomAuthManager (#74022)
---
 .../src/airflow/dag_processing/collection.py       | 18 ++----
 .../tests/unit/dag_processing/test_collection.py   | 64 ++++++++++++++++------
 .../providers/fab/auth_manager/fab_auth_manager.py |  9 +++
 .../unit/fab/auth_manager/test_fab_auth_manager.py | 18 ++++++
 4 files changed, 80 insertions(+), 29 deletions(-)

diff --git a/airflow-core/src/airflow/dag_processing/collection.py 
b/airflow-core/src/airflow/dag_processing/collection.py
index d6fcbfd5646..3e20722c6c2 100644
--- a/airflow-core/src/airflow/dag_processing/collection.py
+++ b/airflow-core/src/airflow/dag_processing/collection.py
@@ -267,6 +267,7 @@ def _serialize_dag_capturing_errors(
 
     We can't place them directly in import_errors, as this may be retried, and 
work the next time
     """
+    from airflow.api_fastapi.app import get_auth_manager_cls
     from airflow.models.dagcode import DagCode
 
     # Updating serialized DAG can not be faster than a minimum interval to 
reduce database write rate.
@@ -291,8 +292,10 @@ def _serialize_dag_capturing_errors(
             DagCode.update_source_code(
                 dag.dag_id, dag.fileloc, dag_source_code=dag_source_code, 
session=session
             )
-        if "FabAuthManager" in conf.get("core", "auth_manager"):
-            _sync_dag_perms(dag, session=session)
+
+        sync_dag_perms = getattr(get_auth_manager_cls(), "sync_dag_perms", 
None)
+        if sync_dag_perms:
+            sync_dag_perms(dag, session=session)
 
         return []
     except OperationalError:
@@ -308,17 +311,6 @@ def _serialize_dag_capturing_errors(
         ]
 
 
-def _sync_dag_perms(dag: LazyDeserializedDAG, session: Session):
-    """Sync DAG specific permissions."""
-    dag_id = dag.dag_id
-
-    log.debug("Syncing DAG permissions: %s to the DB", dag_id)
-    from airflow.providers.fab.www.security_appless import 
ApplessAirflowSecurityManager
-
-    security_manager = ApplessAirflowSecurityManager(session=session)
-    security_manager.sync_perm_for_dag(dag_id, dag.access_control)
-
-
 def _build_duplicate_dag_id_warnings(
     dags: Collection[LazyDeserializedDAG],
     bundle_name: str,
diff --git a/airflow-core/tests/unit/dag_processing/test_collection.py 
b/airflow-core/tests/unit/dag_processing/test_collection.py
index 0f66f067b83..8a7fe712bb5 100644
--- a/airflow-core/tests/unit/dag_processing/test_collection.py
+++ b/airflow-core/tests/unit/dag_processing/test_collection.py
@@ -26,7 +26,6 @@ import textwrap
 import warnings
 from collections.abc import Generator
 from datetime import timedelta
-from typing import TYPE_CHECKING
 from unittest import mock
 from unittest.mock import patch
 
@@ -106,9 +105,6 @@ from tests_common.test_utils.db import (
 from tests_common.test_utils.mock_plugins import mock_plugin_manager
 from unit.plugins.priority_weight_strategy import 
StaticTestPriorityWeightStrategy
 
-if TYPE_CHECKING:
-    from kgb import SpyAgency
-
 mark_fab_auth_manager_test = pytest.mark.skipif(
     condition="FabAuthManager" not in conf.get("core", "auth_manager"),
     reason="This is only for FabAuthManager. Please set the environment 
variable `AIRFLOW__CORE__AUTH_MANAGER` to 
`airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager` in 
`files/airflow-breeze-config/environment_variables.env` before running breeze 
shell. To run the test, add the flag `--keep-env-variables` to the pytest 
command.",
@@ -712,11 +708,50 @@ class TestUpdateDagParsingResults:
         yield dag_import_error_listener
         dag_import_error_listener.clear()
 
+    @patch("airflow.api_fastapi.app._AuthManagerState.instance", None)
+    @patch.object(SerializedDagModel, "write_dag", return_value=True, 
autospec=True)
+    @patch("airflow.api_fastapi.app.get_auth_manager_cls", autospec=True)
+    def test_sync_perms_for_fab_auth_manager_subclass(
+        self, mock_get_auth_manager_cls, mock_write_dag, session
+    ):
+        FabAuthManager = pytest.importorskip(
+            "airflow.providers.fab.auth_manager.fab_auth_manager"
+        ).FabAuthManager
+
+        class CustomAuthManager(FabAuthManager):
+            pass
+
+        mock_get_auth_manager_cls.return_value = CustomAuthManager
+        dag = DAG(dag_id="test")
+
+        with patch.object(FabAuthManager, "sync_dag_perms", autospec=True) as 
mock_sync_dag_perms:
+            assert (
+                
airflow.dag_processing.collection._serialize_dag_capturing_errors(
+                    dag, "testing", session, None
+                )
+                == []
+            )
+            mock_sync_dag_perms.assert_called_once_with(dag, session=session)
+
+    @patch.object(SerializedDagModel, "write_dag", return_value=True, 
autospec=True)
+    @patch("airflow.api_fastapi.app.get_auth_manager_cls", autospec=True)
+    def test_serialize_without_auth_manager_sync_hook(
+        self, mock_get_auth_manager_cls, mock_write_dag, session
+    ):
+        mock_get_auth_manager_cls.return_value = object
+        dag = DAG(dag_id="test")
+
+        assert (
+            
airflow.dag_processing.collection._serialize_dag_capturing_errors(dag, 
"testing", session, None)
+            == []
+        )
+
     @mark_fab_auth_manager_test
     @conf_vars({("core", "min_serialized_dag_update_interval"): "5"})
     @pytest.mark.usefixtures("clean_db")  # sync_perms in fab has bad session 
commit hygiene
+    
@patch("airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager.sync_dag_perms",
 autospec=True)
     def test_sync_perms_syncs_dag_specific_perms_on_update(
-        self, monkeypatch, spy_agency: SpyAgency, session, time_machine, 
testing_dag_bundle
+        self, mock_sync_dag_perms, monkeypatch, session, time_machine, 
testing_dag_bundle
     ):
         """Test DAG-specific permissions are synced when a DAG is new or 
updated"""
         serialized_dags_count = 
session.scalar(select(func.count(SerializedDagModel.dag_id)))
@@ -726,29 +761,26 @@ class TestUpdateDagParsingResults:
 
         dag = DAG(dag_id="test")
 
-        sync_perms_spy = spy_agency.spy_on(
-            airflow.dag_processing.collection._sync_dag_perms,
-            call_original=False,
-        )
-
         def _sync_to_db():
-            sync_perms_spy.reset_calls()
+            mock_sync_dag_perms.reset_mock()
             time_machine.shift(20)
 
-            update_dag_parsing_results_in_db("testing", None, [dag], dict(), 
None, set(), session)
+            import_errors = {}
+            update_dag_parsing_results_in_db("testing", None, [dag], 
import_errors, None, set(), session)
+            assert not import_errors
 
         _sync_to_db()
-        spy_agency.assert_spy_called_with(sync_perms_spy, dag, session=session)
+        mock_sync_dag_perms.assert_called_once_with(dag, session=session)
 
         # DAG isn't updated
         _sync_to_db()
-        # `_sync_dag_perms` should be called even the DAG isn't updated. 
Otherwise, any import error will not show up until DAG is updated.
-        spy_agency.assert_spy_called_with(sync_perms_spy, dag, session=session)
+        # `sync_dag_perms` should be called even the DAG isn't updated. 
Otherwise, any import error will not show up until DAG is updated.
+        mock_sync_dag_perms.assert_called_once_with(dag, session=session)
 
         # DAG is updated
         dag.tags = {"new_tag"}
         _sync_to_db()
-        spy_agency.assert_spy_called_with(sync_perms_spy, dag, session=session)
+        mock_sync_dag_perms.assert_called_once_with(dag, session=session)
 
         serialized_dags_count = 
session.scalar(select(func.count(SerializedDagModel.dag_id)))
 
diff --git 
a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py 
b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
index 01cc28d5670..4a0ed10b73d 100644
--- a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
+++ b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
@@ -97,6 +97,7 @@ from airflow.providers.fab.www.security.permissions import (
     RESOURCE_WEBSITE,
     RESOURCE_XCOM,
 )
+from airflow.providers.fab.www.security_appless import 
ApplessAirflowSecurityManager
 from airflow.providers.fab.www.utils import get_fab_action_from_method_map
 from airflow.utils.session import NEW_SESSION, create_session, provide_session
 
@@ -117,6 +118,7 @@ if TYPE_CHECKING:
         RESOURCE_ASSET,
         RESOURCE_ASSET_ALIAS,
     )
+    from airflow.serialization.serialized_objects import LazyDeserializedDAG
 else:
     from airflow.providers.common.compat.security.permissions import (
         RESOURCE_ASSET,
@@ -214,6 +216,13 @@ class FabAuthManager(BaseAuthManager[User]):
     def init_flask_resources(self) -> None:
         self._sync_appbuilder_roles()
 
+    @staticmethod
+    def sync_dag_perms(dag: LazyDeserializedDAG, *, session: Session) -> None:
+        """Sync Dag-specific permissions without initializing the auth 
manager."""
+        log.debug("Syncing DAG permissions: %s to the DB", dag.dag_id)
+        security_manager = ApplessAirflowSecurityManager(session=session)
+        security_manager.sync_perm_for_dag(dag.dag_id, dag.access_control)
+
     @cached_property
     def apiserver_endpoint(self) -> str:
         return conf.get("api", "base_url", fallback="/")
diff --git a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py 
b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py
index 2d79c00512e..34d5bfa35fe 100644
--- a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py
+++ b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py
@@ -28,6 +28,7 @@ import pytest
 from flask import g
 from flask_appbuilder.const import AUTH_DB, AUTH_LDAP
 from sqlalchemy.exc import OperationalError, PendingRollbackError
+from sqlalchemy.orm import Session
 
 from airflow import settings
 from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
@@ -138,6 +139,23 @@ if TYPE_CHECKING:
     from airflow.api_fastapi.auth.managers.base_auth_manager import 
ResourceMethod
 
 
[email protected](
+    
"airflow.providers.fab.auth_manager.fab_auth_manager.ApplessAirflowSecurityManager",
 autospec=True
+)
+def 
test_sync_dag_perms_without_auth_manager_initialization(mock_security_manager):
+    dag = Mock(spec=["dag_id", "access_control"])
+    dag.dag_id = "test"
+    dag.access_control = {"Viewer": {"can_read"}}
+    session = Mock(spec=Session)
+
+    FabAuthManager.sync_dag_perms(dag, session=session)
+
+    mock_security_manager.assert_called_once_with(session=session)
+    
mock_security_manager.return_value.sync_perm_for_dag.assert_called_once_with(
+        dag.dag_id, dag.access_control
+    )
+
+
 IS_AUTHORIZED_METHODS_SIMPLE = {
     "is_authorized_configuration": RESOURCE_CONFIG,
     "is_authorized_connection": RESOURCE_CONNECTION,

Reply via email to