Files
group_fqcd_jr/tools/grant_risk_permissions.py
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

130 lines
5.6 KiB
Python
Raw Permalink 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 权限。
**背景(实测)**:风控 service 层用 `AuthorizationService.require` 声明了四个权限码,
但 `sys_permission` 表里**只有一个**(`risk:alert:read`,本脚本早先建的)。其余三个不存在,
于是对应功能**全部失败关闭**:
| 权限码 | 用途 | 缺失后果 |
|---|---|---|
| `risk:alert:read` | 预警查询 / 研判 / 日报 / 通知 | (已建,正常) |
| `risk:alert:write` | 预警处置 / 证据归档 | 6 处调用被拒:确认、关闭、升级、归档都做不了 |
| `risk:alert:scan` | 规则扫描 | 扫描端点调不了 |
| `risk:report:mail` | 发送日报邮件 | 本次新增;邮件默认关闭,但仍需授权 |
失效的表现是 `ForbiddenAgentError`,而**只读查询一切正常** —— 所以很容易以为"风控能用",
直到去点处置按钮才发现。这与 `risk:alert:read` 当初缺失是同一类问题,只是面更广。
**字段怎么填**(照现有数据推断,不是自创):
- 权限匹配实际用的是 `permission_code` 全串(`identity_repository.py:25-37`),
`resource` / `action` 只是元数据;
- 参照 `fund:quote:read`(`resource=fund`、`action=quote`):`resource` 取第一段、
`action` 取第二段;
- `data_scope` 取 `all`:风控要处理**全部客户**的预警,且 `risk_query_service.py:194`、
`risk_analysis_service.py:126`、`risk_evidence_archive_service.py:218`、
`risk_action_service.py:212` 都按 `context.data_scope == "all"` 决定是否放行全量数据。
**授权给谁**:`risk_operator` 与 `admin`。风控 Agent 的
`AgentDefinition.allowed_roles = ("risk_operator", "admin")` 两个都声明了,
所以两个角色都该能真正用起来 —— 只给 `risk_operator` 会让 admin 声明了却用不了。
幂等:权限与绑定都已存在时直接跳过。
用法:python tools/grant_risk_permissions.py
"""
import asyncio
import datetime as dt
import sys
from pathlib import Path
import asyncmy
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from app.core.config import get_settings # noqa: E402
# (permission_id, permission_code, resource, action, 授予的角色)
PERMISSIONS: tuple[tuple[int, str, str, str, tuple[str, ...]], ...] = (
(9047, "risk:alert:read", "risk", "alert", ("risk_operator", "admin")),
(9048, "risk:alert:write", "risk", "alert", ("risk_operator", "admin")),
(9049, "risk:alert:scan", "risk", "alert", ("risk_operator", "admin")),
(9050, "risk:report:mail", "risk", "report", ("risk_operator", "admin")),
)
DATA_SCOPE = "all"
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()
granted = 0
try:
cursor = connection.cursor()
now = dt.datetime.now(dt.UTC).replace(tzinfo=None)
for permission_id, code, resource, action, roles in PERMISSIONS:
await cursor.execute(
"SELECT id FROM sys_permission WHERE permission_code = %s", (code,)
)
row = await cursor.fetchone()
if row is None:
await cursor.execute(
"INSERT INTO sys_permission"
" (id, permission_code, resource, `action`, data_scope,"
" created_at, updated_at)"
" VALUES (%s, %s, %s, %s, %s, %s, %s)",
(permission_id, code, resource, action, DATA_SCOPE, now, now),
)
print(f"[权限] 已创建 {code}(scope={DATA_SCOPE})")
else:
existing_id = int(row[0])
if existing_id != permission_id:
raise RuntimeError(
f"权限码 {code} 已存在但主键为 {existing_id},"
f"与约定主键 {permission_id} 不一致;请先执行 "
"python tools/seed_test_rbac.py"
)
print(f"[权限] {code} 已存在")
for role_code in roles:
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},跳过 {code}")
continue
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:
continue
await cursor.execute(
"INSERT INTO sys_role_permission (role_id, permission_id, created_at)"
" VALUES (%s, %s, %s)",
(role_id, permission_id, now),
)
granted += 1
print(f"[绑定] 授予 {role_code} → {code}")
await connection.commit()
print(f"\n本次新增绑定 {granted} 条")
return 0
finally:
connection.close()
if __name__ == "__main__":
sys.exit(asyncio.run(main()))