Skip to content

fix(vector_stores): only list vector stores the caller was granted - #39612

Merged
yassin-berriai merged 2 commits into
litellm_internal_stagingfrom
litellm_vector_store_list_requires_grant
Sep 3, 2026
Merged

yassin-berriai merged 2 commits into
litellm_internal_stagingfrom
litellm_vector_store_list_requires_grant

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • GET /vector_store/list shows every store with no team to any key
  • Dashboard sessions see every store created from the dashboard
  • Explicit object_permission.vector_stores grants were never consulted for those stores

How it solves it:

  • Non-admin listings only include stores the caller was granted
  • A grant is a key or team object_permission.vector_stores entry, or team ownership
  • Dashboard sessions are evaluated through the user's real teams plus the session key's own grants

User Flow

Before: a developer holding a plain key with no vector store grant can enumerate vector stores the admin never shared with them

  1. The admin creates a store with POST http://localhost:4000/vector_store/new, body {"vector_store_id":"vs-unscoped","custom_llm_provider":"openai"}, and does not grant it to any team or key
  2. The developer calls GET http://localhost:4000/vector_store/list with a key that has no team and no object_permission
  3. The response is 200 and data contains vs-unscoped
  4. A non-admin user logs into http://localhost:4000/ui, opens the Vector Stores page, and sees vs-unscoped plus every store any admin created from the dashboard
  5. Result: anyone with a key or a dashboard login can see the ids, names, and provider of stores they were never given access to

After: the same key and the same user see only what they were granted

  1. The admin creates the same store with POST http://localhost:4000/vector_store/new and does not grant it
  2. The developer calls GET http://localhost:4000/vector_store/list with the same key
  3. The response is 200 and data is []
  4. The admin grants the store to that key with POST http://localhost:4000/key/update, body {"key":"<key>","object_permission":{"vector_stores":["vs-unscoped"]}}, or to the developer's team with POST http://localhost:4000/team/update
  5. The same GET now returns vs-unscoped
  6. The non-admin user on http://localhost:4000/ui sees only the stores owned by or granted to a team they belong to
  7. Result: a key or dashboard login can no longer discover stores it was not granted; proxy admins still see everything

Relevant issues

Linear ticket

Resolves LIT-6863

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Root cause: the shared access check returns "allowed" for any store with no team_id before it looks at key or team object_permission.vector_stores, and a dashboard session carries the shared litellm-dashboard team id, which is also the team_id stamped on every store created from the dashboard, so both fall through as "the caller's team owns it"

Shared setup, run once against a proxy on localhost:4000 with Postgres and master_key: sk-1234. The same rows and keys are reused for both arms, nothing is recreated between them

H='Authorization: Bearer sk-1234'
J='Content-Type: application/json'

curl -s -X POST http://localhost:4000/team/new -H "$H" -H "$J" -d '{"team_id":"team-no-perms","team_alias":"team-no-perms"}'
curl -s -X POST http://localhost:4000/team/new -H "$H" -H "$J" -d '{"team_id":"team-granted","team_alias":"team-granted","object_permission":{"vector_stores":["vs-unscoped"]}}'

# admin-created store, no team, no permissions configured anywhere
curl -s -X POST http://localhost:4000/vector_store/new -H "$H" -H "$J" -d '{"vector_store_id":"vs-unscoped","custom_llm_provider":"openai","vector_store_name":"unscoped"}'

# store owned by team-no-perms (created with an admin key scoped to that team, team_id is stamped from the caller key)
curl -s -X POST http://localhost:4000/key/generate -H "$H" -H "$J" -d '{"key_alias":"admin-key-team-no-perms","team_id":"team-no-perms","user_id":"default_user_id"}'   # -> $KEY_ADMIN_TEAM
curl -s -X POST http://localhost:4000/vector_store/new -H "Authorization: Bearer $KEY_ADMIN_TEAM" -H "$J" -d '{"vector_store_id":"vs-team-owned","custom_llm_provider":"openai","vector_store_name":"team owned"}'

# store created through an admin dashboard session (POST /login as admin, then the session key), lands with team_id litellm-dashboard
curl -s -i -X POST http://localhost:4000/login -d 'username=admin&password=sk-1234'   # -> $ADMIN_SESSION
curl -s -X POST http://localhost:4000/vector_store/new -H "Authorization: Bearer $ADMIN_SESSION" -H "$J" -d '{"vector_store_id":"vs-ui-admin-created","custom_llm_provider":"openai","vector_store_name":"created from ui"}'

