Merge pull request #26930 from stuxf/codex/vector-store-tenant-guard

chore(vector stores): tighten managed store access
This commit is contained in:
yuneng-jiang
2026-05-01 14:25:51 -07:00
committed by GitHub
10 changed files with 950 additions and 87 deletions
+20 -3
View File
@@ -13,7 +13,16 @@ from pathlib import Path
from typing import Dict, Optional, Set
SNAPSHOT_FILE = Path(__file__).parent / "_lazy_openapi_snapshot.json"
HTTP_METHODS = {"delete", "get", "head", "options", "patch", "post", "put"}
HTTP_METHOD_SUFFIXES = {
"delete",
"get",
"head",
"options",
"patch",
"post",
"put",
"trace",
}
def load_snapshot() -> Optional[Dict[str, Dict]]:
@@ -90,9 +99,17 @@ def generate_snapshot() -> Dict[str, Dict]:
paths = full.get("paths", {})
_normalize_operation_ids(paths)
# Group all of a feature's routes under one tag.
for path_ops in paths.values():
for op in path_ops.values():
for path_ops in full.get("paths", {}).values():
for method, op in path_ops.items():
if isinstance(op, dict):
operation_id = op.get("operationId")
if isinstance(operation_id, str):
for suffix in HTTP_METHOD_SUFFIXES:
if operation_id.endswith(f"_{suffix}"):
op["operationId"] = (
operation_id[: -len(suffix)] + method
)
break
op["tags"] = [feat.name]
full = ensure_unique_openapi_operation_ids(full, used_operation_ids)
fragments[feat.name] = {
@@ -97,6 +97,55 @@ def _serialize_http_exception_detail(
return str(detail), None
def _collect_response_file_search_vector_store_ids(data: Dict[str, Any]) -> set[str]:
vector_store_ids: set[str] = set()
tools = data.get("tools")
if not isinstance(tools, list):
return vector_store_ids
for tool in tools:
if not isinstance(tool, dict) or tool.get("type") != "file_search":
continue
ids = tool.get("vector_store_ids") or []
if not isinstance(ids, list):
raise HTTPException(
status_code=400,
detail={
"error": "file_search.vector_store_ids must be a list of strings"
},
)
for vector_store_id in ids:
if not isinstance(vector_store_id, str) or not vector_store_id:
raise HTTPException(
status_code=400,
detail={
"error": "file_search.vector_store_ids must be a list of strings"
},
)
vector_store_ids.add(vector_store_id)
return vector_store_ids
async def _authorize_response_file_search_vector_stores(
data: Dict[str, Any],
user_api_key_dict: UserAPIKeyAuth,
) -> None:
vector_store_ids = _collect_response_file_search_vector_store_ids(data)
if not vector_store_ids:
return
from litellm.proxy.vector_store_endpoints.utils import (
assert_user_can_access_vector_store_id,
)
for vector_store_id in sorted(vector_store_ids):
await assert_user_can_access_vector_store_id(
vector_store_id=vector_store_id,
user_api_key_dict=user_api_key_dict,
)
async def _parse_event_data_for_error(event_line: Union[str, bytes]) -> Optional[int]:
"""Parses an event line and returns an error code if present, else None."""
event_line = (
@@ -791,6 +840,11 @@ class ProxyBaseLLMRequestProcessing:
version=version,
proxy_config=proxy_config,
)
if route_type in {"aresponses", "_aresponses_websocket"}:
await _authorize_response_file_search_vector_stores(
data=self.data,
user_api_key_dict=user_api_key_dict,
)
# Calculate request queue time after add_litellm_data_to_request
# which sets arrival_time in proxy_server_request
@@ -47,6 +47,8 @@ from litellm.types.passthrough_endpoints.pass_through_endpoints import (
)
from litellm.proxy.utils import is_known_model
from litellm.proxy.vector_store_endpoints.utils import (
assert_user_can_access_vector_store,
get_litellm_managed_vector_store,
is_allowed_to_call_vector_store_endpoint,
)
from litellm.secret_managers.main import get_secret_str
@@ -533,6 +535,10 @@ async def milvus_proxy_route(
)
if vector_store is None:
raise Exception(f"Vector store not found for {vector_store_name}")
await assert_user_can_access_vector_store(
vector_store=vector_store,
user_api_key_dict=user_api_key_dict,
)
litellm_params = vector_store.get("litellm_params") or {}
auth_credentials = provider_config.get_auth_credentials(
litellm_params=litellm_params
@@ -1438,6 +1444,10 @@ async def azure_proxy_route(
)
if vector_store is None:
raise Exception(f"Vector store not found for {vector_store_name}")
await assert_user_can_access_vector_store(
vector_store=vector_store,
user_api_key_dict=user_api_key_dict,
)
litellm_params = vector_store.get("litellm_params") or {}
auth_credentials = provider_config.get_auth_credentials(
litellm_params=litellm_params
@@ -1777,6 +1787,11 @@ async def _base_vertex_proxy_route(
request=request,
api_key=api_key_to_use,
)
if router_credentials is not None:
await assert_user_can_access_vector_store(
vector_store=router_credentials,
user_api_key_dict=user_api_key_dict,
)
vertex_project: Optional[str] = get_vertex_project_id_from_url(endpoint)
vertex_location: Optional[str] = get_vertex_location_from_url(endpoint)
@@ -1913,11 +1928,11 @@ async def vertex_discovery_proxy_route(
"Extracted vector store ID from endpoint: %s", vector_store_id
)
# Retrieve vector store credentials from the registry
vector_store_credentials = (
passthrough_endpoint_router.get_vector_store_credentials(
vector_store_id=vector_store_id
)
# Retrieve LiteLLM-managed vector store credentials if the datastore id
# is registered with LiteLLM. Unknown datastore ids keep the existing
# direct Vertex pass-through behavior.
vector_store_credentials = await get_litellm_managed_vector_store(
vector_store_id=vector_store_id
)
if vector_store_credentials:
@@ -1925,7 +1940,7 @@ async def vertex_discovery_proxy_route(
"Found vector store credentials for ID: %s", vector_store_id
)
else:
verbose_proxy_logger.warning(
verbose_proxy_logger.debug(
"Vector store ID %s found in endpoint but no credentials found in registry",
vector_store_id,
)
+93
View File
@@ -15,6 +15,7 @@ from fastapi.responses import ORJSONResponse
import litellm
from litellm._logging import verbose_proxy_logger
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
from litellm.proxy._types import *
from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth
from litellm.proxy.common_utils.http_parsing_utils import (
@@ -22,10 +23,88 @@ from litellm.proxy.common_utils.http_parsing_utils import (
_safe_get_request_headers,
get_form_data,
)
from litellm.proxy.vector_store_endpoints.utils import (
assert_user_can_access_vector_store_id,
)
router = APIRouter()
def _raise_vector_store_scan_depth_exceeded() -> None:
raise HTTPException(
status_code=400,
detail={
"error": f"Max depth of {DEFAULT_MAX_RECURSE_DEPTH} exceeded while scanning vector_store_id values"
},
)
def _append_payload_to_scan_stack(
payload_stack: list[tuple[Any, int]],
value: Any,
next_depth: int,
) -> None:
if isinstance(value, dict):
if next_depth > DEFAULT_MAX_RECURSE_DEPTH:
_raise_vector_store_scan_depth_exceeded()
payload_stack.append((value, next_depth))
elif isinstance(value, list):
if next_depth > DEFAULT_MAX_RECURSE_DEPTH:
if any(isinstance(item, (dict, list)) for item in value):
_raise_vector_store_scan_depth_exceeded()
return
payload_stack.append((value, next_depth))
def _collect_vector_store_ids_from_payload(payload: Any) -> set[str]:
vector_store_ids: set[str] = set()
payload_stack = [(payload, 0)]
while payload_stack:
current_payload, depth = payload_stack.pop()
if depth > DEFAULT_MAX_RECURSE_DEPTH:
_raise_vector_store_scan_depth_exceeded()
if isinstance(current_payload, dict):
for key, value in current_payload.items():
if key == "vector_store_id":
if not isinstance(value, str) or not value:
raise HTTPException(
status_code=400,
detail={
"error": "vector_store_id must be a non-empty string"
},
)
vector_store_ids.add(value)
continue
if isinstance(value, (dict, list)):
_append_payload_to_scan_stack(
payload_stack=payload_stack,
value=value,
next_depth=depth + 1,
)
elif isinstance(current_payload, list):
for item in current_payload:
_append_payload_to_scan_stack(
payload_stack=payload_stack,
value=item,
next_depth=depth + 1,
)
return vector_store_ids
async def _authorize_nested_vector_store_ids(
payload: Any,
user_api_key_dict: UserAPIKeyAuth,
) -> None:
for vector_store_id in sorted(_collect_vector_store_ids_from_payload(payload)):
await assert_user_can_access_vector_store_id(
vector_store_id=vector_store_id,
user_api_key_dict=user_api_key_dict,
)
def _build_file_metadata_entry(
response: Any,
file_data: Optional[Tuple[str, bytes, str]] = None,
@@ -385,6 +464,11 @@ async def rag_ingest(
},
)
await _authorize_nested_vector_store_ids(
payload=ingest_options,
user_api_key_dict=user_api_key_dict,
)
# Add litellm data
request_data: Dict[str, Any] = {}
request_data = await add_litellm_data_to_request(
@@ -537,11 +621,20 @@ async def rag_query(
status_code=400,
detail={"error": "retrieval_config is required"},
)
if not isinstance(retrieval_config, dict):
raise HTTPException(
status_code=400,
detail={"error": "retrieval_config must be an object"},
)
if "vector_store_id" not in retrieval_config:
raise HTTPException(
status_code=400,
detail={"error": "retrieval_config must contain 'vector_store_id'"},
)
await _authorize_nested_vector_store_ids(
payload=retrieval_config,
user_api_key_dict=user_api_key_dict,
)
# Add litellm data
request_data: Dict[str, Any] = {}
@@ -1,8 +1,6 @@
from typing import Any, Dict, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, Response
import litellm
from litellm.integrations.vector_store_integrations.vector_store_pre_call_hook import (
LiteLLM_ManagedVectorStore,
)
@@ -10,7 +8,10 @@ from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
from litellm.proxy.utils import jsonify_object
from litellm.proxy.vector_store_endpoints.utils import can_user_access_vector_store
from litellm.proxy.vector_store_endpoints.utils import (
assert_user_can_access_vector_store,
get_litellm_managed_vector_store,
)
from litellm.types.vector_stores import IndexCreateRequest
router = APIRouter()
@@ -19,24 +20,6 @@ router = APIRouter()
########################################################
async def _check_vector_store_access(
vector_store: LiteLLM_ManagedVectorStore,
user_api_key_dict: UserAPIKeyAuth,
) -> bool:
"""
Check if the user has access to the vector store.
Delegates to :func:`can_user_access_vector_store`, which honors:
- PROXY_ADMIN bypass
- legacy vector stores with no team_id
- key-level and team-level ``object_permission.vector_stores`` allowlists
- team_id match between key and store
"""
return await can_user_access_vector_store(
vector_store=vector_store, user_api_key_dict=user_api_key_dict
)
async def _update_request_data_with_litellm_managed_vector_store_registry(
data: Dict,
vector_store_id: str,
@@ -53,35 +36,27 @@ async def _update_request_data_with_litellm_managed_vector_store_registry(
Raises:
HTTPException: If user doesn't have access to the vector store
"""
if litellm.vector_store_registry is not None:
vector_store_to_run: Optional[LiteLLM_ManagedVectorStore] = (
litellm.vector_store_registry.get_litellm_managed_vector_store_from_registry(
vector_store_id=vector_store_id
vector_store_to_run: Optional[LiteLLM_ManagedVectorStore] = (
await get_litellm_managed_vector_store(vector_store_id=vector_store_id)
)
if vector_store_to_run is not None:
if user_api_key_dict is not None:
await assert_user_can_access_vector_store(
vector_store=vector_store_to_run,
user_api_key_dict=user_api_key_dict,
)
)
if vector_store_to_run is not None:
if user_api_key_dict is not None:
if not await _check_vector_store_access(
vector_store_to_run, user_api_key_dict
):
raise HTTPException(
status_code=403,
detail="Access denied: You do not have permission to access this vector store",
)
if "custom_llm_provider" in vector_store_to_run:
data["custom_llm_provider"] = vector_store_to_run.get(
"custom_llm_provider"
)
if "custom_llm_provider" in vector_store_to_run:
data["custom_llm_provider"] = vector_store_to_run.get("custom_llm_provider")
if "litellm_credential_name" in vector_store_to_run:
data["litellm_credential_name"] = vector_store_to_run.get(
"litellm_credential_name"
)
if "litellm_credential_name" in vector_store_to_run:
data["litellm_credential_name"] = vector_store_to_run.get(
"litellm_credential_name"
)
if "litellm_params" in vector_store_to_run:
litellm_params = vector_store_to_run.get("litellm_params", {}) or {}
data.update(litellm_params)
if "litellm_params" in vector_store_to_run:
litellm_params = vector_store_to_run.get("litellm_params", {}) or {}
data.update(litellm_params)
return data
@@ -121,8 +96,7 @@ async def vector_store_search(
)
data = await _read_request_body(request=request)
if "vector_store_id" not in data:
data["vector_store_id"] = vector_store_id
data["vector_store_id"] = vector_store_id
# Check for legacy vector store registry (non-managed vector stores)
data = await _update_request_data_with_litellm_managed_vector_store_registry(
@@ -1,7 +1,9 @@
import json
from typing import Any, Dict, Literal, Optional
from fastapi import HTTPException, Request
import litellm
from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import (
LiteLLM_ObjectPermissionTable,
@@ -13,6 +15,21 @@ from litellm.types.vector_stores import LiteLLM_ManagedVectorStore
from litellm.utils import ProviderConfigManager
def _normalize_litellm_params(
vector_store: LiteLLM_ManagedVectorStore,
) -> LiteLLM_ManagedVectorStore:
litellm_params = vector_store.get("litellm_params")
if isinstance(litellm_params, str):
normalized = LiteLLM_ManagedVectorStore(**dict(vector_store))
try:
parsed = json.loads(litellm_params)
normalized["litellm_params"] = parsed if isinstance(parsed, dict) else {}
except (TypeError, ValueError):
normalized["litellm_params"] = {}
return normalized
return vector_store
def _is_proxy_admin(user_api_key_dict: UserAPIKeyAuth) -> bool:
return (
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
@@ -120,6 +137,104 @@ async def can_user_access_vector_store(
return False
async def get_litellm_managed_vector_store(
vector_store_id: str,
) -> Optional[LiteLLM_ManagedVectorStore]:
"""
Resolve a LiteLLM-managed vector store from the registry or shared cache.
Provider-native vector store IDs will not be present in either location and
return None, preserving direct provider behavior while still protecting
LiteLLM-managed multi-tenant stores.
"""
if not vector_store_id:
return None
if litellm.vector_store_registry is not None:
try:
vector_store = litellm.vector_store_registry.get_litellm_managed_vector_store_from_registry(
vector_store_id=vector_store_id
)
if vector_store is not None:
return _normalize_litellm_params(vector_store)
except Exception as e:
verbose_proxy_logger.warning(
"Failed to resolve vector store id=%s from registry: %s",
vector_store_id,
e,
)
raise HTTPException(
status_code=500,
detail="Unable to validate vector store access",
) from e
try:
from litellm.proxy.auth.auth_checks import (
get_managed_vector_store_rows_by_uuids,
)
from litellm.proxy.proxy_server import (
prisma_client,
proxy_logging_obj,
user_api_key_cache,
)
if prisma_client is None:
return None
rows = await get_managed_vector_store_rows_by_uuids(
uuids=[vector_store_id],
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
proxy_logging_obj=proxy_logging_obj,
)
if not rows:
return None
return _normalize_litellm_params(
LiteLLM_ManagedVectorStore(**rows[0].model_dump())
)
except Exception as e:
verbose_proxy_logger.warning(
"Failed to resolve vector store id=%s from shared cache: %s",
vector_store_id,
e,
)
raise HTTPException(
status_code=500,
detail="Unable to validate vector store access",
) from e
async def assert_user_can_access_vector_store(
vector_store: LiteLLM_ManagedVectorStore,
user_api_key_dict: UserAPIKeyAuth,
detail: str = "Access denied: You do not have permission to access this vector store",
) -> None:
"""Raise 403 unless the caller can access the resolved vector store."""
if not await can_user_access_vector_store(vector_store, user_api_key_dict):
raise HTTPException(status_code=403, detail=detail)
async def assert_user_can_access_vector_store_id(
vector_store_id: str,
user_api_key_dict: UserAPIKeyAuth,
detail: str = "Access denied: You do not have permission to access this vector store",
) -> Optional[LiteLLM_ManagedVectorStore]:
"""
Resolve a managed vector store id and enforce ownership if it exists.
Unknown ids are treated as provider-native ids and are not rejected here.
"""
vector_store = await get_litellm_managed_vector_store(
vector_store_id=vector_store_id
)
if vector_store is not None:
await assert_user_can_access_vector_store(
vector_store=vector_store,
user_api_key_dict=user_api_key_dict,
detail=detail,
)
return vector_store
def _does_endpoint_match(endpoint_path: str, request_path: str) -> bool:
if endpoint_path in request_path:
return True
@@ -17,9 +17,11 @@ from litellm.proxy.openai_files_endpoints.common_utils import (
prepare_data_with_credentials,
)
from litellm.proxy.vector_store_endpoints.utils import (
assert_user_can_access_vector_store_id,
is_allowed_to_call_vector_store_files_endpoint,
)
from litellm.types.utils import LlmProviders
from litellm.types.vector_stores import LiteLLM_ManagedVectorStore
if TYPE_CHECKING:
from litellm.router import Router
@@ -193,6 +195,8 @@ def _update_request_data_with_litellm_managed_vector_store_registry(
data: Dict,
vector_store_id: str,
llm_router: Optional["Router"] = None,
managed_vector_store: Optional[LiteLLM_ManagedVectorStore] = None,
should_lookup_registry: bool = True,
) -> Dict:
"""
Update request data with model routing information from managed vector store.
@@ -262,23 +266,27 @@ def _update_request_data_with_litellm_managed_vector_store_registry(
return data
# Legacy path: Check vector store registry for non-managed vector stores
if litellm.vector_store_registry is not None:
# Legacy path: Check vector store registry for non-managed vector stores.
vector_store_to_run = managed_vector_store
if (
vector_store_to_run is None
and should_lookup_registry
and litellm.vector_store_registry is not None
):
vector_store_to_run = litellm.vector_store_registry.get_litellm_managed_vector_store_from_registry(
vector_store_id=vector_store_id
)
if vector_store_to_run is not None:
if "custom_llm_provider" in vector_store_to_run:
data["custom_llm_provider"] = vector_store_to_run.get(
"custom_llm_provider"
)
if "litellm_credential_name" in vector_store_to_run:
data["litellm_credential_name"] = vector_store_to_run.get(
"litellm_credential_name"
)
if "litellm_params" in vector_store_to_run:
litellm_params = vector_store_to_run.get("litellm_params", {}) or {}
data.update(litellm_params)
if vector_store_to_run is not None:
if "custom_llm_provider" in vector_store_to_run:
data["custom_llm_provider"] = vector_store_to_run.get("custom_llm_provider")
if "litellm_credential_name" in vector_store_to_run:
data["litellm_credential_name"] = vector_store_to_run.get(
"litellm_credential_name"
)
if "litellm_params" in vector_store_to_run:
litellm_params = vector_store_to_run.get("litellm_params", {}) or {}
data.update(litellm_params)
return data
@@ -363,8 +371,11 @@ async def vector_store_file_create(
)
data = await _read_request_body(request=request)
if "vector_store_id" not in data:
data["vector_store_id"] = vector_store_id
data["vector_store_id"] = vector_store_id
managed_vector_store = await assert_user_can_access_vector_store_id(
vector_store_id=vector_store_id,
user_api_key_dict=user_api_key_dict,
)
# Handle managed file IDs if present in request body
original_managed_file_id = None
@@ -375,7 +386,11 @@ async def vector_store_file_create(
# Then handle managed vector store IDs
data = _update_request_data_with_litellm_managed_vector_store_registry(
data=data, vector_store_id=vector_store_id, llm_router=llm_router
data=data,
vector_store_id=vector_store_id,
llm_router=llm_router,
managed_vector_store=managed_vector_store,
should_lookup_registry=False,
)
provider_enum = await _resolve_provider(data=data, request=request)
@@ -459,9 +474,18 @@ async def vector_store_file_list(
query_params = dict(request.query_params)
data: Dict[str, Optional[str]] = {"vector_store_id": vector_store_id}
data.update(query_params)
data["vector_store_id"] = vector_store_id
managed_vector_store = await assert_user_can_access_vector_store_id(
vector_store_id=vector_store_id,
user_api_key_dict=user_api_key_dict,
)
data = _update_request_data_with_litellm_managed_vector_store_registry(
data=data, vector_store_id=vector_store_id, llm_router=llm_router
data=data,
vector_store_id=vector_store_id,
llm_router=llm_router,
managed_vector_store=managed_vector_store,
should_lookup_registry=False,
)
provider_enum = await _resolve_provider(data=data, request=request)
@@ -541,6 +565,10 @@ async def vector_store_file_retrieve(
"vector_store_id": vector_store_id,
"file_id": file_id,
}
managed_vector_store = await assert_user_can_access_vector_store_id(
vector_store_id=vector_store_id,
user_api_key_dict=user_api_key_dict,
)
# Handle managed file IDs first
data, original_managed_file_id = _update_request_data_with_managed_file_id(
@@ -549,7 +577,11 @@ async def vector_store_file_retrieve(
# Then handle managed vector store IDs
data = _update_request_data_with_litellm_managed_vector_store_registry(
data=data, vector_store_id=vector_store_id, llm_router=llm_router
data=data,
vector_store_id=vector_store_id,
llm_router=llm_router,
managed_vector_store=managed_vector_store,
should_lookup_registry=False,
)
provider_enum = await _resolve_provider(data=data, request=request)
@@ -635,6 +667,10 @@ async def vector_store_file_content(
"vector_store_id": vector_store_id,
"file_id": file_id,
}
managed_vector_store = await assert_user_can_access_vector_store_id(
vector_store_id=vector_store_id,
user_api_key_dict=user_api_key_dict,
)
# Handle managed file IDs first
data, original_managed_file_id = _update_request_data_with_managed_file_id(
@@ -643,7 +679,11 @@ async def vector_store_file_content(
# Then handle managed vector store IDs
data = _update_request_data_with_litellm_managed_vector_store_registry(
data=data, vector_store_id=vector_store_id, llm_router=llm_router
data=data,
vector_store_id=vector_store_id,
llm_router=llm_router,
managed_vector_store=managed_vector_store,
should_lookup_registry=False,
)
provider_enum = await _resolve_provider(data=data, request=request)
@@ -729,6 +769,10 @@ async def vector_store_file_update(
data = await _read_request_body(request=request)
data["vector_store_id"] = vector_store_id
data["file_id"] = file_id
managed_vector_store = await assert_user_can_access_vector_store_id(
vector_store_id=vector_store_id,
user_api_key_dict=user_api_key_dict,
)
# Handle managed file IDs first
data, original_managed_file_id = _update_request_data_with_managed_file_id(
@@ -737,7 +781,11 @@ async def vector_store_file_update(
# Then handle managed vector store IDs
data = _update_request_data_with_litellm_managed_vector_store_registry(
data=data, vector_store_id=vector_store_id, llm_router=llm_router
data=data,
vector_store_id=vector_store_id,
llm_router=llm_router,
managed_vector_store=managed_vector_store,
should_lookup_registry=False,
)
provider_enum = await _resolve_provider(data=data, request=request)
@@ -823,6 +871,10 @@ async def vector_store_file_delete(
"vector_store_id": vector_store_id,
"file_id": file_id,
}
managed_vector_store = await assert_user_can_access_vector_store_id(
vector_store_id=vector_store_id,
user_api_key_dict=user_api_key_dict,
)
# Handle managed file IDs first
data, original_managed_file_id = _update_request_data_with_managed_file_id(
@@ -831,7 +883,11 @@ async def vector_store_file_delete(
# Then handle managed vector store IDs
data = _update_request_data_with_litellm_managed_vector_store_registry(
data=data, vector_store_id=vector_store_id, llm_router=llm_router
data=data,
vector_store_id=vector_store_id,
llm_router=llm_router,
managed_vector_store=managed_vector_store,
should_lookup_registry=False,
)
provider_enum = await _resolve_provider(data=data, request=request)
+2 -6
View File
@@ -377,15 +377,11 @@ def search(
_is_async = kwargs.pop("asearch", False) is True
# pull credentials from registry if available
vector_store_id_for_credentials = kwargs.get("vector_store_id", vector_store_id)
if (
litellm.vector_store_registry is not None
and vector_store_id_for_credentials is not None
):
if litellm.vector_store_registry is not None and vector_store_id is not None:
try:
registry_credentials = (
litellm.vector_store_registry.get_credentials_for_vector_store(
vector_store_id_for_credentials
vector_store_id
)
)
kwargs.update(registry_credentials)
@@ -156,8 +156,11 @@ async def test_update_request_data_with_litellm_managed_vector_store_registry():
vector_store_id="test_store_id"
)
# Test with no vector store registry
with patch.object(litellm, "vector_store_registry", None):
# Test with no vector store registry or DB fallback
with (
patch.object(litellm, "vector_store_registry", None),
patch("litellm.proxy.proxy_server.prisma_client", None),
):
original_data = {"existing_key": "existing_value"}
result = await _update_request_data_with_litellm_managed_vector_store_registry(
data=original_data, vector_store_id=vector_store_id
@@ -0,0 +1,540 @@
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import HTTPException, Request, Response
import litellm
from litellm.proxy._types import LiteLLM_ManagedVectorStoresTable, UserAPIKeyAuth
def _mock_request() -> MagicMock:
request = MagicMock(spec=Request)
request.headers = {}
request.method = "POST"
request.query_params = {}
request.url.path = "/v1/vector_stores/vs_path/search"
return request
@pytest.mark.asyncio
async def test_vector_store_search_forces_path_id_over_body_id():
from litellm.proxy.vector_store_endpoints.endpoints import vector_store_search
captured_data = {}
async def fake_base_process(self, **kwargs):
captured_data.update(self.data)
return {"ok": True}
request = _mock_request()
with (
patch(
"litellm.proxy.proxy_server._read_request_body",
new=AsyncMock(
return_value={
"vector_store_id": "vs_body_victim",
"query": "test",
}
),
),
patch.object(litellm, "vector_store_registry", None),
patch("litellm.proxy.proxy_server.prisma_client", None),
patch(
"litellm.proxy.vector_store_endpoints.endpoints.ProxyBaseLLMRequestProcessing.base_process_llm_request",
new=fake_base_process,
),
):
response = await vector_store_search(
request=request,
vector_store_id="vs_path_allowed",
fastapi_response=Response(),
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
)
assert response == {"ok": True}
assert captured_data["vector_store_id"] == "vs_path_allowed"
@pytest.mark.asyncio
async def test_vector_store_file_create_forces_path_id_over_body_id():
from litellm.proxy.vector_store_files_endpoints.endpoints import (
vector_store_file_create,
)
captured_data = {}
async def fake_base_process(self, **kwargs):
captured_data.update(self.data)
return {"ok": True}
mock_registry = MagicMock()
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = {
"vector_store_id": "vs_path_allowed",
"custom_llm_provider": "openai",
"team_id": "team-a",
}
request = _mock_request()
with (
patch(
"litellm.proxy.proxy_server._read_request_body",
new=AsyncMock(
return_value={
"vector_store_id": "vs_body_victim",
"file_id": "file_123",
}
),
),
patch.object(litellm, "vector_store_registry", mock_registry),
patch(
"litellm.proxy.vector_store_files_endpoints.endpoints.ProxyBaseLLMRequestProcessing.base_process_llm_request",
new=fake_base_process,
),
):
response = await vector_store_file_create(
vector_store_id="vs_path_allowed",
request=request,
fastapi_response=Response(),
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
)
assert response == {"ok": True}
assert captured_data["vector_store_id"] == "vs_path_allowed"
assert captured_data["custom_llm_provider"] == "openai"
mock_registry.get_litellm_managed_vector_store_from_registry.assert_called_once_with(
vector_store_id="vs_path_allowed"
)
@pytest.mark.asyncio
async def test_vector_store_file_create_denies_other_team_path_store():
from litellm.proxy.vector_store_files_endpoints.endpoints import (
vector_store_file_create,
)
mock_registry = MagicMock()
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = {
"vector_store_id": "vs_other_team",
"custom_llm_provider": "openai",
"team_id": "team-b",
}
request = _mock_request()
with (
patch(
"litellm.proxy.proxy_server._read_request_body",
new=AsyncMock(return_value={"file_id": "file_123"}),
),
patch.object(litellm, "vector_store_registry", mock_registry),
patch(
"litellm.proxy.vector_store_files_endpoints.endpoints.ProxyBaseLLMRequestProcessing.base_process_llm_request",
new=AsyncMock(),
) as mock_base_process,
):
with pytest.raises(HTTPException) as exc_info:
await vector_store_file_create(
vector_store_id="vs_other_team",
request=request,
fastapi_response=Response(),
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
)
assert exc_info.value.status_code == 403
mock_base_process.assert_not_called()
@pytest.mark.asyncio
async def test_rag_query_denies_nested_other_team_vector_store():
from litellm.proxy.rag_endpoints.endpoints import rag_query
mock_registry = MagicMock()
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = {
"vector_store_id": "vs_other_team",
"custom_llm_provider": "openai",
"team_id": "team-b",
}
request = _mock_request()
with (
patch(
"litellm.proxy.rag_endpoints.endpoints._read_request_body",
new=AsyncMock(
return_value={
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "hello"}],
"retrieval_config": {"vector_store_id": "vs_other_team"},
}
),
),
patch.object(litellm, "vector_store_registry", mock_registry),
patch(
"litellm.proxy.rag_endpoints.endpoints.litellm.aquery",
new=AsyncMock(),
) as mock_aquery,
):
with pytest.raises(HTTPException) as exc_info:
await rag_query(
request=request,
fastapi_response=Response(),
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
)
assert exc_info.value.status_code == 403
mock_aquery.assert_not_called()
@pytest.mark.asyncio
async def test_rag_ingest_denies_nested_other_team_vector_store():
from litellm.proxy.rag_endpoints.endpoints import rag_ingest
mock_registry = MagicMock()
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = {
"vector_store_id": "vs_other_team",
"custom_llm_provider": "openai",
"team_id": "team-b",
}
request = _mock_request()
with (
patch(
"litellm.proxy.rag_endpoints.endpoints.parse_rag_ingest_request",
new=AsyncMock(
return_value=(
{
"vector_store": {
"custom_llm_provider": "openai",
"vector_store_id": "vs_other_team",
}
},
None,
"https://example.com/file.txt",
None,
)
),
),
patch.object(litellm, "vector_store_registry", mock_registry),
patch(
"litellm.proxy.rag_endpoints.endpoints.litellm.aingest",
new=AsyncMock(),
) as mock_aingest,
):
with pytest.raises(HTTPException) as exc_info:
await rag_ingest(
request=request,
fastapi_response=Response(),
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
)
assert exc_info.value.status_code == 403
mock_aingest.assert_not_called()
def test_rag_payload_scan_rejects_excessive_nesting():
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
from litellm.proxy.rag_endpoints.endpoints import (
_collect_vector_store_ids_from_payload,
)
payload = {}
current = payload
for _ in range(DEFAULT_MAX_RECURSE_DEPTH + 1):
current["nested"] = {}
current = current["nested"]
current["vector_store_id"] = "vs_too_deep"
with pytest.raises(HTTPException) as exc_info:
_collect_vector_store_ids_from_payload(payload)
assert exc_info.value.status_code == 400
def test_rag_payload_scan_accepts_vector_store_id_at_depth_limit():
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
from litellm.proxy.rag_endpoints.endpoints import (
_collect_vector_store_ids_from_payload,
)
payload = {}
current = payload
for _ in range(DEFAULT_MAX_RECURSE_DEPTH):
current["nested"] = {}
current = current["nested"]
current["vector_store_id"] = "vs_at_limit"
assert _collect_vector_store_ids_from_payload(payload) == {"vs_at_limit"}
def test_rag_payload_scan_ignores_primitive_list_beyond_depth_limit():
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
from litellm.proxy.rag_endpoints.endpoints import (
_collect_vector_store_ids_from_payload,
)
payload = {}
current = payload
for _ in range(DEFAULT_MAX_RECURSE_DEPTH):
current["nested"] = {}
current = current["nested"]
current["labels"] = ["alpha", "beta"]
assert _collect_vector_store_ids_from_payload(payload) == set()
@pytest.mark.asyncio
async def test_responses_file_search_denies_other_team_vector_store():
from litellm.proxy.common_request_processing import (
_authorize_response_file_search_vector_stores,
)
mock_registry = MagicMock()
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = {
"vector_store_id": "vs_other_team",
"custom_llm_provider": "openai",
"team_id": "team-b",
}
with patch.object(litellm, "vector_store_registry", mock_registry):
with pytest.raises(HTTPException) as exc_info:
await _authorize_response_file_search_vector_stores(
data={
"tools": [
{
"type": "file_search",
"vector_store_ids": ["vs_other_team"],
}
]
},
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
)
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_vertex_discovery_denies_other_team_vector_store_credentials():
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
_base_vertex_proxy_route,
)
request = _mock_request()
request.method = "GET"
vector_store_credentials = {
"vector_store_id": "vs_other_team",
"custom_llm_provider": "vertex_ai",
"team_id": "team-b",
}
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.user_api_key_auth",
new=AsyncMock(return_value=UserAPIKeyAuth(team_id="team-a")),
):
with pytest.raises(HTTPException) as exc_info:
await _base_vertex_proxy_route(
endpoint="projects/p/locations/us-central1/dataStores/vs_other_team",
request=request,
fastapi_response=Response(),
get_vertex_pass_through_handler=MagicMock(),
router_credentials=vector_store_credentials,
)
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_get_managed_vector_store_uses_shared_cache_helper_for_db_fallback():
from litellm.proxy.vector_store_endpoints.utils import (
get_litellm_managed_vector_store,
)
mock_registry = MagicMock()
mock_registry.get_litellm_managed_vector_store_from_registry.return_value = None
cache_helper = AsyncMock(
return_value=[
LiteLLM_ManagedVectorStoresTable(
vector_store_id="vs_cached",
custom_llm_provider="openai",
vector_store_name=None,
vector_store_description=None,
vector_store_metadata=None,
created_at=None,
updated_at=None,
litellm_credential_name=None,
litellm_params={"api_base": "https://example.com"},
team_id="team-a",
user_id=None,
)
]
)
with (
patch.object(litellm, "vector_store_registry", mock_registry),
patch("litellm.proxy.proxy_server.prisma_client", MagicMock()),
patch("litellm.proxy.proxy_server.user_api_key_cache", MagicMock()),
patch("litellm.proxy.proxy_server.proxy_logging_obj", MagicMock()),
patch(
"litellm.proxy.auth.auth_checks.get_managed_vector_store_rows_by_uuids",
new=cache_helper,
),
):
vector_store = await get_litellm_managed_vector_store(
vector_store_id="vs_cached"
)
assert vector_store is not None
assert vector_store["vector_store_id"] == "vs_cached"
assert vector_store["team_id"] == "team-a"
cache_helper.assert_awaited_once()
@pytest.mark.asyncio
async def test_get_managed_vector_store_fails_closed_on_lookup_error():
from litellm.proxy.vector_store_endpoints.utils import (
get_litellm_managed_vector_store,
)
mock_registry = MagicMock()
mock_registry.get_litellm_managed_vector_store_from_registry.side_effect = (
RuntimeError("registry unavailable")
)
with patch.object(litellm, "vector_store_registry", mock_registry):
with pytest.raises(HTTPException) as exc_info:
await get_litellm_managed_vector_store(vector_store_id="vs_registry_only")
assert exc_info.value.status_code == 500
@pytest.mark.asyncio
async def test_vertex_discovery_allows_unregistered_provider_native_datastore_id():
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
vertex_discovery_proxy_route,
)
request = _mock_request()
request.method = "GET"
with (
patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_litellm_managed_vector_store",
new=AsyncMock(return_value=None),
) as mock_lookup,
patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints._base_vertex_proxy_route",
new=AsyncMock(return_value={"ok": True}),
) as mock_base_route,
):
response = await vertex_discovery_proxy_route(
endpoint="projects/p/locations/us-central1/dataStores/vs_unknown",
request=request,
fastapi_response=Response(),
)
assert response == {"ok": True}
mock_lookup.assert_awaited_once_with(vector_store_id="vs_unknown")
assert mock_base_route.call_args.kwargs["router_credentials"] is None
@pytest.mark.asyncio
async def test_milvus_passthrough_denies_other_team_vector_store_index():
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
milvus_proxy_route,
)
request = _mock_request()
request.url.path = "/milvus/v2/vectordb/entities/search"
index_object = MagicMock()
index_object.litellm_params.vector_store_name = "tenant-b-store"
index_object.litellm_params.vector_store_index = "tenant_b_collection"
mock_index_registry = MagicMock()
mock_index_registry.is_vector_store_index.return_value = True
mock_index_registry.get_vector_store_index_by_name.return_value = index_object
mock_vector_registry = MagicMock()
mock_vector_registry.get_litellm_managed_vector_store_from_registry_by_name.return_value = {
"vector_store_id": "vs_other_team",
"custom_llm_provider": "milvus",
"team_id": "team-b",
"litellm_params": {"api_base": "https://milvus.example.com"},
}
with (
patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config",
return_value=MagicMock(),
),
patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_request_body",
new=AsyncMock(return_value={"collectionName": "managed_index"}),
),
patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_allowed_to_call_vector_store_endpoint",
return_value=True,
),
patch.object(litellm, "vector_store_index_registry", mock_index_registry),
patch.object(litellm, "vector_store_registry", mock_vector_registry),
):
with pytest.raises(HTTPException) as exc_info:
await milvus_proxy_route(
endpoint="v2/vectordb/entities/search",
request=request,
fastapi_response=Response(),
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
)
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_azure_passthrough_denies_other_team_vector_store_index():
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
azure_proxy_route,
)
request = _mock_request()
request.url.path = "/azure/indexes/managed_index/docs/search"
index_object = MagicMock()
index_object.litellm_params.vector_store_name = "tenant-b-store"
mock_index_registry = MagicMock()
mock_index_registry.is_vector_store_index.side_effect = (
lambda vector_store_index_name: vector_store_index_name == "managed_index"
)
mock_index_registry.get_vector_store_index_by_name.return_value = index_object
mock_vector_registry = MagicMock()
mock_vector_registry.get_litellm_managed_vector_store_from_registry_by_name.return_value = {
"vector_store_id": "vs_other_team",
"custom_llm_provider": "azure_ai",
"team_id": "team-b",
"litellm_params": {"api_base": "https://azure.example.com"},
}
with (
patch("litellm.proxy.proxy_server.llm_router", MagicMock()),
patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_passthrough_request_using_router_model",
return_value=False,
),
patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.ProviderConfigManager.get_provider_vector_stores_config",
return_value=MagicMock(),
),
patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.is_allowed_to_call_vector_store_endpoint",
return_value=True,
),
patch.object(litellm, "vector_store_index_registry", mock_index_registry),
patch.object(litellm, "vector_store_registry", mock_vector_registry),
):
with pytest.raises(HTTPException) as exc_info:
await azure_proxy_route(
endpoint="indexes/managed_index/docs/search",
request=request,
fastapi_response=Response(),
user_api_key_dict=UserAPIKeyAuth(team_id="team-a"),
)
assert exc_info.value.status_code == 403