fix(auth): inherit organization_alias from the org for JWT and team-linked keys

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
jesus 2026-09-11 00:00:17 +00:00
parent ac66754689
commit 0f3c4ccfbb
2 changed files with 143 additions and 3 deletions

View file

@ -54,6 +54,7 @@ from litellm.proxy.auth.auth_checks import (
get_end_user_object,
get_jwt_key_mapping_object,
get_object_permission,
get_org_object,
get_project_object,
get_team_object,
get_user_object,
@ -2398,6 +2399,37 @@ def _token_can_vouch_for_team(valid_token: UserAPIKeyAuth, lookup_error: BaseExc
return PrismaDBExceptionHandler.should_allow_request_on_db_unavailable()
async def _inherit_org_identity(
user_api_key_auth_obj: UserAPIKeyAuth,
team_object: LiteLLM_TeamTableCachedObj | None,
prisma_client: PrismaClient | None,
user_api_key_cache: UserApiKeyCache,
parent_otel_span: Span | None,
proxy_logging_obj: ProxyLogging | None,
) -> None:
if user_api_key_auth_obj.org_id is None and team_object is not None and team_object.organization_id is not None:
user_api_key_auth_obj.org_id = team_object.organization_id
if (
user_api_key_auth_obj.org_id is None
or user_api_key_auth_obj.organization_alias is not None
or prisma_client is None
):
return
try:
org_object: Final = await get_org_object(
org_id=user_api_key_auth_obj.org_id,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
parent_otel_span=parent_otel_span,
proxy_logging_obj=proxy_logging_obj,
)
except Exception:
verbose_proxy_logger.debug("org alias lookup failed for org_id=%s", user_api_key_auth_obj.org_id, exc_info=True)
return
if org_object is not None:
user_api_key_auth_obj.organization_alias = org_object.organization_alias
@tracer.wrap()
async def _run_centralized_common_checks(
user_api_key_auth_obj: UserAPIKeyAuth,
@ -2622,8 +2654,14 @@ async def _run_centralized_common_checks(
)
global_proxy_spend: float | None = None if isinstance(global_spend_result, BaseException) else global_spend_result
if user_api_key_auth_obj.org_id is None and team_object is not None and team_object.organization_id is not None:
user_api_key_auth_obj.org_id = team_object.organization_id
await _inherit_org_identity(
user_api_key_auth_obj=user_api_key_auth_obj,
team_object=cast(LiteLLM_TeamTableCachedObj | None, team_object),
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
parent_otel_span=parent_otel_span,
proxy_logging_obj=proxy_logging_obj,
)
# common_checks identifies admin via user_object, not the token
# (non_proxy_admin_allowed_routes_check). JWT admin shortcut and

View file

@ -23,6 +23,7 @@ from litellm.proxy._types import (
LiteLLM_JWTAuth,
LiteLLM_BudgetTable,
LiteLLM_EndUserTable,
LiteLLM_OrganizationTable,
LiteLLM_UserTable,
LitellmUserRoles,
ProxyErrorTypes,
@ -31,7 +32,7 @@ from litellm.proxy._types import (
JWTRoutingOverride,
)
from litellm.proxy.auth.handle_jwt import JWTHandler
from litellm.proxy.auth.auth_checks import get_key_object, _cache_key_object
from litellm.proxy.auth.auth_checks import OrganizationNotFoundError, get_key_object, _cache_key_object
from litellm.proxy.auth.route_checks import RouteChecks
from litellm.proxy.auth.user_api_key_auth import (
_check_key_model_budget_with_fallback,
@ -5293,6 +5294,107 @@ async def test_centralized_common_checks_backfills_org_id_from_team(key_org_id,
setattr(_proxy_server_mod, k, v)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"key_org_id,team_id,team_org_id,existing_alias,lookup_mode,expected_org_id,expected_alias",
[
(None, "t1", "org-from-team", None, "success", "org-from-team", "acme-org"),
("org-jwt", None, None, None, "success", "org-jwt", "acme-org"),
("org-pinned", None, None, "preset", "success", "org-pinned", "preset"),
("org-missing", None, None, None, "missing", "org-missing", None),
],
)
async def test_centralized_common_checks_inherits_org_alias(
key_org_id,
team_id,
team_org_id,
existing_alias,
lookup_mode,
expected_org_id,
expected_alias,
):
import litellm.proxy.proxy_server as _proxy_server_mod
from fastapi import Request
from starlette.datastructures import URL
from litellm.proxy._types import LiteLLM_TeamTableCachedObj
token = UserAPIKeyAuth(
api_key="sk-test",
user_id="u",
team_id=team_id,
org_id=key_org_id,
organization_alias=existing_alias,
)
request = Request(scope={"type": "http"})
request._url = URL(url="/chat/completions")
fetched_team = (
LiteLLM_TeamTableCachedObj(team_id="t1", organization_id=team_org_id) if team_id is not None else None
)
organization = LiteLLM_OrganizationTable(
organization_id=expected_org_id,
organization_alias="acme-org",
budget_id="budget-id",
models=[],
created_by="test",
updated_by="test",
)
attrs = _proxy_attrs_for_centralized_checks(user_custom_auth=None)
attrs["prisma_client"] = MagicMock()
originals = {a: getattr(_proxy_server_mod, a, None) for a in attrs}
try:
for k, v in attrs.items():
setattr(_proxy_server_mod, k, v)
identity_seen_by_common_checks = []
with (
patch(
"litellm.proxy.auth.user_api_key_auth.get_team_object",
new_callable=AsyncMock,
return_value=fetched_team,
) as mock_get_team_object,
patch(
"litellm.proxy.auth.user_api_key_auth.get_org_object",
new_callable=AsyncMock,
return_value=organization,
) as mock_get_org_object,
patch(
"litellm.proxy.auth.user_api_key_auth.common_checks",
new_callable=AsyncMock,
side_effect=lambda **kw: identity_seen_by_common_checks.append(
(kw["valid_token"].org_id, kw["valid_token"].organization_alias)
),
) as mock_checks,
):
if lookup_mode == "missing":
mock_get_org_object.side_effect = OrganizationNotFoundError("x")
await _run_centralized_common_checks(
user_api_key_auth_obj=token,
request=request,
request_data={"model": "gpt-4o"},
route="/chat/completions",
)
mock_checks.assert_awaited_once()
assert token.org_id == expected_org_id
assert token.organization_alias == expected_alias
assert identity_seen_by_common_checks == [(expected_org_id, expected_alias)]
if team_id is None:
mock_get_team_object.assert_not_awaited()
else:
mock_get_team_object.assert_awaited_once()
if existing_alias is not None:
mock_get_org_object.assert_not_awaited()
else:
mock_get_org_object.assert_awaited_once()
assert mock_get_org_object.await_args.kwargs["org_id"] == expected_org_id
finally:
for k, v in originals.items():
setattr(_proxy_server_mod, k, v)
@pytest.mark.asyncio
async def test_cli_session_token_org_backfilled_from_team(monkeypatch):
"""LIT-4688 root cause: CLI session tokens (from /sso/cli/poll) are minted