"""补齐风控工具所需的 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()))