fix(proxy): run the openai passthrough gate on the canonical forward path

Forwarding adds the implicit /v1 for OpenAI when the caller omitted it
(_join_url_paths), so /openai_passthrough/assistants reached the same
upstream management routes as /v1/assistants while dodging the prefix
list, as reported in review. The gate now strips the version prefix
before matching management suffixes; both spellings are one decision.
Regression tests cover list/delete of the shared assistants plus both
inference spellings staying open.

Generated-by: termag
This commit is contained in:
JackDanger 2026-10-17 23:44:21 +00:00 • committed by Jack Danger
parent a861c7788c
commit 1f0ea05fb5
2 changed files with 68 additions and 9 deletions

View file

@ -3500,16 +3500,25 @@ async def deepgram_listen_websocket_route(
_OPENAI_PASSTHROUGH_MANAGEMENT_PREFIXES: Final[tuple[str, ...]] = (
"/v1/files",
"/v1/batches",
"/v1/vector_stores",
"/v1/fine_tuning",
"/v1/threads",
"/v1/assistants",
"/v1/models",
"/files",
"/batches",
"/vector_stores",
"/fine_tuning",
"/threads",
"/assistants",
"/models",
)
def _strip_openai_version_prefix(normalized_path: str) -> str:
"""Forwarding adds the implicit /v1 for OpenAI only when the caller omitted
it (see _join_url_paths), so both spellings of the same request must hit
the gate: `/v1/assistants` and `/assistants` are the same upstream call."""
if normalized_path == "/v1" or normalized_path.startswith("/v1/"):
return normalized_path[len("/v1"):]
return normalized_path
def openai_proxy_gate(
*,
user_api_key_dict: UserAPIKeyAuth,
@ -3518,11 +3527,14 @@ def openai_proxy_gate(
"""The /openai passthrough speaks to the shared OpenAI account with
the proxy's credentials. Inference paths stay available to granted keys;
object-management paths address the shared account with no tenant records,
so they are proxy-admin-only."""
so they are proxy-admin-only. The check runs on the canonical forward path:
a caller that omits the /v1 segment reaches the same management routes once
the forwarder injects the version prefix."""
if is_proxy_admin(user_api_key_dict):
return
normalized: Final = posixpath.normpath(f"/{endpoint_path.strip('/')}")
if normalized.startswith(_OPENAI_PASSTHROUGH_MANAGEMENT_PREFIXES):
canonical: Final = _strip_openai_version_prefix(normalized)
if canonical.startswith(_OPENAI_PASSTHROUGH_MANAGEMENT_PREFIXES):
raise HTTPException(
status_code=403,
detail={

View file

@ -568,3 +568,50 @@ def test_openai_proxy_gate_allows_inference_paths_for_any_key():
user_api_key_dict=UserAPIKeyAuth(api_key="sk-team", user_role=None, user_id="u1"),
endpoint_path="/v1/responses",
)
def test_openai_proxy_gate_blocks_prefixless_versionless_management_paths():
"""A caller may omit the implicit /v1 segment that forwarding later adds
(_join_url_paths), so the gate must canonicalize before matching — this is
the bypass the BerriAI PR reviewer caught on the original version."""
from fastapi import HTTPException
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
openai_proxy_gate,
)
for path in ("/assistants", "/assistants/asst_123", "/files/file-1", "/threads", "/models/gpt-5.1"):
with pytest.raises(HTTPException) as exc:
openai_proxy_gate(
user_api_key_dict=UserAPIKeyAuth(api_key="sk-team", user_role=None, user_id="u1"),
endpoint_path=path,
)
assert int(getattr(exc.value, "status_code", 0)) == 403
def test_openai_proxy_gate_canonicalizes_the_v1_spelling_identically():
"""The /v1-prefixed and prefixless shapes are the same decision."""
from litellm.proxy._types import UserAPIKeyAuth
openai_proxy_gate = _openai_proxy_gate_module()
# both spellings of the inference surface stay open
openai_proxy_gate(
user_api_key_dict=UserAPIKeyAuth(api_key="sk-team", user_role=None, user_id="u1"),
endpoint_path="/chat/completions",
)
openai_proxy_gate(
user_api_key_dict=UserAPIKeyAuth(api_key="sk-team", user_role=None, user_id="u1"),
endpoint_path="/v1/chat/completions",
)
# both spellings of the management surface are gated
for path in ("/assistants", "/v1/assistants", "/files", "/v1/files"):
try:
openai_proxy_gate(
user_api_key_dict=UserAPIKeyAuth(api_key="sk-team", user_role=None, user_id="u1"),
endpoint_path=path,
)
except Exception:
continue
raise AssertionError(f"expected the gate to fire for {path!r}")