From d35d121261863eae28853bf40922c125177b6b63 Mon Sep 17 00:00:00 2001 From: Administrator Date: Thu, 25 Jun 2026 17:37:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E7=99=BB=E5=BD=95=E7=99=BD?= =?UTF-8?q?=E5=90=8D=E5=8D=95=E4=B9=9F=E6=8B=A6=E3=80=8C=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E9=97=AE=E7=AD=94=E3=80=8D(=E5=B7=B2=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E7=9A=84=20run=20=E6=8E=A5=E5=8F=A3=E6=8C=89=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=94=BE=E8=A1=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 开关开启时,未放行用户(非 admin)即便持旧 token,直接调发起问答的 run 接口 (/api/threads/{id}/runs[/stream|/wait]、/api/runs/{stream|wait})也返回 403, 不止拦登录。在 AuthMiddleware 解析出 user 后仅对问答 run 请求复用 enforce_user_approved;GET 列表/join/cancel/feedback 及普通浏览不受影响; 内部/渠道调用与无鉴权 /api/open/chat 不在此范围。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../backend/app/gateway/auth_middleware.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/offline-backend-20260512/backend/app/gateway/auth_middleware.py b/offline-backend-20260512/backend/app/gateway/auth_middleware.py index e6c84b4d..9c039c50 100644 --- a/offline-backend-20260512/backend/app/gateway/auth_middleware.py +++ b/offline-backend-20260512/backend/app/gateway/auth_middleware.py @@ -9,6 +9,7 @@ owner filtering works automatically via the sentinel pattern. Fine-grained permission checks remain in authz.py decorators. """ +import re from collections.abc import Callable from fastapi import HTTPException, Request, Response @@ -64,6 +65,20 @@ def _is_public(path: str) -> bool: return any(path.startswith(prefix) for prefix in _PUBLIC_PATH_PREFIXES) +# 「发起问答」的 run 创建接口(会真正调用大模型): +# POST /api/threads/{id}/runs[/stream|/wait] +# POST /api/runs/{stream|wait} +# (含 nginx 未改写到的 /api/langgraph/... 前缀,双保险) +# 只匹配这些「提交问答」的 POST,不碰 GET 列表 / join / cancel / feedback 等, +# 这样登录白名单开启后,未放行用户(凭旧 token)无法再用接口问答,但普通浏览不受影响。 +_QA_RUN_RE = re.compile(r"^/api/(?:langgraph/)?(?:threads/[^/]+/)?runs(?:/stream|/wait)?$") + + +def _is_qa_run_request(method: str, path: str) -> bool: + """True for an authenticated「发起问答」run-creation request (see ``_QA_RUN_RE``).""" + return method.upper() == "POST" and bool(_QA_RUN_RE.match(path.rstrip("/"))) + + class AuthMiddleware(BaseHTTPMiddleware): """Strict auth gate: reject requests without a valid session. @@ -148,6 +163,18 @@ class AuthMiddleware(BaseHTTPMiddleware): # JWT-decode + DB-lookup pipeline a second time per request). request.state.user = user request.state.auth = AuthContext(user=user, permissions=_ALL_PERMISSIONS) + + # 登录白名单:开关开启时,未放行用户(非 admin)不仅被拦在登录外,连「接口 + # 问答」也封掉——已拿到 token 的未放行用户用旧会话直接调问答接口同样被拒。 + # 仅作用于「发起问答」的 run 接口;内部/渠道调用(internal_user)不受限。 + if internal_user is None and _is_qa_run_request(request.method, app_relative_path(request)): + from app.gateway.routers.auth import enforce_user_approved + + try: + enforce_user_approved(user) + except HTTPException as exc: + return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}) + token = set_current_user(user) try: return await call_next(request)