"""API 层依赖注入。 从 ``app.state`` 取 lifespan 装配的共享运行时(乾坤沙箱客户端 / EventStore)。 CRUD 类路由直接调用 ``services`` 函数(函数内部自管 ``AsyncSessionLocal``),不在此注入 session。 """ from __future__ import annotations import hmac from fastapi import Depends, HTTPException, Request from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.db.session import get_session from app.integrations.sandbox.client import SandboxClient from app.modules.runtime.events import EventStore from app.schemas.auth import CurrentUser as CurrentUserSchema from app.services.auth import decode_token, get_user_by_id, get_user_permissions def _unavailable(message: str) -> HTTPException: return HTTPException( status_code=503, detail={"error": {"code": "service_unavailable", "message": message}}, ) def get_sandbox_client(request: Request) -> SandboxClient: sandbox: SandboxClient | None = getattr(request.app.state, "baigong_sandbox", None) if sandbox is None: raise _unavailable("乾坤沙箱未就绪(SANDBOX 未配置或未启动)") return sandbox def get_event_store(request: Request) -> EventStore: store = getattr(request.app.state, "event_store", None) if store is None: raise _unavailable("事件存储未就绪") return store async def require_admin( request: Request, db: AsyncSession = Depends(get_session), ) -> None: """管理端点鉴权:JWT + is_admin 优先,ADMIN_TOKEN fallback。 JWT 模式下: - 有效 JWT 且 is_admin=True → 放行 - 有效 JWT 且 is_admin=False → 403 - 无效 JWT → 回退 ADMIN_TOKEN 校验 ADMIN_TOKEN 模式下(原 v1 逻辑): - 同现有逻辑,常数时间比较 - ADMIN_TOKEN 未配置且无 JWT → 503 fail-closed """ # 0) fail-closed:两种鉴权均未配置时直接拒绝 if not settings.jwt_secret and not settings.admin_token: raise HTTPException( status_code=503, detail={"error": {"code": "admin_not_configured", "message": "管理端点未启用。"}}, ) credential = request.headers.get("Authorization", "") if not credential.startswith("Bearer "): raise HTTPException( status_code=401, detail={"error": {"code": "unauthorized", "message": "缺少管理员凭证。"}}, ) presented = credential[len("Bearer ") :] # 1) 尝试 JWT 模式(如果 jwt_secret 已配置) if settings.jwt_secret: try: payload = decode_token(presented) if payload.get("type") == "access": user_id = payload.get("sub") user = await get_user_by_id(db, user_id) if user is None or not user.is_active: raise HTTPException( status_code=401, detail={ "error": {"code": "unauthorized", "message": "用户不存在或已禁用。"} }, ) if not user.is_admin: raise HTTPException( status_code=403, detail={ "error": {"code": "forbidden", "message": "仅管理员可访问此接口。"} }, ) return # JWT 模式鉴权通过 except HTTPException: raise except Exception: pass # JWT 无效,回退 ADMIN_TOKEN 校验 # 2) Fallback: ADMIN_TOKEN 校验(原 v1 逻辑) token = settings.admin_token if not token: raise HTTPException( status_code=503, detail={"error": {"code": "admin_not_configured", "message": "管理端点未启用。"}}, ) if not hmac.compare_digest(presented, token): raise HTTPException( status_code=401, detail={"error": {"code": "unauthorized", "message": "管理员凭证无效。"}}, ) async def get_current_user( request: Request, db: AsyncSession = Depends(get_session), ) -> CurrentUserSchema: """从 Authorization: Bearer 解析当前用户。 JWT 未配置(jwt_secret 为空)时 503 fail-closed。 """ if not settings.jwt_secret: raise HTTPException( status_code=503, detail={ "error": { "code": "service_unavailable", "message": "认证服务未配置(JWT_SECRET 为空)。", } }, ) credential = request.headers.get("Authorization", "") if not credential.startswith("Bearer "): raise HTTPException( status_code=401, detail={"error": {"code": "unauthorized", "message": "缺少认证凭证。"}}, ) token = credential[len("Bearer ") :] try: payload = decode_token(token) except Exception: raise HTTPException( status_code=401, detail={"error": {"code": "unauthorized", "message": "Token 无效或已过期。"}}, ) from None if payload.get("type") != "access": raise HTTPException( status_code=401, detail={"error": {"code": "unauthorized", "message": "Token 类型不匹配。"}}, ) user_id = payload.get("sub") user = await get_user_by_id(db, user_id) if user is None or not user.is_active: raise HTTPException( status_code=401, detail={"error": {"code": "unauthorized", "message": "用户不存在或已禁用。"}}, ) perms = await get_user_permissions(db, user.id) return CurrentUserSchema( user_id=user.id, username=user.username, email=user.email, owner_scope=user.id, is_admin=user.is_admin, permissions=perms, )