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:
@@ -0,0 +1,137 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 审计日志服务
|
||||
# =============================================================================
|
||||
# 说明: 提供 audit_log 写入/查询的统一入口
|
||||
# 用法:
|
||||
# from app.services.audit_log_service import record_audit_log
|
||||
# await record_audit_log(
|
||||
# db, employee_id="sxn", action="role_change",
|
||||
# resource="agent", resource_id="agent-001",
|
||||
# details={"role": "agent"}, request=request,
|
||||
# )
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import select, func, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.audit_log import AuditLog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def record_audit_log(
|
||||
db: AsyncSession,
|
||||
employee_id: str,
|
||||
action: str,
|
||||
resource: str,
|
||||
resource_id: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
result: str = "success",
|
||||
request: Optional[Request] = None,
|
||||
) -> AuditLog:
|
||||
"""记录一条审计日志。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
employee_id: 操作人企微 UserID,系统操作传 "system"
|
||||
action: 操作类型
|
||||
resource: 目标资源类型
|
||||
resource_id: 目标资源 ID(可选)
|
||||
details: 详细上下文 JSON(可选)
|
||||
result: success / failure / partial
|
||||
request: FastAPI Request(可选,自动取 IP + UA)
|
||||
|
||||
Returns:
|
||||
AuditLog: 写入的日志对象
|
||||
"""
|
||||
ip_address = None
|
||||
user_agent = None
|
||||
if request is not None:
|
||||
# 优先用 X-Forwarded-For / X-Real-IP(proxy 后面)
|
||||
ip_address = (
|
||||
request.headers.get("x-forwarded-for", "").split(",")[0].strip()
|
||||
or request.headers.get("x-real-ip")
|
||||
or (request.client.host if request.client else None)
|
||||
)
|
||||
user_agent = request.headers.get("user-agent")
|
||||
|
||||
log = AuditLog(
|
||||
employee_id=employee_id,
|
||||
action=action,
|
||||
resource=resource,
|
||||
resource_id=resource_id,
|
||||
details=details or {},
|
||||
result=result,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
db.add(log)
|
||||
# 注:不 commit,让调用方跟主操作一起 commit(避免日志写一半就回滚)
|
||||
return log
|
||||
|
||||
|
||||
async def list_audit_logs(
|
||||
db: AsyncSession,
|
||||
employee_id: Optional[str] = None,
|
||||
action: Optional[str] = None,
|
||||
resource: Optional[str] = None,
|
||||
from_time: Optional[datetime] = None,
|
||||
to_time: Optional[datetime] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Dict[str, Any]:
|
||||
"""查询审计日志(分页 + 多维过滤)。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
employee_id: 按操作人过滤(可选)
|
||||
action: 按操作类型过滤(可选)
|
||||
resource: 按资源类型过滤(可选)
|
||||
from_time: 起始时间(可选)
|
||||
to_time: 结束时间(可选)
|
||||
page: 页码,从 1 开始
|
||||
page_size: 每页条数,默认 50
|
||||
|
||||
Returns:
|
||||
Dict: {items: [...], total: int, page, page_size}
|
||||
"""
|
||||
stmt = select(AuditLog)
|
||||
conditions = []
|
||||
if employee_id:
|
||||
conditions.append(AuditLog.employee_id == employee_id)
|
||||
if action:
|
||||
conditions.append(AuditLog.action == action)
|
||||
if resource:
|
||||
conditions.append(AuditLog.resource == resource)
|
||||
if from_time:
|
||||
conditions.append(AuditLog.created_at >= from_time)
|
||||
if to_time:
|
||||
conditions.append(AuditLog.created_at <= to_time)
|
||||
if conditions:
|
||||
stmt = stmt.where(and_(*conditions))
|
||||
|
||||
# 倒序 + 分页
|
||||
stmt = stmt.order_by(AuditLog.created_at.desc())
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
items = result.scalars().all()
|
||||
|
||||
# 总数
|
||||
count_stmt = select(func.count()).select_from(AuditLog)
|
||||
if conditions:
|
||||
count_stmt = count_stmt.where(and_(*conditions))
|
||||
total = (await db.execute(count_stmt)).scalar() or 0
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
Reference in New Issue
Block a user