mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(vector_stores): classify write endpoints before reads on substring collisions
This commit is contained in:
parent
08966c842b
commit
b14c4a8d45
3 changed files with 71 additions and 11 deletions
|
|
@ -48,8 +48,11 @@ class AzureAIVectorStoreConfig(BaseVectorStoreConfig, BaseAzureLLM):
|
|||
|
||||
Patterns stay literal rather than ``{placeholder}`` templates because the
|
||||
matcher falls back to the substring before a ``{``, which here is always
|
||||
``/indexes/`` -- broad enough that a templated read, matched first, would
|
||||
shadow the ``/docs/index`` write.
|
||||
``/indexes/``. The matcher is substring-based, so an index name may
|
||||
itself contain a read fragment (an index named ``analyze*`` puts
|
||||
``/analyze`` inside the batch-write path); writes are classified before
|
||||
reads, so such a path demands the write grant rather than being
|
||||
shadowed into a read.
|
||||
"""
|
||||
return {
|
||||
"read": [
|
||||
|
|
|
|||
|
|
@ -387,17 +387,19 @@ def is_allowed_to_call_vector_store_endpoint(
|
|||
)
|
||||
return True
|
||||
|
||||
# Determine the permission type based on the request
|
||||
# Writes are classified before reads so a path matching both patterns
|
||||
# requires the stronger grant (e.g. the azure batch write on an index
|
||||
# named "analyze*" also contains the "/analyze" read fragment)
|
||||
permission_type = None
|
||||
for endpoint in provider_vector_store_endpoints["read"]:
|
||||
for endpoint in provider_vector_store_endpoints["write"]:
|
||||
if request.method == endpoint[0] and _does_endpoint_match(endpoint[1], request_route):
|
||||
permission_type = "read"
|
||||
permission_type = "write"
|
||||
break
|
||||
|
||||
if permission_type is None:
|
||||
for endpoint in provider_vector_store_endpoints["write"]:
|
||||
for endpoint in provider_vector_store_endpoints["read"]:
|
||||
if request.method == endpoint[0] and _does_endpoint_match(endpoint[1], request_route):
|
||||
permission_type = "write"
|
||||
permission_type = "read"
|
||||
break
|
||||
|
||||
if permission_type is None:
|
||||
|
|
@ -454,15 +456,15 @@ def is_allowed_to_call_vector_store_files_endpoint(
|
|||
request_route: Final = get_request_route(request)
|
||||
|
||||
permission_type: str | None = None
|
||||
for endpoint in provider_vector_store_endpoints.get("read", ()):
|
||||
for endpoint in provider_vector_store_endpoints.get("write", ()):
|
||||
if request.method == endpoint[0] and _does_endpoint_match(endpoint[1], request_route):
|
||||
permission_type = "read"
|
||||
permission_type = "write"
|
||||
break
|
||||
|
||||
if permission_type is None:
|
||||
for endpoint in provider_vector_store_endpoints.get("write", ()):
|
||||
for endpoint in provider_vector_store_endpoints.get("read", ()):
|
||||
if request.method == endpoint[0] and _does_endpoint_match(endpoint[1], request_route):
|
||||
permission_type = "write"
|
||||
permission_type = "read"
|
||||
break
|
||||
|
||||
if permission_type is None:
|
||||
|
|
|
|||
|
|
@ -3057,3 +3057,58 @@ class TestAzureAIDocumentWritePassthroughPermission:
|
|||
)
|
||||
assert exc_info.value.status_code == 403
|
||||
assert f"Only proxy admins can {operation}" in exc_info.value.detail
|
||||
|
||||
|
||||
class TestAzureAIAnalyzeNamedIndexClassification:
|
||||
"""Regression tests for write-before-read endpoint classification.
|
||||
|
||||
The endpoint matcher is substring-based, so the batch-write path of an
|
||||
index named ``analyze*`` contains the ``("POST", "/analyze")`` read
|
||||
fragment. Reads-first classification labeled that write a read, letting a
|
||||
read-only grant upload, merge, and delete documents (and refusing
|
||||
legitimate write-only grants). Writes are classified first now, so an
|
||||
ambiguous path demands the stronger grant.
|
||||
"""
|
||||
|
||||
def _request(self, method: str, path: str) -> MagicMock:
|
||||
request = MagicMock(spec=Request)
|
||||
request.method = method
|
||||
request.url.path = path
|
||||
return request
|
||||
|
||||
def _team_member(self, index: str, permissions: list) -> MagicMock:
|
||||
user = MagicMock(spec=UserAPIKeyAuth)
|
||||
user.user_role = None
|
||||
user.metadata = {"allowed_vector_store_indexes": [{"index_name": index, "index_permissions": permissions}]}
|
||||
user.team_metadata = None
|
||||
return user
|
||||
|
||||
@pytest.mark.parametrize("index", ["analyze", "analyzer-reports"])
|
||||
def test_read_only_grant_cannot_upload_to_analyze_named_index(self, index):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
is_allowed_to_call_vector_store_endpoint(
|
||||
provider=LlmProviders.AZURE_AI,
|
||||
index_name=index,
|
||||
request=self._request("POST", f"/azure_ai/indexes/{index}/docs/index"),
|
||||
user_api_key_dict=self._team_member(index, ["read"]),
|
||||
)
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
@pytest.mark.parametrize("index", ["analyze", "analyzer-reports"])
|
||||
def test_write_grant_can_upload_to_analyze_named_index(self, index):
|
||||
result = is_allowed_to_call_vector_store_endpoint(
|
||||
provider=LlmProviders.AZURE_AI,
|
||||
index_name=index,
|
||||
request=self._request("POST", f"/azure_ai/indexes/{index}/docs/index"),
|
||||
user_api_key_dict=self._team_member(index, ["write"]),
|
||||
)
|
||||
assert result is True
|
||||
|
||||
def test_read_only_grant_can_still_analyze_on_analyze_named_index(self):
|
||||
result = is_allowed_to_call_vector_store_endpoint(
|
||||
provider=LlmProviders.AZURE_AI,
|
||||
index_name="analyze",
|
||||
request=self._request("POST", "/azure_ai/indexes/analyze/analyze"),
|
||||
user_api_key_dict=self._team_member("analyze", ["read"]),
|
||||
)
|
||||
assert result is True
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue