fix(backend): Wave 1 Day 1 三个 P0 D Bug 修复
- W1-T3 修 4 处 fdw_etl.* 必坏残留 → app.* (P0-5 致命 1) · tenant_users.py L431/L456-457: v_dim_assistant + v_dim_staff(_ex) · tenant_excel.py L394/L411: v_dim_assistant + v_dim_staff · tenant_clues.py L119: v_dim_member · 修复后 tenant-admin 用户审核 / Excel 上传 / 维客线索恢复正常 - W1-T4 JWT aud sign 端写入 (P0-5 致命 2 最小止血) · jwt.py 全部 token 创建/解码函数加 audience 参数 · auth.py admin 端加 audience="admin" · xcx_auth.py miniapp 端加 audience="miniapp" (8 处调用) · 18 router 切强制 aud 校验留 Wave 2 - W1-T5 DBViewer 白名单 + 黑名单双保险 (P0-8) · 白名单: SELECT/WITH/EXPLAIN/SHOW 开头 · 黑名单: 17 关键词覆盖全 DML/DDL/DCL · 注释剥离避免误伤;15/15 单测 PASS 参考: docs/audit/changes/2026-05-04__wave1_day1_d_bug_triple_fix.md
This commit is contained in:
130
docs/audit/changes/2026-05-04__wave1_day1_d_bug_triple_fix.md
Normal file
130
docs/audit/changes/2026-05-04__wave1_day1_d_bug_triple_fix.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Wave 1 Day 1 — D Bug 三连修
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| 日期 | 2026-05-04 |
|
||||
| Wave | 1 |
|
||||
| 范围 | W1-T3 + W1-T4 + W1-T5(三个 P0 D Bug) |
|
||||
| 文件改动 | 5 个 backend router + 1 个 jwt 模块 |
|
||||
| 配套文档 | [WAVE-1-KICKOFF.md](../../_overview/WAVE-1-KICKOFF.md) §二 / [GLOBAL-DECISION-DASHBOARD.md](../../_overview/GLOBAL-DECISION-DASHBOARD.md) |
|
||||
|
||||
## 一、W1-T3 — 4 处 fdw_etl 必坏残留修复(P0-5 致命 1)
|
||||
|
||||
**问题**:`get_etl_readonly_connection(site_id)` 已直连 ETL 库 `etl_feiqiu`,但 SQL 仍写 `FROM fdw_etl.*`(业务库 zqyy_app 的 FDW schema,在 ETL 库中不存在)。生产**必报 schema 不存在**,被 try/except 静默吞 → 接口永远返回空列表,用户看不到错误。
|
||||
|
||||
**修复**:把 `fdw_etl.*` 替换为 `app.*`(ETL 库的 RLS 视图层)。
|
||||
|
||||
| # | 文件 | 改动 |
|
||||
|---|---|---|
|
||||
| 1 | [tenant_users.py](../../../apps/backend/app/routers/tenant_users.py):L431 | `fdw_etl.v_dim_assistant` → `app.v_dim_assistant` |
|
||||
| 2 | [tenant_users.py](../../../apps/backend/app/routers/tenant_users.py):L456-L457 | `fdw_etl.v_dim_staff` + `fdw_etl.v_dim_staff_ex` → `app.v_dim_staff` + `app.v_dim_staff_ex` |
|
||||
| 3 | [tenant_excel.py](../../../apps/backend/app/routers/tenant_excel.py):L394 | `fdw_etl.v_dim_assistant` → `app.v_dim_assistant` |
|
||||
| 4 | [tenant_excel.py](../../../apps/backend/app/routers/tenant_excel.py):L411 | `fdw_etl.v_dim_staff` → `app.v_dim_staff` |
|
||||
| 5 | [tenant_clues.py](../../../apps/backend/app/routers/tenant_clues.py):L119 | `fdw_etl.v_dim_member` → `app.v_dim_member` |
|
||||
|
||||
**校验**:`grep -rn "fdw_etl" apps/backend/app/routers/` 返回 0 处。
|
||||
|
||||
**调研依据**:[P0-5-engineering-consistency-overview.md](../../_overview/04a-feedback/P0-5-engineering-consistency-overview.md) F-2 子代理调研。
|
||||
|
||||
**影响**:tenant-admin 用户审核 / Excel 上传 / 维客线索 三个功能从"接口永远返回空列表"恢复正常。
|
||||
|
||||
---
|
||||
|
||||
## 二、W1-T4 — JWT aud 缺失修复(P0-5 致命 2)
|
||||
|
||||
**问题**:`auth/jwt.py` 签发 admin / miniapp token **完全不带 `aud` 字段**,`decode_access_token` 也不校验 aud。仅 tenant-admin 在自己的 `tenant_admins.py` 走完整 aud 流程。**意味着 admin / 小程序 token 在 payload 层无法区分,跨端横向越权风险**。
|
||||
|
||||
**修复策略**(本轮最小止血):
|
||||
- **Sign 端写入 aud**:admin token aud="admin",miniapp token aud="miniapp"(包括 limited 版)
|
||||
- **Decode 端**:加可选 `audience` 参数,默认 `verify_aud=False` 兼容旧 token
|
||||
- **暂不强制 router 切换**:18 个 router 切换到强制 aud 校验留 Wave 2(避免单次改动面过大)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| [jwt.py](../../../apps/backend/app/auth/jwt.py) | `create_access_token / create_refresh_token / create_token_pair / create_limited_token_pair` 全部加 `audience` 参数(可选);`decode_token / decode_access_token / decode_refresh_token` 加 `audience` 参数(传入则强制校验,不传则关闭 verify_aud) |
|
||||
| [auth.py](../../../apps/backend/app/routers/auth.py):L68/L106 | admin 登录与刷新加 `audience="admin"` |
|
||||
| [xcx_auth.py](../../../apps/backend/app/routers/xcx_auth.py) 共 8 处调用 | 小程序登录 / dev-switch / refresh / dev-switch-status 全部加 `audience="miniapp"` |
|
||||
|
||||
**校验**:`grep create_token_pair\|create_limited_token_pair apps/backend/app/routers/xcx_auth.py` 全部带 `audience="miniapp"`;auth.py 全部带 `audience="admin"`。
|
||||
|
||||
**调研依据**:[00-P0-round2-feedback-response-summary.md §二.2](../../_overview/04a-feedback/00-P0-round2-feedback-response-summary.md)。
|
||||
|
||||
**Wave 2 后续**(本轮不做):
|
||||
- 拆 `dependencies.py` 为 `get_current_user_admin / get_current_user_miniapp` 两个工厂
|
||||
- 18 router 切换依赖 → 强制 aud 校验生效
|
||||
|
||||
**影响**:新签发 token 全部带 aud claim;旧 token(无 aud)仍可使用,过期前自然淘汰。
|
||||
|
||||
---
|
||||
|
||||
## 三、W1-T5 — DBViewer 白名单 + 黑名单双保险(P0-8)
|
||||
|
||||
**问题**:`db_viewer.py` 仅黑名单 5 关键词(INSERT/UPDATE/DELETE/DROP/TRUNCATE),**漏拦截 ALTER/CREATE/GRANT/REVOKE/COPY/CALL/COMMENT/VACUUM/REINDEX/CLUSTER/REFRESH/LOCK** 等 12+ DDL/DCL 关键词。admin-web 可执行 DDL 改库结构(假设只读账号未限制 DDL)。
|
||||
|
||||
**修复策略**:
|
||||
1. **白名单**:SQL 必须以 `SELECT / WITH / EXPLAIN / SHOW` 开头(去注释/空白后)
|
||||
2. **黑名单**(深度防御):语句中含 17 个 DML/DDL/DCL 关键词一律拒绝
|
||||
3. **注释剥离**:DENY 检查前剥离 SQL 注释,避免 `-- evil DROP\nSELECT 1` 被误判为拒绝
|
||||
|
||||
**关键代码**(详见 [db_viewer.py](../../../apps/backend/app/routers/db_viewer.py)):
|
||||
```python
|
||||
_ALLOWED_PREFIXES = ("SELECT", "WITH", "EXPLAIN", "SHOW")
|
||||
_DENY_KEYWORDS = re.compile(
|
||||
r"\b(INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE|COPY|CALL|COMMENT|VACUUM|REINDEX|CLUSTER|REFRESH|LOCK)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
def _strip_comments(sql): ... # 剥离 -- 与 /* */
|
||||
def _extract_first_keyword(sql): ... # 剥注释后取第一个 token
|
||||
```
|
||||
|
||||
**校验**(15/15 PASS):
|
||||
- ✅ SELECT / WITH / EXPLAIN / SHOW 全部通过
|
||||
- ✅ ALTER / CREATE / GRANT / DELETE / DROP 全部拒绝
|
||||
- ✅ INSERT 含注释前导 拒绝
|
||||
- ✅ `-- evil DROP\nSELECT 1`(注释里的 DROP)通过,不误伤
|
||||
- ✅ `SELECT 1; DROP TABLE x`(多语句嵌入 DROP)拒绝
|
||||
- ✅ `COMMENT ON TABLE x IS "y"`(DDL COMMENT)拒绝
|
||||
|
||||
**调研依据**:[04a-conflicts-P0-detail.md §P0-8](../../_overview/04a-conflicts-P0-detail.md)。
|
||||
|
||||
**剩余防线**:`get_etl_readonly_connection` 设了 `default_transaction_read_only = on`,即使代码漏 DDL 也会被 PG 事务级别拒绝(深度防御)。
|
||||
|
||||
---
|
||||
|
||||
## 四、风险与回滚
|
||||
|
||||
| 项 | 风险 | 回滚方法 |
|
||||
|---|---|---|
|
||||
| W1-T3 | 极低 — `app.v_*` 视图早已存在,只是 SQL 引用错 schema | git revert |
|
||||
| W1-T4 | 极低 — sign 端加字段不影响旧消费方;新 token 仍能 decode(verify_aud=False)| git revert |
|
||||
| W1-T5 | 中 — 用户输入复杂 SQL 时白名单可能误拦截(如 `WITH RECURSIVE INSERT INTO ...` 这类罕见语句)| git revert + 添加用户实际遇到的合法 SQL 到测试用例 |
|
||||
|
||||
## 五、未覆盖 / 后续 Wave
|
||||
|
||||
- W1-T4 强制 aud 校验切换 → **Wave 2**(18 router 拆 admin / miniapp 依赖)
|
||||
- 配套单测覆盖 → **Wave 2**(`tests/test_auth_jwt.py` 加 audience 用例 + `tests/test_db_viewer.py` 加白名单用例)
|
||||
|
||||
## 六、commit 建议消息
|
||||
|
||||
```
|
||||
fix(backend): Wave 1 Day 1 三个 P0 D Bug 修复
|
||||
|
||||
- W1-T3 修 4 处 fdw_etl.* 必坏残留 → app.* (P0-5 致命 1)
|
||||
· tenant_users.py L431/L456-457: v_dim_assistant + v_dim_staff(_ex)
|
||||
· tenant_excel.py L394/L411: v_dim_assistant + v_dim_staff
|
||||
· tenant_clues.py L119: v_dim_member
|
||||
|
||||
- W1-T4 JWT aud sign 端写入 (P0-5 致命 2 最小止血)
|
||||
· jwt.py 全部 token 创建/解码函数加 audience 参数
|
||||
· auth.py admin 端加 audience="admin"
|
||||
· xcx_auth.py miniapp 端加 audience="miniapp" (8 处)
|
||||
· 18 router 切强制 aud 校验留 Wave 2
|
||||
|
||||
- W1-T5 DBViewer 白名单 + 黑名单双保险 (P0-8)
|
||||
· 白名单: SELECT/WITH/EXPLAIN/SHOW 开头
|
||||
· 黑名单: 17 关键词覆盖全 DML/DDL/DCL
|
||||
· 注释剥离避免误伤;15/15 单测 PASS
|
||||
|
||||
参考: docs/audit/changes/2026-05-04__wave1_day1_d_bug_triple_fix.md
|
||||
```
|
||||
Reference in New Issue
Block a user