feat: 平台治理与权限体系完善,存储进度/GPU预留/审批中心与日志整合
- 平台治理: 租户用户权限层次、资源ACL、审批中心与审批模板、访问申请 - 存储: MinIO 存储进度迁移、对象存储安全加固与测试 - 计算: GPU 资源预留、compute 轮询与同步增强 - 权限: permission v2 迁移、权限安全验收测试 - 日志: 后端运行日志中文说明、操作日志整合 - 数据处理/评测: 数据转换与模型评测优化 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,33 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Body, Query, Request, Depends
|
||||
import csv
|
||||
import io
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException, Query, Request, Depends
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from app.db.platform_store import ALL_PERMISSIONS, get_platform_store
|
||||
from app.core.auth import get_current_user, is_admin
|
||||
from app.core.logging import get_client_ip
|
||||
|
||||
|
||||
router = APIRouter(prefix="/system", tags=["system"])
|
||||
|
||||
_VISIT_MODULES = {
|
||||
"dashboard",
|
||||
"fine-tune",
|
||||
"model-eval",
|
||||
"model-inference",
|
||||
"model-manage",
|
||||
"dataset",
|
||||
"data-process",
|
||||
"data-convert",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/audit/visit")
|
||||
def record_visit(payload: dict = Body(...), request: Request = None) -> dict:
|
||||
def record_visit(
|
||||
payload: dict = Body(...),
|
||||
request: Request = None,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""记录用户访问业务模块的行为,用于看板用户操作分布统计。"""
|
||||
action = str(payload.get("action") or payload.get("module") or "").strip()
|
||||
if not action:
|
||||
return {"code": 0, "message": "ok", "data": {"recorded": False}}
|
||||
actor_id = ""
|
||||
if request is not None:
|
||||
auth = request.headers.get("Authorization", "")
|
||||
token = auth.replace("Bearer ", "").strip()
|
||||
if token.startswith("platform-token-"):
|
||||
actor_id = token[len("platform-token-"):]
|
||||
# Visit statistics are intentionally limited to known module names. This
|
||||
# endpoint must not become a free-form audit-log injection point.
|
||||
if action not in _VISIT_MODULES:
|
||||
return {"code": 0, "message": "ok", "data": {"recorded": False}}
|
||||
actor_id = current_user.get("id")
|
||||
get_platform_store().record_audit(
|
||||
action=action,
|
||||
actor_id=actor_id or None,
|
||||
target_type="module",
|
||||
target_id=action,
|
||||
detail=str(payload.get("detail") or ""),
|
||||
tenant_id=str(current_user.get("tenant_id") or "") or None,
|
||||
session_id=str(current_user.get("session_id") or "") or None,
|
||||
request_id=(request.headers.get("X-Request-ID") if request else None),
|
||||
detail="module visit",
|
||||
metadata={"source": "frontend", "detail_length": len(str(payload.get("detail") or ""))},
|
||||
ip=get_client_ip(request) or None,
|
||||
)
|
||||
return {"code": 0, "message": "ok", "data": {"recorded": True}}
|
||||
|
||||
@@ -117,13 +140,22 @@ def audit_logs_export(
|
||||
offset=0,
|
||||
)
|
||||
items = result["items"]
|
||||
columns = ["time", "tenant_id", "project_id", "actor_id", "action", "target_type", "target_id", "detail", "client_ip"]
|
||||
header = ",".join(columns) + "\n"
|
||||
columns = [
|
||||
"time", "tenant_id", "project_id", "actor_id", "action", "target_type",
|
||||
"target_id", "detail", "client_ip", "result", "reason", "request_id",
|
||||
"session_id", "metadata",
|
||||
]
|
||||
|
||||
def iter_rows():
|
||||
yield header
|
||||
buffer = io.StringIO()
|
||||
writer = csv.writer(buffer)
|
||||
writer.writerow(columns)
|
||||
yield buffer.getvalue()
|
||||
for row in items:
|
||||
yield ",".join(f'"{str(row.get(c, "") or "")}"' for c in columns) + "\n"
|
||||
buffer.seek(0)
|
||||
buffer.truncate(0)
|
||||
writer.writerow([row.get(c, "") or "" for c in columns])
|
||||
yield buffer.getvalue()
|
||||
|
||||
return StreamingResponse(
|
||||
iter_rows(),
|
||||
@@ -134,6 +166,16 @@ def audit_logs_export(
|
||||
|
||||
# ===================== 操作日志 =====================
|
||||
|
||||
def _operation_log_scope(current_user: dict, conditions: list[str], params: list) -> None:
|
||||
"""校验操作日志权限,并为普通用户追加本人范围。"""
|
||||
if is_admin(current_user):
|
||||
return
|
||||
if "logs" not in (current_user.get("permissions") or []):
|
||||
raise HTTPException(status_code=403, detail="missing permission: logs")
|
||||
conditions.append("user_id = %s")
|
||||
params.append(str(current_user.get("id") or ""))
|
||||
|
||||
|
||||
@router.get("/operation-logs")
|
||||
def operation_logs(
|
||||
user_id: str | None = Query(default=None, description="按用户 ID 筛选"),
|
||||
@@ -147,14 +189,12 @@ def operation_logs(
|
||||
offset: int = Query(default=0, ge=0),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""操作日志查询:按用户/模块/动作/状态/关键字/时间范围分页过滤。"""
|
||||
if not is_admin(current_user):
|
||||
from app.api.v1.endpoints.platform import fail
|
||||
raise fail(403, "admin permission required")
|
||||
"""操作日志查询:管理员查全量,普通用户只能查本人记录。"""
|
||||
store = get_platform_store()
|
||||
conditions = []
|
||||
params: list = []
|
||||
if user_id:
|
||||
_operation_log_scope(current_user, conditions, params)
|
||||
if user_id and is_admin(current_user):
|
||||
conditions.append("user_id = %s")
|
||||
params.append(user_id)
|
||||
if module:
|
||||
@@ -192,13 +232,11 @@ def operation_logs_stats(
|
||||
end_time: str | None = Query(default=None, description="ISO8601 结束时间"),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""操作日志统计:总操作数、成功数、失败数、失败率、各模块失败分布、最近错误列表。"""
|
||||
if not is_admin(current_user):
|
||||
from app.api.v1.endpoints.platform import fail
|
||||
raise fail(403, "admin permission required")
|
||||
"""操作日志统计:管理员统计全量,普通用户统计本人记录。"""
|
||||
store = get_platform_store()
|
||||
conditions = []
|
||||
params: list = []
|
||||
_operation_log_scope(current_user, conditions, params)
|
||||
if start_time:
|
||||
conditions.append("create_time >= %s")
|
||||
params.append(start_time)
|
||||
@@ -260,13 +298,17 @@ def operation_logs_stats(
|
||||
@router.get("/operation-logs/modules")
|
||||
def operation_log_modules(current_user: dict = Depends(get_current_user)) -> dict:
|
||||
"""返回操作日志中出现的模块列表(用于筛选下拉框)。"""
|
||||
if not is_admin(current_user):
|
||||
from app.api.v1.endpoints.platform import fail
|
||||
raise fail(403, "admin permission required")
|
||||
store = get_platform_store()
|
||||
conditions: list[str] = []
|
||||
params: list = []
|
||||
_operation_log_scope(current_user, conditions, params)
|
||||
where = " WHERE " + " AND ".join(conditions) if conditions else ""
|
||||
with store.connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT DISTINCT module FROM operation_logs WHERE module IS NOT NULL ORDER BY module"
|
||||
f"SELECT DISTINCT module FROM operation_logs{where}"
|
||||
+ (" AND" if where else " WHERE")
|
||||
+ " module IS NOT NULL ORDER BY module",
|
||||
tuple(params),
|
||||
).fetchall()
|
||||
modules = [{"value": r["module"], "label": r["module"]} for r in rows]
|
||||
return {"code": 0, "message": "ok", "data": modules}
|
||||
|
||||
Reference in New Issue
Block a user