Files
cma-management/docs/manuals/multitenant-isolation.md
T

9.7 KiB
Raw Blame History

多租户隔离 / 账套切换 操作手册

模块 slug: multitenant-isolation(对应队列 cma-module-queue.json) 最近一次修复后状态定稿:2026-09-07(project-20260907-opencode-multitenant-fix 完成并验证后,commit ac645a5) 这是横切所有数据模块的隔离机制手册:任何新增模块/API 都要先读「④ 已知边界/坑」再动手。

① 模块定位

  • 模块名:多租户隔离 / 账套切换(Multi-tenant Isolation / Entity Switch)
  • 解决什么问题:同一套 CMA 系统服务多家企业,各企业(账套/entity)数据天然隔离、互不可见、互不可写。企业清单:entity 1 = 陕西酣客(白酒)、entity 2 = 陕西博海(IT,本司)、3-5 = demo。登录 token 绑定 entity_id,全系统按 entity_id 过滤。
  • 所属流程环节:数据 → 账套切换(登录/会话/所有业务查询写入口的前置)
  • 主要角色:所有用户(ceo/finance/business/it,登录选账套);系统管理员(多账套授权切换);Bot 服务通道(X-BOT-KEY 无 token 白名单)

② 操作步骤

  1. 登录选账套:/login 页先点开公司下拉(登录页公司列表来自公开接口 GET /api/cma/auth/login-entities?username=xxx,按用户名返回其授权企业)→ 选企业 → 填账号密码 → 点「登 录」按钮(按钮文本是"登 录"中间有空格)。不带 entity_id 登录会返回「账套模式:请选择登录企业」。登录成功返回 {"token": "...", "entity_id": N, "entity_name": "...", "user": {...}};token 是 Redis cma:token:{token} 的 key,value=JSON {user_id, entity_id}(旧 int 格式 token 一律视为无效 → 401 强制重新登录)
  2. 切换账套:登录后右上角账套切换器 → 列出 GET /api/cma/auth/my-entities(当前用户授权企业)→ 选目标企业 → 前端调 POST /api/cma/auth/switch-entity(body {"entity_id": N},带当前 Bearer token):后端校验 user_entities 授权 → 删除旧 token → 重新签发新 token → 写 OperationLog → 前端替换 localStorage(cma_token/cma_entity_id/cma_entity_name/cma_user)+ location.reload()。切后所有数据视图变为新账套
  3. 越权防护语义(后端 deps.py get_entity_id 解析链):
    • 有 Bearer token → 强制用 token 绑定 entity;显式 query/header/body 的 entity_id 与 token 不一致 → 403
    • 无 token(Bot 白名单通道 X-BOT-KEY / X-Entity-Id header / body entity_id)→ 校验 Entity.status == "active",不通过 403,缺省回落 entity 1
    • 用户未授权企业:登录该企业 403、切换该企业 403
  4. 隔离表现:登录 A 账套只看到 A 账套数据(KPI/预算/成本/预警/待办/知识分区);跨账套直连 API 访问他人资源 → 404(资源归属校验语义,见下);跨账套提醒/待办不会出现
  5. Bot 数据通道:/api/cma/bot/*(X-BOT-KEY)与 /api/cma/bot-bridge/*(X-BRIDGE-TOKEN)无 token,entity 经 header/body 显式传递,隔离同样生效(bot_kpis 等按 KPI 归属校验)

③ 验证路径

  • 自动脚本:
    1. 专项越权测试(新增隔离用例):cd /root/cma-management/backend && venv/bin/python3 -m pytest tests/test_multitenant_ai_20260907.py -q → 期望 11 passed
    2. 工作台隔离脚本:cd /root/cma-management/backend && venv/bin/python3 scripts/verify_dashboard_isolation.py → 登录 entity1 → my-dashboard action_plans/reminders 回查 DB 归属全为 entity1,输出末尾 PASS: entity1 工作台数据无跨账套泄漏
    3. 生产双 token 越权实测配方(建临时用户→断言→删除):见 cma-fullstack-ops 技能 references/multitenant-opencode-20260907.md「生产库双 token 越权实测配方」段(cost 双实体零交集 / ai kpi-analysis 跨 entity 404 / dashboard alert_count 与 DB 各自相等 / review-plans stats / bot-kpis 跨 entity 404 / kpis create 白名单注入拦截)
  • 期望输出:上列 1/2 脚本全过;配方断言逐项 PASS
  • 已测基线(最近一次验证日期+结果):2026-09-07 pytest 专项 11 passed(16.56s);生产双 token 全断言 PASS;同批全量 pytest 745 passed / 10 failed(10 failed 经 git stash 在基线 HEAD 复跑同批 = 存量非回归,勿当回归)/ 1 xfailed

④ 已知边界 / 坑

  • 历史 bug:
    • 2026-08-11 账套模式上线:token 改 JSON 结构绑 entity_id;此前登录无企业概念,存量接口 entity_id 回落默认 1(隔离失效)
    • 2026-08-31 首轮全量隔离修复(security-multitenant-isolation):12+ API 文件统一 Depends(get_entity_id)(原 Query(1)/Form(1)/裸 int 默认全部回落 entity 1);KPIAlert/ActionPlan/OrgNode 等表补 entity_id 列并回填(kpi_alerts 472 行回填后 216 行属 entity≠1,证明隔离必要);dashboard.py my_plans/reminders 曾漏 entity 过滤 → 待办含其他账套方案(commit 815acda)
    • 2026-09-07 OpenCode 二轮审查 7 处缺口(commit ac645a5,P0):
      • ai_analysis.py:generate_rule_suggestions 的 pending 预警按 KPIAlert.entity_id 过滤;kpi_analysis/dashboard_analysis 查询带 entity(跨企业 kpi_id → 404);dashboard_analysis 系列端点补 Depends(get_entity_id) 签名;stream/分析缓存 key 改为 dashboard_analysis:{role}:{entity_id}(原无 entity → 跨企业串缓存)
      • dashboard.py:summary alert_count 补 KPIAlert.entity_id 过滤
      • bot_kpis.py:update_bot_kpi_value 查 KPI 后 if kpi.entity_id != entity_id: 404(归属校验)
      • cost.py + cost_engine.py:cost_overview/variance/breakdown/dashboard 补 get_entity_id;engine 函数加 entity_id: Optional[int]=None 参数(None=兼容存量调用不过滤,API 端点必须传)
      • kpis.py create_kpi:字段白名单 UPDATE_KPI_WHITELIST | {"kpi_code"} 循环拦截,防 status/important_flag/主键/entity_id=999 注入(此前 KPIDefinition(**data) 裸展开)
  • 边界条件:
    • 404 vs 403 语义:跨账套访问具体资源(他人 KPI 详情/他人 bot KPI/跨企业 kpi_id)→ 404(资源不存在语义,防探测);登录/切换未授权企业、token 与显式 entity 不一致 → 403。改隔离代码勿把 404 改成 403 或空返回混用
    • 同名 KPI 跨账套交集是正常现象(两家企业可各自定义 process 指标)——实测隔离用 KPI id 回查 DB 归属断言,勿用 code 集合判交集
    • 空态 ≠ bug:entity2(博海)无逾期数据时 reminders 为空属正常空态
    • 知识侧分区:knowledge_events/summaries 严格 entity_id 隔离(企业记忆);knowledge_articles 共享层 entity_id NULL = 全局共享(查询 or_(entity_id==cur, entity_id.is_(None)),勿按企业复制)
    • OperationLog 无 entity_id 列:按 user_id 成员过滤(宁可漏不可串),P2 另立项
  • ⚠️ 易踩点(新增/修改任何写端点前自查):
    1. 签名缺 entity_id: int = Depends(get_entity_id) + 构造函数漏 entity_id=entity_id + update/delete 缺 entity_id == entity_id 过滤——三类缺口都会静默把数据写进错误账套(2026-09-05 cost 实证:写端点整个没绑 entity,前端任何账套录入全落 entity 1)
    2. 带 Redis 缓存的接口 cache key 必须含 entity_id(串缓存是最隐蔽的隔离失效)
    3. 改函数签名后 grep 所有内部直调点(FastAPI 内部直调需显式传 db/entity_id,否则 Depends 对象 AttributeError)
    4. body 读 entity_id 的创建接口用 resolve_entity_for_request(request, data.get("entity_id") or 1)(token 优先),勿直接 or 1
    5. 截断会话残留:函数体引用 entity_id 但签名缺失 → NameError;改完逐端点扫「签名有/函数体有」两列(可复用 scan_entity_isolation.py)
    6. 任务单快照可能双向过期(说"有列"实际没有 / 说"没有列"实际 8-31 已加)——动手前 inspect 生产库列再定 直滤 vs JOIN

⑤ 修改记录

日期 修改人 改动 commit 影响
2026-09-07 - 手册创建(修复后状态定稿) - -
2026-09-07 OpenCode审查+全栈Bot ai_analysis/dashboard/bot_kpis/cost/kpis 隔离缺口修复(缓存key/归属404/写白名单/engine entity 参数)+ 11 条越权测试 ac645a5 P0 越权/串缓存
2026-09-07 全栈Bot .gitignore 补 cma.db/cma_import.db 1fa0836 CI deploy hook
2026-08-31 全栈Bot 首轮全量隔离:API 统一 Depends(get_entity_id) + KPIAlert/ActionPlan 等加列回填 + dashboard 提醒过滤 815acda 等 隔离门禁 5 项
2026-08-11 全栈Bot 账套模式上线(token 绑 entity、user_entities 授权) account-mode-refactor 登录选企业

⑥ 关联

  • 依赖表:users / user_entities(授权,多对多唯一约束)/ entities(账套,status=active 校验)/ OperationLog;业务表一律带 entity_id 列(kpi_definitions/kpi_values/kpi_alerts/action_plans/budget_plans/cost*/ai_suggestions...)
  • 依赖接口:POST /api/cma/auth/login(必带 entity_id);POST /api/cma/auth/switch-entity;GET /api/cma/auth/my-entities;GET /api/cma/auth/login-entities(公开);deps.py get_entity_id / resolve_entity_for_request
  • 关联模块:全部数据模块(横切);Bot 通道(/api/cma/bot/、/bot-bridge/)
  • 验证资产:tests/test_multitenant_ai_20260907.py(11 条)、tests/test_security_multitenant.py、tests/test_security_knowledge_partition.py(8 条)、backend/scripts/verify_dashboard_isolation.py、verify_knowledge_partition_e2e.py;配方见 cma-fullstack-ops references/multitenant-opencode-20260907.md + security-multitenant-isolation-20260831.md