fix: enforce team MCP permissions when using JWT authentication

Root cause: When JWT auth was used with teams in groups (via team_ids_jwt_field),
the team's MCP permissions were not being enforced because:

1. The default team_allowed_routes did not include mcp_routes
2. allowed_routes_check() failed for MCP endpoints like /mcp/tools/list
3. find_team_with_model_access() skipped the team due to failed route check
4. team_id was None in UserAPIKeyAuth
5. MCPRequestHandler._get_allowed_mcp_servers_for_team() returned empty list

Fix: Add 'mcp_routes' to the default team_allowed_routes in LiteLLM_JWTAuth.

This ensures that teams can access MCP endpoints by default, allowing the
team's MCP server permissions to be properly enforced.

Added tests:
- test_reproduce_jwt_mcp_enforcement_issue: Reproduces the exact bug scenario
- test_verify_mcp_routes_in_default_team_allowed_routes: Verifies fix
- test_mcp_route_check_passes_for_team: Verifies route check works

Co-authored-by: ishaan <ishaan@berri.ai>
This commit is contained in:
Cursor Agent 2026-02-04 02:48:06 +00:00
parent f9669cc132
commit 4294d28a4c
2 changed files with 160 additions and 1 deletions

View file

@ -3673,7 +3673,7 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase):
team_id_upsert: bool = False
team_ids_jwt_field: Optional[str] = None
upsert_sso_user_to_team: bool = False
team_allowed_routes: List[str] = ["openai_routes", "info_routes"]
team_allowed_routes: List[str] = ["openai_routes", "info_routes", "mcp_routes"]
team_id_default: Optional[str] = Field(
default=None,
description="If no team_id given, default permissions/spend-tracking to this team.s",

View file

@ -0,0 +1,159 @@
"""
Test to reproduce the issue where Team MCP permissions are not enforced when using JWT authentication.
Scenario:
1. Team "ABC" exists with models configured and MCPs assigned
2. User JWT has team "ABC" in groups (via team_ids_jwt_field)
3. Call MCP list endpoint
4. EXPECTED: Team MCP permissions should be enforced
5. ACTUAL: Team MCP permissions are NOT enforced (BUG)
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from litellm.proxy._types import (
LiteLLM_JWTAuth,
LiteLLM_TeamTable,
LiteLLM_ObjectPermissionTable,
UserAPIKeyAuth,
)
from litellm.proxy.auth.handle_jwt import JWTAuthManager, JWTHandler
from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import (
MCPRequestHandler,
)
@pytest.mark.asyncio
async def test_reproduce_jwt_mcp_enforcement_issue(monkeypatch):
"""
Reproduce the bug where Team MCP permissions are NOT enforced when using JWT.
Setup:
- Team "ABC" has models ["gpt-4"] and MCPs ["mcp-server-1"] assigned
- JWT has team "ABC" in groups field
- User calls MCP list endpoint (no model requested)
Expected: team_id should be set to "ABC" so MCP permissions are enforced
Actual (BUG): team_id is None because route check fails for MCP routes
"""
from litellm.caching import DualCache
from litellm.proxy.utils import ProxyLogging
from litellm.router import Router
# Setup mock router
router = Router(model_list=[{"model_name": "gpt-4", "litellm_params": {"model": "gpt-4"}}])
import sys
import types
proxy_server_module = types.ModuleType("proxy_server")
proxy_server_module.llm_router = router
monkeypatch.setitem(sys.modules, "litellm.proxy.proxy_server", proxy_server_module)
# Team "ABC" has models configured AND MCPs assigned
team_with_mcp = LiteLLM_TeamTable(
team_id="ABC",
models=["gpt-4"], # Team HAS models
object_permission=LiteLLM_ObjectPermissionTable(
object_permission_id="perm-123",
mcp_servers=["mcp-server-1"], # Team has MCPs assigned
),
)
async def mock_get_team_object(*args, **kwargs):
team_id = kwargs.get("team_id") or args[0]
if team_id == "ABC":
return team_with_mcp
return None
monkeypatch.setattr(
"litellm.proxy.auth.handle_jwt.get_team_object", mock_get_team_object
)
# Setup JWT handler with team_ids_jwt_field (groups)
jwt_handler = JWTHandler()
jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth(
team_ids_jwt_field="groups", # Use groups field for teams
# NOTE: team_allowed_routes defaults to ["openai_routes", "info_routes"]
# which does NOT include "mcp_routes"
)
user_api_key_cache = DualCache()
proxy_logging_obj = ProxyLogging(user_api_key_cache=user_api_key_cache)
# Simulate JWT payload with team in groups
jwt_token = {
"sub": "user-123",
"groups": ["ABC"], # Team "ABC" is in groups
"scope": "",
}
# Mock auth_jwt to return our token
with patch.object(jwt_handler, "auth_jwt", new_callable=AsyncMock) as mock_auth_jwt:
mock_auth_jwt.return_value = jwt_token
# Call auth_builder for MCP route (like /mcp/tools/list)
result = await JWTAuthManager.auth_builder(
api_key="test-jwt-token",
jwt_handler=jwt_handler,
request_data={}, # No model in request (MCP endpoint)
general_settings={},
route="/mcp/tools/list", # MCP route
prisma_client=None,
user_api_key_cache=user_api_key_cache,
parent_otel_span=None,
proxy_logging_obj=proxy_logging_obj,
)
# THIS IS THE BUG: team_id should be "ABC" but it's None!
print(f"Result team_id: {result['team_id']}")
print(f"Result team_object: {result['team_object']}")
# The test should FAIL if the bug exists (team_id is None)
# If the fix is applied, team_id should be "ABC"
assert result["team_id"] == "ABC", (
f"BUG: team_id should be 'ABC' but got '{result['team_id']}'. "
f"This happens because default team_allowed_routes does not include 'mcp_routes', "
f"so allowed_routes_check() fails and the team is skipped in find_team_with_model_access()."
)
@pytest.mark.asyncio
async def test_verify_mcp_routes_in_default_team_allowed_routes():
"""
Verify that mcp_routes IS in the default team_allowed_routes.
This is required for team MCP permissions to work with JWT auth.
"""
default_jwt_auth = LiteLLM_JWTAuth()
print(f"Default team_allowed_routes: {default_jwt_auth.team_allowed_routes}")
# mcp_routes must be in defaults for team MCP permissions to work
assert "mcp_routes" in default_jwt_auth.team_allowed_routes, (
"mcp_routes must be in default team_allowed_routes for JWT MCP enforcement to work"
)
@pytest.mark.asyncio
async def test_mcp_route_check_passes_for_team():
"""
Verify that allowed_routes_check returns True for MCP routes with default settings.
This is required for teams to access MCP endpoints with JWT auth.
"""
from litellm.proxy._types import LitellmUserRoles
from litellm.proxy.auth.auth_checks import allowed_routes_check
jwt_auth = LiteLLM_JWTAuth() # Use defaults
# Check if MCP route is allowed for TEAM role
is_allowed = allowed_routes_check(
user_role=LitellmUserRoles.TEAM,
user_route="/mcp/tools/list",
litellm_proxy_roles=jwt_auth,
)
print(f"Is /mcp/tools/list allowed for TEAM with defaults? {is_allowed}")
# MCP routes should be allowed by default for teams
assert is_allowed is True, (
"MCP routes must be allowed by default for teams for JWT MCP enforcement to work"
)