curl -s -X POST http://localhost:4000/key/generate -H "$H" -H "$J" -d '{"key_alias":"key-no-team-no-perms"}'                                              # -> $KEY_NOTEAM
curl -s -X POST http://localhost:4000/key/generate -H "$H" -H "$J" -d '{"key_alias":"key-team-no-perms","team_id":"team-no-perms"}'                       # -> $KEY_TEAM_NOPERMS
curl -s -X POST http://localhost:4000/key/generate -H "$H" -H "$J" -d '{"key_alias":"key-team-granted","team_id":"team-granted"}'                         # -> $KEY_TEAM_GRANTED
curl -s -X POST http://localhost:4000/key/generate -H "$H" -H "$J" -d '{"key_alias":"key-perm-granted","object_permission":{"vector_stores":["vs-unscoped"]}}'  # -> $KEY_PERM_GRANTED

# non-admin dashboard user alice, internal_user, member of team-no-perms only, no object_permission anywhere
curl -s -X POST http://localhost:4000/user/new -H "$H" -H "$J" -d '{"user_id":"alice","user_email":"alice@example.com","user_role":"internal_user","teams":["team-no-perms"],"auto_create_key":false}'
curl -s -X POST http://localhost:4000/user/update -H "$H" -H "$J" -d '{"user_id":"alice","password":"<password>"}'
curl -s -i -X POST http://localhost:4000/login -d 'username=alice@example.com&password=<password>'   # -> $ALICE_SESSION, the session key inside the returned token

# MCP server for comparison, no permissions configured anywhere
curl -s -X POST http://localhost:4000/v1/mcp/server -H "$H" -H "$J" -d '{"server_id":"mcp_unscoped","server_name":"mcp_unscoped","url":"http://127.0.0.1:1/mcp","transport":"http"}'

Rows in LiteLLM_ManagedVectorStoresTable after setup

vector_store_id      |      team_id      |     user_id
---------------------+-------------------+-----------------
vs-team-owned        | team-no-perms     | default_user_id
vs-ui-admin-created  | litellm-dashboard | default_user_id
vs-unscoped          |                   | default_user_id

Every case below is curl -s http://localhost:4000/vector_store/list -H "Authorization: Bearer <credential>" and the output is the sorted list of vector_store_id in data, except the last two which hit GET /v1/mcp/server and list server_id

Before (4b1e24e)

admin (sk-1234): control, sees everything

  1. curl -s http://localhost:4000/vector_store/list -H 'Authorization: Bearer sk-1234'
  2. ['vs-team-owned', 'vs-ui-admin-created', 'vs-unscoped']

key: no team, no object_permission

  1. curl -s http://localhost:4000/vector_store/list -H "Authorization: Bearer $KEY_NOTEAM"
  2. ['vs-unscoped']

key: team-no-perms (team owns vs-team-owned, no object_permission)

  1. curl -s http://localhost:4000/vector_store/list -H "Authorization: Bearer $KEY_TEAM_NOPERMS"
  2. ['vs-team-owned', 'vs-unscoped']

key: team-granted (team object_permission.vector_stores=[vs-unscoped])

  1. curl -s http://localhost:4000/vector_store/list -H "Authorization: Bearer $KEY_TEAM_GRANTED"
  2. ['vs-unscoped']

key: key-level object_permission.vector_stores=[vs-unscoped]

  1. curl -s http://localhost:4000/vector_store/list -H "Authorization: Bearer $KEY_PERM_GRANTED"
  2. ['vs-unscoped']

dashboard session: alice (internal_user, member of team-no-perms, no object_permission anywhere)

  1. curl -s http://localhost:4000/vector_store/list -H "Authorization: Bearer $ALICE_SESSION"
  2. ['vs-ui-admin-created', 'vs-unscoped']

MCP comparison, key: no team, no object_permission

  1. curl -s http://localhost:4000/v1/mcp/server -H "Authorization: Bearer $KEY_NOTEAM"
  2. []

MCP comparison, dashboard session: alice

  1. curl -s http://localhost:4000/v1/mcp/server -H "Authorization: Bearer $ALICE_SESSION"
  2. []

After (dfb6990)

admin (sk-1234): control, sees everything

  1. curl -s http://localhost:4000/vector_store/list -H 'Authorization: Bearer sk-1234'
  2. ['vs-team-owned', 'vs-ui-admin-created', 'vs-unscoped']

