rusackas commented on code in PR #40659:
URL: https://github.com/apache/superset/pull/40659#discussion_r3338262542


##########
superset/mcp_service/auth.py:
##########
@@ -119,7 +119,15 @@ def check_tool_permission(func: Callable[..., Any]) -> 
bool:
 
         class_permission_name = getattr(func, CLASS_PERMISSION_ATTR, None)
         if not class_permission_name:
-            # No RBAC configured for this tool; allow by default.
+            # No RBAC configured for this tool; allow by default. This is a
+            # supported configuration (a protected tool may intentionally
+            # declare no permission class), but surface it so an accidental
+            # omission on a sensitive tool doesn't silently fail open.
+            logger.warning(
+                "Tool %s is permission-protected but declares no "
+                "class_permission_name; allowing access without an RBAC check",
+                func.__name__,
+            )

Review Comment:
   Valid point. Emitting a WARNING on every protected-tool call for tools 
without a class_permission_name would be noisy in practice. Downgrading to 
debug or moving to registration-time is the right call — will address in a 
follow-up.



##########
superset/mcp_service/utils/permissions_utils.py:
##########
@@ -89,10 +89,15 @@ def user_has_permission(
         return False
 
     try:
-        # Check if user is admin (has all permissions)
+        from flask import current_app
+
+        # Check if user is admin (has all permissions). Use the configured
+        # admin role name rather than hardcoding "Admin", so deployments that
+        # rename the admin role (AUTH_ROLE_ADMIN) still grant admins the 
bypass.
+        admin_role_name = current_app.config["AUTH_ROLE_ADMIN"]

Review Comment:
   The broad except is intentional fail-safe behavior (deny on any error). The 
function is designed to be called within a Flask request context; calling it 
outside one is not a supported use-case here. That said, the concern about the 
except masking legitimate permission denials is noted — a narrower RuntimeError 
catch would be cleaner without changing semantics.



##########
superset/mcp_service/utils/permissions_utils.py:
##########
@@ -89,10 +89,15 @@ def user_has_permission(
         return False
 
     try:
-        # Check if user is admin (has all permissions)
+        from flask import current_app
+
+        # Check if user is admin (has all permissions). Use the configured
+        # admin role name rather than hardcoding "Admin", so deployments that
+        # rename the admin role (AUTH_ROLE_ADMIN) still grant admins the 
bypass.
+        admin_role_name = current_app.config["AUTH_ROLE_ADMIN"]
         if hasattr(user, "roles"):
             for role in user.roles:
-                if role.name in ("Admin", "admin"):
+                if role.name == admin_role_name:
                     return True

Review Comment:
   Correct — the bypass only inspects user.roles directly, not group-inherited 
roles. FAB group-based role inheritance is rare in practice and the MCP code 
already loads User.groups/Group.roles for permission checks. The short-circuit 
is intentional for the direct-role case. Group-inherited admin detection can be 
added as a follow-up if operators report it.



##########
tests/unit_tests/mcp_service/utils/test_permissions_utils.py:
##########
@@ -19,13 +19,40 @@
 
 from unittest.mock import Mock

Review Comment:
   The suggested test fix in your previous comment referenced patch, but the 
test file already uses current_app.config directly (see the 
test_user_has_permission_admin_uses_configured_role_name test above). No patch 
import is needed for the actual test added in this PR.



##########
tests/unit_tests/mcp_service/utils/test_permissions_utils.py:
##########
@@ -19,13 +19,40 @@
 
 from unittest.mock import Mock
 
+from flask import current_app
+
 from superset.mcp_service.utils.permissions_utils import (
     apply_field_permissions_to_columns,
     filter_sensitive_data,
     get_allowed_fields,
+    user_has_permission,
 )
 
 
+def test_user_has_permission_admin_uses_configured_role_name():
+    """The admin bypass honors AUTH_ROLE_ADMIN, not a hardcoded 'Admin'."""
+
+    def _role(name: str) -> Mock:
+        role = Mock()
+        role.name = name  # set after construction (Mock intercepts name=)
+        return role
+
+    original = current_app.config["AUTH_ROLE_ADMIN"]
+    current_app.config["AUTH_ROLE_ADMIN"] = "Superuser"
+    try:
+        admin = Mock()
+        admin.roles = [_role("Superuser")]
+        assert user_has_permission(admin, "can_read", "Chart") is True
+
+        # The previously-hardcoded "Admin" name no longer grants the bypass;
+        # it falls through to the real permission check, which denies here.
+        not_admin = Mock()
+        not_admin.roles = [_role("Admin")]
+        assert user_has_permission(not_admin, "can_read", "Chart") is False

Review Comment:
   Agreed the test relies on incidental behavior from passing a bare Mock to 
has_access. Explicitly mocking security_manager.has_access to return False and 
asserting it was called would make the intent clearer. Will improve in a 
follow-up.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to