修 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: 缺少工具权限。
113 lines
4.4 KiB
Python
113 lines
4.4 KiB
Python
"""补齐风控工具所需的 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()))
|