Skip to content

[Infra] Merge daily branch with main#24055

Merged
yuneng-jiang merged 18 commits intomainfrom
litellm_yj_march_17_2026
Mar 18, 2026
Merged

[Infra] Merge daily branch with main#24055
yuneng-jiang merged 18 commits intomainfrom
litellm_yj_march_17_2026

Conversation

@yuneng-jiang
Copy link
Copy Markdown
Contributor

Relevant issues

Pre-Submission checklist

https://app.circleci.com/pipelines/github/BerriAI/litellm?branch=litellm_yj_march_17_2026

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🚄 Infrastructure

Changes

yuneng-jiang and others added 17 commits March 17, 2026 13:30
Add Vitest + RTL tests for HelpLink, DebugWarningBanner, ExportFormatSelector,
ExportTypeSelector, ExportSummary, MetricCard, PolicySelect,
ComplexityRouterConfig, RateLimitTypeFormItem, and AgentCardGrid (67 tests total).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
[Test] UI: Add unit tests for 10 untested components
… and indexes

Add org admin support to /v2/team/list so org admins can list teams
within their organizations instead of getting 401. Also enrich the
response with members_count and add missing indexes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix org admin own-query regression: always check org admin status
  before the standard route check so own-queries see all org teams
- Clear user_id when org admin is detected so org scope replaces
  user-membership scope
- Remove dead isinstance(organization_id, list) branch
- Remove unused datetime import
- Remove orphaned _convert_teams_to_response helper

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- _get_org_admin_org_ids: catch only ValueError (user not found) instead
  of bare Exception — DB errors now propagate as 500s instead of silently
  demoting org admins to regular users
- _build_team_list_where_conditions: return None (not a sentinel string)
  when user has no team memberships; list_team_v2 short-circuits to empty
  response without hitting the DB
- Org admin + team_id + user_id: use exact team_id match with org scope
  instead of OR expansion that effectively ignored the team_id filter
- Org admin + user_id (no team_id): OR(org teams, direct memberships)
  now matches legacy _authorize_and_filter_teams behaviour

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace raw find_unique with get_user_object in
  _build_team_list_where_conditions for cache/metrics consistency
- Remove over-complex OR clause for org admin + user_id: when user_id
  is provided, filter by that user's direct team memberships (same as
  regular users) since the access control gate already verified the
  org admin's authority
- Preserve caller-supplied organization_id instead of overwriting with
  org_admin_org_ids
- Update test mock to match get_user_object call path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Document intentional legacy-matching behavior: when user_id is
  provided to an org admin, no org filter is applied (returns all of
  that user's teams across all orgs, same as legacy endpoint)
- Fix two existing security tests to properly patch user_api_key_cache,
  proxy_logging_obj, and get_user_object instead of relying on
  incidental error handling
- Add three new org admin test cases:
  - Org admin sees org-scoped teams (200 with correct where clause)
  - Org admin rejected when filtering by other org (403)
  - Org admin with user_id filter returns target user's teams

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… auth

On /key/block, /key/unblock, and /key/update, the request body 'key'
field could contaminate the api_key Security dependency, causing the
auth layer to authenticate against the target key instead of the
caller's bearer token. This returned 401 for a nonexistent body key
even when the Authorization header contained a valid master key.

Added a guard in user_api_key_auth that re-reads the Authorization
header directly from the request, ensuring the header is always the
authoritative source for authentication.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[Feature] /v2/team/list: Add org admin access control, members_count, and indexes
Extract duplicate file preview JSX blocks (responses and chat image
previews) into a reusable FilePreviewCard component, reducing ~50
lines of duplicated markup in ChatUI.tsx.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[Refactor] UI - Playground: Extract FilePreviewCard from ChatUI
list_team_v2 had 51 statements (limit 50). Extract the team-to-response-model
conversion loop into a helper function to satisfy ruff PLR0915.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Mar 18, 2026 10:19pm

Request Review

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq bot commented Mar 18, 2026

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing litellm_yj_march_17_2026 (1e6abf8) with main (cbb4c2c)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (8e61b32) during the generation of this report, so cbb4c2c was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

)
verbose_proxy_logger.debug(
"list_team_v2: org admin access for user=%s, org_ids=%s, user_id_filter=%s",
user_api_key_dict.user_id,

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 30 days ago

In general, to fix log injection issues, any user-controlled value written to logs should be sanitized to remove or neutralize control characters that can affect log structure (e.g., \r, \n, \t). For plain-text logs, a common approach is to strip or replace line breaks with safe characters or spaces before passing them to the logger. This preserves the semantic content of the value while ensuring each log entry remains on a single line and cannot be spoofed by attackers.

For this specific case, the vulnerable sink is the verbose_proxy_logger.debug call in list_team_v2 that logs user_api_key_dict.user_id. The best minimal fix without changing functionality is to introduce a small local sanitization helper (or just sanitize inline) for user_api_key_dict.user_id before it is logged, replacing any \r and \n characters with empty strings. We will only touch the shown snippet in litellm/proxy/management_endpoints/team_endpoints.py. A clear and localized change is to define a sanitized variable safe_user_id_for_log immediately before the log statement:

safe_user_id_for_log = (
    str(user_api_key_dict.user_id).replace("\r", "").replace("\n", "")
    if user_api_key_dict.user_id is not None
    else None
)
verbose_proxy_logger.debug(
    "list_team_v2: org admin access for user=%s, org_ids=%s, user_id_filter=%s",
    safe_user_id_for_log,
    org_admin_org_ids,
    user_id,
)

This preserves the log message semantics while ensuring that any newline characters in user_id cannot affect log structure. No new imports or external dependencies are required; we use only built-in string methods. We are careful not to alter the values used for actual authorization logic—only the value passed into the logger is sanitized, so functionality remains unchanged.

Suggested changeset 1
litellm/proxy/management_endpoints/team_endpoints.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py
--- a/litellm/proxy/management_endpoints/team_endpoints.py
+++ b/litellm/proxy/management_endpoints/team_endpoints.py
@@ -3456,9 +3456,14 @@
                         "error": "You can only view teams within your organizations."
                     },
                 )
