mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-17 23:51:30 +00:00
fix(mcp): persist OAuth credentials for rowless JWT admins
This commit is contained in:
parent
72a4824acb
commit
61e3b5ddae
3 changed files with 40 additions and 19 deletions
|
|
@ -371,10 +371,9 @@ async def _extract_jwt_user_id(request: Request, token: str) -> str | None:
|
|||
identity_only=True,
|
||||
)
|
||||
resolved_user: Final = identity["user_object"]
|
||||
if resolved_user is None:
|
||||
if resolved_user is not None and isinstance(_active_user_record(resolved_user), str):
|
||||
return None
|
||||
owner: Final = _active_user_record(resolved_user)
|
||||
return None if isinstance(owner, str) else identity["user_id"]
|
||||
return identity["user_id"]
|
||||
except Exception as exc: # noqa: BLE001 # public OAuth exchange stays available; unvalidated identities never write credentials
|
||||
verbose_logger.debug("OAuth JWT identity could not be validated (%s)", type(exc).__name__)
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ from litellm.proxy.common_utils.user_api_key_cache import (
|
|||
from litellm.proxy.utils import PrismaClient, ProxyLogging
|
||||
from litellm.repositories.user_repository import UserRepository
|
||||
from litellm.types.agents import AgentResponse
|
||||
from litellm.types.proxy.auth.auth_checks import UserNotFoundError
|
||||
|
||||
from .auth_checks import (
|
||||
_allowed_routes_check,
|
||||
|
|
@ -2343,21 +2344,26 @@ class JWTAuthManager:
|
|||
)
|
||||
|
||||
if identity_only:
|
||||
identity_user, _, _, _, identity_user_id = await JWTAuthManager.get_objects(
|
||||
user_id=user_id,
|
||||
user_email=user_email,
|
||||
org_id=None,
|
||||
end_user_id=None,
|
||||
team_id=None,
|
||||
valid_user_email=valid_user_email,
|
||||
jwt_handler=jwt_handler,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
parent_otel_span=parent_otel_span,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
route=route,
|
||||
user_id_upsert=False,
|
||||
)
|
||||
try:
|
||||
identity_user, _, _, _, identity_user_id = await JWTAuthManager.get_objects(
|
||||
user_id=user_id,
|
||||
user_email=user_email,
|
||||
org_id=None,
|
||||
end_user_id=None,
|
||||
team_id=None,
|
||||
valid_user_email=valid_user_email,
|
||||
jwt_handler=jwt_handler,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
parent_otel_span=parent_otel_span,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
route=route,
|
||||
user_id_upsert=False,
|
||||
)
|
||||
except UserNotFoundError:
|
||||
if not jwt_handler.is_admin(scopes=scopes):
|
||||
raise
|
||||
identity_user, identity_user_id = None, user_id
|
||||
return JWTAuthBuilderResult(
|
||||
is_proxy_admin=False,
|
||||
# Admin admission uses the claim ID; other callers use the canonical DB ID.
|
||||
|
|
|
|||
|
|
@ -11444,11 +11444,13 @@ def _oauth_identity_jwt(
|
|||
@pytest.mark.parametrize("header", ["Authorization", "x-litellm-api-key"])
|
||||
@pytest.mark.parametrize("policy_allowed", [False, True])
|
||||
@pytest.mark.parametrize("admin", [False, True])
|
||||
@pytest.mark.parametrize("owner_state", ["active", "missing", "inactive", "database_error"])
|
||||
async def test_oauth_exchange_stores_token_for_validated_jwt_user(
|
||||
jwt_oauth_identity: tuple["JWTHandler", "RSAPrivateKey"],
|
||||
header: str,
|
||||
policy_allowed: bool,
|
||||
admin: bool,
|
||||
owner_state: str,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
import httpx
|
||||
|
|
@ -11475,6 +11477,7 @@ async def test_oauth_exchange_stores_token_for_validated_jwt_user(
|
|||
from litellm.caching.llm_caching_handler import LLMClientCache
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
|
||||
from litellm.proxy import proxy_server
|
||||
from litellm.models.user import LiteLLM_UserTable
|
||||
from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
|
||||
from litellm.types.llms.custom_http import httpxSpecialProvider
|
||||
|
||||
|
|
@ -11485,6 +11488,18 @@ async def test_oauth_exchange_stores_token_for_validated_jwt_user(
|
|||
return httpx.Response(200, json={"access_token": "upstream-token", "token_type": "Bearer"})
|
||||
|
||||
database: Final = MagicMock()
|
||||
users: Final = database.db.litellm_usertable
|
||||
users.find_unique = AsyncMock(return_value=None)
|
||||
users.find_first = AsyncMock(return_value=None)
|
||||
users.create = AsyncMock()
|
||||
if owner_state in ("missing", "database_error"):
|
||||
handler.user_api_key_cache.delete_cache("jwt-owner")
|
||||
if owner_state == "database_error":
|
||||
users.find_unique.side_effect = RuntimeError("database unavailable")
|
||||
if owner_state == "inactive":
|
||||
handler.user_api_key_cache.set_cache(
|
||||
"jwt-owner", LiteLLM_UserTable(user_id="jwt-owner", metadata={"scim_active": False})
|
||||
)
|
||||
table: Final = database.db.litellm_mcpusercredentials
|
||||
table.find_unique = AsyncMock(return_value=None)
|
||||
table.upsert = AsyncMock()
|
||||
|
|
@ -11509,7 +11524,8 @@ async def test_oauth_exchange_stores_token_for_validated_jwt_user(
|
|||
)
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.body)["access_token"] == "upstream-token"
|
||||
if not policy_allowed:
|
||||
users.create.assert_not_awaited()
|
||||
if not policy_allowed or owner_state in ("inactive", "database_error") or (owner_state == "missing" and not admin):
|
||||
table.upsert.assert_not_awaited()
|
||||
return
|
||||
table.upsert.assert_awaited_once()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue