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"