+            safe_user_id_for_log = (
+                str(user_api_key_dict.user_id).replace("\r", "").replace("\n", "")
+                if user_api_key_dict.user_id is not None
+                else None
+            )
             verbose_proxy_logger.debug(
                 "list_team_v2: org admin access for user=%s, org_ids=%s, user_id_filter=%s",
-                user_api_key_dict.user_id,
+                safe_user_id_for_log,
                 org_admin_org_ids,
                 user_id,
             )
EOF
@@ -3456,9 +3456,14 @@
"error": "You can only view teams within your organizations."
},
)
safe_user_id_for_log = (
str(user_api_key_dict.user_id).replace("\r", "").replace("\n", "")
if user_api_key_dict.user_id is not None
else None
)
verbose_proxy_logger.debug(
"list_team_v2: org admin access for user=%s, org_ids=%s, user_id_filter=%s",
user_api_key_dict.user_id,
safe_user_id_for_log,
org_admin_org_ids,
user_id,
)
Copilot is powered by AI and may make mistakes. Always verify output.
"list_team_v2: org admin access for user=%s, org_ids=%s, user_id_filter=%s",
user_api_key_dict.user_id,
org_admin_org_ids,
user_id,

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 30 days ago

To fix the problem, sanitize the user-controlled value before it is written to the log. In this context that means ensuring user_id (and any other directly logged user input) cannot inject line breaks or other control characters that could confuse log parsing. A simple and compatible mitigation for plain-text logs is to strip or replace \r and \n (and optionally other control characters) from the string before logging.

The single best minimally invasive fix here is to introduce a small helper function in this file that takes a string (or None) and returns a sanitized version with \r and \n removed. Then, wrap user_id in this helper when passing it to verbose_proxy_logger.debug at line 3463. This preserves all existing functionality (same value, minus dangerous line breaks) while eliminating the injection vector. The helper can be defined once near the top of the file (after imports) and reused wherever needed.

Concretely:

  • In litellm/proxy/management_endpoints/team_endpoints.py, add a function such as _sanitize_for_logging(value: Optional[str]) -> Optional[str] that removes \r and \n from non-None strings.
  • At the debug logging call around lines 3459–3464, change the third argument from user_id to _sanitize_for_logging(user_id) so that the logged value is safe.
  • No new imports are required; we only use basic string operations.
Suggested changeset 1
litellm/proxy/management_endpoints/team_endpoints.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py
--- a/litellm/proxy/management_endpoints/team_endpoints.py
+++ b/litellm/proxy/management_endpoints/team_endpoints.py
@@ -22,6 +22,16 @@
 import litellm
 from litellm._logging import verbose_proxy_logger
 from litellm._uuid import uuid
+
+
+def _sanitize_for_logging(value: Optional[str]) -> Optional[str]:
+    """
+    Remove newline characters from user-controlled values before logging
+    to reduce the risk of log injection.
+    """
+    if value is None:
+        return None
+    return value.replace("\r", "").replace("\n", "")
 from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
 from litellm.proxy._types import (
     BlockTeamRequest,
@@ -3460,7 +3470,7 @@
                 "list_team_v2: org admin access for user=%s, org_ids=%s, user_id_filter=%s",
                 user_api_key_dict.user_id,
                 org_admin_org_ids,
-                user_id,
+                _sanitize_for_logging(user_id),
             )
         else:
             # Not an org admin — fall back to standard route check
