mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(proxy): scope file list pagination cursors to the caller
GET /v1/files filters data down to the caller's own managed files but left first_id and last_id as the upstream page's, so a non-owner got back file ids belonging to other users even with an empty data array
This commit is contained in:
parent
b66d4e6965
commit
357f90fa39
2 changed files with 115 additions and 0 deletions
|
|
@ -1270,10 +1270,25 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|||
)
|
||||
## Filter the response to only include the files created by the user
|
||||
response.data = user_created_file_ids # type: ignore
|
||||
self._scope_list_page_cursors(response, user_created_file_ids)
|
||||
return response
|
||||
return response
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _scope_list_page_cursors(response: AsyncCursorPage, data: List[OpenAIFileObject]) -> None:
|
||||
"""Rebuild ``first_id`` / ``last_id`` from the caller-scoped page.
|
||||
|
||||
The upstream cursors point at rows that were just filtered out, so
|
||||
leaving them in place discloses other callers' file ids.
|
||||
"""
|
||||
if hasattr(response, "first_id"):
|
||||
response.first_id = data[0].id if data else None
|
||||
if hasattr(response, "last_id"):
|
||||
response.last_id = data[-1].id if data else None
|
||||
if not data and hasattr(response, "has_more"):
|
||||
response.has_more = False
|
||||
|
||||
async def afile_retrieve(
|
||||
self, file_id: str, litellm_parent_otel_span: Optional[Span], llm_router=None
|
||||
) -> OpenAIFileObject:
|
||||
|
|
|
|||
|
|
@ -2861,3 +2861,103 @@ async def test_same_user_different_keys_can_access_batch():
|
|||
assert "batch_id" in result2
|
||||
# Both keys should get the same result
|
||||
assert result1["batch_id"] == result2["batch_id"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_list_cursors_are_scoped_to_the_caller():
|
||||
"""A non-owner must not learn other callers' file ids through the page cursors."""
|
||||
from openai.pagination import AsyncCursorPage
|
||||
from openai.types import FileObject
|
||||
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
|
||||
owner_file = FileObject(
|
||||
id="file-owner-1",
|
||||
bytes=100,
|
||||
created_at=1,
|
||||
filename="owner.jsonl",
|
||||
object="file",
|
||||
purpose="batch",
|
||||
status="processed",
|
||||
)
|
||||
upstream_page = AsyncCursorPage[FileObject].construct(
|
||||
data=[owner_file],
|
||||
has_more=True,
|
||||
first_id=owner_file.id,
|
||||
last_id=owner_file.id,
|
||||
object="list",
|
||||
)
|
||||
|
||||
prisma_client = AsyncMock()
|
||||
prisma_client.db.litellm_managedfiletable.find_many.return_value = []
|
||||
proxy_managed_files = _PROXY_LiteLLMManagedFiles(
|
||||
DualCache(), prisma_client=prisma_client
|
||||
)
|
||||
|
||||
response = await proxy_managed_files.async_post_call_success_hook(
|
||||
data={},
|
||||
user_api_key_dict=UserAPIKeyAuth(
|
||||
user_id="other-user", team_id="other-team", parent_otel_span=MagicMock()
|
||||
),
|
||||
response=upstream_page,
|
||||
)
|
||||
|
||||
assert response.data == []
|
||||
assert response.first_id is None
|
||||
assert response.last_id is None
|
||||
assert response.has_more is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_list_cursors_follow_the_owner_scoped_page():
|
||||
from openai.pagination import AsyncCursorPage
|
||||
from openai.types import FileObject
|
||||
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
|
||||
def _raw_file(file_id: str) -> FileObject:
|
||||
return FileObject(
|
||||
id=file_id,
|
||||
bytes=100,
|
||||
created_at=1,
|
||||
filename=f"{file_id}.jsonl",
|
||||
object="file",
|
||||
purpose="batch",
|
||||
status="processed",
|
||||
)
|
||||
|
||||
upstream_page = AsyncCursorPage[FileObject].construct(
|
||||
data=[_raw_file("file-someone-else"), _raw_file("file-mine")],
|
||||
has_more=False,
|
||||
first_id="file-someone-else",
|
||||
last_id="file-mine",
|
||||
object="list",
|
||||
)
|
||||
|
||||
managed_row = MagicMock()
|
||||
managed_row.file_object = {
|
||||
"id": "litellm_proxy:mine",
|
||||
"bytes": 100,
|
||||
"created_at": 1,
|
||||
"filename": "mine.jsonl",
|
||||
"object": "file",
|
||||
"purpose": "batch",
|
||||
"status": "processed",
|
||||
}
|
||||
prisma_client = AsyncMock()
|
||||
prisma_client.db.litellm_managedfiletable.find_many.return_value = [managed_row]
|
||||
proxy_managed_files = _PROXY_LiteLLMManagedFiles(
|
||||
DualCache(), prisma_client=prisma_client
|
||||
)
|
||||
|
||||
response = await proxy_managed_files.async_post_call_success_hook(
|
||||
data={},
|
||||
user_api_key_dict=UserAPIKeyAuth(
|
||||
user_id="mine-user", parent_otel_span=MagicMock()
|
||||
),
|
||||
response=upstream_page,
|
||||
)
|
||||
|
||||
assert [file_object.id for file_object in response.data] == ["litellm_proxy:mine"]
|
||||
assert response.first_id == "litellm_proxy:mine"
|
||||
assert response.last_id == "litellm_proxy:mine"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue