From 0b82053ca2defd7b649f12a9df7ddd00fe6f8fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=BF=E4=BA=91=E7=A7=8B=E6=9C=88?= <15273589815@163.com> Date: Sun, 13 Sep 2026 22:57:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(rbac):=20=E4=BA=A4=E6=98=93=E6=9D=83?= =?UTF-8?q?=E9=99=90=E7=9A=84=20data=5Fscope=20=E5=86=99=E6=88=90=E4=BA=86?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E5=90=8D=EF=BC=8C=E5=AF=BC=E8=87=B4=E4=B8=8B?= =?UTF-8?q?=E5=8D=95/=E6=88=90=E4=BA=A4=E9=9D=99=E9=BB=98=20403?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tools/seed_test_rbac.py` 的 `PERMISSIONS` 行形状是 `(id, 权限码, resource, action, data_scope)`,而 T 段那四条写成了 `(9061, "trade:order:create", "trade", "order", "create")` —— 「resource, action」多写了一段,于是 action 落进了 `data_scope` 的位置。 ## 后果是静默失效,不是放宽 `IdentityRepository.load_context` 只收集 `data_scope ∈ {self, own_customers, all}` 的权限,其余**整条丢弃**: if row["permission_code"] and row["data_scope"] in rank: 于是客户在库里**明明有**这四个权限,`POST /api/v1/users/me/orders`、 `GET /api/v1/users/me/orders`、`GET /api/v1/users/me/transactions` 等 T 段端点 却全部返回 `403 AGENT_PERMISSION_DENIED`「缺少操作权限」。 而同一批里 `account:read:self`、`holding:read:self` 的 scope 是 `self`,照常 200 —— 现象特别像"只有交易坏了",几乎不会有人去怀疑**权限行本身写错了字段**。 因为 seed 是 `DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099` 的**重建**语义, **每跑一次种子就重现一次**,而 `tools/seed_demo_data.py` 的第 1 步正是它。 ## 修复与守卫 - 四条改为 `"self"`,并就地写清第 5 个字段是 `data_scope`、只能取三种值; - `tools/check_rbac_seed_consistency.py` 增加**第 5 条检查**:每行字段数必须是 5, 且 `data_scope` 取值合法。已用模拟对象确认它能抓住事故写法 (非法 scope 报 2 条、字段数不足报 1 条、合法写法通过), 并由 `tests/unit/tools/test_rbac_seed_consistency.py` 纳入门禁。 修复后实测:库内非法 `data_scope` 归零(`self` 22 → 26); `/users/me/{account/dashboard,holdings,transactions,orders}` 全部 200; 下单成交价 4.579(真实行情);e2e 冒烟 40/40;integration 106 passed。 --- tools/check_rbac_seed_consistency.py | 49 ++++++++++++++++++++++++++-- tools/seed_test_rbac.py | 16 ++++++--- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/tools/check_rbac_seed_consistency.py b/tools/check_rbac_seed_consistency.py index d3092c0..ab16b03 100644 --- a/tools/check_rbac_seed_consistency.py +++ b/tools/check_rbac_seed_consistency.py @@ -18,14 +18,15 @@ DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099 advisor 会静默拿到**语义完全错误**的权限组合(例如 id 9020 从 `asset-allocation:generate:self` 变成 `investment-goal:write:self`),全程不报错。 -本脚本把这条约束变成可自动检查的四条: +本脚本把这条约束变成可自动检查的五条: 1. 种子内 id 不重复; 2. 每个 `grant_*.py` 声明的 `(id, code)` 都能在种子里找到**完全一致**的一条; 3. `CUSTOMER_PERMISSIONS` 引用的 id 都存在; -4. 各 `grant_*.py` 之间不抢同一个 id。 +4. 各 `grant_*.py` 之间不抢同一个 id; +5. 每行 `PERMISSIONS` 的字段数与 `data_scope` 取值合法(见 `_scope_findings`)。 -`tests/unit/tools/test_rbac_seed_consistency.py` 会调用它,所以这四条是纳入门禁的。 +`tests/unit/tools/test_rbac_seed_consistency.py` 会调用它,所以这五条是纳入门禁的。 用法:python tools/check_rbac_seed_consistency.py """ @@ -117,9 +118,51 @@ def collect_findings() -> list[str]: + ", ".join(sorted(missing_risk_codes)) ) + problems.extend(_scope_findings(seed)) + return problems +#: `IdentityRepository.load_context` 只收集这三种 `data_scope` 的权限,其余**整条丢弃**。 +VALID_DATA_SCOPES = frozenset({"self", "own_customers", "all"}) + + +def _scope_findings(seed: ModuleType) -> list[str]: + """第 5 条:每行 `PERMISSIONS` 的字段数与 `data_scope` 取值都必须合法。 + + ## 为什么值得单独守 + + `PERMISSIONS` 的行形状是 `(id, 权限码, resource, action, data_scope)`。 + 2026-09-13 出过一次事故:交易那 4 条写成了 `(9061, "trade:order:create", + "trade", "order", "create")` —— 「resource, action」多写了一段, + 于是 action 落进了 `data_scope`。 + + 它**不报错、也不放宽**,而是**静默失效**:`load_context` 里 + `if row["permission_code"] and row["data_scope"] in rank` 会把整条权限丢掉, + 客户在库里"明明有"这四个权限,下单/委托/成交却全部 `403 AGENT_PERMISSION_DENIED`; + 而同一批里 `account:read:self`、`holding:read:self` 的 scope 是 `self`, + 照常 200 —— 所以现象特别像"只有交易坏了",几乎不会有人去怀疑权限行本身写错。 + + 因为 `seed_test_rbac.py` 是 DELETE 重建语义,这个错误**每跑一次种子就重现一次**。 + """ + findings: list[str] = [] + for index, row in enumerate(seed.PERMISSIONS, 1): + if len(row) != 5: + findings.append( + f"种子 PERMISSIONS 第 {index} 行有 {len(row)} 个字段,应为 5 个" + f"(id, 权限码, resource, action, data_scope):{row!r}" + ) + continue + permission_id, code, scope = int(row[0]), str(row[1]), str(row[4]) + if scope not in VALID_DATA_SCOPES: + findings.append( + f"种子 id {permission_id}({code})的 data_scope={scope!r} 非法:" + f"只能是 {sorted(VALID_DATA_SCOPES)} 之一 —— " + "否则该权限会被 IdentityRepository.load_context 静默丢弃,接口一律 403" + ) + return findings + + def main() -> int: if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr] diff --git a/tools/seed_test_rbac.py b/tools/seed_test_rbac.py index b06a30b..da49891 100644 --- a/tools/seed_test_rbac.py +++ b/tools/seed_test_rbac.py @@ -144,11 +144,19 @@ PERMISSIONS: tuple[tuple[int, str, str, str, str], ...] = ( # `PERMISSIONS`,同时挂在 `CUSTOMER_PERMISSIONS` 让 customer 角色自带。 # 号段续 9060(避开 9047-9059 qyqy 风险/推广/探针/投资目标号段):与 9041-9046 客服二期间隔 1,避免与既有迁移/种子冲突。 (9060, "account:read:self", "account", "read", "self"), - (9061, "trade:order:create", "trade", "order", "create"), - (9062, "trade:order:read", "trade", "order", "read"), - (9063, "trade:order:cancel", "trade", "order", "cancel"), + # ⚠️ 第 5 个字段是 **data_scope**,只能取 `self` / `own_customers` / `all`。 + # 这四条原先误写成 `trade, order, create` 这样的「resource, action, action」三段, + # 于是 action 落进了 data_scope 位置(值为 'create'/'read'/'cancel')。 + # 后果不是"宽松"而是**静默失效**:`IdentityRepository.load_context` 只收集 + # data_scope 合法的权限,这四条会被整条丢掉 ⇒ 客户在**库里明明有**这四个权限, + # 下单/委托/成交却全部 `403 AGENT_PERMISSION_DENIED`, + # 而 T001/T006(`account:read:self`/`holding:read:self` 是 'self')照常 200, + # 所以现象特别像"只有交易坏了",很难联想到权限码本身写错。 + (9061, "trade:order:create", "trade", "order", "self"), + (9062, "trade:order:read", "trade", "order", "self"), + (9063, "trade:order:cancel", "trade", "order", "self"), (9064, "holding:read:self", "holding", "read", "self"), - (9065, "trade:txn:read", "trade", "txn", "read"), + (9065, "trade:txn:read", "trade", "txn", "self"), ) # 客户:业务侧自助能力(自己的会话、反馈、转人工、自己的记忆画像)。