fix(proxy): match Prisma index names + extend listing to team for user-keyed callers

Two follow-ups to the managed-resource isolation fix:

1. Rename the new composite indexes to match Prisma's auto-generated naming
   convention (`<Table>_created_by_team_id_created_at_idx`). The previous
   `*_team_owner_created_at_idx` names left `prisma migrate diff` reporting
   an outstanding `RENAME INDEX`, failing `test_aaaasschema_migration_check`.

2. Make `build_owner_filter` return an OR clause when the caller has both
   a `user_id` and a `team_id`, so listings include team-shared resources
   the same way `can_access_resource` already permits reading them. Without
   this a user could fetch a team-shared resource by id but never see it
   in their list view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
user
2026-05-01 20:44:51 +00:00
co-authored by Claude Opus 4.7
parent 84fede37b4
commit 799d79160a
3 changed files with 34 additions and 9 deletions
@@ -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);
@@ -38,17 +38,31 @@ def build_owner_filter(
- ``{"created_by": <user_id>}`` for user-keyed callers.
- ``{"created_by_team_id": <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
@@ -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():