Files
group_fqcd_jr/tools/grant_risk_permissions.py

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()))