"""造 RBAC 测试数据,验证真实链路身份加载与越权拦截。 数据使用 9001/9002/9003 号段(角色、权限分别用 9001 起的独立号段),便于清理, 不影响业务数据。 为什么需要这份种子:权限码必须与**代码实际声明**一致——工具声明写在 `app/service/agent/bootstrap.py`(`check_suitability` 需 `suitability:read`、 `query_fund_quote` 需 `fund:quote:read`),接口校验写在各 Service 的 `AuthorizationService.require(...)`。种子缺权限的后果不是"少测一项",而是 "功能明明实现了却一直 403",让验收与集成测试得出错误结论。 """ from __future__ import annotations import asyncio from datetime import UTC, datetime, timedelta from sqlalchemy import text from app.core.contracts import RequestContext from app.infrastructure.db import SessionFactory from app.service.identity_service import IdentityService CUSTOMER_USER = "9001" RISK_USER = "9002" ADMIN_USER = "9003" # (权限 id, 权限码, resource, action, data_scope) PERMISSIONS: tuple[tuple[int, str, str, str, str], ...] = ( (9001, "agent:run", "agent", "run", "self"), (9002, "suitability:read", "suitability", "read", "self"), (9003, "fund:quote:read", "fund", "quote", "self"), (9004, "conversation:create", "conversation", "create", "self"), (9005, "conversation:close", "conversation", "close", "self"), (9006, "conversation:feedback", "conversation", "feedback", "self"), (9007, "agent:cancel", "agent", "cancel", "self"), (9008, "handover:create", "handover", "create", "self"), (9009, "memory:read:self", "memory", "read", "self"), (9010, "memory:read:customer", "memory", "read", "own_customers"), (9011, "knowledge:reference:read", "knowledge", "reference", "self"), (9012, "audit:read", "audit", "read", "all"), (9013, "config:read", "config", "read", "all"), (9014, "config:write", "config", "write", "all"), (9015, "config:review", "config", "review", "all"), (9016, "config:activate", "config", "activate", "all"), (9017, "model-endpoint:manage", "model-endpoint", "manage", "all"), (9018, "investment-goal:write:self", "investment-goal", "write", "self"), (9019, "investment-goal:confirm:self", "investment-goal", "confirm", "self"), (9020, "investment-goal:read:self", "investment-goal", "read", "self"), (9021, "product-recommendation:generate:self", "product-recommendation", "generate", "self"), (9022, "product-recommendation:read:self", "product-recommendation", "read", "self"), (9023, "portfolio-analysis:read:self", "portfolio-analysis", "read", "self"), (9024, "asset-allocation:generate:self", "asset-allocation", "generate", "self"), (9025, "profile-governance:read", "profile-governance", "read", "all"), (9026, "profile-governance:review", "profile-governance", "review", "all"), (9027, "investment-goal:review", "investment-goal", "review", "all"), (9028, "investment-goal:publish", "investment-goal", "publish", "all"), (9029, "product-recommendation:review", "product-recommendation", "review", "all"), (9030, "product-recommendation:publish", "product-recommendation", "publish", "all"), (9031, "asset-allocation:backtest", "asset-allocation", "backtest", "all"), (9032, "product-comparison:read:self", "product-comparison", "read", "self"), ) # 客户:业务侧自助能力(自己的会话、反馈、转人工、自己的记忆画像)。 CUSTOMER_PERMISSIONS = ( 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009, 9011, 9018, 9019, 9020, 9021, 9022, 9023, 9024, 9032, ) # 风控专员:业务侧只读 + 跨客户记忆 + 审计只读,不含配置写权限。 RISK_PERMISSIONS = (9001, 9002, 9003, 9010, 9011, 9012) # 平台管理员:管理面全套(配置发布四态 + 模型端点 + 审计)。 ADMIN_PERMISSIONS = tuple(row[0] for row in PERMISSIONS) ROLES: tuple[tuple[int, str, str], ...] = ( (9001, "customer", "客户"), (9002, "risk_operator", "风控专员"), (9003, "admin", "平台管理员"), ) USERS: tuple[tuple[int, str, str, str], ...] = ( (9001, "T-CUST", "cust_t", "customer"), (9002, "T-RISK", "risk_t", "employee"), (9003, "T-ADMIN", "admin_t", "employee"), ) GRANTS: tuple[tuple[int, tuple[int, ...]], ...] = ( (9001, CUSTOMER_PERMISSIONS), (9002, RISK_PERMISSIONS), (9003, ADMIN_PERMISSIONS), ) async def seed() -> None: now = datetime.now(UTC).replace(tzinfo=None) # MySQL 的 DATETIME(0) 会把微秒四舍五入到秒:若用"当前时间"写 assigned_at, # 进位后可能落在未来,使紧随其后的 `assigned_at <= now` 授权校验判定 # "尚未生效",于是刚种好的账号一个权限都拿不到(表现为 roles=() 而非报错)。 # 往前留 5 秒,彻底避开这个舍入窗口。 effective_at = now - timedelta(seconds=5) user_ids = tuple(user[0] for user in USERS) role_ids = tuple(role[0] for role in ROLES) async with SessionFactory() as session: await session.execute( 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)")) 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)")) for user_id, user_no, user_name, user_type in USERS: await session.execute( text( "INSERT INTO sys_user (id, user_no, username, password_hash, user_type," " professional_investor_status, fund_account_status, status," " created_at, updated_at)" " VALUES (:id,:no,:name,'x',:type,'none','closed','正常',:now,:now)" " ON DUPLICATE KEY UPDATE user_no=VALUES(user_no), username=VALUES(username)," " user_type=VALUES(user_type), status=VALUES(status), updated_at=VALUES(updated_at)" ), {"id": user_id, "no": user_no, "name": user_name, "type": user_type, "now": now}, ) for role_id, role_code, role_name in ROLES: await session.execute( text( "INSERT INTO sys_role (id, role_code, role_name, status, created_at, updated_at)" " VALUES (:id,:code,:name,'active',:now,:now)" ), {"id": role_id, "code": role_code, "name": role_name, "now": now}, ) for permission_id, code, resource, action, scope in PERMISSIONS: await session.execute( text( "INSERT INTO sys_permission" " (id, permission_code, resource, action, data_scope, created_at, updated_at)" " VALUES (:id,:code,:resource,:action,:scope,:now,:now)" ), {"id": permission_id, "code": code, "resource": resource, "action": action, "scope": scope, "now": now}, ) for user_id, role_id in zip(user_ids, role_ids, strict=True): await session.execute( text( "INSERT INTO sys_user_role (user_id, role_id, assigned_at)" " VALUES (:user,:role,:now)" ), {"user": user_id, "role": role_id, "now": effective_at}, ) for role_id, permission_ids in GRANTS: for permission_id in permission_ids: await session.execute( text( "INSERT INTO sys_role_permission (role_id, permission_id, created_at)" " VALUES (:role,:perm,:now)" ), {"role": role_id, "perm": permission_id, "now": now}, ) await session.commit() async def verify() -> None: service = IdentityService() for label, user_id in (("customer", CUSTOMER_USER), ("risk_operator", RISK_USER), ("admin", ADMIN_USER)): context = await service.resolve(RequestContext(user_id=user_id, trace_id="verify-trace")) print( f"{label:14s} roles={context.roles} permissions={sorted(context.permissions)} " f"portal={context.portal} data_scope={context.data_scope}" ) customer = await service.resolve( RequestContext(user_id=CUSTOMER_USER, trace_id="verify-trace") ) missing = {"agent:run", "suitability:read", "fund:quote:read"} - set(customer.permissions) if missing: raise SystemExit(f"客户角色缺少公共工具所需权限:{sorted(missing)}") print("verified: customer can pass public tool authorization checks") async def main() -> None: await seed() await verify() if __name__ == "__main__": asyncio.run(main())