完善风控登录权限和模型能力配置
This commit is contained in:
@@ -57,15 +57,18 @@ Agent 不能把截断结果表述成覆盖全部数据,也不能据此给出
|
||||
|
||||
以下内容属于环境准备或数据初始化,不是代码缺陷:
|
||||
|
||||
1. 执行 `python tools/grant_risk_permissions.py`,确认风控角色具备:
|
||||
1. 先执行 `python tools/seed_test_rbac.py`,再执行
|
||||
`python tools/grant_risk_permissions.py`,确认风控角色具备:
|
||||
- `risk:alert:read`
|
||||
- `risk:alert:write`
|
||||
- `risk:alert:scan`
|
||||
- `risk:report:mail`
|
||||
2. 执行 `python tools/publish_risk_agent_config.py`,确认奶龙风控智能助手的工具白名单和意图配置已发布。
|
||||
3. 按主项目发布的迁移流程执行 Alembic 升级,确认 `trigger_rule_codes` 多值索引已生效。
|
||||
4. 演示前准备足够的客户、交易、资金、持仓、登录和预警数据。
|
||||
5. 使用前确认日报邮件开关、SMTP 配置和收件人范围符合演示要求。
|
||||
2. 执行 `python tools/set_user_password.py --user 9002 --password 666666`,
|
||||
为风控演示账号设置密码;登录使用 `risk_t / 666666`。
|
||||
3. 执行 `python tools/publish_risk_agent_config.py`,确认奶龙风控智能助手的工具白名单和意图配置已发布。
|
||||
4. 按主项目发布的迁移流程执行 Alembic 升级,确认 `trigger_rule_codes` 多值索引已生效。
|
||||
5. 演示前准备足够的客户、交易、资金、持仓、登录和预警数据。
|
||||
6. 使用前确认日报邮件开关、SMTP 配置和收件人范围符合演示要求。
|
||||
|
||||
## 当前保留限制
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
GRANT_SCRIPTS: tuple[tuple[str, str, str], ...] = (
|
||||
("tools/grant_advisor_role.py", "ADVISOR_PERMISSIONS", "投顾"),
|
||||
("tools/grant_customer_service_phase2_permissions.py", "PHASE2_PERMISSIONS", "客服二期"),
|
||||
("tools/grant_risk_permissions.py", "PERMISSIONS", "风控"),
|
||||
)
|
||||
|
||||
|
||||
@@ -94,6 +95,28 @@ def collect_findings() -> list[str]:
|
||||
if int(permission_id) not in seed_map:
|
||||
problems.append(f"CUSTOMER_PERMISSIONS 引用了不存在的 id {permission_id}")
|
||||
|
||||
for permission_id in seed.RISK_PERMISSIONS:
|
||||
if int(permission_id) not in seed_map:
|
||||
problems.append(f"RISK_PERMISSIONS 引用了不存在的 id {permission_id}")
|
||||
|
||||
required_risk_codes = {
|
||||
"risk:alert:read",
|
||||
"risk:alert:write",
|
||||
"risk:alert:scan",
|
||||
"risk:report:mail",
|
||||
}
|
||||
risk_codes = {
|
||||
seed_map[int(permission_id)]
|
||||
for permission_id in seed.RISK_PERMISSIONS
|
||||
if int(permission_id) in seed_map
|
||||
}
|
||||
missing_risk_codes = required_risk_codes - risk_codes
|
||||
if missing_risk_codes:
|
||||
problems.append(
|
||||
"RISK_PERMISSIONS 缺少风控权限码:"
|
||||
+ ", ".join(sorted(missing_risk_codes))
|
||||
)
|
||||
|
||||
return problems
|
||||
|
||||
|
||||
|
||||
@@ -29,13 +29,18 @@ import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.contracts import RequestContext
|
||||
from app.infrastructure.db import SessionFactory
|
||||
from app.service.auth_service import hash_password
|
||||
from app.service.identity_service import IdentityService
|
||||
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.contracts import RequestContext # noqa: E402
|
||||
from app.infrastructure.db import SessionFactory # noqa: E402
|
||||
from app.service.auth_service import hash_password # noqa: E402
|
||||
from app.service.identity_service import IdentityService # noqa: E402
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr]
|
||||
|
||||
@@ -46,12 +46,12 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
|
||||
from app.core.config import get_settings # noqa: E402
|
||||
|
||||
# (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")),
|
||||
# (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"
|
||||
|
||||
@@ -75,7 +75,7 @@ async def main() -> int:
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
now = dt.datetime.now(dt.UTC).replace(tzinfo=None)
|
||||
for code, resource, action, roles in PERMISSIONS:
|
||||
for permission_id, code, resource, action, roles in PERMISSIONS:
|
||||
await cursor.execute(
|
||||
"SELECT id FROM sys_permission WHERE permission_code = %s", (code,)
|
||||
)
|
||||
@@ -83,18 +83,21 @@ async def main() -> int:
|
||||
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),
|
||||
" (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),
|
||||
)
|
||||
await cursor.execute(
|
||||
"SELECT id FROM sys_permission WHERE permission_code = %s", (code,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
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} 已存在")
|
||||
permission_id = int(row[0])
|
||||
|
||||
for role_code in roles:
|
||||
await cursor.execute("SELECT id FROM sys_role WHERE role_code = %s", (role_code,))
|
||||
@@ -122,5 +125,5 @@ async def main() -> int:
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
sys.exit(asyncio.run(main()))
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
|
||||
@@ -5,11 +5,17 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.infrastructure.db import SessionFactory
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.infrastructure.db import SessionFactory # noqa: E402
|
||||
|
||||
ADMIN_USER_ID = 9003
|
||||
ENDPOINT_CODE = "deepseek-flash"
|
||||
@@ -53,7 +59,12 @@ async def seed() -> None:
|
||||
status='active', reviewer_id=:admin_id, reviewed_at=:now, updated_at=:now
|
||||
"""), {
|
||||
"endpoint_code": ENDPOINT_CODE,
|
||||
"capabilities": json.dumps(["chat", "intent_classification", "risk_answer"]),
|
||||
"capabilities": json.dumps([
|
||||
"chat",
|
||||
"intent_classification",
|
||||
"risk_answer",
|
||||
"text_generation",
|
||||
]),
|
||||
"data_levels": json.dumps(["internal"]),
|
||||
"admin_id": ADMIN_USER_ID,
|
||||
"now": now,
|
||||
|
||||
+39
-4
@@ -26,13 +26,19 @@ id BETWEEN 9001 AND 9099` 会删掉该号段内**所有**权限(包括别的
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.contracts import RequestContext
|
||||
from app.infrastructure.db import SessionFactory
|
||||
from app.service.identity_service import IdentityService
|
||||
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.contracts import RequestContext # noqa: E402
|
||||
from app.infrastructure.db import SessionFactory # noqa: E402
|
||||
from app.service.identity_service import IdentityService # noqa: E402
|
||||
|
||||
CUSTOMER_USER = "9001"
|
||||
RISK_USER = "9002"
|
||||
@@ -94,6 +100,14 @@ PERMISSIONS: tuple[tuple[int, str, str, str, str], ...] = (
|
||||
(9044, "memory:candidate:confirm", "memory:candidate", "confirm", "self"),
|
||||
(9045, "memory:candidate:review", "memory:candidate", "review", "all"),
|
||||
(9046, "handover:read", "handover", "read", "all"),
|
||||
# ---- 9047-9050:风控模块四个权限码 ----
|
||||
# 风控 Service 和 Agent 工具都按这四个权限码失败关闭。此前依赖
|
||||
# `grant_risk_permissions.py` 临时补种,重跑本脚本时会删除 9001-9099 号段内的
|
||||
# 风控权限,导致登录成功后风控接口全部 403。这里纳入唯一权限定义源。
|
||||
(9047, "risk:alert:read", "risk", "alert", "all"),
|
||||
(9048, "risk:alert:write", "risk", "alert", "all"),
|
||||
(9049, "risk:alert:scan", "risk", "alert", "all"),
|
||||
(9050, "risk:report:mail", "risk", "report", "all"),
|
||||
)
|
||||
|
||||
# 客户:业务侧自助能力(自己的会话、反馈、转人工、自己的记忆画像)。
|
||||
@@ -104,7 +118,10 @@ CUSTOMER_PERMISSIONS = (
|
||||
9044,
|
||||
)
|
||||
# 风控专员:业务侧只读 + 跨客户记忆 + 审计只读,不含配置写权限。
|
||||
RISK_PERMISSIONS = (9001, 9002, 9003, 9010, 9011, 9012)
|
||||
RISK_PERMISSIONS = (
|
||||
9001, 9002, 9003, 9010, 9011, 9012,
|
||||
9047, 9048, 9049, 9050,
|
||||
)
|
||||
# 平台管理员:管理面全套(配置发布四态 + 模型端点 + 审计)。
|
||||
ADMIN_PERMISSIONS = tuple(row[0] for row in PERMISSIONS)
|
||||
|
||||
@@ -159,6 +176,24 @@ async def seed() -> None:
|
||||
text("DELETE FROM sys_role_permission WHERE role_id IN (9001,9002,9003)")
|
||||
)
|
||||
await session.execute(text("DELETE FROM sys_user_role WHERE user_id IN (9001,9002,9003)"))
|
||||
# 风控权限在旧环境中可能由 `grant_risk_permissions.py` 自动分配了任意主键。
|
||||
# 先按 permission_code 清掉角色绑定和旧主键,保证本次能用固定号段重建;
|
||||
# 否则 `risk:report:mail` 已存在于 9001-9099 之外时会触发 permission_code 唯一键冲突。
|
||||
await session.execute(text("""
|
||||
DELETE rp FROM sys_role_permission rp
|
||||
JOIN sys_permission p ON p.id = rp.permission_id
|
||||
WHERE p.permission_code IN (
|
||||
'risk:alert:read', 'risk:alert:write',
|
||||
'risk:alert:scan', 'risk:report:mail'
|
||||
)
|
||||
"""))
|
||||
await session.execute(text("""
|
||||
DELETE FROM sys_permission
|
||||
WHERE permission_code IN (
|
||||
'risk:alert:read', 'risk:alert:write',
|
||||
'risk:alert:scan', 'risk:report:mail'
|
||||
)
|
||||
"""))
|
||||
await session.execute(text("DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099"))
|
||||
await session.execute(text("DELETE FROM sys_role WHERE id IN (9001,9002,9003)"))
|
||||
# 不再 DELETE sys_user:投顾域的 advisor_profile_tag 等表用 FK 引用它,库里有数据引用
|
||||
|
||||
@@ -28,11 +28,16 @@ import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.infrastructure.db import SessionFactory
|
||||
from app.service.auth_service import hash_password
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.infrastructure.db import SessionFactory # noqa: E402
|
||||
from app.service.auth_service import hash_password # noqa: E402
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr]
|
||||
|
||||
Reference in New Issue
Block a user