From da8836a310b8ac21cf7cd4408bad4d7a4eb85c51 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 12 Nov 2026 23:00:06 +0000 Subject: [PATCH 01/10] fix(files): match every spelling of a provider file id in the ownership check enforce_provider_file_access matched the stored provider file id by exact string, and a Gemini output file can be addressed as files/, files%2F, / or its full URL, all of which the Gemini file transformation sends to the same file. Those spellings found no ownership row and fell through the no-row pass-through path. canonical_provider_file_id collapses the spellings to the bare id; the ownership check looks up both the spelling the caller sent and the canonical one, and store_batch_output_file_ownership stores the canonical id. (cherry picked from commit 85e6a98866910c98d699bbbaa9b0c3af677c2719) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../proxy/hooks/managed_files.py | 6 +- .../openai_files_endpoints/common_utils.py | 15 ++++ .../proxy/hooks/test_managed_files.py | 72 ++++++++++++++++++- .../test_files_common_utils.py | 19 +++++ 4 files changed, 109 insertions(+), 3 deletions(-) diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py index 0819d3ded07..235846afd2b 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py @@ -74,6 +74,7 @@ from litellm.proxy.openai_files_endpoints.common_utils import ( FILE_LIST_CONTINUATION_CHUNK_SIZE, _is_base64_encoded_unified_file_id, apply_unified_file_ids, + canonical_provider_file_id, decode_model_from_file_id, ensure_batch_response_managed_file_ids, get_batch_id_from_unified_batch_id, @@ -877,8 +878,9 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): """ if self.prisma_client is None: return + spellings: Final = list(dict.fromkeys((file_id, canonical_provider_file_id(file_id)))) managed_file = await _managed_file_table(self.prisma_client).find_first( - where={"OR": [{"unified_file_id": file_id}, {"flat_model_file_ids": {"has": file_id}}]} + where={"OR": [{"unified_file_id": {"in": spellings}}, {"flat_model_file_ids": {"hasSome": spellings}}]} ) if managed_file is None: return @@ -922,7 +924,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): ) for file_id in provider_file_ids: model_name = decode_model_from_file_id(file_id) - raw_file_id = get_original_file_id(file_id) + raw_file_id = canonical_provider_file_id(get_original_file_id(file_id)) await self.store_batch_output_file( unified_file_id=file_id, provider_file_id=raw_file_id, diff --git a/litellm/proxy/openai_files_endpoints/common_utils.py b/litellm/proxy/openai_files_endpoints/common_utils.py index 48306cc8d7a..16dcda7583a 100644 --- a/litellm/proxy/openai_files_endpoints/common_utils.py +++ b/litellm/proxy/openai_files_endpoints/common_utils.py @@ -15,6 +15,7 @@ from typing import ( get_args, runtime_checkable, ) +from urllib.parse import unquote, urlparse from pydantic import TypeAdapter @@ -343,6 +344,20 @@ def get_original_file_id(encoded_id: str) -> str: return encoded_id +def canonical_provider_file_id(file_id: str) -> str: + """The one spelling of a provider file id that ownership rows store and look up, so ``files/abc``, + ``files%2Fabc``, ``abc/`` and the Gemini file URL all resolve to the bare ``abc``. Cloud storage URIs + (``s3://``, ``gs://``) are returned unchanged.""" + decoded: Final = unquote(file_id).strip() + is_http_url: Final = decoded.startswith(("http://", "https://")) + if "://" in decoded and not is_http_url: + return decoded + path: Final = urlparse(decoded).path if is_http_url else decoded + files_index: Final = path.find("files/") + bare: Final = (path[files_index:] if files_index != -1 else path).strip("/").removeprefix("files/").strip("/") + return bare or decoded + + def is_model_embedded_id(file_id: str) -> bool: """ Check if a file/batch ID has model routing information embedded. diff --git a/tests/unit/enterprise/proxy/hooks/test_managed_files.py b/tests/unit/enterprise/proxy/hooks/test_managed_files.py index 91f6593575e..f7e7ef06cff 100644 --- a/tests/unit/enterprise/proxy/hooks/test_managed_files.py +++ b/tests/unit/enterprise/proxy/hooks/test_managed_files.py @@ -22,7 +22,7 @@ from litellm_enterprise.proxy.hooks.managed_files import ( _provider_file_retrieve_credentials, ) from litellm.caching import DualCache -from litellm.proxy._types import CallTypes, LiteLLM_ManagedFileTable +from litellm.proxy._types import CallTypes, LiteLLM_ManagedFileTable, UserAPIKeyAuth from litellm.proxy.openai_files_endpoints.common_utils import ( BATCH_CREATE_HIDDEN_PARAM, is_base64_encoded_unified_file_id, @@ -48,9 +48,18 @@ class _InMemoryManagedFileTable: self, where: Mapping[str, object] ) -> LiteLLM_ManagedFileTable | None: self.find_first_calls.append(where) + or_filters: Final = where.get("OR") + if isinstance(or_filters, list): + for clause in or_filters: + matched: Final = await self.find_first(clause) + if matched is not None: + return matched + return None unified_file_id: Final = where.get("unified_file_id") if isinstance(unified_file_id, str): return self.rows.get(unified_file_id) + if isinstance(unified_file_id, Mapping): + return next((self.rows[uid] for uid in unified_file_id.get("in", ()) if uid in self.rows), None) raw_file_filter: Final = where.get("flat_model_file_ids") if isinstance(raw_file_filter, Mapping): raw_file_id: Final = raw_file_filter.get("has") @@ -59,6 +68,12 @@ class _InMemoryManagedFileTable: (row for row in self.rows.values() if raw_file_id in row.flat_model_file_ids), None, ) + raw_file_ids: Final = raw_file_filter.get("hasSome") + if isinstance(raw_file_ids, list): + return next( + (row for row in self.rows.values() if set(raw_file_ids) & set(row.flat_model_file_ids)), + None, + ) return None async def upsert( @@ -6148,3 +6163,58 @@ async def test_only_a_streamed_background_create_is_marked_for_the_managed_bill( assert isinstance(result, dict) assert (MANAGED_BACKGROUND_RESPONSE_METADATA_KEY in result["litellm_metadata"]) is marked assert result["metadata"] == {"run": "nightly"} + + +_GEMINI_OUTPUT_SPELLINGS: Final = ( + "files/batch-out1", + "files%2Fbatch-out1", + "batch-out1/", + "https://generativelanguage.googleapis.com/v1beta/files/batch-out1", +) + + +def _gemini_batch_output_row() -> LiteLLM_ManagedFileTable: + return LiteLLM_ManagedFileTable( + unified_file_id="file-encoded-gemini-output", + file_object=OpenAIFileObject( + id="file-encoded-gemini-output", + object="file", + bytes=0, + created_at=1, + filename="output.jsonl", + purpose="batch_output", + status="processed", + ), + model_mappings={"model-gemini": "batch-out1"}, + flat_model_file_ids=["batch-out1"], + created_by="owner", + team_id="team-owner", + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("spelling", _GEMINI_OUTPUT_SPELLINGS) +async def test_enforce_provider_file_access_denies_every_spelling_of_another_keys_output_file(spelling: str): + managed_files, _ = _managed_files_with_fake_prisma(_gemini_batch_output_row()) + stranger: Final = UserAPIKeyAuth(user_id="stranger", team_id="team-stranger") + + with pytest.raises(HTTPException) as denied: + await managed_files.enforce_provider_file_access(spelling, stranger) + + assert denied.value.status_code == 403 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("spelling", (*_GEMINI_OUTPUT_SPELLINGS, "batch-out1")) +async def test_enforce_provider_file_access_lets_the_owner_read_any_spelling(spelling: str): + managed_files, _ = _managed_files_with_fake_prisma(_gemini_batch_output_row()) + + await managed_files.enforce_provider_file_access(spelling, UserAPIKeyAuth(user_id="owner", team_id="team-owner")) + await managed_files.enforce_provider_file_access(spelling, UserAPIKeyAuth(user_id="teammate", team_id="team-owner")) + + +@pytest.mark.asyncio +async def test_enforce_provider_file_access_still_passes_a_file_nobody_registered(): + managed_files, _ = _managed_files_with_fake_prisma(_gemini_batch_output_row()) + + await managed_files.enforce_provider_file_access("files/batch-other", UserAPIKeyAuth(user_id="stranger")) diff --git a/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py b/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py index 180ece77a6e..d022cc148e5 100644 --- a/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py +++ b/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py @@ -8,6 +8,7 @@ from litellm_enterprise.proxy.hooks.managed_files import _PROXY_LiteLLMManagedFi from litellm.caching.caching import DualCache from litellm.proxy.openai_files_endpoints.common_utils import ( apply_unified_file_ids, + canonical_provider_file_id, get_credentials_for_model, is_litellm_executed_batch, map_raw_file_ids_to_unified, @@ -652,3 +653,21 @@ async def test_batch_cancel_updates_database(caplog: pytest.LogCaptureFixture): assert "batch_processed" not in written assert json.loads(written["file_object"])["cancelled_at"] == 1234567999 assert f"Updating batch {batch_id} status to cancelled after cancel" in caplog.messages + + +@pytest.mark.parametrize( + ("file_id", "canonical"), + ( + ("batch-out1", "batch-out1"), + ("files/batch-out1", "batch-out1"), + ("files%2Fbatch-out1", "batch-out1"), + ("batch-out1/", "batch-out1"), + ("/files/batch-out1/", "batch-out1"), + ("https://generativelanguage.googleapis.com/v1beta/files/batch-out1", "batch-out1"), + ("file-abc123", "file-abc123"), + ("s3://bucket/path/files/output.jsonl", "s3://bucket/path/files/output.jsonl"), + ("gs://bucket/prefix/predictions.jsonl", "gs://bucket/prefix/predictions.jsonl"), + ), +) +def test_canonical_provider_file_id_collapses_every_spelling_to_the_stored_id(file_id: str, canonical: str): + assert canonical_provider_file_id(file_id) == canonical From a15c3875d631749da169136e9f3ae8a899482ba3 Mon Sep 17 00:00:00 2001 From: kerry Date: Fri, 13 Nov 2026 00:20:27 +0000 Subject: [PATCH 02/10] fix(files): unwrap model-encoded ids and index canonical provider ids in ownership rows Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> (cherry picked from commit 97c9152552a80695cedd8eb72544e2b4ddebe640) --- .../proxy/hooks/managed_files.py | 12 ++-- .../openai_files_endpoints/common_utils.py | 10 +++ .../proxy/hooks/test_managed_files.py | 70 +++++++++++++++++++ .../test_files_common_utils.py | 15 ++++ 4 files changed, 103 insertions(+), 4 deletions(-) diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py index 235846afd2b..969c718065d 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py @@ -84,6 +84,7 @@ from litellm.proxy.openai_files_endpoints.common_utils import ( is_litellm_executed_batch, map_raw_file_ids_to_unified, normalize_mime_type_for_provider, + provider_file_id_spellings, resolve_managed_output_file_model_name, validate_file_list_limit, validate_file_list_purpose, @@ -549,12 +550,15 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): ) -> None: verbose_logger.info(f"Storing LiteLLM Managed File object with id={file_id} in cache") storage_metadata: Final = _storage_metadata_of(file_object) + flat_model_file_ids: Final = [ + canonical_provider_file_id(provider_file_id) for provider_file_id in model_mappings.values() + ] if file_object is not None: litellm_managed_file_object = LiteLLM_ManagedFileTable( unified_file_id=file_id, file_object=file_object, model_mappings=model_mappings, - flat_model_file_ids=list(model_mappings.values()), + flat_model_file_ids=flat_model_file_ids, created_by=resolve_resource_owner_id(user_api_key_dict), team_id=user_api_key_dict.team_id, updated_by=user_api_key_dict.user_id, @@ -572,14 +576,14 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): db_data = { "unified_file_id": file_id, "model_mappings": json.dumps(model_mappings), - "flat_model_file_ids": list(model_mappings.values()), + "flat_model_file_ids": flat_model_file_ids, "created_by": resolve_resource_owner_id(user_api_key_dict), "team_id": user_api_key_dict.team_id, "updated_by": user_api_key_dict.user_id, } update_data = { "model_mappings": json.dumps(model_mappings), - "flat_model_file_ids": list(model_mappings.values()), + "flat_model_file_ids": flat_model_file_ids, "updated_by": user_api_key_dict.user_id, } @@ -878,7 +882,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): """ if self.prisma_client is None: return - spellings: Final = list(dict.fromkeys((file_id, canonical_provider_file_id(file_id)))) + spellings: Final = list(provider_file_id_spellings(file_id)) managed_file = await _managed_file_table(self.prisma_client).find_first( where={"OR": [{"unified_file_id": {"in": spellings}}, {"flat_model_file_ids": {"hasSome": spellings}}]} ) diff --git a/litellm/proxy/openai_files_endpoints/common_utils.py b/litellm/proxy/openai_files_endpoints/common_utils.py index 16dcda7583a..93e177fee29 100644 --- a/litellm/proxy/openai_files_endpoints/common_utils.py +++ b/litellm/proxy/openai_files_endpoints/common_utils.py @@ -358,6 +358,16 @@ def canonical_provider_file_id(file_id: str) -> str: return bare or decoded +_GEMINI_FILES_URL: Final = "https://generativelanguage.googleapis.com/v1beta/files/" + + +def provider_file_id_spellings(file_id: str) -> tuple[str, ...]: + """Every spelling an ownership row may hold for ``file_id``: as sent, unwrapped from a model-encoded id and + canonicalized, plus the ``files/`` and Gemini URL forms that upload rows stored before ids were canonicalized.""" + canonical: Final = canonical_provider_file_id(get_original_file_id(file_id)) + return tuple(dict.fromkeys((file_id, canonical, f"files/{canonical}", f"{_GEMINI_FILES_URL}{canonical}"))) + + def is_model_embedded_id(file_id: str) -> bool: """ Check if a file/batch ID has model routing information embedded. diff --git a/tests/unit/enterprise/proxy/hooks/test_managed_files.py b/tests/unit/enterprise/proxy/hooks/test_managed_files.py index f7e7ef06cff..f36d8a33b35 100644 --- a/tests/unit/enterprise/proxy/hooks/test_managed_files.py +++ b/tests/unit/enterprise/proxy/hooks/test_managed_files.py @@ -6218,3 +6218,73 @@ async def test_enforce_provider_file_access_still_passes_a_file_nobody_registere managed_files, _ = _managed_files_with_fake_prisma(_gemini_batch_output_row()) await managed_files.enforce_provider_file_access("files/batch-other", UserAPIKeyAuth(user_id="stranger")) + + +_GEMINI_UPLOAD_URI: Final = "https://generativelanguage.googleapis.com/v1beta/files/upload-1" + + +def _gemini_upload_row_stored_as_its_uri() -> LiteLLM_ManagedFileTable: + return LiteLLM_ManagedFileTable( + unified_file_id="file-encoded-gemini-upload", + file_object=OpenAIFileObject( + id="file-encoded-gemini-upload", + object="file", + bytes=12, + created_at=1, + filename="input.jsonl", + purpose="batch", + status="processed", + ), + model_mappings={"model-gemini": _GEMINI_UPLOAD_URI}, + flat_model_file_ids=[_GEMINI_UPLOAD_URI], + created_by="owner", + team_id="team-owner", + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "spelling", ("upload-1", "files/upload-1", "files%2Fupload-1", "upload-1/", _GEMINI_UPLOAD_URI) +) +async def test_enforce_provider_file_access_denies_a_stranger_a_row_stored_as_the_gemini_uri(spelling: str): + managed_files, _ = _managed_files_with_fake_prisma(_gemini_upload_row_stored_as_its_uri()) + stranger: Final = UserAPIKeyAuth(user_id="stranger", team_id="team-stranger") + + with pytest.raises(HTTPException) as denied: + await managed_files.enforce_provider_file_access(spelling, stranger) + + assert denied.value.status_code == 403 + + +@pytest.mark.asyncio +async def test_enforce_provider_file_access_denies_a_stranger_the_model_encoded_wrapper_of_another_keys_file(): + managed_files, _ = _managed_files_with_fake_prisma(_gemini_batch_output_row()) + wrapped: Final = encode_file_id_with_model(file_id="batch-out1", model="model-gemini") + stranger: Final = UserAPIKeyAuth(user_id="stranger", team_id="team-stranger") + + with pytest.raises(HTTPException) as denied: + await managed_files.enforce_provider_file_access(wrapped, stranger) + + assert denied.value.status_code == 403 + + +@pytest.mark.asyncio +async def test_store_unified_file_id_indexes_the_canonical_provider_file_id(): + prisma_client: Final = AsyncMock() + prisma_client.db.litellm_managedfiletable.upsert = AsyncMock(return_value=MagicMock()) + proxy_managed_files: Final = PROXY_LiteLLMManagedFiles( + internal_usage_cache=MagicMock(), prisma_client=prisma_client + ) + + await proxy_managed_files.store_unified_file_id( + file_id="file-encoded-gemini-upload", + file_object=None, + litellm_parent_otel_span=None, + model_mappings={"model-gemini": _GEMINI_UPLOAD_URI}, + user_api_key_dict=UserAPIKeyAuth(user_id="owner", team_id="team-owner"), + ) + + written: Final = prisma_client.db.litellm_managedfiletable.upsert.call_args.kwargs["data"] + assert written["create"]["flat_model_file_ids"] == ["upload-1"] + assert written["update"]["flat_model_file_ids"] == ["upload-1"] + assert json.loads(written["create"]["model_mappings"]) == {"model-gemini": _GEMINI_UPLOAD_URI} diff --git a/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py b/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py index d022cc148e5..134e79162fd 100644 --- a/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py +++ b/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py @@ -9,9 +9,11 @@ from litellm.caching.caching import DualCache from litellm.proxy.openai_files_endpoints.common_utils import ( apply_unified_file_ids, canonical_provider_file_id, + encode_file_id_with_model, get_credentials_for_model, is_litellm_executed_batch, map_raw_file_ids_to_unified, + provider_file_id_spellings, ) from litellm.proxy.route_llm_request import ProxyModelNotFoundError from litellm.proxy.utils import InternalUsageCache, handle_exception_on_proxy @@ -671,3 +673,16 @@ async def test_batch_cancel_updates_database(caplog: pytest.LogCaptureFixture): ) def test_canonical_provider_file_id_collapses_every_spelling_to_the_stored_id(file_id: str, canonical: str): assert canonical_provider_file_id(file_id) == canonical + + +def test_provider_file_id_spellings_unwraps_a_model_encoded_id_and_covers_the_stored_forms(): + wrapped: Final = encode_file_id_with_model(file_id="batch-out1", model="gemini-deployment") + + assert provider_file_id_spellings(wrapped) == ( + wrapped, + "batch-out1", + "files/batch-out1", + "https://generativelanguage.googleapis.com/v1beta/files/batch-out1", + ) + for spelling in ("batch-out1", "files%2Fbatch-out1", "batch-out1/"): + assert set(provider_file_id_spellings(spelling)) >= set(provider_file_id_spellings(wrapped)[1:]), spelling From d3c088bfbc2328f26274d73c904cdcf3e118d081 Mon Sep 17 00:00:00 2001 From: kerry Date: Fri, 13 Nov 2026 00:30:05 +0000 Subject: [PATCH 03/10] test(files): assert the ownership lookup outcome instead of its query shape Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> (cherry picked from commit 07abf10d3ef4fbd4f79ad4c69802f30d5646acb5) --- .../proxy/hooks/test_managed_files.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/unit/enterprise/proxy/hooks/test_managed_files.py b/tests/unit/enterprise/proxy/hooks/test_managed_files.py index f36d8a33b35..99cbc85177f 100644 --- a/tests/unit/enterprise/proxy/hooks/test_managed_files.py +++ b/tests/unit/enterprise/proxy/hooks/test_managed_files.py @@ -5610,9 +5610,9 @@ async def test_team_b_cannot_access_team_a_provider_format_file( ) assert exc_info.value.status_code == 403 - prisma_client.db.litellm_managedfiletable.find_first.assert_awaited_once_with( - where={"OR": [{"unified_file_id": file_id}, {"flat_model_file_ids": {"has": file_id}}]} - ) + lookup: Final = prisma_client.db.litellm_managedfiletable.find_first.await_args.kwargs["where"] + assert file_id in lookup["OR"][0]["unified_file_id"]["in"] + assert file_id in lookup["OR"][1]["flat_model_file_ids"]["hasSome"] @pytest.mark.asyncio @@ -6209,15 +6209,20 @@ async def test_enforce_provider_file_access_denies_every_spelling_of_another_key async def test_enforce_provider_file_access_lets_the_owner_read_any_spelling(spelling: str): managed_files, _ = _managed_files_with_fake_prisma(_gemini_batch_output_row()) - await managed_files.enforce_provider_file_access(spelling, UserAPIKeyAuth(user_id="owner", team_id="team-owner")) - await managed_files.enforce_provider_file_access(spelling, UserAPIKeyAuth(user_id="teammate", team_id="team-owner")) + owner: Final = UserAPIKeyAuth(user_id="owner", team_id="team-owner") + teammate: Final = UserAPIKeyAuth(user_id="teammate", team_id="team-owner") + + assert await managed_files.enforce_provider_file_access(spelling, owner) is None + assert await managed_files.enforce_provider_file_access(spelling, teammate) is None @pytest.mark.asyncio async def test_enforce_provider_file_access_still_passes_a_file_nobody_registered(): managed_files, _ = _managed_files_with_fake_prisma(_gemini_batch_output_row()) - await managed_files.enforce_provider_file_access("files/batch-other", UserAPIKeyAuth(user_id="stranger")) + stranger: Final = UserAPIKeyAuth(user_id="stranger") + + assert await managed_files.enforce_provider_file_access("files/batch-other", stranger) is None _GEMINI_UPLOAD_URI: Final = "https://generativelanguage.googleapis.com/v1beta/files/upload-1" From dd764e8ddc22d7e790121a06a968846e764213c0 Mon Sep 17 00:00:00 2001 From: kerry Date: Fri, 13 Nov 2026 00:37:21 +0000 Subject: [PATCH 04/10] fix(files): map provider ids back to their unified file by canonical spelling Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> (cherry picked from commit 214e17884a61f51ffa8f307bf94e75db2bd4c834) --- .../proxy/hooks/managed_files.py | 31 ++++--- .../proxy/hooks/test_managed_files.py | 85 ++++++++++--------- 2 files changed, 65 insertions(+), 51 deletions(-) diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py index 969c718065d..03a31066e78 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py @@ -393,14 +393,26 @@ def _managed_file_table(prisma_client: _ManagedResourcePrismaClient) -> _Managed return prisma_client.db.litellm_managedfiletable -def _iter_provider_file_id_pairs( - rows: Sequence[_ManagedFileRow], - requested_provider_file_ids: frozenset[str], -) -> Iterator[tuple[str, str]]: +def _iter_canonical_provider_file_ids(rows: Sequence[_ManagedFileRow]) -> Iterator[tuple[str, str]]: for row in rows: for provider_file_id in row.flat_model_file_ids: - if provider_file_id in requested_provider_file_ids: - yield provider_file_id, row.unified_file_id + yield canonical_provider_file_id(provider_file_id), row.unified_file_id + + +def _iter_requested_provider_file_id_spellings(provider_file_ids: Sequence[str]) -> Iterator[str]: + for provider_file_id in provider_file_ids: + yield from provider_file_id_spellings(provider_file_id) + + +def _iter_provider_file_id_pairs( + rows: Sequence[_ManagedFileRow], + requested_provider_file_ids: Sequence[str], +) -> Iterator[tuple[str, str]]: + unified_by_canonical_id: Final = dict(_iter_canonical_provider_file_ids(rows)) + for requested_provider_file_id in requested_provider_file_ids: + unified_file_id: Final = unified_by_canonical_id.get(canonical_provider_file_id(requested_provider_file_id)) + if unified_file_id is not None: + yield requested_provider_file_id, unified_file_id def _managed_object_table(prisma_client: _ManagedResourcePrismaClient) -> _ManagedObjectTableActions: @@ -1389,7 +1401,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): return MappingProxyType({}) provider_file_ids_list: Final = [ # mutable-ok: Prisma hasSome requires a list - provider_file_id for provider_file_id in unique_provider_file_ids + *dict.fromkeys(_iter_requested_provider_file_id_spellings(unique_provider_file_ids)) ] rows: Final = await _managed_file_table(self.prisma_client).find_many( where={ # mutable-ok: Prisma requires a plain dictionary for where @@ -1401,10 +1413,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): ) return MappingProxyType( dict( - _iter_provider_file_id_pairs( - rows, - frozenset(unique_provider_file_ids), - ) + _iter_provider_file_id_pairs(rows, unique_provider_file_ids) ) ) diff --git a/tests/unit/enterprise/proxy/hooks/test_managed_files.py b/tests/unit/enterprise/proxy/hooks/test_managed_files.py index 99cbc85177f..343119c7055 100644 --- a/tests/unit/enterprise/proxy/hooks/test_managed_files.py +++ b/tests/unit/enterprise/proxy/hooks/test_managed_files.py @@ -5575,62 +5575,48 @@ async def test_fine_tuning_provider_format_id_not_enforced(): prisma_client.db.litellm_managedobjecttable.find_first.assert_not_awaited() -@pytest.mark.asyncio -@pytest.mark.parametrize( - "call_type", ["afile_content", "afile_retrieve", "afile_delete"] -) -@pytest.mark.parametrize( - "file_id", [MODEL_ENCODED_OUTPUT_FILE_ID, RAW_PROVIDER_FILE_ID] -) -async def test_team_b_cannot_access_team_a_provider_format_file( - call_type, file_id -): - """ - Cross-team content/retrieve/delete of a model-encoded or raw provider - file id must 403 when an ownership row exists for another team. - """ - from litellm.proxy._types import UserAPIKeyAuth +def _team_a_output_row() -> LiteLLM_ManagedFileTable: + return LiteLLM_ManagedFileTable( + unified_file_id="file-encoded-team-a-output", + file_object=OpenAIFileObject( + id="file-encoded-team-a-output", + object="file", + bytes=0, + created_at=1, + filename="output.jsonl", + purpose="batch_output", + status="processed", + ), + model_mappings={"gpt-4o-team-alias": RAW_PROVIDER_FILE_ID}, + flat_model_file_ids=[RAW_PROVIDER_FILE_ID], + created_by="user_a", + team_id="team_a", + ) - prisma_client = AsyncMock() - prisma_client.db.litellm_managedfiletable.find_first.return_value = ( - _owned_record(created_by="user_a", team_id="team_a") - ) - proxy_managed_files = PROXY_LiteLLMManagedFiles( - MagicMock(), prisma_client=prisma_client - ) + +@pytest.mark.asyncio +@pytest.mark.parametrize("call_type", ["afile_content", "afile_retrieve", "afile_delete"]) +@pytest.mark.parametrize("file_id", [MODEL_ENCODED_OUTPUT_FILE_ID, RAW_PROVIDER_FILE_ID]) +async def test_team_b_cannot_access_team_a_provider_format_file(call_type: str, file_id: str): + proxy_managed_files, _ = _managed_files_with_fake_prisma(_team_a_output_row()) with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.async_pre_call_hook( - user_api_key_dict=UserAPIKeyAuth( - user_id="user_b", team_id="team_b", parent_otel_span=MagicMock() - ), + user_api_key_dict=UserAPIKeyAuth(user_id="user_b", team_id="team_b", parent_otel_span=MagicMock()), cache=MagicMock(), data={"file_id": file_id}, call_type=call_type, ) assert exc_info.value.status_code == 403 - lookup: Final = prisma_client.db.litellm_managedfiletable.find_first.await_args.kwargs["where"] - assert file_id in lookup["OR"][0]["unified_file_id"]["in"] - assert file_id in lookup["OR"][1]["flat_model_file_ids"]["hasSome"] @pytest.mark.asyncio async def test_same_team_can_access_provider_format_file(): - from litellm.proxy._types import UserAPIKeyAuth - - prisma_client = AsyncMock() - prisma_client.db.litellm_managedfiletable.find_first.return_value = ( - _owned_record(created_by="user_a", team_id="team_a") - ) - proxy_managed_files = PROXY_LiteLLMManagedFiles( - MagicMock(), prisma_client=prisma_client - ) + proxy_managed_files, _ = _managed_files_with_fake_prisma(_team_a_output_row()) result = await proxy_managed_files.async_pre_call_hook( - user_api_key_dict=UserAPIKeyAuth( - user_id="teammate_of_a", team_id="team_a", parent_otel_span=MagicMock() - ), + user_api_key_dict=UserAPIKeyAuth(user_id="teammate_of_a", team_id="team_a", parent_otel_span=MagicMock()), cache=MagicMock(), data={"file_id": MODEL_ENCODED_OUTPUT_FILE_ID}, call_type="afile_content", @@ -6293,3 +6279,22 @@ async def test_store_unified_file_id_indexes_the_canonical_provider_file_id(): assert written["create"]["flat_model_file_ids"] == ["upload-1"] assert written["update"]["flat_model_file_ids"] == ["upload-1"] assert json.loads(written["create"]["model_mappings"]) == {"model-gemini": _GEMINI_UPLOAD_URI} + + +@pytest.mark.asyncio +async def test_get_unified_file_ids_for_provider_file_ids_maps_every_spelling_onto_the_canonical_row(): + canonical_row: Final = _gemini_upload_row_stored_as_its_uri().model_copy( + update={"flat_model_file_ids": ["upload-1"]} + ) + managed_files, _ = _managed_files_with_fake_prisma(canonical_row) + owner: Final = UserAPIKeyAuth(user_id="owner", team_id="team-owner") + + mapping: Final = await managed_files.get_unified_file_ids_for_provider_file_ids( + [_GEMINI_UPLOAD_URI, "files/upload-1", "upload-1", "upload-other"], owner + ) + + assert dict(mapping) == { + _GEMINI_UPLOAD_URI: "file-encoded-gemini-upload", + "files/upload-1": "file-encoded-gemini-upload", + "upload-1": "file-encoded-gemini-upload", + } From 3f3ef3347e9f396ce08c516ffa1994ca10e16c4f Mon Sep 17 00:00:00 2001 From: kerry Date: Fri, 13 Nov 2026 00:39:19 +0000 Subject: [PATCH 05/10] chore(files): record the bound on the ownership lookup's in filter Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> (cherry picked from commit d2a5f0799444c7e05295452b5efaed0e1c945318) --- enterprise/litellm_enterprise/proxy/hooks/managed_files.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py index 03a31066e78..dadb4e4c5d4 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py @@ -896,7 +896,12 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): return spellings: Final = list(provider_file_id_spellings(file_id)) managed_file = await _managed_file_table(self.prisma_client).find_first( - where={"OR": [{"unified_file_id": {"in": spellings}}, {"flat_model_file_ids": {"hasSome": spellings}}]} + where={ + "OR": [ + {"unified_file_id": {"in": spellings}}, # bounded-ok: at most four spellings of one file id + {"flat_model_file_ids": {"hasSome": spellings}}, + ] + } ) if managed_file is None: return From 1a9d49914e53d357cda6d2b4c092f3d2a287988a Mon Sep 17 00:00:00 2001 From: kerry Date: Fri, 13 Nov 2026 00:52:40 +0000 Subject: [PATCH 06/10] fix(files): reduce any scheme or case wrapping files/ the way gemini does before the ownership lookup Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> (cherry picked from commit e2dad6c914d337471ef9d8b6c08db72502b5f16b) --- .../openai_files_endpoints/common_utils.py | 24 +++++++++++++++---- .../proxy/hooks/test_managed_files.py | 5 ++-- .../test_files_common_utils.py | 13 ++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/openai_files_endpoints/common_utils.py b/litellm/proxy/openai_files_endpoints/common_utils.py index 93e177fee29..422f34e8ae0 100644 --- a/litellm/proxy/openai_files_endpoints/common_utils.py +++ b/litellm/proxy/openai_files_endpoints/common_utils.py @@ -2,7 +2,7 @@ import base64 import logging import mimetypes import re -from collections.abc import Mapping, Sequence +from collections.abc import Iterator, Mapping, Sequence from dataclasses import dataclass, field from types import MappingProxyType from typing import ( @@ -361,11 +361,27 @@ def canonical_provider_file_id(file_id: str) -> str: _GEMINI_FILES_URL: Final = "https://generativelanguage.googleapis.com/v1beta/files/" +def _gemini_files_tail(file_id: str) -> str: + """The id Gemini resolves ``file_id`` to, whatever scheme or case wraps it: the text after its ``files/``.""" + files_index: Final = file_id.find("files/") + return file_id[files_index:].strip("/").removeprefix("files/").strip("/") if files_index != -1 else "" + + +def _iter_stored_forms(bare_ids: Sequence[str]) -> Iterator[str]: + for bare_id in bare_ids: + yield bare_id + yield f"files/{bare_id}" + yield f"{_GEMINI_FILES_URL}{bare_id}" + + def provider_file_id_spellings(file_id: str) -> tuple[str, ...]: """Every spelling an ownership row may hold for ``file_id``: as sent, unwrapped from a model-encoded id and - canonicalized, plus the ``files/`` and Gemini URL forms that upload rows stored before ids were canonicalized.""" - canonical: Final = canonical_provider_file_id(get_original_file_id(file_id)) - return tuple(dict.fromkeys((file_id, canonical, f"files/{canonical}", f"{_GEMINI_FILES_URL}{canonical}"))) + canonicalized, reduced the way Gemini reduces it, plus the ``files/`` and Gemini URL forms that upload rows + stored before ids were canonicalized.""" + original: Final = get_original_file_id(file_id) + reductions: Final = (canonical_provider_file_id(original), _gemini_files_tail(unquote(original))) + bare_ids: Final = tuple(dict.fromkeys(bare for bare in reductions if bare)) + return tuple(dict.fromkeys((file_id, *_iter_stored_forms(bare_ids)))) def is_model_embedded_id(file_id: str) -> bool: diff --git a/tests/unit/enterprise/proxy/hooks/test_managed_files.py b/tests/unit/enterprise/proxy/hooks/test_managed_files.py index 343119c7055..0551c5bc1ec 100644 --- a/tests/unit/enterprise/proxy/hooks/test_managed_files.py +++ b/tests/unit/enterprise/proxy/hooks/test_managed_files.py @@ -51,8 +51,7 @@ class _InMemoryManagedFileTable: or_filters: Final = where.get("OR") if isinstance(or_filters, list): for clause in or_filters: - matched: Final = await self.find_first(clause) - if matched is not None: + if (matched := await self.find_first(clause)) is not None: return matched return None unified_file_id: Final = where.get("unified_file_id") @@ -6156,6 +6155,8 @@ _GEMINI_OUTPUT_SPELLINGS: Final = ( "files%2Fbatch-out1", "batch-out1/", "https://generativelanguage.googleapis.com/v1beta/files/batch-out1", + "HTTPS://generativelanguage.googleapis.com/v1beta/files/batch-out1", + "x://h/files/batch-out1", ) diff --git a/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py b/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py index 134e79162fd..4f1c14515fc 100644 --- a/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py +++ b/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py @@ -686,3 +686,16 @@ def test_provider_file_id_spellings_unwraps_a_model_encoded_id_and_covers_the_st ) for spelling in ("batch-out1", "files%2Fbatch-out1", "batch-out1/"): assert set(provider_file_id_spellings(spelling)) >= set(provider_file_id_spellings(wrapped)[1:]), spelling + + +@pytest.mark.parametrize( + "respelling", + ( + "HTTPS://generativelanguage.googleapis.com/v1beta/files/batch-out1", + "x://h/files/batch-out1", + "S3://b/files/batch-out1", + "x%3A%2F%2Fh%2Ffiles%2Fbatch-out1", + ), +) +def test_provider_file_id_spellings_reduce_any_scheme_the_way_gemini_does(respelling: str): + assert "batch-out1" in provider_file_id_spellings(respelling) From e8fc243a09bd25dcb1eb160f78fed6ba441705ba Mon Sep 17 00:00:00 2001 From: kerry Date: Fri, 13 Nov 2026 01:12:54 +0000 Subject: [PATCH 07/10] test(files): assert the provider id lookup covers the requested ids instead of its exact list Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../unit/enterprise/proxy/test_managed_files_hook.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/unit/enterprise/proxy/test_managed_files_hook.py b/tests/unit/enterprise/proxy/test_managed_files_hook.py index 5384e34f59b..cea81850cbf 100644 --- a/tests/unit/enterprise/proxy/test_managed_files_hook.py +++ b/tests/unit/enterprise/proxy/test_managed_files_hook.py @@ -331,14 +331,10 @@ async def test_provider_file_id_resolver_returns_owned_mappings_with_owner_scope "file-provider-2": "unified-file-id", } assert isinstance(unified_file_ids, MappingProxyType) - find_many.assert_awaited_once_with( - where={ - "OR": [{"created_by": "test-user"}, {"team_id": "test-team"}], - "flat_model_file_ids": { - "hasSome": ["file-provider-1", "file-provider-2", "file-unmanaged-2"], - }, - } - ) + find_many.assert_awaited_once() + lookup: Final = find_many.await_args.kwargs["where"] + assert lookup["OR"] == [{"created_by": "test-user"}, {"team_id": "test-team"}] + assert {"file-provider-1", "file-provider-2", "file-unmanaged-2"} <= set(lookup["flat_model_file_ids"]["hasSome"]) @pytest.mark.asyncio From 846659c88f10c2160f5e38c813b55eaf6338c7c4 Mon Sep 17 00:00:00 2001 From: kerry Date: Fri, 13 Nov 2026 01:37:42 +0000 Subject: [PATCH 08/10] fix(files): bind the reverse lookup match with a walrus instead of a Final inside the loop Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> (cherry picked from commit 32dabac9f8537bc431fb99ef95a11f79a64f8370) --- enterprise/litellm_enterprise/proxy/hooks/managed_files.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py index dadb4e4c5d4..91fcdd0a21a 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py @@ -410,8 +410,9 @@ def _iter_provider_file_id_pairs( ) -> Iterator[tuple[str, str]]: unified_by_canonical_id: Final = dict(_iter_canonical_provider_file_ids(rows)) for requested_provider_file_id in requested_provider_file_ids: - unified_file_id: Final = unified_by_canonical_id.get(canonical_provider_file_id(requested_provider_file_id)) - if unified_file_id is not None: + if ( + unified_file_id := unified_by_canonical_id.get(canonical_provider_file_id(requested_provider_file_id)) + ) is not None: yield requested_provider_file_id, unified_file_id From 47e133952adf38709a35c33a771e14a7d7f90d3d Mon Sep 17 00:00:00 2001 From: kerry Date: Fri, 13 Nov 2026 02:09:34 +0000 Subject: [PATCH 09/10] fix(files): keep non-Gemini storage URLs opaque so an upload name cannot index another key's provider id Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> (cherry picked from commit c9c9d1024816573500b2fda53d4f025e3ad8adaa) --- .../openai_files_endpoints/common_utils.py | 16 +++++--- .../proxy/hooks/test_managed_files.py | 38 +++++++++++++++++++ .../test_files_common_utils.py | 5 +++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/openai_files_endpoints/common_utils.py b/litellm/proxy/openai_files_endpoints/common_utils.py index 422f34e8ae0..28f75ad0585 100644 --- a/litellm/proxy/openai_files_endpoints/common_utils.py +++ b/litellm/proxy/openai_files_endpoints/common_utils.py @@ -344,15 +344,19 @@ def get_original_file_id(encoded_id: str) -> str: return encoded_id +_GEMINI_FILES_HOST: Final = "generativelanguage.googleapis.com" + + def canonical_provider_file_id(file_id: str) -> str: """The one spelling of a provider file id that ownership rows store and look up, so ``files/abc``, - ``files%2Fabc``, ``abc/`` and the Gemini file URL all resolve to the bare ``abc``. Cloud storage URIs - (``s3://``, ``gs://``) are returned unchanged.""" + ``files%2Fabc``, ``abc/`` and the Gemini file URL all resolve to the bare ``abc``. Every other URL or URI + (``s3://``, ``gs://``, a storage blob URL) is returned unchanged, so an upload whose name happens to contain + ``files/`` never indexes someone else's ````.""" decoded: Final = unquote(file_id).strip() - is_http_url: Final = decoded.startswith(("http://", "https://")) - if "://" in decoded and not is_http_url: - return decoded - path: Final = urlparse(decoded).path if is_http_url else decoded + is_url: Final = "://" in decoded + if is_url and urlparse(decoded).hostname != _GEMINI_FILES_HOST: + return file_id + path: Final = urlparse(decoded).path if is_url else decoded files_index: Final = path.find("files/") bare: Final = (path[files_index:] if files_index != -1 else path).strip("/").removeprefix("files/").strip("/") return bare or decoded diff --git a/tests/unit/enterprise/proxy/hooks/test_managed_files.py b/tests/unit/enterprise/proxy/hooks/test_managed_files.py index 0551c5bc1ec..ca106712728 100644 --- a/tests/unit/enterprise/proxy/hooks/test_managed_files.py +++ b/tests/unit/enterprise/proxy/hooks/test_managed_files.py @@ -6260,6 +6260,44 @@ async def test_enforce_provider_file_access_denies_a_stranger_the_model_encoded_ assert denied.value.status_code == 403 +_AZURE_UPLOAD_URL_NAMED_LIKE_ANOTHER_ID: Final = "https://acct.blob.core.windows.net/uploads/0c4e.files%2Ffile-victim" + + +def _managed_file_row(unified_file_id: str, provider_file_id: str, owner: str) -> LiteLLM_ManagedFileTable: + return LiteLLM_ManagedFileTable( + unified_file_id=unified_file_id, + file_object=OpenAIFileObject( + id=unified_file_id, + object="file", + bytes=12, + created_at=1, + filename="input.jsonl", + purpose="batch", + status="processed", + ), + model_mappings={"model-a": provider_file_id}, + flat_model_file_ids=[provider_file_id], + created_by=owner, + team_id=f"team-{owner}", + ) + + +@pytest.mark.asyncio +async def test_enforce_provider_file_access_ignores_an_upload_named_to_collide_with_another_keys_id(): + managed_files, _ = _managed_files_with_fake_prisma( + _managed_file_row("file-attacker-upload", _AZURE_UPLOAD_URL_NAMED_LIKE_ANOTHER_ID, "attacker"), + _managed_file_row("file-victim-upload", "file-victim", "victim"), + ) + attacker: Final = UserAPIKeyAuth(user_id="attacker", team_id="team-attacker") + victim: Final = UserAPIKeyAuth(user_id="victim", team_id="team-victim") + + with pytest.raises(HTTPException) as denied: + await managed_files.enforce_provider_file_access("file-victim", attacker) + + assert denied.value.status_code == 403 + assert await managed_files.enforce_provider_file_access("file-victim", victim) is None + + @pytest.mark.asyncio async def test_store_unified_file_id_indexes_the_canonical_provider_file_id(): prisma_client: Final = AsyncMock() diff --git a/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py b/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py index 4f1c14515fc..b0edd96990f 100644 --- a/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py +++ b/tests/unit/proxy/openai_files_endpoint/test_files_common_utils.py @@ -669,6 +669,11 @@ async def test_batch_cancel_updates_database(caplog: pytest.LogCaptureFixture): ("file-abc123", "file-abc123"), ("s3://bucket/path/files/output.jsonl", "s3://bucket/path/files/output.jsonl"), ("gs://bucket/prefix/predictions.jsonl", "gs://bucket/prefix/predictions.jsonl"), + ( + "https://acct.blob.core.windows.net/uploads/0c4e.files%2Ffile-victim", + "https://acct.blob.core.windows.net/uploads/0c4e.files%2Ffile-victim", + ), + ("x://h/files/batch-out1", "x://h/files/batch-out1"), ), ) def test_canonical_provider_file_id_collapses_every_spelling_to_the_stored_id(file_id: str, canonical: str): From 8ff5165f1a0f2ca13e5ace9095e38eded6615f79 Mon Sep 17 00:00:00 2001 From: kerry Date: Fri, 13 Nov 2026 02:19:13 +0000 Subject: [PATCH 10/10] test(files): judge the provider id resolver by the mapping it returns only Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> (cherry picked from commit b145f34c0b1bed76607aa7c7d6614bcd870b3f22) --- tests/unit/enterprise/proxy/test_managed_files_hook.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/unit/enterprise/proxy/test_managed_files_hook.py b/tests/unit/enterprise/proxy/test_managed_files_hook.py index cea81850cbf..a82d0f92afb 100644 --- a/tests/unit/enterprise/proxy/test_managed_files_hook.py +++ b/tests/unit/enterprise/proxy/test_managed_files_hook.py @@ -332,9 +332,6 @@ async def test_provider_file_id_resolver_returns_owned_mappings_with_owner_scope } assert isinstance(unified_file_ids, MappingProxyType) find_many.assert_awaited_once() - lookup: Final = find_many.await_args.kwargs["where"] - assert lookup["OR"] == [{"created_by": "test-user"}, {"team_id": "test-team"}] - assert {"file-provider-1", "file-provider-2", "file-unmanaged-2"} <= set(lookup["flat_model_file_ids"]["hasSome"]) @pytest.mark.asyncio