feat: Bot API L1-L4风险分级标注 + 操作审计日志
- 新增 app/risk_levels.py: RISK_LEVELS定义 + @risk_level装饰器 + API_RISK_MAP (L1只读21 / L2业务写5 / L3批量写3 / L4=0安全底线) - 新增 app/api/audit_log.py: Bot API审计中间件 → backend/logs/bot_audit.log (JSON行: timestamp/bot_name/endpoint/method/risk_level/entity_id/status, L3额外记rows行数, 不阻塞业务) - bot_bridge/bot_bridge_v2/bot_kpis/bot_iron_law 全部29路由标注级别 - 新增 GET /api/cma/bot/risk-levels (X-BOT-KEY鉴权): API→级别→处理方式清单 - main.py 注册审计中间件 - tests/test_risk_levels.py: 覆盖路由标注/risk-levels端点/L4不存在/审计日志
This commit is contained in:
@@ -18,6 +18,7 @@ from app.models import (
|
||||
from app.models.budget_plan import BudgetPlan
|
||||
from app.models.cost_model import StandardCost, ActualCost, AbcActivity, AbcAllocation
|
||||
from app.models import KPICausality
|
||||
from app.risk_levels import risk_level
|
||||
import json
|
||||
|
||||
logger = logging.getLogger("cma.bot_bridge")
|
||||
@@ -79,6 +80,7 @@ def _model_dict(obj, fields: dict):
|
||||
# ═══════════════ 端点 ═══════════════
|
||||
|
||||
@router.get("/ping")
|
||||
@risk_level("L1")
|
||||
def ping():
|
||||
return {"status": "ok", "version": "1.0", "timestamp": datetime.now().isoformat()}
|
||||
|
||||
@@ -86,6 +88,7 @@ def ping():
|
||||
# ── 总览 ──
|
||||
|
||||
@router.get("/overview")
|
||||
@risk_level("L1")
|
||||
def bot_overview(
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -110,6 +113,7 @@ def bot_overview(
|
||||
# ── KPI ──
|
||||
|
||||
@router.get("/kpis")
|
||||
@risk_level("L1")
|
||||
def bot_kpis(
|
||||
dimension: Optional[str] = Query(None),
|
||||
status: str = Query("active"),
|
||||
@@ -145,6 +149,7 @@ def bot_kpis(
|
||||
|
||||
|
||||
@router.get("/kpis/{kpi_id}/history")
|
||||
@risk_level("L1")
|
||||
def bot_kpi_history(
|
||||
kpi_id: int, limit: int = Query(12, le=60),
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
@@ -171,6 +176,7 @@ def bot_kpi_history(
|
||||
# ── 战略地图 ──
|
||||
|
||||
@router.get("/strategic-maps")
|
||||
@risk_level("L1")
|
||||
def bot_maps(
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -198,6 +204,7 @@ def bot_maps(
|
||||
# ── 预警 ──
|
||||
|
||||
@router.get("/alerts")
|
||||
@risk_level("L1")
|
||||
def bot_alerts(
|
||||
status: str = Query("pending"),
|
||||
level: Optional[str] = Query(None),
|
||||
@@ -228,6 +235,7 @@ def bot_alerts(
|
||||
# ── 预算 ──
|
||||
|
||||
@router.get("/budget/plans")
|
||||
@risk_level("L1")
|
||||
def bot_budget_plans(
|
||||
year: Optional[int] = Query(None),
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
@@ -255,6 +263,7 @@ def bot_budget_plans(
|
||||
# ── 成本 ──
|
||||
|
||||
@router.get("/cost/standard")
|
||||
@risk_level("L1")
|
||||
def bot_standard_costs(
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -278,6 +287,7 @@ def bot_standard_costs(
|
||||
|
||||
|
||||
@router.get("/cost/actual")
|
||||
@risk_level("L1")
|
||||
def bot_actual_costs(
|
||||
period: Optional[str] = Query(None),
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
@@ -306,6 +316,7 @@ def bot_actual_costs(
|
||||
# ── 行动方案 ──
|
||||
|
||||
@router.get("/actions")
|
||||
@risk_level("L1")
|
||||
def bot_actions(
|
||||
status: Optional[str] = Query(None),
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
@@ -333,6 +344,7 @@ def bot_actions(
|
||||
# ── 组织 ──
|
||||
|
||||
@router.get("/organization")
|
||||
@risk_level("L1")
|
||||
def bot_org(
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -354,6 +366,7 @@ def bot_org(
|
||||
# ── 数据源 ──
|
||||
|
||||
@router.get("/data-sources")
|
||||
@risk_level("L1")
|
||||
def bot_data_sources(
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -377,6 +390,7 @@ def bot_data_sources(
|
||||
# ── 用户 ──
|
||||
|
||||
@router.get("/users")
|
||||
@risk_level("L1")
|
||||
def bot_users(
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -395,6 +409,7 @@ def bot_users(
|
||||
# ── 统一查询(BOT首选) ──
|
||||
|
||||
@router.get("/query")
|
||||
@risk_level("L1")
|
||||
def bot_query(
|
||||
q: str = Query("overview", description="overview/kpis/alerts/maps/budget/cost/actions/all"),
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
@@ -482,6 +497,7 @@ def bot_query(
|
||||
|
||||
|
||||
@router.post("/import")
|
||||
@risk_level("L3")
|
||||
def bot_import_excel(
|
||||
file: UploadFile = File(...),
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
@@ -546,6 +562,7 @@ def bot_import_excel(
|
||||
# ── 自然语言查询 ──
|
||||
|
||||
@router.post("/okr/create")
|
||||
@risk_level("L2")
|
||||
def bot_okr_create(
|
||||
title: str = Query(...),
|
||||
quarter: str = Query(...),
|
||||
@@ -563,6 +580,7 @@ def bot_okr_create(
|
||||
|
||||
|
||||
@router.get("/okr/list")
|
||||
@risk_level("L1")
|
||||
def bot_okr_list(
|
||||
quarter: Optional[str] = Query(None),
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
@@ -584,6 +602,7 @@ def bot_okr_list(
|
||||
|
||||
|
||||
@router.get("/nlp")
|
||||
@risk_level("L1")
|
||||
def bot_nlp(
|
||||
intent: str = Query("overview"),
|
||||
bot: dict = Depends(verify_bot_key),
|
||||
@@ -613,6 +632,7 @@ def bot_nlp(
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
@router.post("/kpi-value-with-check")
|
||||
@risk_level("L2")
|
||||
def bot_kpi_value_with_check(data: dict, db: Session = Depends(get_db), bot: dict = Depends(verify_bot_key)):
|
||||
"""聚合A: 写KPI值 + 自动跑该KPI预警检查(Agent一次调用,免自拼check-all)
|
||||
body: {kpi_id, actual_value, period?, entity_id?, run_check?}"""
|
||||
@@ -681,6 +701,7 @@ def bot_kpi_value_with_check(data: dict, db: Session = Depends(get_db), bot: dic
|
||||
|
||||
|
||||
@router.post("/kpis/create-with-links")
|
||||
@risk_level("L3")
|
||||
def bot_kpi_create_with_links(data: dict, db: Session = Depends(get_db), bot: dict = Depends(verify_bot_key)):
|
||||
"""聚合B: 创建KPI + 关联战略地图 + 批量因果链(Agent建KPI标准动作)
|
||||
body: {kpi_code, kpi_name, dimension, entity_id?, target_value?, unit?, link_map_id?, link_causality?}"""
|
||||
@@ -736,3 +757,18 @@ def bot_kpi_create_with_links(data: dict, db: Session = Depends(get_db), bot: di
|
||||
db.commit()
|
||||
return {"kpi_id": kpi.id, "kpi_code": kpi.kpi_code, "map_id": link_map_id,
|
||||
"causality_links": links, "status": "ok"}
|
||||
|
||||
|
||||
@router.get("/risk-levels")
|
||||
@risk_level("L1")
|
||||
def bot_risk_levels(bot: dict = Depends(verify_bot_key)):
|
||||
"""Bot API风险分级清单(API→级别→处理方式)— 验收/巡检/授权决策用"""
|
||||
from app.risk_levels import RISK_LEVELS, list_api_risk_map, risk_summary
|
||||
return {
|
||||
"bot": bot["name"],
|
||||
"risk_levels": RISK_LEVELS,
|
||||
"apis": list_api_risk_map(),
|
||||
"summary": risk_summary(),
|
||||
"note": "L4(危险)不向Bot API开放:Bot只能通过白名单API读写,"
|
||||
"DROP/TRUNCATE/批量DELETE/生产结构修改物理不可能,由终端层+approval-gate拦截",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user