"""统一查询 API — 支持 MySQL、PostgreSQL、Redis、MongoDB 的只读查询。 核心设计: - 一个入口查询所有数据库,减少学习成本 - 自动审计记录所有查询(谁在什么时间查了什么) - 自动记录查询历史(方便复用) - 数据脱敏保护敏感信息 - 完整错误处理和人性化提示 """ import time from fastapi import APIRouter, Depends, HTTPException, Query as QueryParam from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.core.security import get_current_user from app.models.user import User from app.models.instance import Instance from app.models.audit_log import AuditLog from app.schemas.query import QueryRequest, UnifiedQueryResult from app.services.query_service import QueryService from app.services.data_masker import DataMasker, load_masking_rules from app.services.audit_log_service import AuditLogService from app.services.query_history_service import QueryHistoryService from app.core.crypto import decrypt_password from app.core.permissions import check_instance_access, check_instance_permission router = APIRouter(prefix="/query", tags=["统一查询"]) @router.post("", response_model=UnifiedQueryResult) async def execute_query( req: QueryRequest, mask: bool = QueryParam(True, description="是否启用数据脱敏"), save_history: bool = QueryParam(True, description="是否保存到查询历史"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """执行统一查询(只读)— 支持 MySQL、PostgreSQL、Redis、MongoDB。 **一个入口,查询所有数据库。** 系统会根据数据源类型自动路由到对应的查询引擎, 并进行只读安全验证。 **自动审计**: 每次查询都会被记录在案,包含查询内容、耗时、返回行数等。 **查询历史**: 自动保存查询记录,方便后续复用。 **不同数据库的查询格式:** - **MySQL/PostgreSQL**: 标准 SQL SELECT 语句,如 `SELECT * FROM users LIMIT 10` - **Redis**: 命令字符串,如 `GET mykey`、`HGETALL myhash`、`LRANGE mylist 0 -1` - **MongoDB**: JSON 格式查询,如: ```json {"collection": "users", "operation": "find", "filter": {"age": {"$gt": 18}}, "limit": 10} ``` Args: req: 查询请求(instance_id + database + sql) mask: 是否启用数据脱敏(默认 True) save_history: 是否保存到查询历史(默认 True) Returns: UnifiedQueryResult: 统一格式的查询结果 """ if current_user.role == "guest": raise HTTPException(status_code=403, detail="访客无权执行查询,请联系管理员申请权限") # ── 系统设置集成 ── from app.api.v1.endpoints.settings import get_setting_value require_approval = await get_setting_value(db, "query_require_approval", False) max_limit = await get_setting_value(db, "query_max_limit", 10000) query_timeout = await get_setting_value(db, "query_timeout", 30) max_length = await get_setting_value(db, "query_max_length", 10000) auto_limit = await get_setting_value(db, "query_auto_limit", True) enable_masking = await get_setting_value(db, "query_enable_masking", True) save_hist = await get_setting_value(db, "query_save_history", True) # 查询审批检查 if require_approval and current_user.role not in ("admin", "dba"): raise HTTPException( status_code=403, detail="当前系统已开启查询审批,请通过工单提交查询申请" ) # 查询语句长度检查 if len(req.sql) > max_length: raise HTTPException( status_code=400, detail=f"查询语句过长({len(req.sql)} 字符),上限 {max_length} 字符" ) # 查询实例信息 result = await db.execute(select(Instance).where(Instance.id == req.instance_id)) instance = result.scalar_one_or_none() if not instance: raise HTTPException(status_code=404, detail="数据源不存在,请确认实例ID正确") # 权限组检查:用户是否有权访问该数据源 await check_instance_access(current_user, instance, db) # P1: 操作级权限检查 await check_instance_permission(current_user, instance, "query:select", db) # 解密密码 instance.password = decrypt_password(instance.password) # 记录开始时间 start_time = time.time() is_success = True error_msg = None try: query_result = await QueryService.execute_query( instance, req.database, req.sql, current_user.id, req.options ) except ValueError as e: is_success = False error_msg = str(e) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: is_success = False error_msg = f"{type(e).__name__}: {e}" raise HTTPException(status_code=500, detail=f"查询执行失败: {e}") finally: # 无论成功失败都记录审计日志 elapsed_ms = int((time.time() - start_time) * 1000) # 1. 记录审计日志(完整上下文) try: username = current_user.display_name or current_user.username await AuditLogService.log( db=db, user_id=current_user.id, action="QUERY", target_type="query", target_id=instance.id, detail=f"查询{'成功' if is_success else '失败'}: {req.sql[:200]}", ) # 直接写入增强字段(AuditLogService.log只写基础字段) log_entry = AuditLog( user_id=current_user.id, username=username, action="QUERY", target_type="query", target_id=instance.id, instance_name=instance.name, db_type=instance.db_type, database_name=req.database or instance.database, query_content=req.sql[:2000], query_type=getattr(query_result, 'query_type', 'UNKNOWN') if is_success else None, execution_time_ms=elapsed_ms, rows_affected=getattr(query_result, 'total', 0) if is_success else 0, result_status="success" if is_success else "failed", detail=f"查询{'成功' if is_success else '失败'}: {req.sql[:200]}{f', 错误: {error_msg}' if error_msg else ''}", ) db.add(log_entry) await db.commit() except Exception: pass # 审计记录失败不影响查询结果 # 数据脱敏处理 if is_success and mask and query_result.rows and instance.db_type in ("mysql", "postgres", "postgresql"): try: if req.database: masking_rules = await load_masking_rules(db, req.instance_id, req.database) if masking_rules: masker = DataMasker(masking_rules) from app.services.sql_analyzer import SQLAnalyzer analyzer = SQLAnalyzer() analysis = analyzer.analyze(req.sql) tables = analysis.tables if analysis.tables else ["*"] table_name = tables[0] if tables else "*" if masker.has_rules_for_table(table_name): query_result.rows = masker.mask_rows(table_name, query_result.rows) except Exception: pass # 脱敏失败不影响查询结果 # 记录查询历史(人性化:方便用户复用) if is_success and save_history: try: username = current_user.display_name or current_user.username await QueryHistoryService.record_query( user_id=current_user.id, username=username, instance_id=instance.id, instance_name=instance.name, db_type=instance.db_type, database_name=req.database or instance.database, query_content=req.sql, query_type=getattr(query_result, 'query_type', 'QUERY'), execution_time_ms=elapsed_ms, result_rows=getattr(query_result, 'total', 0), is_success=is_success, error_message=error_msg, ) except Exception: pass # 历史记录失败不影响查询结果 return query_result @router.get("/supported-types") async def supported_db_types( current_user: User = Depends(get_current_user), ): """获取支持的数据库类型和查询格式说明。 人性化设计:帮助用户了解如何查询不同类型的数据库。 """ return { "types": [ { "type": "mysql", "name": "MySQL", "query_format": "标准 SQL SELECT 语句", "example": "SELECT id, name FROM users WHERE status = 'active' LIMIT 10", "security": "AST语法分析验证只读,禁止DDL/DML", "features": ["语法高亮", "EXPLAIN分析", "查询建议", "数据脱敏"], }, { "type": "postgres", "name": "PostgreSQL", "query_format": "标准 SQL SELECT 语句", "example": "SELECT id, name FROM users WHERE status = 'active' LIMIT 10", "security": "AST语法分析验证只读,禁止DDL/DML", "features": ["语法高亮", "EXPLAIN分析", "查询建议", "数据脱敏"], }, { "type": "redis", "name": "Redis", "query_format": "Redis 命令字符串", "example": "HGETALL user:1001", "security": "130+只读命令白名单验证", "features": ["命令自动补全", "结果格式化", "键值浏览"], "readonly_commands": [ "GET", "MGET", "HGET", "HGETALL", "LRANGE", "SMEMBERS", "ZRANGE", "SCAN", "KEYS", "INFO", "TTL", "TYPE", "EXISTS", ], }, { "type": "mongo", "name": "MongoDB", "query_format": "JSON 格式查询", "example": '{"collection": "users", "operation": "find", "filter": {}, "limit": 10}', "security": "6个只读操作白名单:find/findOne/countDocuments/distinct/aggregate", "features": ["JSON格式化", "结果表格化", "集合浏览"], "readonly_operations": ["find", "findOne", "countDocuments", "estimatedDocumentCount", "distinct", "aggregate"], }, ] }