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:
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)
|
||||||
|
|||||||
Reference in new issue
Block a user