diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 9dce42bd3a9..e4e2ceb6621 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -2697,7 +2697,11 @@ def compute_effective_models( effective = union(team_defaults, member_models), capped by team_pool. - If neither defaults nor overrides are set, falls back to team_pool (backward compat). - - If cap empties the list (all stale), falls back to team_pool (NOT [] which = allow-all). + - If cap empties the list (all overrides/defaults are stale), falls back to team_pool + rather than returning [] (which would mean "allow all"). This is a deliberate security + trade-off: a member with entirely stale overrides gets team-pool access (the team's + restriction is still enforced) instead of unrestricted access. Admins should clean up + stale overrides via /team/member_update when narrowing team.models. - team_pool=[] means "allow all" — cap is skipped. """ # dict.fromkeys preserves insertion order while deduplicating diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index a1cf563144e..5b47861899f 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -2533,7 +2533,7 @@ async def team_member_update( # noqa: PLR0915 from litellm.proxy.proxy_server import user_api_key_cache _cache_key = f"team_membership:{received_user_id}:{data.team_id}" - user_api_key_cache.delete_cache(key=_cache_key) + await user_api_key_cache.async_delete_cache(key=_cache_key) ### update team member role # Resolve the effective models for this member (from the authoritative diff --git a/litellm/proxy/management_helpers/utils.py b/litellm/proxy/management_helpers/utils.py index 15385b2a42b..afb0c59bc2c 100644 --- a/litellm/proxy/management_helpers/utils.py +++ b/litellm/proxy/management_helpers/utils.py @@ -140,7 +140,7 @@ async def handle_budget_for_entity( return existing_budget_id -async def add_new_member( +async def add_new_member( # noqa: PLR0915 new_member: Member, max_budget_in_team: Optional[float], prisma_client: PrismaClient, @@ -271,6 +271,13 @@ async def add_new_member( **_returned_team_membership.model_dump() ) + # Invalidate any stale cached membership for this user+team pair + # (e.g., from a previous add/remove cycle). + from litellm.proxy.proxy_server import user_api_key_cache + + _cache_key = f"team_membership:{returned_user.user_id}:{team_id}" + await user_api_key_cache.async_delete_cache(key=_cache_key) + if returned_user is None: raise Exception("Unable to update user table with membership information!")