Skip to content

fix(auth): resolve team object_permission independently in the unresolvable-team fallback - #37960

Merged
yassin-berriai merged 2 commits into
litellm_internal_stagingfrom
litellm_lit5539_team_fallback_object_permission
Aug 22, 2026
Merged

fix(auth): resolve team object_permission independently in the unresolvable-team fallback#37960
yassin-berriai merged 2 commits into
litellm_internal_stagingfrom
litellm_lit5539_team_fallback_object_permission

Conversation

@yassin-berriai

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • When a token's team row cannot be read, the auth fallback dropped the team's object_permission
  • That silently widened a token's effective vector-store/MCP/agent access beyond what its own object_permission_id vouches for

How it solves it:

  • Resolve the team's object_permission independently by its own object_permission_id when the team row itself is unresolvable
  • Matches how vector store access checks and MCP resolvers already treat an unresolvable team

User Flow

Before: a token whose team's database row becomes unreadable (deleted, or a transient lookup miss with a stale key-level cache) has its effective access silently re-derived without the team's object-permission restriction, even though the restriction still exists in the database

  1. An admin creates a team with object_permission.vector_stores restricted to one store, and a key scoped to that team
  2. The team's underlying database row is later deleted (or otherwise becomes unreadable) while the key stays cached
  3. The key sends a request that resolves through the auth layer's unresolvable-team fallback
  4. The returned auth object carries team_object_permission_id (the restriction's id) but team_object_permission itself is None, so any code that trusts that field directly sees no restriction at all instead of the one the id names

After: the same fallback re-resolves the restriction by its own id, so the returned auth object accurately reflects it

  1. Same team, key, and deleted team row as above
  2. The key sends the same request through the same fallback
  3. The returned auth object's team_object_permission is the actual restricted permission (naming the one allowed vector store), matching team_object_permission_id
  4. Any caller that reads the field directly sees the true restriction instead of an empty one; a caller could no longer effectively bypass the team's restriction by relying on the field alone

Relevant issues

Linear ticket

Resolves LIT-5539

Pre-Submission checklist

  • 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

Setup shared by both runs: a real proxy against real Postgres and real Redis (enable_redis_auth_cache: true plus general_settings.coordination_redis, so the auth cache is externally inspectable). A team ("owner-team") owns two vector stores, vs-lit5539-allowed and vs-lit5539-forbidden. A second team ("caller-team") is created with object_permission.vector_stores: ["vs-lit5539-allowed"], and a key is generated scoped to caller-team. One request through the intact team warms both the key-level cache (which bakes in team_object_permission_id from a live join) and the team-level cache. The team-level cache entry is then evicted directly in Redis and the team's row is deleted from Postgres, while the object_permission row is left intact, reproducing the exact scenario: the team is unresolvable but its object_permission id is still known. A temporary debug log (not part of the diff) placed immediately after the fallback assignment records team_object_permission_id and the resolved team_object_permission on the real auth object built for that request

Before (490c9f9, the merge base this PR is built on; reproduced by locally reverting just this PR's object_permission= assignment back to that commit's omission of the kwarg, which is behaviorally identical since both leave it at the pydantic default of None)

GET /vector_store/list against a key whose team became unresolvable

  1. curl $BASE/vector_store/list -H "Authorization: Bearer $CALLER_KEY" returns vs-lit5539-allowed only (total_count: 1)
  2. Proxy log for that request: LIT5539_PROOF team_object_permission_id=891dfb2b-19e2-4ccc-b839-3e458aba97ba team_object_permission=None, so the auth object built for this request carries the restriction's id but not the restriction itself
  3. The HTTP response still happens to be correctly scoped only because can_user_access_vector_store independently re-resolves the permission by id when the field is None; a caller that trusts team_object_permission directly (as a sibling code path, or any future consumer, already does for the identical key-level field) would see no restriction at all

After (e4272c6)

GET /vector_store/list against a key whose team became unresolvable

  1. curl $BASE/vector_store/list -H "Authorization: Bearer $CALLER_KEY" returns vs-lit5539-allowed only (total_count: 1)
  2. Proxy log for that request: LIT5539_PROOF team_object_permission_id=31160444-2bbe-405f-9c46-d88fd0610b3b team_object_permission=object_permission_id='31160444-2bbe-405f-9c46-d88fd0610b3b' ... vector_stores=['vs-lit5539-allowed'] ..., so the auth object now accurately carries the restriction itself, matching its id
  3. The list only ever contains vs-lit5539-allowed, never vs-lit5539-forbidden, in either run, so the observable HTTP behavior is unchanged while the underlying auth object is now correct at the source rather than by accident of one consumer's own defensive re-resolution

Type

🐛 Bug Fix

Caveats (if any)

  • The vector-store access check already re-resolves the permission by id when the field is None, so this fix closes the gap at the source rather than changing today's HTTP-observable behavior for that one endpoint; the risk it removes is any other or future consumer trusting team_object_permission directly, the same way the identical key-level field is already trusted elsewhere

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

…lvable-team fallback

When get_team_object fails for a token's team_id, _user_api_key_auth_builder
reconstructs a LiteLLM_TeamTableCachedObj from the token's own cached fields,
carrying team_object_permission_id but leaving object_permission unset. That
silently dropped any vector-store or MCP restriction the team carried,
granting more access than the token's own object_permission_id vouches for.

Resolve the object permission by its id directly via get_object_permission,
independent of the unreadable team row, matching how every other consumer of
a team's object_permission (vector store access checks, MCP tool/server
resolvers) already treats an unresolvable team as "no restriction at this
level" and re-resolves on its own.
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai

@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.

@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes the authentication fallback construct a consistent team authorization object when the team record cannot be resolved.

  • Adds a cache-backed lookup for the independently stored permission object.
  • Adds regression coverage for successful resolution and an unreadable permission record.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
litellm/proxy/auth/user_api_key_auth.py Adds a focused helper and wires the resolved permission into the existing unresolvable-team fallback.
tests/test_litellm/proxy/auth/test_user_api_key_auth.py Covers both successful permission resolution and graceful handling when the permission record is unavailable.

Reviews (2): Last reviewed commit: "fix(auth): trim ticket references and na..." | Re-trigger Greptile

Comment thread litellm/proxy/auth/user_api_key_auth.py
Comment thread litellm/proxy/auth/user_api_key_auth.py Outdated
…le review

Drop the LIT-5539 ticket id from test names and fixture strings, and shorten
both the new helper's docstring and the regression test docstrings to their
contracts rather than restating the fix's history.
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 563f2a1: trimmed the helper and test docstrings to their contracts and dropped the ticket id from names and fixtures.

@codspeed-hq

codspeed-hq Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_lit5539_team_fallback_object_permission (563f2a1) with litellm_internal_staging (490c9f9)

Open in CodSpeed

@yassin-berriai
yassin-berriai merged commit 15510f0 into litellm_internal_staging Aug 22, 2026
71 checks passed
@yassin-berriai
yassin-berriai deleted the litellm_lit5539_team_fallback_object_permission branch August 22, 2026 21:25
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.

3 participants