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
@@ -0,0 +1,256 @@
|
||||
"""MongoDB 管理 API endpoints.
|
||||
|
||||
提供数据库/集合列表、集合统计、只读查询等功能。
|
||||
所有端点要求进行身份认证,部分端点限制为 admin/dba 角色。
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
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, require_role
|
||||
from app.core.permissions import check_instance_access
|
||||
from app.models.instance import Instance
|
||||
from app.models.user import User
|
||||
from app.services.mongo_connector import MongoConnector, validate_operation
|
||||
|
||||
router = APIRouter(tags=["MongoDB管理"])
|
||||
|
||||
|
||||
# ── 辅助函数 ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def _get_mongo_instance(instance_id: int, db: AsyncSession) -> Instance:
|
||||
"""获取并验证 MongoDB 实例。
|
||||
|
||||
Args:
|
||||
instance_id: 实例 ID。
|
||||
db: 数据库会话。
|
||||
|
||||
Returns:
|
||||
Instance 模型对象(db_type 为 "mongo" 或 "mongodb")。
|
||||
|
||||
Raises:
|
||||
HTTPException: 实例不存在或不是 MongoDB 类型时。
|
||||
"""
|
||||
result = await db.execute(select(Instance).where(Instance.id == instance_id))
|
||||
instance = result.scalar_one_or_none()
|
||||
if not instance:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
if instance.db_type not in ("mongo", "mongodb"):
|
||||
raise HTTPException(status_code=400, detail="该实例不是 MongoDB 类型")
|
||||
return instance
|
||||
|
||||
|
||||
# ── 请求/响应模型 ────────────────────────────────────────────────
|
||||
|
||||
|
||||
class MongoQueryRequest(BaseModel):
|
||||
"""MongoDB 只读查询请求体。"""
|
||||
|
||||
database: Optional[str] = Field(None, description="目标数据库名称,为空时使用实例默认库")
|
||||
collection: str = Field(..., description="目标集合名称")
|
||||
operation: str = Field("find", description="操作类型: find/findOne/countDocuments/distinct/aggregate")
|
||||
filter: Dict[str, Any] = Field(default_factory=dict, description="查询过滤器")
|
||||
projection: Optional[Dict[str, Any]] = Field(None, description="字段投影")
|
||||
sort: Optional[Dict[str, int]] = Field(None, description="排序规则,如 {\"age\": -1}")
|
||||
limit: int = Field(1000, ge=1, le=10000, description="返回上限")
|
||||
skip: int = Field(0, ge=0, description="跳过数量")
|
||||
pipeline: Optional[List[Dict[str, Any]]] = Field(None, description="aggregate 管道")
|
||||
field: Optional[str] = Field(None, description="distinct 字段名")
|
||||
|
||||
|
||||
class MongoChangeRequest(BaseModel):
|
||||
"""MongoDB 变更操作请求体(工单执行用)。"""
|
||||
|
||||
database: Optional[str] = Field(None, description="目标数据库名称")
|
||||
collection: str = Field(..., description="目标集合名称")
|
||||
operation: str = Field(..., description="操作类型: insertOne/insertMany/updateOne/updateMany/deleteOne/deleteMany")
|
||||
document: Optional[Dict[str, Any]] = Field(None, description="insertOne 文档")
|
||||
documents: Optional[List[Dict[str, Any]]] = Field(None, description="insertMany 文档列表")
|
||||
update: Optional[Dict[str, Any]] = Field(None, description="update 操作文档")
|
||||
replacement: Optional[Dict[str, Any]] = Field(None, description="replaceOne 替换文档")
|
||||
filter: Dict[str, Any] = Field(default_factory=dict, description="查询过滤器")
|
||||
|
||||
|
||||
# ── Endpoints ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/mongo/{instance_id}/databases")
|
||||
async def list_mongo_databases(
|
||||
instance_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""列出 MongoDB 实例中的所有数据库。"""
|
||||
instance = await _get_mongo_instance(instance_id, db)
|
||||
# P0: 权限检查
|
||||
await check_instance_access(current_user, instance, db)
|
||||
try:
|
||||
databases = await MongoConnector.list_databases(instance)
|
||||
return {"databases": databases}
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"获取数据库列表失败: {exc}")
|
||||
|
||||
|
||||
@router.get("/mongo/{instance_id}/databases/{database}/collections")
|
||||
async def list_mongo_collections(
|
||||
instance_id: int,
|
||||
database: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""列出指定数据库中的所有集合。"""
|
||||
instance = await _get_mongo_instance(instance_id, db)
|
||||
# P0: 权限检查
|
||||
await check_instance_access(current_user, instance, db)
|
||||
try:
|
||||
collections = await MongoConnector.list_collections(instance, database)
|
||||
return {"database": database, "collections": collections}
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"获取集合列表失败: {exc}")
|
||||
|
||||
|
||||
@router.get("/mongo/{instance_id}/collections/{collection}/stats")
|
||||
async def get_mongo_collection_stats(
|
||||
instance_id: int,
|
||||
collection: str,
|
||||
database: Optional[str] = Query(None, description="数据库名称"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取指定集合的统计信息。"""
|
||||
instance = await _get_mongo_instance(instance_id, db)
|
||||
# P0: 权限检查
|
||||
await check_instance_access(current_user, instance, db)
|
||||
db_name = database or getattr(instance, "database", None)
|
||||
if not db_name:
|
||||
raise HTTPException(status_code=400, detail="未指定数据库名称")
|
||||
try:
|
||||
stats = await MongoConnector.get_collection_stats(instance, db_name, collection)
|
||||
return stats
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"获取集合统计失败: {exc}")
|
||||
|
||||
|
||||
@router.post("/mongo/{instance_id}/query")
|
||||
async def execute_mongo_query(
|
||||
instance_id: int,
|
||||
request: MongoQueryRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""执行 MongoDB 只读查询。
|
||||
|
||||
支持的只读操作:
|
||||
- find: 需 filter, projection, sort, limit, skip
|
||||
- findOne: 需 filter, projection
|
||||
- countDocuments: 需 filter
|
||||
- estimatedDocumentCount: 无需额外参数
|
||||
- distinct: 需 field, filter
|
||||
- aggregate: 需 pipeline
|
||||
|
||||
请求示例:
|
||||
```json
|
||||
{
|
||||
"database": "test",
|
||||
"collection": "users",
|
||||
"operation": "find",
|
||||
"filter": {"age": {"$gt": 18}},
|
||||
"projection": {"name": 1, "age": 1},
|
||||
"sort": {"age": -1},
|
||||
"limit": 100
|
||||
}
|
||||
```
|
||||
"""
|
||||
instance = await _get_mongo_instance(instance_id, db)
|
||||
# P0: 权限检查
|
||||
await check_instance_access(current_user, instance, db)
|
||||
db_name = request.database or getattr(instance, "database", None)
|
||||
if not db_name:
|
||||
raise HTTPException(status_code=400, detail="未指定数据库名称")
|
||||
|
||||
# 验证只读操作
|
||||
is_readonly, error_msg = validate_operation(request.operation)
|
||||
if not is_readonly:
|
||||
raise HTTPException(status_code=403, detail=error_msg)
|
||||
|
||||
try:
|
||||
query_json = request.model_dump(exclude_none=True, exclude={"database"})
|
||||
# 移除未设置的 pipeline/field 避免错误分发
|
||||
if request.operation != "aggregate":
|
||||
query_json.pop("pipeline", None)
|
||||
if request.operation != "distinct":
|
||||
query_json.pop("field", None)
|
||||
|
||||
result = await MongoConnector.execute_query_json(
|
||||
instance, db_name, query_json
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"operation": request.operation,
|
||||
"collection": request.collection,
|
||||
"result": result,
|
||||
}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"查询执行失败: {exc}")
|
||||
|
||||
|
||||
@router.post("/mongo/{instance_id}/execute")
|
||||
async def execute_mongo_change(
|
||||
instance_id: int,
|
||||
request: MongoChangeRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin", "dba")),
|
||||
):
|
||||
"""执行 MongoDB 变更操作(仅 admin/dba)。
|
||||
|
||||
用于工单变更执行,支持 insert/update/delete 等写操作。
|
||||
"""
|
||||
instance = await _get_mongo_instance(instance_id, db)
|
||||
# P0: 权限检查
|
||||
await check_instance_access(current_user, instance, db)
|
||||
db_name = request.database or getattr(instance, "database", None)
|
||||
if not db_name:
|
||||
raise HTTPException(status_code=400, detail="未指定数据库名称")
|
||||
|
||||
try:
|
||||
change_json = request.model_dump(exclude_none=True, exclude={"database"})
|
||||
result = await MongoConnector.execute_change_json(
|
||||
instance, db_name, change_json
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"operation": request.operation,
|
||||
"collection": request.collection,
|
||||
"result": result,
|
||||
}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"变更执行失败: {exc}")
|
||||
|
||||
|
||||
@router.post("/mongo/{instance_id}/test-connection")
|
||||
async def test_mongo_connection(
|
||||
instance_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""测试 MongoDB 实例连接。"""
|
||||
instance = await _get_mongo_instance(instance_id, db)
|
||||
# P0: 权限检查
|
||||
await check_instance_access(current_user, instance, db)
|
||||
try:
|
||||
result = await MongoConnector.test_connection(instance)
|
||||
return result.to_dict()
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"连接测试失败: {exc}")
|
||||
Reference in new issue
Block a user