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 <[email protected]>
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"