feat(auth): 登录白名单也拦「接口问答」(已登录的 run 接口按用户放行)

开关开启时,未放行用户(非 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) <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-25 17:37:47 +08:00
1 parent 0aa55eb333
commit d35d121261
1 file changed
+27
@@ -9,6 +9,7 @@ owner filtering works automatically via the sentinel pattern.
Fine-grained permission checks remain in authz.py decorators. Fine-grained permission checks remain in authz.py decorators.
""" """
import re
from collections.abc import Callable from collections.abc import Callable
from fastapi import HTTPException, Request, Response 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) 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): class AuthMiddleware(BaseHTTPMiddleware):
"""Strict auth gate: reject requests without a valid session. """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). # JWT-decode + DB-lookup pipeline a second time per request).
request.state.user = user request.state.user = user
request.state.auth = AuthContext(user=user, permissions=_ALL_PERMISSIONS) 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) token = set_current_user(user)
try: try:
return await call_next(request) return await call_next(request)