EOF
@@ -22,6 +22,16 @@
import litellm
from litellm._logging import verbose_proxy_logger
from litellm._uuid import uuid


def _sanitize_for_logging(value: Optional[str]) -> Optional[str]:
"""
Remove newline characters from user-controlled values before logging
to reduce the risk of log injection.
"""
if value is None:
return None
return value.replace("\r", "").replace("\n", "")
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.proxy._types import (
BlockTeamRequest,
@@ -3460,7 +3470,7 @@
"list_team_v2: org admin access for user=%s, org_ids=%s, user_id_filter=%s",
user_api_key_dict.user_id,
org_admin_org_ids,
user_id,
_sanitize_for_logging(user_id),
)
else:
# Not an org admin — fall back to standard route check
Copilot is powered by AI and may make mistakes. Always verify output.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 18, 2026

Greptile Summary

This PR merges the litellm_yj_march_17_2026 daily branch into main, delivering three main changes: (1) org admin access control for the /v2/team/list endpoint, (2) a members_count field on team list response items, and (3) performance indexes on LiteLLM_TeamTable. On the UI side, duplicate file-preview markup in ChatUI.tsx is extracted into a new FilePreviewCard component.

Key changes:

  • list_team_v2: org admins may now query teams scoped to their organizations, bypassing the previous proxy-admin-only gate. A new _get_org_admin_org_ids helper fetches the caller's org memberships on every non-admin request.
  • _build_team_list_where_conditions: refactored to use get_user_object (cached) instead of a raw prisma_client.db.litellm_usertable.find_unique call; returns None early when no results are possible to skip the DB round-trip.
  • New TeamListItem type extends LiteLLM_TeamTable with members_count: int.
  • DB migration adds indexes on organization_id, team_alias, and created_at — directly supporting the new query patterns.
  • New FilePreviewCard component with full unit test coverage.
  • Existing security tests updated to mock get_user_object instead of the raw Prisma table (appropriate — the underlying code changed, not the contract).

Confidence Score: 3/5

  • Safe to review further; contains a privilege-escalation risk in the org admin + user_id code path that should be resolved before merging.
  • The schema changes and UI refactor are clean. However, the new org admin access path in list_team_v2 allows an org admin to retrieve any user's teams across ALL organizations (not just their own) by passing an arbitrary user_id parameter. The intentional comment in the code acknowledges the lack of an org filter, but granting cross-org visibility to org admins is a privilege escalation beyond what they should be allowed to see. The redundant get_user_object call per request also adds unnecessary latency for all non-admin list requests.
  • litellm/proxy/management_endpoints/team_endpoints.py — specifically the _build_team_list_where_conditions function's user_id + org_admin_org_ids interaction.

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/team_endpoints.py Core change: org admin access added to /v2/team/list, members_count added to list items, and user lookup refactored to get_user_object. Contains a privilege escalation risk when org admin queries with user_id parameter.
litellm/types/proxy/management_endpoints/team_endpoints.py New TeamListItem type extending LiteLLM_TeamTable with a members_count: int = 0 field; TeamListResponse.teams union updated to include it. Backward-compatible change.
litellm-proxy-extras/litellm_proxy_extras/migrations/20260318140652_add_index_to_team_table/migration.sql Adds three indexes (organization_id, team_alias, created_at) to LiteLLM_TeamTable. Clean performance improvement aligned with the new query patterns.
schema.prisma Prisma schema updated with the same three indexes on LiteLLM_TeamTable; mirrors the migration SQL and the proxy-extras schema.
tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py Existing security tests updated to mock get_user_object (no real DB calls); three new org-admin scenario tests added (sees org teams, blocked from other orgs, user_id filter). All tests remain mock-only, appropriate for this directory.
ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.tsx New reusable component extracted from duplicated inline code in ChatUI.tsx; clean and well-tested.
ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx Replaced two copies of the file-preview card markup with <FilePreviewCard /> — pure refactor, no logic changes.
ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.test.tsx Comprehensive unit tests for the new FilePreviewCard component, covering PDF vs image rendering, remove callback, and uppercase .PDF extension handling.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[GET /v2/team/list] --> B{prisma_client connected?}
    B -- No --> Z[500 Error]
    B -- Yes --> C{is_proxy_admin?}
    C -- Yes --> G[Build where conditions\nno user filter imposed]
    C -- No --> D[_get_org_admin_org_ids\ncalls get_user_object for caller]
    D --> E{org_admin_org_ids\nreturned?}
    E -- Yes --> F{org filter\nprovided?}
    F -- org not in admin orgs --> Z2[403 Error]
    F -- allowed --> G
    E -- No --> H{allowed_route_check\ninside_route?}
    H -- Fails --> Z3[401 Error]
    H -- Passes --> I[Auto-inject\ncaller user_id]
    I --> G
    G --> J[_build_team_list_where_conditions]
    J --> K{user_id provided?}
    K -- Yes --> L[get_user_object for target user\n2nd DB call]
    L --> M{user has teams?}
    M -- No --> N[Return empty result\nskip DB]
    M -- Yes --> O[Filter by user team IDs\nNo org constraint applied ⚠️]
    K -- No --> P{org_admin_org_ids?}
    P -- Yes --> Q[Filter by admin org IDs]
    P -- No --> R[No user/org filter]
    O --> S[DB query + paginate]
    Q --> S
    R --> S
    N --> T[Return empty page]
    S --> U[_convert_teams_to_response_models\nadds members_count]
    U --> V[Return TeamListResponse]