key: no team, no object_permission

  1. curl -s http://localhost:4000/vector_store/list -H "Authorization: Bearer $KEY_NOTEAM"
  2. []

key: team-no-perms (team owns vs-team-owned, no object_permission)

  1. curl -s http://localhost:4000/vector_store/list -H "Authorization: Bearer $KEY_TEAM_NOPERMS"
  2. ['vs-team-owned']

key: team-granted (team object_permission.vector_stores=[vs-unscoped])

  1. curl -s http://localhost:4000/vector_store/list -H "Authorization: Bearer $KEY_TEAM_GRANTED"
  2. ['vs-unscoped']

key: key-level object_permission.vector_stores=[vs-unscoped]

  1. curl -s http://localhost:4000/vector_store/list -H "Authorization: Bearer $KEY_PERM_GRANTED"
  2. ['vs-unscoped']

dashboard session: alice (internal_user, member of team-no-perms, no object_permission anywhere)

  1. curl -s http://localhost:4000/vector_store/list -H "Authorization: Bearer $ALICE_SESSION"
  2. ['vs-team-owned']

MCP comparison, key: no team, no object_permission

  1. curl -s http://localhost:4000/v1/mcp/server -H "Authorization: Bearer $KEY_NOTEAM"
  2. []

MCP comparison, dashboard session: alice

  1. curl -s http://localhost:4000/v1/mcp/server -H "Authorization: Bearer $ALICE_SESSION"
  2. []

Served build check for the After arm: the proxy process was started with PYTHONPATH=/home/ubuntu/repos/litellm at 19:43:57 UTC, after utils.py in that tree was last written at 19:41:05 UTC, and that file contains filter_listable_vector_stores once. The Before arm ran against the same tree at 4b1e24eae9, where that symbol does not exist

Type

🐛 Bug Fix

Caveats (if any)

Severe

  • Non-admin keys with no grant now get an empty list where they used to see every unscoped store
  • Deployments that relied on unscoped stores being visible to everyone must grant them via object_permission.vector_stores on the key or team

Medium

  • Only /vector_store/list (and /v1/vector_store/list) changes
  • Using an unscoped store in a request, or deleting it, still goes through the older check, which allows any caller when the store has no team. Tightening that is a separate decision because it would break existing requests that use unscoped stores

Low

  • A grant placed directly on a user row is not consulted for dashboard listings, only the user's teams and the session key's own object_permission are

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/7563ac587ff34765bacc7885ecade1ab
Open in Devin Desktop: https://app.devin.ai/desktop/session/7563ac587ff34765bacc7885ecade1ab?variant=devin
Requested by: @yassin-berriai

/vector_store/list returned every managed vector store with no team_id to any
key, and let a dashboard session see stores created from the dashboard because
every session shares the litellm-dashboard team id. Non-admin listings now show
a store only when the key or one of the caller's real teams is allowlisted for
it via object_permission.vector_stores, or the team owns it

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codspeed

codspeed Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_vector_store_list_requires_grant (dfb6990) with litellm_internal_staging (9212208)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds security hardening to vector-store listings while preserving administrator access

  • Filters non-administrator listing results using key grants, real team grants, and team ownership
  • Evaluates dashboard sessions through a separate session-key context plus resolved real-team contexts
  • Adds regression coverage for unscoped stores, team access, administrator access, and dashboard users with no teams

Confidence Score: 5/5

The PR appears safe to merge

No blocking failure remains

Important Files Changed

Filename Overview
litellm/proxy/vector_store_endpoints/management_endpoints.py The listing endpoint now delegates non-administrator resource filtering to the shared listing authorization helper before redaction and pagination
litellm/proxy/vector_store_endpoints/utils.py The authorization helper preserves session-key grants for dashboard users with no real teams while separately evaluating each resolved team
tests/test_litellm/proxy/vector_store_endpoints/test_vector_store_access_control.py Regression cases cover key grants, team grants, ownership, administrator access, and zero-team dashboard session grants

Reviews (2): Last reviewed commit: "fix(vector_stores): keep a dashboard ses..." | Re-trigger Greptile

Comment thread litellm/proxy/vector_store_endpoints/utils.py Outdated
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…e user has no teams

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review at dfb6990, the zero team dashboard session now keeps its session key grants

@yassin-berriai
yassin-berriai merged commit 1f20b38 into litellm_internal_staging Sep 3, 2026
81 checks passed
@yassin-berriai
yassin-berriai deleted the litellm_vector_store_list_requires_grant branch September 3, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants