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
+104
View File
@@ -284,6 +284,110 @@ def require_admin(func):
return require_role("admin")(func)
# =============================================================================
# 细粒度权限装饰器 (v0.7.1 task #86 — RBAC 5 角色 × 4 资源 × 4 操作 × 3 范围)
# =============================================================================
# 权限字符串格式: "resource:action:scope"
# 例: "conversation:read:all"
#
# 用法:
# @router.get("/api/admin/agents")
# @require_permission("agent:read:all")
# async def list_agents(...): ...
#
# 行为:
# 1. 装饰器只检查"是否拥有权限字符串",不直接执行 DB 查询
# 2. 实际检查在 rbac_service.check_permission() 里
# 3. 用户的权限从 UserInfo.permissions 字段读(由 get_current_user 解析 token 时填入)
# =============================================================================
def require_permission(
resource: str,
action: str,
scope: str = "own",
):
"""细粒度权限验证装饰器(v0.7.1 task #86)。
Args:
resource: 资源(conversation/agent/system_config/audit_log)
action: 操作(read/create/update/delete)
scope: 数据范围(own/department/all)
Example:
@router.get("/api/admin/agents")
@require_permission("agent", "read", "all")
async def list_agents(current_user: UserInfo = Depends(get_current_user)):
...
"""
perm_string = f"{resource}:{action}:{scope}"
def decorator(func):
sig = inspect.signature(func)
params = list(sig.parameters.values())
params.append(
inspect.Parameter(
'current_user',
inspect.Parameter.KEYWORD_ONLY,
annotation=UserInfo,
default=Depends(get_current_user),
)
)
new_sig = sig.replace(parameters=params)
@wraps(func)
async def wrapper(*args, **kwargs):
current_user = kwargs.pop('current_user')
# 拉用户所有角色的 permissions
# 注: UserInfo.roles 是角色名列表,permissions 是 {role: [perm]} 字典
# 首次实现简化: 角色判断 + admin 通配符
# 完整实现需要查 DB 拉 permissions,见 rbac_service.check_permission
user_roles = set(current_user.roles or [])
# 1. admin 角色直通(通配符 *:*:all)
if "admin" in user_roles:
return await func(*args, current_user=current_user, **kwargs)
# 2. 其他角色: 走 rbac_service.check_permission
# 简化: 这里只看角色名,不查 DB(性能考虑)
# 实际生产可加缓存或预加载到 token
from app.services.rbac_service import (
ROLE_PERMISSIONS,
check_permission,
)
# 把 ROLE_PERMISSIONS 转成 {role_name: [perm_string]} 格式
user_perms_dict = {
role: [f"{r}:{a}:{s}" for (r, a, s) in perms]
for role, perms in ROLE_PERMISSIONS.items()
}
has_perm = check_permission(
user_roles=list(user_roles),
user_permissions=user_perms_dict,
required_resource=resource,
required_action=action,
required_scope=scope,
)
if not has_perm:
logger.warning(
f"用户 {current_user.employee_id} 权限不足: "
f"角色 {list(user_roles)}, 缺 {perm_string}"
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"权限不足: 需要 {perm_string}",
)
return await func(*args, current_user=current_user, **kwargs)
wrapper.__signature__ = new_sig
return wrapper
return decorator
# =============================================================================
# 高危操作 OTP 守卫依赖(Phase 1.3 task #19
# =============================================================================