Loading

Last reviewed commit: "Merge branch 'main' ..."

Comment on lines 3277 to +3285

if organization_id:
where_conditions["organization_id"] = organization_id
elif org_admin_org_ids is not None and not user_id:
# Org admin without explicit org or user filter: scope to their orgs.
# NOTE: when user_id is provided, no org filter is applied — the
# query returns all teams the target user belongs to across all
# organisations. This matches the legacy /team/list behaviour in
# _authorize_and_filter_teams which fetches direct-membership teams
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Org admin can view any user's teams across all orgs (privilege escalation)

When user_id is provided, the org-admin scope check is deliberately skipped (elif org_admin_org_ids is not None and not user_id). This means an org admin from org_A can pass user_id=<any_user> and retrieve all of that user's teams — even teams that belong to org_B or org_C, organizations the admin has no authority over.

The original /v2/team/list endpoint rejected org admins entirely; this PR grants them broader access than intended. The org-filter should be applied even when user_id is specified, e.g. by intersecting the user's team IDs with teams belonging to org_admin_org_ids.

Example of a more constrained fix:

# When user_id AND org_admin_org_ids are both present, combine the filters
if not user_team_ids:
    return None
where_conditions["team_id"] = {"in": user_team_ids}
if org_admin_org_ids is not None:
    # Limit to teams in the admin's orgs
    where_conditions["organization_id"] = {"in": org_admin_org_ids}

Comment on lines 3213 to +3248
return available_teams_correct_type


async def _get_org_admin_org_ids(
user_id: str,
prisma_client: Any,
user_api_key_cache: Any,
proxy_logging_obj: Any,
) -> Optional[List[str]]:
"""
Return the list of organization IDs where the user is an org admin.
Returns None if the user is not an org admin of any organization or if
the user cannot be found.
"""
try:
caller_user = await get_user_object(
user_id=user_id,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
user_id_upsert=False,
proxy_logging_obj=proxy_logging_obj,
)
except ValueError:
# get_user_object raises ValueError when the user doesn't exist
return None

if caller_user is None:
return None

org_ids = [
m.organization_id
for m in (caller_user.organization_memberships or [])
if m.user_role == LitellmUserRoles.ORG_ADMIN.value
]
return org_ids if org_ids else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Extra DB call on every non-admin /v2/team/list request

_get_org_admin_org_ids invokes get_user_object unconditionally for every non-proxy-admin call to list_team_v2. This adds a database round-trip (or cache lookup) on every request even when the caller is a plain user who will never be an org admin. While list_team_v2 is a management endpoint (not the inference critical path), the lookup is also repeated inside _build_team_list_where_conditions when a user_id filter is provided, so an org-admin query with user_id causes two get_user_object calls.

Consider passing the already-fetched caller user object into _build_team_list_where_conditions to avoid the second lookup, and caching the org-admin status on the UserAPIKeyAuth dict if this endpoint is called frequently.


mock_prisma_client = Mock()

# Mock prisma_client to be non-None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Whitespace-only leftover in test file

The change to test_list_team_v2_with_invalid_status converted indentation/trailing whitespace in surrounding tests but left one trailing-space line:

    mock_prisma_client = Mock()
    
    # Mock prisma_client to be non-None

The blank line between mock_prisma_client = Mock() and the comment still has trailing whitespace (the old style). Minor, but inconsistent with the rest of the cleanup in this file.

Copy link
Copy Markdown
Contributor

@ryan-crabbe ryan-crabbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@yuneng-jiang yuneng-jiang merged commit 488b93c into main Mar 18, 2026
71 of 99 checks passed
@superpoussin22
Copy link
Copy Markdown
Contributor

think you should add IF NOT EXISTS in litellm-proxy-extras/litellm_proxy_extras/migrations/20260318140652_add_index_to_team_table/migration.sql @ryan-crabbe @yuneng-jiang

@ishaan-berri ishaan-berri deleted the litellm_yj_march_17_2026 branch March 26, 2026 22:30
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.

4 participants