2026-09-11 13:37:23 +08:00
|
|
|
|
"""补齐风控模块需要的 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 声明了却用不了。
|
|
|
|
|
|
|
|
|
|
|
|
幂等:权限与绑定都已存在时直接跳过。
|
|
|
|
|
|
|
2026-09-11 15:25:45 +08:00
|
|
|
|
用法:python tools/grant_risk_permissions.py
|
2026-09-11 13:37:23 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import datetime as dt
|
|
|
|
|
|
import sys
|
2026-09-11 16:40:15 +08:00
|
|
|
|
from pathlib import Path
|
2026-09-11 13:37:23 +08:00
|
|
|
|
|
|
|
|
|
|
import asyncmy
|
|
|
|
|
|
|
2026-09-11 16:40:15 +08:00
|
|
|
|
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
|
2026-09-11 13:37:23 +08:00
|
|
|
|
|
|
|
|
|
|
# (permission_code, resource, action, 授予的角色)
|
|
|
|
|
|
PERMISSIONS: tuple[tuple[str, str, str, tuple[str, ...]], ...] = (
|
|
|
|
|
|
("risk:alert:read", "risk", "alert", ("risk_operator", "admin")),
|
|
|
|
|
|
("risk:alert:write", "risk", "alert", ("risk_operator", "admin")),
|
|
|
|
|
|
("risk:alert:scan", "risk", "alert", ("risk_operator", "admin")),
|
|
|
|
|
|
("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 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"
|
|
|
|
|
|
" (permission_code, resource, `action`, data_scope, created_at, updated_at)"
|
|
|
|
|
|
" VALUES (%s, %s, %s, %s, %s, %s)",
|
|
|
|
|
|
(code, resource, action, DATA_SCOPE, now, now),
|
|
|
|
|
|
)
|
|
|
|
|
|
await cursor.execute(
|
|
|
|
|
|
"SELECT id FROM sys_permission WHERE permission_code = %s", (code,)
|
|
|
|
|
|
)
|
|
|
|
|
|
row = await cursor.fetchone()
|
|
|
|
|
|
print(f"[权限] 已创建 {code}(scope={DATA_SCOPE})")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"[权限] {code} 已存在")
|
|
|
|
|
|
permission_id = int(row[0])
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sys.exit(asyncio.run(main()))
|