mirror of
https://github.com/apache/superset.git
synced 2026-04-20 08:34:37 +00:00
feat(mcp): MCP service implementation (PRs 3-9 consolidated) (#35877)
This commit is contained in:
313
superset/mcp_service/utils/permissions_utils.py
Normal file
313
superset/mcp_service/utils/permissions_utils.py
Normal file
@@ -0,0 +1,313 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Field-level permissions utilities for MCP service.
|
||||
Provides functionality to filter sensitive data based on user permissions.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, List, Optional, Set
|
||||
|
||||
from flask_appbuilder.security.sqla.models import User
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Define sensitive fields by object type
|
||||
SENSITIVE_FIELDS = {
|
||||
"dataset": {
|
||||
"sql", # Raw SQL queries may contain sensitive logic
|
||||
"extra", # May contain connection strings or credentials
|
||||
"database_id", # Internal database references
|
||||
"changed_by_fk", # Internal user references
|
||||
"created_by_fk", # Internal user references
|
||||
},
|
||||
"chart": {
|
||||
"query_context", # May contain sensitive filters or parameters
|
||||
"cache_key", # Internal cache references
|
||||
"changed_by_fk", # Internal user references
|
||||
"created_by_fk", # Internal user references
|
||||
},
|
||||
"dashboard": {
|
||||
"json_metadata", # May contain sensitive configuration
|
||||
"position_json", # Internal layout data
|
||||
"css", # May contain sensitive styling info
|
||||
"changed_by_fk", # Internal user references
|
||||
"created_by_fk", # Internal user references
|
||||
},
|
||||
"common": {
|
||||
"uuid", # Internal identifiers (keep for some use cases)
|
||||
"changed_by_fk", # Internal user references
|
||||
"created_by_fk", # Internal user references
|
||||
},
|
||||
}
|
||||
|
||||
# Permissions required to access sensitive fields
|
||||
SENSITIVE_FIELD_PERMISSIONS = {
|
||||
"sql": "can_sql_json", # SQL Lab permissions
|
||||
"extra": "can_this_form_get", # Advanced form permissions
|
||||
"database_id": "can_this_form_get", # Database access permissions
|
||||
"query_context": "can_explore_json", # Explore permissions
|
||||
"cache_key": "can_warm_up_cache", # Cache management permissions
|
||||
"json_metadata": "can_this_form_get", # Advanced dashboard permissions
|
||||
"position_json": "can_this_form_get", # Dashboard edit permissions
|
||||
"css": "can_this_form_get", # Dashboard styling permissions
|
||||
}
|
||||
|
||||
|
||||
def get_current_user() -> Optional[User]:
|
||||
"""Get the current authenticated user."""
|
||||
try:
|
||||
from flask import g
|
||||
|
||||
return getattr(g, "user", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def user_has_permission(
|
||||
user: Optional[User], permission: str, resource: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if user has a specific permission.
|
||||
|
||||
Args:
|
||||
user: User object or None
|
||||
permission: Permission name (e.g., 'can_sql_json')
|
||||
resource: Resource name (e.g., 'Superset', 'Chart', etc.)
|
||||
|
||||
Returns:
|
||||
True if user has permission, False otherwise
|
||||
"""
|
||||
if not user:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Check if user is admin (has all permissions)
|
||||
if hasattr(user, "roles"):
|
||||
for role in user.roles:
|
||||
if role.name in ("Admin", "admin"):
|
||||
return True
|
||||
|
||||
# Check specific permission
|
||||
from superset import security_manager
|
||||
|
||||
if resource:
|
||||
return security_manager.has_access(permission, resource, user)
|
||||
else:
|
||||
# Check if user has permission on any resource
|
||||
for pvm in user.get_permissions():
|
||||
if pvm.permission.name == permission:
|
||||
return True
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Error checking permission %s for user %s: %s", permission, user, e
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def get_allowed_fields(
|
||||
object_type: str,
|
||||
user: Optional[User] = None,
|
||||
requested_fields: Optional[List[str]] = None,
|
||||
) -> Set[str]:
|
||||
"""
|
||||
Get the set of fields that the user is allowed to access for a given object type.
|
||||
|
||||
Args:
|
||||
object_type: Type of object ('dataset', 'chart', 'dashboard')
|
||||
user: User object (if None, will try to get current user)
|
||||
requested_fields: List of fields requested (if None, all allowed fields)
|
||||
|
||||
Returns:
|
||||
Set of allowed field names
|
||||
"""
|
||||
if not user:
|
||||
user = get_current_user()
|
||||
|
||||
# Get sensitive fields for this object type
|
||||
sensitive_fields = SENSITIVE_FIELDS.get(object_type, set())
|
||||
sensitive_fields.update(SENSITIVE_FIELDS.get("common", set()))
|
||||
|
||||
# If no user, only allow non-sensitive fields
|
||||
if not user:
|
||||
if requested_fields:
|
||||
return set(requested_fields) - sensitive_fields
|
||||
else:
|
||||
# Return empty set - caller should use default safe fields
|
||||
return set()
|
||||
|
||||
# Check permissions for sensitive fields
|
||||
allowed_fields = set()
|
||||
|
||||
if requested_fields:
|
||||
for field in requested_fields:
|
||||
if field not in sensitive_fields:
|
||||
# Non-sensitive field, always allowed
|
||||
allowed_fields.add(field)
|
||||
else:
|
||||
# Check if user has permission for this sensitive field
|
||||
required_permission = SENSITIVE_FIELD_PERMISSIONS.get(field)
|
||||
if required_permission and user_has_permission(
|
||||
user, required_permission
|
||||
):
|
||||
allowed_fields.add(field)
|
||||
elif not required_permission:
|
||||
# No specific permission required, but still sensitive
|
||||
# Allow for authenticated users (basic sensitivity)
|
||||
allowed_fields.add(field)
|
||||
else:
|
||||
# No specific fields requested, return empty set
|
||||
# Caller should specify default fields
|
||||
return set()
|
||||
|
||||
return allowed_fields
|
||||
|
||||
|
||||
def filter_sensitive_data(
|
||||
data: Any,
|
||||
object_type: str,
|
||||
user: Optional[User] = None,
|
||||
allowed_fields: Optional[Set[str]] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Filter sensitive data from an object based on user permissions.
|
||||
|
||||
Args:
|
||||
data: Data to filter (dict, Pydantic model, or list)
|
||||
object_type: Type of object ('dataset', 'chart', 'dashboard')
|
||||
user: User object (if None, will try to get current user)
|
||||
allowed_fields: Pre-computed allowed fields (optimization)
|
||||
|
||||
Returns:
|
||||
Filtered data with sensitive fields removed
|
||||
"""
|
||||
if not data:
|
||||
return data
|
||||
|
||||
if not user:
|
||||
user = get_current_user()
|
||||
|
||||
# Handle different data types
|
||||
if isinstance(data, list):
|
||||
return [
|
||||
filter_sensitive_data(item, object_type, user, allowed_fields)
|
||||
for item in data
|
||||
]
|
||||
|
||||
if isinstance(data, BaseModel):
|
||||
# Convert Pydantic model to dict for filtering
|
||||
data_dict = data.model_dump()
|
||||
filtered_dict = filter_sensitive_data(
|
||||
data_dict, object_type, user, allowed_fields
|
||||
)
|
||||
# Return as dict since we can't easily reconstruct the Pydantic model
|
||||
return filtered_dict
|
||||
|
||||
if not isinstance(data, dict):
|
||||
# Not a dict-like object, return as-is
|
||||
return data
|
||||
|
||||
# Get allowed fields if not provided
|
||||
if allowed_fields is None:
|
||||
requested_fields = list(data.keys())
|
||||
allowed_fields = get_allowed_fields(object_type, user, requested_fields)
|
||||
|
||||
# Filter the dictionary
|
||||
filtered_data = {}
|
||||
for key, value in data.items():
|
||||
if key in allowed_fields:
|
||||
filtered_data[key] = value
|
||||
else:
|
||||
# Log when we filter out sensitive data
|
||||
logger.debug(
|
||||
"Filtered sensitive field '%s' for object type '%s'", key, object_type
|
||||
)
|
||||
|
||||
return filtered_data
|
||||
|
||||
|
||||
def apply_field_permissions_to_columns(
|
||||
columns: List[str], object_type: str, user: Optional[User] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
Filter a list of column names based on field-level permissions.
|
||||
|
||||
Args:
|
||||
columns: List of column names to filter
|
||||
object_type: Type of object ('dataset', 'chart', 'dashboard')
|
||||
user: User object (if None, will try to get current user)
|
||||
|
||||
Returns:
|
||||
Filtered list of allowed column names
|
||||
"""
|
||||
allowed_fields = get_allowed_fields(object_type, user, columns)
|
||||
return [col for col in columns if col in allowed_fields]
|
||||
|
||||
|
||||
class PermissionAwareSerializer:
|
||||
"""
|
||||
Wrapper for serializers that automatically applies field-level permissions.
|
||||
"""
|
||||
|
||||
def __init__(self, object_type: str, base_serializer: Any):
|
||||
self.object_type = object_type
|
||||
self.base_serializer = base_serializer
|
||||
|
||||
def serialize(
|
||||
self, obj: Any, columns: List[str], user: Optional[User] = None
|
||||
) -> Any:
|
||||
"""
|
||||
Serialize object with field-level permissions applied.
|
||||
|
||||
Args:
|
||||
obj: Object to serialize
|
||||
columns: Requested columns
|
||||
user: User object for permission checking
|
||||
|
||||
Returns:
|
||||
Serialized object with sensitive fields filtered
|
||||
"""
|
||||
# Filter columns based on permissions
|
||||
allowed_columns = apply_field_permissions_to_columns(
|
||||
columns, self.object_type, user
|
||||
)
|
||||
|
||||
# Use base serializer with filtered columns
|
||||
serialized = self.base_serializer(obj, allowed_columns)
|
||||
|
||||
# Apply additional filtering to the serialized result
|
||||
return filter_sensitive_data(serialized, self.object_type, user)
|
||||
|
||||
|
||||
# Convenience functions for common object types
|
||||
def filter_dataset_data(data: Any, user: Optional[User] = None) -> Any:
|
||||
"""Filter sensitive data from dataset objects."""
|
||||
return filter_sensitive_data(data, "dataset", user)
|
||||
|
||||
|
||||
def filter_chart_data(data: Any, user: Optional[User] = None) -> Any:
|
||||
"""Filter sensitive data from chart objects."""
|
||||
return filter_sensitive_data(data, "chart", user)
|
||||
|
||||
|
||||
def filter_dashboard_data(data: Any, user: Optional[User] = None) -> Any:
|
||||
"""Filter sensitive data from dashboard objects."""
|
||||
return filter_sensitive_data(data, "dashboard", user)
|
||||
Reference in New Issue
Block a user