Files
group_fqcd_jr/tools/grant_risk_alert_read_permission.py
T
lzf_0626 39c7ab51f4 feat(risk): 发布风控配置并补齐缺失的 RBAC 权限
修 docs/25 里的 P0:风控的意图配置与工具白名单一条都没发布,而白名单是失败关闭的,
导致任何工具调用都被拒。按组员交付的《20-Agent工具白名单与意图配置》补齐。

1. tools/publish_risk_agent_config.py:导入 4 条 risk 意图配置(id=61-64,active)与
   4 条 agent_tools 白名单(risk_overview / risk_search / risk_evidence / general)。
   发布版本 id=188 active,并且**继承了现有 5 条配置项**——config_release 是整版本替换
   语义,不继承会把客服的 4 条白名单和示例 Agent 的 fund_query_demo:fund_quote 静默清空。

2. tools/grant_risk_alert_read_permission.py:补齐 risk:alert:read 权限。
   实测发现这条权限在 sys_permission 里**根本不存在**,连 risk_operator 角色也没有,
   所以任何身份调用风控工具都会拿到"缺少工具权限"。交付文档第 116 行正把这一项列为
   接入前置条件。权限匹配实际用 permission_code 全串(identity_repository.py:25-37),
   resource/action 只是元数据(照 fund:quote:read 的拆法);data_scope 取 all,因为
   risk_query_service.py:194、risk_analysis_service.py:126 等按 context.data_scope == "all"
   决定是否放行全量数据。只授权 risk_operator,不动 admin(交付文档只要求前者)。

验证(以 9002 risk_operator 身份实测):
- "查看当前风险概览" → status=succeeded、意图 risk_overview、工具真实返回数据
- "查询高风险预警"   → status=succeeded、意图 risk_search
修复前两者均为 failed + ForbiddenAgentError: 缺少工具权限。
2026-09-11 12:18:34 +08:00

113 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""补齐风控工具所需的 RBAC 权限 `risk:alert:read`。
**为什么需要这个脚本**(实测证据):
风控的三个工具声明了 `required_permission="risk:alert:read"`
(`bootstrap.py:209` / `:216` / `:223`),但 `sys_permission` 表里**没有这条权限**——
连 `risk_operator` 角色也没有。于是任何身份调用风控工具都会拿到
`ForbiddenAgentError: 缺少工具权限`(`tool_executor.py:78`)。
组员交付的 `20-Agent工具白名单与意图配置.md` 第 116 行也把它列为接入前置条件:
「确认 `risk_operator` 拥有 `agent:run` 和 `risk:alert:read`」。
**字段怎么填**(照现有数据推断,不是自创):
- 权限匹配实际用的是 `permission_code` 全串(`identity_repository.py:25-37` 直接读
`p.permission_code`),`resource`/`action` 只是元数据;
- 参照 `fund:quote:read`(`resource=fund`、`action=quote`):`resource` 取第一段、
`action` 取第二段,故本权限为 `resource=risk`、`action=alert`;
- `data_scope` 取 `all`:风控要查**全部客户**的预警,而 `risk_query_service.py:194`、
`risk_analysis_service.py:126`、`risk_evidence_archive_service.py:218` 都按
`context.data_scope == "all"` 决定是否放行全量数据。`risk_operator` 已有
`audit:read`(scope=all),所以它的 data_scope 本来就是 all。
**只授权 `risk_operator`,不动 `admin`**:交付文档只要求前者;admin 是否应有风控读权限
是业务决策,留给项目方定。
幂等:权限与绑定都已存在时直接跳过。
用法:python tools/grant_risk_alert_read_permission.py
"""
import asyncio
import datetime as dt
import sys
import asyncmy
from app.core.config import get_settings
PERMISSION_CODE = "risk:alert:read"
ROLE_CODE = "risk_operator"
async def connect() -> "asyncmy.Connection":
settings = get_settings()
# MYSQL_DSN 形如 mysql+asyncmy://user:pass@host:port/db
dsn = settings.mysql_dsn.split("://", 1)[1]
credentials, location = dsn.split("@", 1)
user, password = credentials.split(":", 1)
host_port, database = location.split("/", 1)
host, _, port = host_port.partition(":")
return await asyncmy.connect(
host=host, port=int(port or 3306), user=user, password=password, db=database
)
async def main() -> int:
connection = await connect()
try:
cursor = connection.cursor()
now = dt.datetime.now(dt.UTC).replace(tzinfo=None)
await cursor.execute(
"SELECT id, data_scope FROM sys_permission WHERE permission_code = %s",
(PERMISSION_CODE,),
)
row = await cursor.fetchone()
if row is None:
await cursor.execute(
"INSERT INTO sys_permission"
" (permission_code, resource, `action`, data_scope, created_at, updated_at)"
" VALUES (%s, %s, %s, %s, %s, %s)",
(PERMISSION_CODE, "risk", "alert", "all", now, now),
)
await connection.commit()
await cursor.execute(
"SELECT id FROM sys_permission WHERE permission_code = %s", (PERMISSION_CODE,)
)
permission_id = int((await cursor.fetchone())[0])
print(f"[权限] 已创建 {PERMISSION_CODE}(id={permission_id}, scope=all)")
else:
permission_id = int(row[0])
print(f"[权限] {PERMISSION_CODE} 已存在(id={permission_id}, scope={row[1]})")
await cursor.execute("SELECT id FROM sys_role WHERE role_code = %s", (ROLE_CODE,))
role = await cursor.fetchone()
if role is None:
print(f"[角色] 找不到 {ROLE_CODE},请先建角色")
return 1
role_id = int(role[0])
await cursor.execute(
"SELECT 1 FROM sys_role_permission WHERE role_id = %s AND permission_id = %s",
(role_id, permission_id),
)
if await cursor.fetchone() is not None:
print(f"[绑定] {ROLE_CODE} 已拥有 {PERMISSION_CODE},无需变更")
return 0
await cursor.execute(
"INSERT INTO sys_role_permission (role_id, permission_id, created_at)"
" VALUES (%s, %s, %s)",
(role_id, permission_id, now),
)
await connection.commit()
print(f"[绑定] 已授予 {ROLE_CODE}(role_id={role_id})权限 {PERMISSION_CODE}")
return 0
finally:
connection.close()
sys.exit(asyncio.run(main()))