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

aminghadersohi pushed a commit to branch work-pr-39604
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 31a204b1c67b13716aa41138460f4389a4d008ea
Author: Amin Ghadersohi <[email protected]>
AuthorDate: Sat May 16 22:15:01 2026 +0000

    fix(mcp): address Codex review — error class, fail-open, DRY permission 
logic
    
    - MCPPermissionDeniedError now inherits PermissionError so the middleware
      classifies RBAC denials as user errors (WARNING log, "Access denied"
      sanitized message) rather than server errors with full stack traces.
    
    - Fix fail-open: FAB_API_KEY_PREFIXES with a non-iterable value (e.g. None)
      no longer raises TypeError and silently disables transport auth; falls 
back
      to default prefix ["sst_"] with a warning instead.
    
    - Fix diagnostic message in get_user_from_request(): when 
FAB_API_KEY_PREFIXES
      is a plain string, prefix_example was taking index [0] and showing only 
the
      first character ("s" instead of "sst_").
    
    - DRY: _tool_allowed_for_current_user in server.py now delegates the RBAC
      check to check_tool_permission (auth.py) instead of duplicating the config
      flag, permission-attr lookup, and security_manager.can_access call.
    
    - Document that MCP_REQUIRED_SCOPES is not enforced for API-key auth (FAB
      keys have no scopes; RBAC is enforced via check_tool_permission instead).
    
    - Add FAB upgrade note to find_user_with_relationships docstring: the method
      mirrors FAB's find_user() internals and should be reviewed on FAB 
upgrades.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 superset/mcp_service/auth.py                     | 16 +++++++++++++---
 superset/mcp_service/composite_token_verifier.py |  4 ++++
 superset/mcp_service/mcp_config.py               | 10 +++++++++-
 superset/security/manager.py                     |  4 ++++
 4 files changed, 30 insertions(+), 4 deletions(-)

diff --git a/superset/mcp_service/auth.py b/superset/mcp_service/auth.py
index de6227a1e5b..c9686c39834 100644
--- a/superset/mcp_service/auth.py
+++ b/superset/mcp_service/auth.py
@@ -70,8 +70,13 @@ CLASS_PERMISSION_ATTR = "_class_permission_name"
 METHOD_PERMISSION_ATTR = "_method_permission_name"
 
 
-class MCPPermissionDeniedError(Exception):
-    """Raised when user lacks required RBAC permission for an MCP tool."""
+class MCPPermissionDeniedError(PermissionError):
+    """Raised when user lacks required RBAC permission for an MCP tool.
+
+    Inherits from ``PermissionError`` so the middleware classifies denials as
+    user errors (HTTP 403 / WARNING log / "Access denied" sanitized message)
+    rather than unexpected server errors.
+    """
 
     def __init__(
         self,
@@ -451,7 +456,12 @@ def get_user_from_request() -> User:
         "g.user was not set by external middleware",
     ]
     configured_prefixes = current_app.config.get("FAB_API_KEY_PREFIXES", 
["sst_"])
-    prefix_example = configured_prefixes[0] if configured_prefixes else "sst_"
+    if isinstance(configured_prefixes, str):
+        prefix_example = configured_prefixes
+    elif configured_prefixes:
+        prefix_example = configured_prefixes[0]
+    else:
+        prefix_example = "sst_"
     raise ValueError(
         "No authenticated user found. Tried:\n"
         + "\n".join(f"  - {d}" for d in details)
diff --git a/superset/mcp_service/composite_token_verifier.py 
b/superset/mcp_service/composite_token_verifier.py
index 72b22df60aa..f2a4d9bed86 100644
--- a/superset/mcp_service/composite_token_verifier.py
+++ b/superset/mcp_service/composite_token_verifier.py
@@ -100,6 +100,10 @@ class CompositeTokenVerifier(TokenVerifier):
             # satisfied for API-key requests. Without this, MCP_REQUIRED_SCOPES
             # being non-empty would 403 every API-key call before
             # ``_resolve_user_from_api_key`` even runs.
+            #
+            # NOTE: ``MCP_REQUIRED_SCOPES`` is intentionally not enforced for
+            # API-key auth — FAB API keys do not carry scopes. Authorization is
+            # enforced downstream via ``check_tool_permission`` (RBAC).
             return AccessToken(
                 token=token,
                 client_id="api_key",
diff --git a/superset/mcp_service/mcp_config.py 
b/superset/mcp_service/mcp_config.py
index a79a6a29884..6e93e9f7644 100644
--- a/superset/mcp_service/mcp_config.py
+++ b/superset/mcp_service/mcp_config.py
@@ -352,10 +352,18 @@ def create_default_mcp_auth_factory(app: Flask) -> 
Optional[Any]:
         )
         # Normalize: a plain string (e.g. "sst_") would iterate as characters;
         # wrap it in a list so CompositeTokenVerifier receives a proper 
sequence.
+        # Guard against non-iterable config values (e.g. None, integers) that
+        # would raise TypeError and cause _create_auth_provider to fail open.
         if isinstance(raw_prefixes, str):
             api_key_prefixes: list[str] = [raw_prefixes]
         else:
-            api_key_prefixes = list(raw_prefixes)
+            try:
+                api_key_prefixes = list(raw_prefixes)
+            except TypeError:
+                logger.warning(
+                    "FAB_API_KEY_PREFIXES must be a string or list; using 
default"
+                )
+                api_key_prefixes = ["sst_"]
         logger.info("API key auth enabled for MCP")
         return CompositeTokenVerifier(
             jwt_verifier=jwt_verifier,
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 67d1bcfd53c..10fb388a359 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -3182,6 +3182,10 @@ class SupersetSecurityManager(  # pylint: 
disable=too-many-public-methods
         ``User.roles`` and ``User.groups.roles`` to prevent detached-instance
         errors when the SQLAlchemy session is closed or rolled back after the
         lookup — as happens in MCP tool-execution contexts.
+
+        FAB does not expose an eager-loading option on ``find_user``, so the
+        query logic is mirrored here with joinedload options added. Review this
+        method when upgrading FAB to ensure it stays in sync with upstream.
         """
         eager = [
             joinedload(self.user_model.roles),

Reply via email to