fix(cma): 工作台跨实体治本 · DAMA 回查收口 — 审计可归属 + 通知日志口径

DAMA 独立回查 dama-recheck-workbench-entity-isolation-20260922 的 §4b/§5 两处待办:

1. POST /api/cma/tenant/switch 审计行原来不可归属:
   - user_id=None → 改为注入会话用户(user=Depends(require_auth_401))
   - target_type/target_id 从 system_config/None 对齐为 entity/<entity_id>,
     与 /auth/switch-entity 的 switch_entity 共用目标维度 ⇒ 按账套可过滤,零 DDL
   - action 字面 switch_tenant_state 保留不合并(与会话级 switch_entity 语义不同),
     口径登记 docs/audit-and-notification-caliber-20260922.md

2. /api/cma/notifications/logs 增加 policy/note 口径字段,前端发送记录表
   empty-text 提示同一口径 —— 避免「B2 上线后 e1/e2 均 0 条」被当成页面故障
   (现库 8 行历史记录全为 alert_id IS NULL 的 OKR 复盘提醒,无账套归属)

回归:tests/test_workbench_entity_isolation_20260922.py 22/22;全量 1370 passed;前端 vite build 通过
This commit is contained in:
fullstack
2026-09-22 11:21:24 +08:00
parent 0c2e57a4cf
commit 30fa745135
5 changed files with 103 additions and 5 deletions
+7
View File
@@ -130,6 +130,13 @@ def list_logs(page: int = 1,
return {
"total": total,
"entity_id": entity_id,
# 口径字段(2026-09-22 DAMA 回查 §5 收口):上线后本页在无预警推送的账套会显示 0 条,
# 属口径而非故障——现有 8 行历史记录全是 `alert_id IS NULL` 的 OKR 复盘提醒(无账套归属),
# 按「宁少勿错」不返回。前端据此文案提示,避免被当成页面坏了。
"policy": "alert-linked-only",
"note": ("仅显示与预警推送关联的记录(按父键 kpi_alerts 归属会话账套)。"
"无父键的历史通知(如 OKR 复盘提醒,写入方 backend/scripts/okr_review_reminder.py)"
"不属于任何账套,不在本列表返回。口径登记:docs/audit-and-notification-caliber-20260922.md"),
"data": [{
"id": l.id,
"alert_id": l.alert_id,
+15 -4
View File
@@ -17,6 +17,13 @@
收紧会切断其原始用途。
5. 每次写都落 `operation_logs`(action=`switch_tenant_state`)留痕 —— 修复前本端点
无审计(ops 止血单假定「走端点即有审计」不成立,已在本单补齐)。
6. **审计行可归属(2026-09-22 DAMA 独立回查 §4b 后补)**:`user_id` = 当前会话用户
(原写 None ⇒ 事后无法追操作人);`target_type="entity"` + `target_id=<entity_id>`
与 `POST /auth/switch-entity`(action=`switch_entity`,`auth.py`)**共用同一目标维度**
⇒「按账套查换账套记录」= `WHERE target_type='entity' AND target_id=?`(`operation_logs`
无 `entity_id` 列,无需 DDL)。两字面**刻意不合并**:一个改「用户自己的会话账套」、
一个改「平台全局状态记录」,合并后审计上不可分辨。口径登记见
`docs/audit-and-notification-caliber-20260922.md`。
"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
@@ -64,7 +71,8 @@ def get_current_tenant(
@router.post("/switch")
def switch_tenant(data: TenantSwitchRequest, db: Session = Depends(get_db)):
def switch_tenant(data: TenantSwitchRequest, db: Session = Depends(get_db),
user=Depends(require_auth_401)):
"""切换当前租户状态记录(项目Bot 分发 A2A 任务用;CMA前端企业切换器已下线)
2026-09-22:写操作**不动**(保留 Bot 通道可用性),补 `operation_logs` 审计留痕。
@@ -101,11 +109,14 @@ def switch_tenant(data: TenantSwitchRequest, db: Session = Depends(get_db)):
))
# 审计留痕(2026-09-22 补齐:本端点原无 operation_logs 记录)
# 2026-09-22 DAMA 回查 §4b 后补:user_id 记操作人(原 None ⇒ 无法追人);
# target_type/target_id 与 /auth/switch-entity 的 switch_entity 对齐 ⇒ 按账套可过滤
# (operation_logs 无 entity_id 列,无须 DDL)。
db.add(OperationLog(
user_id=None,
user_id=user.id,
action="switch_tenant_state",
target_type="system_config",
target_id=None,
target_type="entity",
target_id=data.entity_id,
detail={
"entity_id": data.entity_id,
"entity_name": ent.name,
@@ -298,6 +298,14 @@ class TestP0CurrentTenantConvergence:
db.expire_all()
n = db.query(OperationLog).filter(OperationLog.action == "switch_tenant_state").count()
assert n == 1, f"switch 应落 1 条审计,实际 {n}"
# 审计行必须可归属(2026-09-22 DAMA 独立回查 §4b):操作人 + 账套目标维度
log = (db.query(OperationLog)
.filter(OperationLog.action == "switch_tenant_state")
.order_by(OperationLog.id.desc()).first())
assert log.user_id is not None, "审计行 user_id 不得为 None(事后须能追操作人)"
assert log.target_type == "entity" and log.target_id == 2, (
f"审计须按账套可过滤(target_type/target_id),实际 {log.target_type}/{log.target_id}")
assert (log.detail or {}).get("entity_id") == 2, f"detail 须含 entity_id:{log.detail}"
# 全局键虽被改成 e2,e1 会话工作台仍只见 e1 数据
codes1 = {k["kpi_code"] for k in
client.get(f"{DASH}/my-kpis", headers=auth_header(t1)).json()["data"]}
@@ -522,6 +530,10 @@ class TestB2NotificationLogScoping:
assert titles2 == {"e2 通知"}, f"e2 会话通知日志混入他账套/无归属行:{titles2}"
assert l1["total"] == 1 and l2["total"] == 1
assert l1["entity_id"] == 1 and l2["entity_id"] == 2
# 口径字段(2026-09-22 DAMA 回查 §5):空页要能解释成口径而非故障
for l in (l1, l2):
assert l["policy"] == "alert-linked-only", l
assert "无父键" in l["note"] and "okr_review_reminder" in l["note"], l
# ══════════════════════════════════════════════