From 5da2fb5790015e952cdde8680446832c165cc6ee 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: Mon, 14 Sep 2026 22:56:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=95=E9=A1=BE=E4=BB=A3=E5=AE=A2=E4=B8=89?= =?UTF-8?q?=E9=A1=B9=E7=9A=84=E6=9D=83=E9=99=90=E7=A0=81=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=EF=BC=9A=E6=95=B4=E7=89=87=20403=EF=BC=88=E4=B8=8E=209057-9059?= =?UTF-8?q?=20=E5=90=8C=E4=B8=80=E4=B8=AA=E5=9D=91=E7=9A=84=E7=AC=AC?= =?UTF-8?q?=E4=BA=8C=E6=89=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 现象 投顾工作台(`advisor_t`,9020)一选客户就整片失败,页面只显示 「请求失败,请检查权限或稍后重试」。API 访问日志给出真相: ``` POST /api/v1/advisor/recommendations 403 POST /api/v1/advisor/asset-allocation 403 POST /api/v1/advisor/portfolio-analysis 403 GET /api/v1/advisor/recommendations/published 200 ← 只有它不需要代客权限 ``` ## 根因:三个 `*:customer` 权限码在库里根本不存在 三个服务在"代客"(`customer_id != context.user_id`)时要求的是**动态拼出来的 `:customer` 变体**: - `product_recommendation_service.py:71` → `product-recommendation:generate:customer` - `asset_allocation_service.py:57` → `asset-allocation:generate:customer` - `portfolio_analysis_service.py:46` → `portfolio-analysis:read:customer` 而 `sys_permission` 里 `:customer` 后缀**只有 4 个**(`memory:read:customer`、 `investment-goal:{read,write,confirm}:customer`)—— 这三个从来没登记过。 `AuthorizationService.require()` 第一步就查不到该码 ⇒ 直接 403。 这与种子里 **9057-9059 的注释是同一个坑**("动态拼出来的权限码,对账工具抓不到字面量, 于是投顾查/建/确认客户目标全部 403"),前人修了 investment-goal 那三个,这三个漏了。 ## 修法(按仓库纪律:种子定义权限码 → grant 脚本绑定角色) 1. `tools/seed_test_rbac.py`:新增 **9066-9068** 三个码,`data_scope=own_customers` (`require_customer_scope` 只放行 `all`,或 `own_customers` 且客户确在 `context.customer_ids` = 归属客户内 —— 这正是投顾该有的最小权限), 并写明三处调用点,避免后人再漏; 2. `tools/grant_advisor_role.py`:把三个码加入 `ADVISOR_GRANTED_CODES` (admin 因 `ADMIN_PERMISSIONS = 全部种子权限` 自动获得)。 已执行:`seed_test_rbac.py` → `grant_advisor_role.py` (advisor 新增 3 项,共 31 项;admin 62 项)。 ## 验证(真实 HTTP,9020 身份) | 客户 | 推荐方案 | 资产配置 | 组合分析 | |---|---|---|---| | 9101(演示客户) | 200 `profile_required` | 200 `profile_required` | 200 `no_positions` | | **9001(真实客户)** | **200 已生成方案**(content_id=5, pending_review) | **200 ready**(含配置比例) | **200 ready**(7 个持仓,市值 10698.60) | **403 全部消失**;对真实客户三项均返回真实结果。 ## 同时补的归属数据 `sys_customer_assignment` 原有 9020→9001 一行;工作台把演示客户的 id (9101-9104)当真实 `customer_id` 发给后端,而 `own_customers` 要求客户在投顾名下, 因此用 `tools/assign_customer_scope.py` 补了 4 行(9020 → 9101/9102/9103/9104), 回验 `customer_ids = ('9001','9101','9102','9103','9104')`。 ## 仍未解决(属**数据**缺口,不是权限) 1. **9101-9104 在库里没有任何数据**:`advisor-config.js:126` 注释指向的 `_seed_demo_customers.py` 在仓库、git 历史与桌面上**都不存在**(从未提交), 所以这四位没有账号 / 风险测评 / 投资目标 / 持仓 —— 只能返回 `profile_required`。 2. **推荐候选恒为 0**:`AdvisorProductRepository.authoritative_tradable_products` 要求**已验证**的适当性证据与合同快照(`review_status='verified'` 且 `source_url` / `document_sha256` 非空,fail closed),而 `advisor_product_suitability_reference` / `advisor_product_contract_snapshot` 均为 0 行。这需要产品治理线提供可核查证据,**不应伪造**。 ## 门禁 `tools/check_rbac_seed_consistency.py` 通过(种子内 id 唯一、各 grant 脚本与种子逐条一致); `pytest tests/unit tests/contract` 全绿。 --- tools/grant_advisor_role.py | 8 ++++++++ tools/seed_test_rbac.py | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/tools/grant_advisor_role.py b/tools/grant_advisor_role.py index 190b3d8..219db95 100644 --- a/tools/grant_advisor_role.py +++ b/tools/grant_advisor_role.py @@ -92,6 +92,14 @@ ADVISOR_GRANTED_CODES: tuple[str, ...] = ( "product-comparison:read:self", "product-recommendation:read:self", "product-recommendation:generate:self", + # 代客三项:对**名下客户**(而非自己)做推荐 / 资产配置 / 组合分析。 + # 同样是服务层按 `customer_id == 自己` 动态拼出来的 `:customer` 变体, + # `data_scope=own_customers` —— 缺这三个码,投顾工作台一选客户就整片 403 + # (`product_recommendation_service.py:71`、`asset_allocation_service.py:57`、 + # `portfolio_analysis_service.py:46`)。 + "product-recommendation:generate:customer", + "asset-allocation:generate:customer", + "portfolio-analysis:read:customer", "product-recommendation:review", "product-recommendation:publish", # 平台通用:投顾同样要跑 Agent、查行情、检索知识、看所服务客户的画像 diff --git a/tools/seed_test_rbac.py b/tools/seed_test_rbac.py index da49891..e3070a9 100644 --- a/tools/seed_test_rbac.py +++ b/tools/seed_test_rbac.py @@ -157,6 +157,23 @@ PERMISSIONS: tuple[tuple[int, str, str, str, str], ...] = ( (9063, "trade:order:cancel", "trade", "order", "self"), (9064, "holding:read:self", "holding", "read", "self"), (9065, "trade:txn:read", "trade", "txn", "self"), + + # ---- 9066-9068:投顾**代客**三项(`*:customer` 变体) ---- + # 与 9057-9059 是**同一个坑的第二批**:这三个码同样是服务层按 + # `customer_id == context.user_id` **动态拼**出来的,对账工具抓不到字面量, + # 于是库里从来没有它们,投顾一在投顾工作台对**客户**做操作就整片 403: + # · `product_recommendation_service.py:71` `product-recommendation:generate:customer` + # · `asset_allocation_service.py:57` `asset-allocation:generate:customer` + # · `portfolio_analysis_service.py:46` `portfolio-analysis:read:customer` + # `data_scope` 必须是 `own_customers`:`AuthorizationService.require_customer_scope` + # 只放行 `all`,或 `own_customers` 且客户确在 `context.customer_ids`(= 归属客户)内。 + # 这正是投顾该有的最小权限 —— 只对自己服务的客户出方案。 + (9066, "product-recommendation:generate:customer", "product-recommendation", + "generate", "own_customers"), + (9067, "asset-allocation:generate:customer", "asset-allocation", "generate", + "own_customers"), + (9068, "portfolio-analysis:read:customer", "portfolio-analysis", "read", + "own_customers"), ) # 客户:业务侧自助能力(自己的会话、反馈、转人工、自己的记忆画像)。