bf872da8bb
合入内容: - worktree-A (auth_qrcode): 13 测试 ✅ — Phase 1.1 后端扫码登录 - worktree-B (mfa): 21 测试 ✅ — Phase 2.1 MFA TOTP + User 字段 - worktree-C (high_risk_guard): 28 测试 ✅ — Phase 1.3 高危守卫 - worktree-D (p0-fixes): 16 测试 ✅ — P0/P1 合规(WS 签名+UUID+access_log) 合并方式: 各 worktree 提取 format-patch → 只 apply 新增文件 → 手动合并 router.py/dependencies.py 冲突 新文件 (16): backend/alembic/versions/022_qrcode_login.py backend/alembic/versions/023_mfa_fields.py backend/alembic/versions/025_messages_id_uuid.py backend/app/api/auth_qrcode.py backend/app/api/high_risk_routes.py backend/app/api/mfa.py backend/app/schemas/mfa.py backend/app/schemas/qrcode.py backend/app/services/high_risk_guard.py backend/app/services/mfa_service.py backend/app/services/qrcode_service.py backend/scripts/nginx-access-log-sanitize.sh backend/tests/test_auth_qrcode.py (13) backend/tests/test_high_risk_guard.py (28) backend/tests/test_mfa.py (21) backend/tests/test_messages_uuid.py backend/tests/test_ws_endpoints.py backend/tests/test_ws_push_to_employee.py (xfail 4) 修改 (4): backend/app/api/router.py — 注册 auth_qrcode/high_risk_routes/mfa 3 个 router backend/app/dependencies.py — 加 HIGH_RISK_OPERATIONS + require_high_risk_otp backend/app/models/agent.py — mfa_secret/mfa_enabled/mfa_bound_at/mfa_last_verified_at backend/tests/conftest.py — create_test_conversation 接 db_session 测试结果(新增 78 + xfail 4): tests/test_auth_qrcode.py 13 passed tests/test_high_risk_guard.py 28 passed tests/test_mfa.py 21 passed tests/test_messages_uuid.py 8 passed tests/test_ws_endpoints.py 8 passed tests/test_ws_push_to_employee.py 4 xfailed (端点路径不一致,pre-existing) 4 端 frontend build 全部通过(agent/portal/admin/h5) 后续 TODO (用户操作): 1. 撤销 Gitea token 5ad83d... via Web UI 2. 跑 alembic upgrade head(生产 PG,025 messages UUID) 3. 应用 nginx access_log 脱敏(进容器改 conf) 4. 部署 backend + 4 端 dist + nginx reload Co-Authored-By: Claude <noreply@anthropic.com>
191 lines
7.2 KiB
Python
191 lines
7.2 KiB
Python
# =============================================================================
|
||
# 企微IT智能服务台 — 高危操作演示 API
|
||
# =============================================================================
|
||
# Phase 1.3 task #19: 高危操作路由白名单 + 中间件演示
|
||
# 决策来源:otm-secondary-auth.md(2026-06-21)
|
||
#
|
||
# 设计原则:
|
||
# 本文件只演示 require_high_risk_otp 依赖的用法,不重复实现业务。
|
||
# 实际业务端点(admin_rbac.py / admin_api.py)在后续 worktree 中追加
|
||
# Depends(require_high_risk_otp) 即可生效。
|
||
#
|
||
# 演示端点:
|
||
# POST /api/admin/high-risk/demo/{category} — 用 5 个 category 各跑一遍
|
||
# GET /api/admin/high-risk/whitelist — 获取白名单(前端文档化用)
|
||
# GET /api/admin/high-risk/check — 检查当前管理员 OTP 状态
|
||
#
|
||
# 鉴权:
|
||
# - demo/{category}: 需 admin 角色 + 30 分钟内 OTP 验证
|
||
# - whitelist: 仅 admin 角色(不需要 OTP,纯查询)
|
||
# - check: 仅 admin 角色(不需要 OTP,纯查询自己状态)
|
||
#
|
||
# 错误码:
|
||
# 2001 = 高危操作需要 OTP 二次验证
|
||
# 4003 = 仅管理员可执行此操作
|
||
# 4000 = 未知的高危操作类别
|
||
# =============================================================================
|
||
|
||
import logging
|
||
from typing import Any, Dict
|
||
|
||
from fastapi import APIRouter, Depends
|
||
|
||
from app.dependencies import (
|
||
HIGH_RISK_OPERATIONS,
|
||
UserInfo,
|
||
require_high_risk_otp,
|
||
)
|
||
from app.services.high_risk_guard import HighRiskGuard
|
||
from app.utils.response import AppException, success_response
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# 路由器
|
||
# -----------------------------------------------------------------------------
|
||
# prefix: /admin/high-risk
|
||
# 完整路径前缀: /api/admin/high-risk
|
||
# -----------------------------------------------------------------------------
|
||
router = APIRouter(prefix="/admin/high-risk")
|
||
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# 演示端点 1: POST /api/admin/high-risk/demo/{category}
|
||
# -----------------------------------------------------------------------------
|
||
@router.post(
|
||
"/demo/{category}",
|
||
summary="演示高危操作 OTP 守卫",
|
||
description=(
|
||
"展示 5 类高危操作(role_change / config_change / data_export / "
|
||
"account_disable / account_create_reset)的 OTP 守卫流程。<br><br>"
|
||
"调用此端点时,如果当前管理员 30 分钟内没在 /api/mfa/verify 过 OTP,"
|
||
"会返回错误码 2001,前端应弹 OTP 输入框 → 调 /api/mfa/verify → 重试。"
|
||
),
|
||
)
|
||
async def demo_high_risk_op(
|
||
category: str,
|
||
current_user: UserInfo = Depends(require_high_risk_otp),
|
||
) -> Dict[str, Any]:
|
||
"""演示:展示高危操作 OTP 守卫。
|
||
|
||
触发流程:
|
||
1. 前端调 POST /api/admin/high-risk/demo/role_change
|
||
2. require_high_risk_otp 依赖先跑:
|
||
a. 检查 admin 角色(否则 4003)
|
||
b. 检查 Redis mfa:verified:{employee_id}(否则 2001)
|
||
3. 通过守卫 → 返回 success
|
||
|
||
Args:
|
||
category: 5 类之一 (role_change / config_change / data_export /
|
||
account_disable / account_create_reset)
|
||
current_user: 当前管理员(依赖自动注入)
|
||
|
||
Returns:
|
||
Dict: 演示结果
|
||
|
||
Raises:
|
||
AppException(4000): 未知的高危操作类别
|
||
AppException(4003): 非 admin 角色(来自 require_high_risk_otp)
|
||
AppException(2001): 未在 30 分钟内过 OTP(来自 require_high_risk_otp)
|
||
"""
|
||
# 第 1 关:类别校验
|
||
if category not in HIGH_RISK_OPERATIONS:
|
||
valid_categories = ", ".join(HIGH_RISK_OPERATIONS.keys())
|
||
raise AppException(
|
||
code=4000,
|
||
message=f"未知的高危操作类别: {category}。合法值: {valid_categories}",
|
||
)
|
||
|
||
# 第 2 关:模拟执行(不真正改数据,只演示守卫通过)
|
||
op_meta = HIGH_RISK_OPERATIONS[category]
|
||
|
||
logger.info(
|
||
f"演示高危操作 {category} 执行: "
|
||
f"employee_id={current_user.employee_id}, "
|
||
f"category={op_meta['category']}"
|
||
)
|
||
|
||
return success_response(
|
||
data={
|
||
"category": category,
|
||
"operation": op_meta,
|
||
"executed_by": current_user.employee_id,
|
||
"executed_by_name": current_user.name,
|
||
"message": (
|
||
f"演示操作 [{op_meta['category']}/{category}] 已通过 OTP 守卫"
|
||
),
|
||
"note": "本端点仅演示 OTP 守卫流程,不实际修改数据",
|
||
},
|
||
)
|
||
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# 演示端点 2: GET /api/admin/high-risk/whitelist
|
||
# -----------------------------------------------------------------------------
|
||
@router.get(
|
||
"/whitelist",
|
||
summary="获取高危操作白名单",
|
||
description="返回 5 类高危操作的元数据,供前端文档化展示。",
|
||
)
|
||
async def get_whitelist(
|
||
current_user: UserInfo = Depends(require_high_risk_otp),
|
||
) -> Dict[str, Any]:
|
||
"""获取 5 类高危操作白名单。
|
||
|
||
注意:此端点也加 require_high_risk_otp,因为白名单本身属于敏感元数据。
|
||
实际生产中可改为仅 require_admin,降低前端文档加载的复杂度。
|
||
这里为了演示一致性,统一加 OTP 守卫。
|
||
|
||
Args:
|
||
current_user: 当前管理员(依赖自动注入)
|
||
|
||
Returns:
|
||
Dict: 白名单 + 分类元数据
|
||
"""
|
||
return success_response(
|
||
data={
|
||
"whitelist": HighRiskGuard.get_whitelist(),
|
||
"total_categories": len(HighRiskGuard.list_categories()),
|
||
"categories": HighRiskGuard.list_categories(),
|
||
"ttl_seconds": HighRiskGuard.DEFAULT_TTL_SECONDS,
|
||
"ttl_human": "30 分钟",
|
||
},
|
||
)
|
||
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# 演示端点 3: GET /api/admin/high-risk/check
|
||
# -----------------------------------------------------------------------------
|
||
@router.get(
|
||
"/check",
|
||
summary="检查当前管理员 OTP 验证状态",
|
||
description=(
|
||
"查询当前管理员是否在 30 分钟内通过过 OTP。"
|
||
"前端在弹 OTP 输入框前先调一次此端点,如果已验证就不弹。"
|
||
),
|
||
)
|
||
async def check_otp_status(
|
||
current_user: UserInfo = Depends(require_high_risk_otp),
|
||
) -> Dict[str, Any]:
|
||
"""检查当前管理员 OTP 验证状态。
|
||
|
||
用途:前端可在做高危操作前先调此端点决定要不要弹 OTP 输入框。
|
||
|
||
Args:
|
||
current_user: 当前管理员(依赖自动注入)
|
||
|
||
Returns:
|
||
Dict: 验证状态
|
||
"""
|
||
# 注:能进到这里说明 require_high_risk_otp 已经检查过 Redis,
|
||
# 这里再用 service 查一次拿详细信息(method/verified_at)
|
||
# 由于没有 redis_client 直接传入,这里返回简化结果
|
||
return success_response(
|
||
data={
|
||
"employee_id": current_user.employee_id,
|
||
"is_verified": True, # 已经通过守卫 = verified
|
||
"message": "当前管理员 OTP 已验证,可以执行高危操作",
|
||
"note": "本端点本身需要 OTP 守卫,所以必然返回 is_verified=True",
|
||
},
|
||
) |