1616# under the License.
1717
1818"""
19- Authentication and authorization hooks for MCP tools.
20-
21- This module provides a flexible authentication framework that can be customized
22- by deployers through configuration and the FastMCP auth provider system.
23-
24- Default Behavior:
25- - Uses configured user (MCP_DEV_USERNAME) for authentication
26- - Leverages Superset's existing RBAC and security manager
27- - Provides dataset-level access control via has_dataset_access()
28-
29- Customization Options for Deployers:
30- 1. JWT Authentication: Configure BearerAuthProvider in mcp_config.py with your
31- JWT issuer, audience, and public key/JWKS URI. See create_default_mcp_auth_factory()
32- for reference implementation.
33-
34- 2. User Resolution: Override get_user_from_request() to extract user from JWT
35- claims, headers, or other authentication mechanisms.
36-
37- 3. Permission Checks: Add custom permission logic in mcp_auth_hook before tool
38- execution. Use Superset's security_manager for role-based checks.
39-
40- 4. Audit Logging: Extend mcp_auth_hook to log tool access, integrate with your
41- SIEM/logging infrastructure, or track usage metrics.
42-
43- 5. Rate Limiting: Integrate rate limiting middleware in the mcp_auth_hook wrapper
44- based on user, IP, or tool-specific quotas.
45-
46- The hook pattern allows production deployments to layer additional security
47- without modifying tool implementations.
19+ Minimal authentication hooks for MCP tools.
20+ This is a placeholder implementation that provides basic user context.
21+
22+ Future enhancements (to be added in separate PRs):
23+ - JWT token authentication and validation
24+ - User impersonation support
25+ - Permission checking with scopes
26+ - Comprehensive audit logging
27+ - Field-level permissions
4828"""
4929
5030import logging
@@ -69,18 +49,6 @@ def get_user_from_request() -> User:
6949 Default implementation uses MCP_DEV_USERNAME from configuration for
7050 development and single-user deployments.
7151
72- Deployers can customize this function to:
73- - Extract user from JWT claims (when using BearerAuthProvider)
74- - Implement user impersonation based on request headers
75- - Add fallback authentication for service accounts
76- - Integrate with external identity providers
77-
78- Example JWT customization:
79- from flask import request
80- token = request.headers.get('Authorization', '').replace('Bearer ', '')
81- # Parse JWT and extract user from claims
82- # Then query user from database as shown below
83-
8452 Returns:
8553 User object with roles and groups eagerly loaded
8654
@@ -93,7 +61,6 @@ def get_user_from_request() -> User:
9361 from superset .extensions import db
9462
9563 # Use configured username for authentication
96- # Deployers: Replace this with your authentication logic
9764 username = current_app .config .get ("MCP_DEV_USERNAME" )
9865
9966 if not username :
@@ -154,97 +121,142 @@ def mcp_auth_hook(tool_func: F) -> F:
154121 """
155122 Authentication and authorization decorator for MCP tools.
156123
157- This hook ensures proper Flask app context and user authentication for all
158- MCP tool calls. Deployers can extend this decorator to add:
159-
160- - Permission checks: Validate user has specific roles or tool access rights
161- - JWT scope validation: Check OAuth scopes match required tool permissions
162- - Audit logging: Track tool usage, parameters, and results for compliance
163- - Rate limiting: Enforce per-user or per-tool rate limits
164- - Custom middleware: Inject organization-specific security policies
165-
166- Default behavior:
167- 1. Ensures Flask app context is active (critical for stateless_http mode)
168- 2. Authenticates user via get_user_from_request()
169- 3. Sets g.user for Superset's security manager integration
170- 4. Manages database session lifecycle
171-
172- Example extensions:
173- # Add permission check before tool execution:
174- if tool_func.__name__ in ['execute_sql'] and not user.is_admin():
175- raise PermissionError("SQL execution requires admin role")
176-
177- # Add audit logging after successful execution:
178- audit_log.info({
179- 'user': user.username,
180- 'tool': tool_func.__name__,
181- 'timestamp': datetime.now(),
182- 'result': 'success'
183- })
124+ This decorator assumes Flask application context and g.user
125+ have already been set by WorkspaceContextMiddleware.
126+
127+ Supports both sync and async tool functions.
128+
129+ TODO (future PR): Add permission checking
130+ TODO (future PR): Add JWT scope validation
131+ TODO (future PR): Add comprehensive audit logging
184132 """
133+ import asyncio
185134 import functools
135+ import inspect
186136
187- @functools .wraps (tool_func )
188- def wrapper (* args : Any , ** kwargs : Any ) -> Any :
189- from superset .extensions import db
190- from superset .mcp_service .flask_singleton import get_flask_app
191-
192- # Get Flask app instance
193- app = get_flask_app ()
194-
195- # Ensure Flask app context is active
196- # This is critical for stateless_http mode where context isn't preserved
197- with app .app_context ():
198- # Get user and set Flask context OUTSIDE try block
199- user = get_user_from_request ()
200-
201- # Force load relationships NOW while session is definitely active
202- # This prevents lazy-loading errors after session closure
203- user_roles = user .roles # noqa: F841
204- if hasattr (user , "groups" ):
205- user_groups = user .groups # noqa: F841
206-
207- g .user = user
208-
209- try :
210- # Deployers: Add custom permission checks here
211- # Example: Check tool-specific permissions, validate JWT scopes,
212- # or enforce organizational policies
213-
214- logger .debug (
215- "MCP tool call: user=%s, tool=%s" , user .username , tool_func .__name__
216- )
217-
218- result = tool_func (* args , ** kwargs )
219-
220- # Deployers: Add audit logging or post-execution hooks here
221- # Example: Log successful tool execution, track metrics,
222- # or trigger downstream workflows
223-
224- return result
225-
226- except Exception :
227- # On error, rollback and cleanup session
228- # pylint: disable=consider-using-transaction
229- try :
230- db .session .rollback ()
231- db .session .remove ()
232- except Exception as cleanup_error :
233- # Log cleanup failure but re-raise original exception
234- # We want to preserve the original error, not the cleanup error
235- logger .warning (
236- "Error cleaning up session after exception: %s" , cleanup_error
237- )
238- raise
239-
240- finally :
241- # Only rollback if session is still active (no exception occurred)
242- # Do NOT call remove() on success to avoid detaching user
243- try :
244- if db .session .is_active :
245- # pylint: disable=consider-using-transaction
246- db .session .rollback ()
247- except Exception as e :
248- logger .warning ("Error in finally block: %s" , e )
137+ # Check if the tool function is async
138+ is_async = inspect .iscoroutinefunction (tool_func )
249139
250- return wrapper # type: ignore[return-value]
140+ if is_async :
141+
142+ @functools .wraps (tool_func )
143+ async def async_wrapper (* args : Any , ** kwargs : Any ) -> Any :
144+ from superset .extensions import db
145+ from superset .mcp_service .flask_singleton import get_flask_app
146+
147+ # Get Flask app instance
148+ app = get_flask_app ()
149+
150+ # Ensure Flask app AND request context are active
151+ # app_context() provides current_app, config, db
152+ # test_request_context() provides g, request
153+ with app .app_context ():
154+ with app .test_request_context ():
155+ # Get user from config
156+ user = get_user_from_request ()
157+
158+ # Validate user has necessary relationships loaded
159+ # (Force access to ensure they're loaded if lazy)
160+ user_roles = user .roles # noqa: F841
161+ if hasattr (user , "groups" ):
162+ user_groups = user .groups # noqa: F841
163+
164+ g .user = user
165+
166+ try :
167+ # TODO: Add permission checks here in future PR
168+ # TODO: Add audit logging here in future PR
169+
170+ logger .debug (
171+ "MCP tool call: user=%s, tool=%s" ,
172+ user .username ,
173+ tool_func .__name__ ,
174+ )
175+
176+ result = await tool_func (* args , ** kwargs )
177+
178+ return result
179+
180+ except Exception :
181+ # On error, rollback and cleanup session
182+ # pylint: disable=consider-using-transaction
183+ try :
184+ db .session .rollback ()
185+ db .session .remove ()
186+ except Exception as e :
187+ logger .warning ("Error cleaning up session after exception: %s" , e )
188+ raise
189+
190+ finally :
191+ # Only rollback if session is still active (no exception occurred)
192+ # Do NOT call remove() on success to avoid detaching user
193+ try :
194+ if db .session .is_active :
195+ # pylint: disable=consider-using-transaction
196+ db .session .rollback ()
197+ except Exception as e :
198+ logger .warning ("Error in finally block: %s" , e )
199+
200+ return async_wrapper # type: ignore[return-value]
201+
202+ else :
203+
204+ @functools .wraps (tool_func )
205+ def sync_wrapper (* args : Any , ** kwargs : Any ) -> Any :
206+ from superset .extensions import db
207+ from superset .mcp_service .flask_singleton import get_flask_app
208+
209+ # Get Flask app instance
210+ app = get_flask_app ()
211+
212+ # Ensure Flask app AND request context are active
213+ # app_context() provides current_app, config, db
214+ # test_request_context() provides g, request
215+ with app .app_context ():
216+ with app .test_request_context ():
217+ # Get user from config
218+ user = get_user_from_request ()
219+
220+ # Validate user has necessary relationships loaded
221+ # (Force access to ensure they're loaded if lazy)
222+ user_roles = user .roles # noqa: F841
223+ if hasattr (user , "groups" ):
224+ user_groups = user .groups # noqa: F841
225+
226+ g .user = user
227+
228+ try :
229+ # TODO: Add permission checks here in future PR
230+ # TODO: Add audit logging here in future PR
231+
232+ logger .debug (
233+ "MCP tool call: user=%s, tool=%s" ,
234+ user .username ,
235+ tool_func .__name__ ,
236+ )
237+
238+ result = tool_func (* args , ** kwargs )
239+
240+ return result
241+
242+ except Exception :
243+ # On error, rollback and cleanup session
244+ # pylint: disable=consider-using-transaction
245+ try :
246+ db .session .rollback ()
247+ db .session .remove ()
248+ except Exception as e :
249+ logger .warning ("Error cleaning up session after exception: %s" , e )
250+ raise
251+
252+ finally :
253+ # Only rollback if session is still active (no exception occurred)
254+ # Do NOT call remove() on success to avoid detaching user
255+ try :
256+ if db .session .is_active :
257+ # pylint: disable=consider-using-transaction
258+ db .session .rollback ()
259+ except Exception as e :
260+ logger .warning ("Error in finally block: %s" , e )
261+
262+ return sync_wrapper # type: ignore[return-value]
0 commit comments