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

potiuk 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 a68c63fc87 Add `get_permitted_menu_items` in auth manager (#36871)
a68c63fc87 is described below

commit a68c63fc8713d5d7b5f6c1ef516d2fa03ac2db67
Author: Vincent <97131062+vincb...@users.noreply.github.com>
AuthorDate: Fri Jan 19 02:48:42 2024 -0500

    Add `get_permitted_menu_items` in auth manager (#36871)
---
 airflow/auth/managers/base_auth_manager.py         | 22 ++++++++++++++++++++++
 airflow/www/templates/appbuilder/navbar_menu.html  |  6 +++---
 docs/apache-airflow/core-concepts/auth-manager.rst |  1 +
 tests/auth/managers/test_base_auth_manager.py      | 20 ++++++++++++++++++++
 4 files changed, 46 insertions(+), 3 deletions(-)

diff --git a/airflow/auth/managers/base_auth_manager.py 
b/airflow/auth/managers/base_auth_manager.py
index 25a3b514d4..ba20da16cc 100644
--- a/airflow/auth/managers/base_auth_manager.py
+++ b/airflow/auth/managers/base_auth_manager.py
@@ -28,11 +28,13 @@ from airflow.auth.managers.models.resource_details import (
 )
 from airflow.exceptions import AirflowException
 from airflow.models import DagModel
+from airflow.security.permissions import ACTION_CAN_ACCESS_MENU
 from airflow.utils.log.logging_mixin import LoggingMixin
 from airflow.utils.session import NEW_SESSION, provide_session
 
 if TYPE_CHECKING:
     from flask import Blueprint
+    from flask_appbuilder.menu import MenuItem
     from sqlalchemy.orm import Session
 
     from airflow.auth.managers.models.base_user import BaseUser
@@ -372,6 +374,26 @@ class BaseAuthManager(LoggingMixin):
             if _is_permitted_dag_id("GET", methods, dag_id) or 
_is_permitted_dag_id("PUT", methods, dag_id)
         }
 
+    def get_permitted_menu_items(self, menu_items: list[MenuItem]) -> 
list[MenuItem]:
+        """
+        Filter menu items based on user permissions.
+
+        :param menu_items: list of all menu items
+        """
+        items = filter(
+            lambda item: 
self.security_manager.has_access(ACTION_CAN_ACCESS_MENU, item.name), menu_items
+        )
+        accessible_items = []
+        for menu_item in items:
+            if menu_item.childs:
+                accessible_children = []
+                for child in menu_item.childs:
+                    if 
self.security_manager.has_access(ACTION_CAN_ACCESS_MENU, child.name):
+                        accessible_children.append(child)
+                menu_item.childs = accessible_children
+            accessible_items.append(menu_item)
+        return accessible_items
+
     @abstractmethod
     def get_url_login(self, **kwargs) -> str:
         """Return the login page url."""
diff --git a/airflow/www/templates/appbuilder/navbar_menu.html 
b/airflow/www/templates/appbuilder/navbar_menu.html
index 924f5411be..72855df270 100644
--- a/airflow/www/templates/appbuilder/navbar_menu.html
+++ b/airflow/www/templates/appbuilder/navbar_menu.html
@@ -21,8 +21,8 @@
   <a href="{{item.get_url()}}">{{_(item.label)}}</a>
 {% endmacro %}
 
-{% for item1 in menu.get_list() %}
-  {% if item1 | is_menu_visible %}
+{% for item1 in auth_manager.get_permitted_menu_items(menu.get_list()) %}
+  {% if item1 %}
     {% if item1.childs %}
       <li class="dropdown">
         <a class="dropdown-toggle" href="javascript:void(0)">
@@ -34,7 +34,7 @@
                 {% if not loop.last %}
                   <li class="divider"></li>
                 {% endif %}
-              {% elif item2 | is_menu_visible %}
+              {% elif item2 %}
                 <li>{{ menu_item(item2) }}</li>
               {% endif %}
             {% endif %}
diff --git a/docs/apache-airflow/core-concepts/auth-manager.rst 
b/docs/apache-airflow/core-concepts/auth-manager.rst
index 55056f7f46..bb54795f18 100644
--- a/docs/apache-airflow/core-concepts/auth-manager.rst
+++ b/docs/apache-airflow/core-concepts/auth-manager.rst
@@ -124,6 +124,7 @@ The following methods aren't required to override to have a 
functional Airflow a
 * ``batch_is_authorized_pool``: Batch version of ``is_authorized_pool``. If 
not overridden, it will call ``is_authorized_pool`` for every single item.
 * ``batch_is_authorized_variable``: Batch version of 
``is_authorized_variable``. If not overridden, it will call 
``is_authorized_variable`` for every single item.
 * ``get_permitted_dag_ids``: Return the list of DAG IDs the user has access 
to.  If not overridden, it will call ``is_authorized_dag`` for every single DAG 
available in the environment.
+* ``get_permitted_menu_items``: Return the menu items the user has access to.  
If not overridden, it will call ``has_access`` in 
:class:`~airflow.www.security_manager.AirflowSecurityManagerV2` for every 
single menu item.
 
 CLI
 ^^^
diff --git a/tests/auth/managers/test_base_auth_manager.py 
b/tests/auth/managers/test_base_auth_manager.py
index 6655c01132..9fac23a062 100644
--- a/tests/auth/managers/test_base_auth_manager.py
+++ b/tests/auth/managers/test_base_auth_manager.py
@@ -21,6 +21,7 @@ from unittest.mock import MagicMock, Mock, patch
 
 import pytest
 from flask import Flask
+from flask_appbuilder.menu import Menu
 
 from airflow.auth.managers.base_auth_manager import BaseAuthManager, 
ResourceMethod
 from airflow.auth.managers.models.resource_details import (
@@ -296,3 +297,22 @@ class TestBaseAuthManager:
         session.execute.return_value = dags
         result = auth_manager.get_permitted_dag_ids(user=user, session=session)
         assert result == expected
+
+    @patch.object(EmptyAuthManager, "security_manager")
+    def test_get_permitted_menu_items(self, mock_security_manager, 
auth_manager):
+        mock_security_manager.has_access.side_effect = [True, False, True, 
True, False]
+
+        menu = Menu()
+        menu.add_link("item1")
+        menu.add_link("item2")
+        menu.add_link("item3")
+        menu.add_link("item3.1", category="item3")
+        menu.add_link("item3.2", category="item3")
+
+        result = auth_manager.get_permitted_menu_items(menu.get_list())
+
+        assert len(result) == 2
+        assert result[0].name == "item1"
+        assert result[1].name == "item3"
+        assert len(result[1].childs) == 1
+        assert result[1].childs[0].name == "item3.1"

Reply via email to