mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(azure_ai): enforce admin-only index create on the passthrough route
POST /azure_ai/indexes carries no index name, so
get_azure_ai_search_index_from_endpoint returns None,
is_vector_store_index never matches any segment, and the request falls
through to the generic Azure passthrough on the proxy's own
AZURE_API_BASE and AZURE_API_KEY without ever reaching
is_allowed_to_call_vector_store_endpoint. A non-admin could therefore
create a Search index whenever AZURE_API_BASE points at the Search
service.
The earlier lifecycle commit made this look covered. Its test asserts
that POST /indexes?api-version=... is refused with "Only proxy admins can
create", but it calls the permission gate directly, and that gate is
exactly what the route skips for a path with no index name, so the guard
was verified in isolation while the route stayed open.
Gate the service-level create on the route itself, before the segment
loop, with assert_proxy_admin_for_vector_store_index_management. Scope it
to POST on a path whose last segment is indexes, mirroring the
endswith("/indexes") branch the lifecycle helper already uses, so the
managed-index paths and ordinary Azure OpenAI passthrough traffic are
untouched.
Add route-level tests: a non-admin is refused with the admin-only message
and never reaches the passthrough handler, an admin still creates, and the
new predicate is parametrized over the service-level, per-index, and
non-Search paths.
This commit is contained in:
parent
c1125f0abb
commit
f8fccec108
2 changed files with 110 additions and 2 deletions
|
|
@ -44,6 +44,7 @@ from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
|||
)
|
||||
from litellm.proxy.utils import is_known_model
|
||||
from litellm.proxy.vector_store_endpoints.utils import (
|
||||
assert_proxy_admin_for_vector_store_index_management,
|
||||
assert_user_can_access_vector_store,
|
||||
get_litellm_managed_vector_store,
|
||||
is_allowed_to_call_vector_store_endpoint,
|
||||
|
|
@ -1250,6 +1251,21 @@ def get_azure_ai_search_index_from_endpoint(endpoint: str) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def is_azure_ai_search_service_level_index_create(method: str, endpoint: str) -> bool:
|
||||
"""Return True for ``POST /indexes``, Azure AI Search's service-level index create.
|
||||
|
||||
No index name appears in that path, so ``get_azure_ai_search_index_from_endpoint``
|
||||
yields None and the managed-index branch can never claim the request. Without an
|
||||
explicit guard it reaches the generic Azure passthrough on the proxy's own
|
||||
credential, so a non-admin could create an index whenever ``AZURE_API_BASE``
|
||||
points at the Search service.
|
||||
"""
|
||||
if method != "POST":
|
||||
return False
|
||||
path: Final = endpoint.split("?", 1)[0].strip("/")
|
||||
return path == "indexes" or path.endswith("/indexes")
|
||||
|
||||
|
||||
@router.api_route(
|
||||
"/azure_ai/{endpoint:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
||||
|
|
@ -1275,6 +1291,9 @@ async def azure_proxy_route(
|
|||
"""
|
||||
from litellm.proxy.proxy_server import llm_router
|
||||
|
||||
if is_azure_ai_search_service_level_index_create(method=request.method, endpoint=endpoint):
|
||||
assert_proxy_admin_for_vector_store_index_management(user_api_key_dict, operation="create")
|
||||
|
||||
parts: Final = endpoint.split(
|
||||
"/"
|
||||
) # azure model is in the url - e.g. https://{endpoint}/openai/deployments/{deployment-id}/completions?api-version=2024-10-21
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
|||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import Request, Response
|
||||
from fastapi import HTTPException, Request, Response
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
sys.path.insert(
|
||||
|
|
@ -25,6 +25,7 @@ from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
|
|||
cursor_proxy_route,
|
||||
get_azure_ai_search_index_from_endpoint,
|
||||
get_vertex_base_url,
|
||||
is_azure_ai_search_service_level_index_create,
|
||||
llm_passthrough_factory_proxy_route,
|
||||
milvus_proxy_route,
|
||||
mistral_proxy_route,
|
||||
|
|
@ -33,7 +34,7 @@ from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
|
|||
vertex_proxy_route,
|
||||
vllm_proxy_route,
|
||||
)
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
|
||||
from litellm.types.passthrough_endpoints.vertex_ai import VertexPassThroughCredentials
|
||||
|
||||
|
||||
|
|
@ -3381,3 +3382,91 @@ class TestAzureProxyRouteCrossIndexAuthorization:
|
|||
mock_is_allowed.assert_not_called()
|
||||
mock_handler.assert_awaited_once()
|
||||
assert mock_handler.await_args.kwargs["custom_llm_provider"] == litellm.LlmProviders.AZURE
|
||||
|
||||
|
||||
class TestAzureProxyRouteServiceLevelIndexCreate:
|
||||
"""``POST /indexes`` carries no index name, so the managed-index branch cannot
|
||||
claim it and it would otherwise reach the generic Azure passthrough on the
|
||||
proxy's own credential. The admin-only index management guard has to be
|
||||
enforced on the route itself, not just on the permission gate the route skips.
|
||||
"""
|
||||
|
||||
def _request(self, method: str, path: str) -> MagicMock:
|
||||
request = MagicMock(spec=Request)
|
||||
request.method = method
|
||||
request.headers = {"content-type": "application/json"}
|
||||
request.url = MagicMock()
|
||||
request.url.path = path
|
||||
return request
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"method, endpoint, expected",
|
||||
[
|
||||
("POST", "indexes", True),
|
||||
("POST", "indexes?api-version=2024-07-01", True),
|
||||
("POST", "/indexes/", True),
|
||||
("POST", "indexes/my-index", False),
|
||||
("POST", "indexes/my-index/docs/index", False),
|
||||
("GET", "indexes", False),
|
||||
("POST", "openai/deployments/gpt-4o/chat/completions", False),
|
||||
],
|
||||
)
|
||||
def test_recognizes_service_level_create(self, method, endpoint, expected):
|
||||
assert is_azure_ai_search_service_level_index_create(method=method, endpoint=endpoint) is expected
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_admin_cannot_create_an_index(self):
|
||||
with (
|
||||
patch("litellm.proxy.proxy_server.llm_router", MagicMock()),
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_secret_str",
|
||||
return_value="https://svc.search.windows.net",
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.BaseOpenAIPassThroughHandler._base_openai_pass_through_handler",
|
||||
new=AsyncMock(return_value=Response()),
|
||||
) as mock_handler,
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await azure_proxy_route(
|
||||
endpoint="indexes?api-version=2024-07-01",
|
||||
request=self._request("POST", "/azure_ai/indexes"),
|
||||
fastapi_response=MagicMock(spec=Response),
|
||||
user_api_key_dict=UserAPIKeyAuth(
|
||||
token="sk-team-token",
|
||||
user_role=LitellmUserRoles.INTERNAL_USER,
|
||||
),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert "Only proxy admins can create" in exc_info.value.detail
|
||||
mock_handler.assert_not_awaited()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_can_still_create_an_index(self):
|
||||
with (
|
||||
patch("litellm.proxy.proxy_server.llm_router", MagicMock()),
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.get_secret_str",
|
||||
return_value="https://svc.search.windows.net",
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router.get_credentials",
|
||||
return_value="azure-key",
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.BaseOpenAIPassThroughHandler._base_openai_pass_through_handler",
|
||||
new=AsyncMock(return_value=Response()),
|
||||
) as mock_handler,
|
||||
):
|
||||
await azure_proxy_route(
|
||||
endpoint="indexes?api-version=2024-07-01",
|
||||
request=self._request("POST", "/azure_ai/indexes"),
|
||||
fastapi_response=MagicMock(spec=Response),
|
||||
user_api_key_dict=UserAPIKeyAuth(
|
||||
token="sk-admin-token",
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN,
|
||||
),
|
||||
)
|
||||
|
||||
mock_handler.assert_awaited_once()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue