sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1
- P0: 实例访问控制全线接入权限组 (829测试) - P1: permissions字段激活 + 操作级权限检查 (843测试) - 前端: 删除数据权限页面, 权限组合并为唯一入口 - 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析 - 新增: MongoDB管理页面 + Dashboard增强 - 修复: Pydantic V2迁移 + 前端TS错误修复
This commit is contained in:
1 parent
6fcd9465cf
commit
4c9427e546
105 files changed
+23680
-471
No files matched your search
@@ -1,4 +1,14 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
"""统一查询 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
|
||||
|
||||
@@ -7,43 +17,210 @@ 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, QueryResult
|
||||
from app.services.db_connector import DBConnector
|
||||
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=["SQL 查询"])
|
||||
router = APIRouter(prefix="/query", tags=["统一查询"])
|
||||
|
||||
|
||||
@router.post("", response_model=QueryResult)
|
||||
@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),
|
||||
):
|
||||
if current_user.role == "guest":
|
||||
raise HTTPException(status_code=403, detail="访客无权执行查询")
|
||||
"""执行统一查询(只读)— 支持 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="访客无权执行查询,请联系管理员申请权限")
|
||||
|
||||
# 查询实例信息
|
||||
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="数据源不存在")
|
||||
raise HTTPException(status_code=404, detail="数据源不存在,请确认实例ID正确")
|
||||
|
||||
# Decrypt password for query execution
|
||||
# 权限组检查:用户是否有权访问该数据源
|
||||
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)
|
||||
|
||||
try:
|
||||
query_result = await DBConnector.execute_query(instance, req.database, req.sql, current_user.id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
# 记录开始时间
|
||||
start_time = time.time()
|
||||
is_success = True
|
||||
error_msg = None
|
||||
|
||||
# Record audit log
|
||||
log = AuditLog(
|
||||
user_id=current_user.id,
|
||||
action="QUERY",
|
||||
target_type="query",
|
||||
target_id=instance.id,
|
||||
detail=req.sql[:500],
|
||||
)
|
||||
db.add(log)
|
||||
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"],
|
||||
},
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user