diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260501195714_managed_resource_team_owner/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260501195714_managed_resource_team_owner/migration.sql index 9020f41fe8..793a02c08a 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260501195714_managed_resource_team_owner/migration.sql +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260501195714_managed_resource_team_owner/migration.sql @@ -13,6 +13,9 @@ ALTER TABLE "LiteLLM_ManagedFileTable" ADD COLUMN IF NOT EXISTS "created_by_team ALTER TABLE "LiteLLM_ManagedObjectTable" ADD COLUMN IF NOT EXISTS "created_by_team_id" TEXT; ALTER TABLE "LiteLLM_ManagedVectorStoreTable" ADD COLUMN IF NOT EXISTS "created_by_team_id" TEXT; -CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedFileTable_team_owner_created_at_idx" ON "LiteLLM_ManagedFileTable" ("created_by_team_id", "created_at" DESC); -CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedObjectTable_team_owner_created_at_idx" ON "LiteLLM_ManagedObjectTable" ("created_by_team_id", "created_at" DESC); -CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedVectorStoreTable_team_owner_created_at_idx" ON "LiteLLM_ManagedVectorStoreTable" ("created_by_team_id", "created_at" DESC); +-- Index names follow Prisma's auto-generated convention so `prisma migrate diff` +-- against the schema is clean. Postgres caps identifier length at 63 chars, +-- which truncates the vector-store name to `_created__idx`. +CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedFileTable_created_by_team_id_created_at_idx" ON "LiteLLM_ManagedFileTable" ("created_by_team_id", "created_at" DESC); +CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedObjectTable_created_by_team_id_created_at_idx" ON "LiteLLM_ManagedObjectTable" ("created_by_team_id", "created_at" DESC); +CREATE INDEX IF NOT EXISTS "LiteLLM_ManagedVectorStoreTable_created_by_team_id_created__idx" ON "LiteLLM_ManagedVectorStoreTable" ("created_by_team_id", "created_at" DESC); diff --git a/litellm/llms/base_llm/managed_resources/isolation.py b/litellm/llms/base_llm/managed_resources/isolation.py index dea7f1e23d..267643e1f5 100644 --- a/litellm/llms/base_llm/managed_resources/isolation.py +++ b/litellm/llms/base_llm/managed_resources/isolation.py @@ -38,17 +38,31 @@ def build_owner_filter( - ``{"created_by": }`` for user-keyed callers. - ``{"created_by_team_id": }`` for service-account callers that have a team but no user_id. + - ``{"OR": [...]}`` when the caller has both — listing must include + both their own resources and team-shared ones so it stays consistent + with ``can_access_resource``. - ``None`` means deny: callers MUST skip the query rather than fall back to an unscoped fetch. """ if _user_has_admin_view(user_api_key_dict): return {} - if user_api_key_dict.user_id is not None: - return {"created_by": user_api_key_dict.user_id} + user_id = user_api_key_dict.user_id + team_id = user_api_key_dict.team_id - if user_api_key_dict.team_id is not None: - return {"created_by_team_id": user_api_key_dict.team_id} + if user_id is not None and team_id is not None: + return { + "OR": [ + {"created_by": user_id}, + {"created_by_team_id": team_id}, + ] + } + + if user_id is not None: + return {"created_by": user_id} + + if team_id is not None: + return {"created_by_team_id": team_id} return None diff --git a/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py b/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py index b11fa351e7..a6bc9874ac 100644 --- a/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py +++ b/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py @@ -34,9 +34,17 @@ def test_owner_filter_service_account_scoped_to_team(): assert build_owner_filter(service_account) == {"created_by_team_id": "team-eng"} -def test_owner_filter_user_id_takes_precedence_over_team_id(): +def test_owner_filter_user_with_team_returns_or_filter(): + """List view must mirror `can_access_resource`: a user-keyed caller in a + team can also access team-shared resources, so the listing returns both + their own records and team records via an OR filter.""" user = UserAPIKeyAuth(user_id="alice", team_id="team-eng") - assert build_owner_filter(user) == {"created_by": "alice"} + assert build_owner_filter(user) == { + "OR": [ + {"created_by": "alice"}, + {"created_by_team_id": "team-eng"}, + ] + } def test_owner_filter_no_identity_returns_none():