"""API 层依赖注入。 从 ``app.state`` 取 lifespan 装配的共享运行时(ToolRuntime / ConfirmationStore / 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.application.tool_runtime.runtime import ToolRuntime from app.core.config import settings from app.db.session import get_session from app.modules.runtime.events import EventStore from app.modules.safety.confirmation import ConfirmationStore 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_tool_runtime(request: Request) -> ToolRuntime: runtime: ToolRuntime | None = getattr(request.app.state, "tool_runtime", None) if runtime is None: raise _unavailable("工具运行时未就绪(沙箱未配置或未启动)") return runtime def get_optional_tool_runtime(request: Request) -> ToolRuntime | None: """宽松版:tool_runtime 未就绪时返回 None(不抛 503),供聚合视图降级呈现。""" return getattr(request.app.state, "tool_runtime", None) def get_confirmation_store(request: Request) -> ConfirmationStore: store = getattr(request.app.state, "confirmation_store", None) if store is None: raise _unavailable("确认存储未就绪") return store 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, )