feat(v0.7.1): P0 修复 + 企微 SSO + RBAC 细粒度 + audit_log

P0 修复:
- /api/ready import 错误 (_get_engine + settings.create_redis_client)
- 删 agent.otp_secret/otp_enabled 双字段 (migration 026)
- 重建 021_rbac migration (IF NOT EXISTS 兼容)

P1 新增:
- 企微 SSO (auth_wecom_sso.py, useWeChatWorkSSO composable, PortalSelect UA 检测)
- RBAC 5 角色 × 4 资源 × 4 操作 × 3 范围 (rbac_service + seed_rbac + require_permission)
- audit_log 模型 + migration 027 + 服务 + API
- 管理后台 RBAC 权限矩阵 UI (PermissionsMatrix.vue)

质量:
- pytest 405 passed / 33 pre-existing failed / 4 xfailed (v0.7.1 引入失败 = 0)
- conftest GBK patch 强制 UTF-8 读 .env
- .gitignore 排除 *.b64 (含 admin token 凭据)
- DEPLOY-v0.7.1.md 7 步 runbook + 4 坑 + 回滚预案
This commit is contained in:
Simon
2026-06-22 17:38:47 +08:00
parent 2e6ac0f0ab
commit 78f60c6857
30 changed files with 2928 additions and 49 deletions
+23 -17
View File
@@ -257,8 +257,9 @@ async def agent_login(
await db.flush()
logger.info(f"坐席登录: user_id={body.user_id}, name={body.name}")
# 2. OTP 二次验证(admin 角色且已绑定 OTP
if agent.role == "admin" and agent.otp_enabled == 1:
# 2. MFA 二次验证(admin 角色且已绑定 MFA
# v0.7.1: 用 mfa_secret/mfa_enabled 替代旧 otp_secret/otp_enabled
if agent.role == "admin" and agent.mfa_enabled:
if not body.otp_code:
# 需要 OTP 验证,返回 require_otp 标记
return success_response(data={
@@ -269,7 +270,7 @@ async def agent_login(
})
else:
# 验证 OTP 码
totp = pyotp.TOTP(agent.otp_secret)
totp = pyotp.TOTP(agent.mfa_secret)
if not totp.verify(body.otp_code, valid_window=1):
raise AppException(1006, "OTP验证码错误,请重新输入")
@@ -414,15 +415,16 @@ async def bind_agent_otp(
Dict: 二维码图片(base64)和密钥
"""
try:
# v0.7.1: 用 mfa_secret 替代 otp_secret
# 检查是否已绑定
if agent.otp_secret:
if agent.mfa_secret:
# 已绑定,返回现有密钥的二维码
totp = pyotp.TOTP(agent.otp_secret)
totp = pyotp.TOTP(agent.mfa_secret)
else:
# 生成新密钥
secret = pyotp.random_base32()
agent.otp_secret = secret
# otp_enabled 保持 0,等待首次验证后启用
agent.mfa_secret = secret
# mfa_enabled 保持 False,等待首次验证后启用
db.add(agent)
await db.flush()
totp = pyotp.TOTP(secret)
@@ -439,11 +441,11 @@ async def bind_agent_otp(
qr.save(buffer, format="PNG")
qr_base64 = base64.b64encode(buffer.getvalue()).decode()
logger.info(f"OTP绑定: agent={agent.user_id}, secret={agent.otp_secret[:4]}...")
logger.info(f"OTP绑定: agent={agent.user_id}, secret={agent.mfa_secret[:4]}...")
return success_response(data={
"qr_code": f"data:image/png;base64,{qr_base64}",
"secret": agent.otp_secret,
"secret": agent.mfa_secret,
})
except AppException:
@@ -475,16 +477,18 @@ async def verify_agent_otp(
result = await db.execute(stmt)
agent = result.scalars().first()
if not agent or not agent.otp_secret:
if not agent or not agent.mfa_secret:
raise AppException(1008, "请先绑定OTP")
# 验证 OTP 码
totp = pyotp.TOTP(agent.otp_secret)
totp = pyotp.TOTP(agent.mfa_secret)
if not totp.verify(body.otp_code, valid_window=1):
raise AppException(1006, "OTP验证码错误")
# 验证成功,启用 OTP
agent.otp_enabled = 1
# 验证成功,启用 MFA
agent.mfa_enabled = True
agent.mfa_bound_at = datetime.now()
agent.mfa_last_verified_at = datetime.now()
agent.updated_at = datetime.now()
db.add(agent)
await db.flush()
@@ -492,7 +496,7 @@ async def verify_agent_otp(
logger.info(f"OTP验证成功并启用: agent={agent.user_id}")
return success_response(data={
"otp_enabled": True,
"mfa_enabled": True,
"message": "OTP验证成功,已启用",
})
@@ -510,15 +514,17 @@ async def unbind_agent_otp(
):
"""解绑 OTP。
解绑后 otp_secret 和 otp_enabled 都清空。
解绑后 mfa_secret 和 mfa_enabled 都清空。
需要管理员操作。
Returns:
Dict: 解绑结果
"""
try:
agent.otp_secret = None
agent.otp_enabled = 0
agent.mfa_secret = None
agent.mfa_enabled = False
agent.mfa_bound_at = None
agent.mfa_last_verified_at = None
agent.updated_at = datetime.now()
db.add(agent)
await db.flush()