mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-27 06:13:07 +00:00
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:
parent
a861c7788c
commit
1f0ea05fb5
2 changed files with 68 additions and 9 deletions
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue