[Refactor] Align /v2/key/info response handling with v1#25313
[Refactor] Align /v2/key/info response handling with v1#25313yuneng-berri merged 1 commit intomainfrom
Conversation
The /v2/key/info endpoint was missing response filtering that the v1 /key/info endpoint already had. This aligns the two endpoints so v2 applies the same per-key permission checks and strips internal fields from the response. Also fixes the key_aliases query path to resolve aliases before querying.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR closes real security and correctness gaps in
Confidence Score: 4/5Safe to merge after addressing the missing attach_object_permission_to_dict call; remaining items are P2 style or cleanup One P1 issue — attach_object_permission_to_dict is called in v1 but omitted in v2, silently returning incomplete data for keys with object permissions. The remaining three items (unnecessary DB join, null key field, missing tests) are P2 and do not block merge. key_management_endpoints.py lines 2662-2668 — the serialization block in info_key_fn_v2 is missing the object permission attachment present in v1
|
| Filename | Overview |
|---|---|
| litellm/proxy/management_endpoints/key_management_endpoints.py | Adds access-control filtering, token stripping, and alias resolution to /v2/key/info; one P1 gap — attach_object_permission_to_dict is present in v1 but omitted in v2, causing incomplete responses for keys with object permissions |
Sequence Diagram
sequenceDiagram
participant C as Client
participant V2 as POST /v2/key/info
participant DB as Database (Prisma)
participant AC as _can_user_query_key_info
C->>V2: {keys: [...], key_aliases: [...]}
alt key_aliases present
V2->>DB: find_many(key_alias IN key_aliases)
DB-->>V2: alias rows → extract tokens
end
V2->>DB: get_data(token=tokens_to_query, find_all)
DB-->>V2: key_info rows
loop for each key
V2->>AC: check user permission
AC-->>V2: allowed / denied
alt allowed
V2->>V2: model_dump(), pop(token)
Note over V2: ⚠️ attach_object_permission missing
else denied
V2->>V2: skip key silently
end
end
V2-->>C: {key: data.keys, info: filtered_key_info}
Comments Outside Diff (1)
-
litellm/proxy/management_endpoints/key_management_endpoints.py, line 2601-2668 (link)No tests added for new access-control and alias-resolution paths
CLAUDE.mdexplicitly requires: "Always add tests when adding new entity types or features" and for name→ID resolution: test (a) alias resolves and is allowed, (b) alias resolves but is not allowed, (c) alias does not resolve at all. None of these paths have coverage. The silent-skip behavior for unauthorized keys (rather than a 403) is especially important to cover — this is where access-control bugs tend to hide. Relevant test file:tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py.Context Used: CLAUDE.md (source)
Reviews (1): Last reviewed commit: "[Refactor] Align /v2/key/info response h..." | Re-trigger Greptile
| try: | ||
| k = k.model_dump() # noqa | ||
| k_dict = k.model_dump() | ||
| except Exception: | ||
| # if using pydantic v1 | ||
| k = k.dict() | ||
| filtered_key_info.append(k) | ||
| k_dict = k.dict() | ||
| k_dict.pop("token", None) | ||
| filtered_key_info.append(k_dict) |
There was a problem hiding this comment.
Missing
attach_object_permission_to_dict — incomplete v1 parity
The PR's stated goal is full v1 parity. The v1 endpoint (info_key_fn, line 2751) calls attach_object_permission_to_dict(key_info, prisma_client) after serializing each key, enriching response objects that carry an object_permission_id with the full object_permission sub-object. The v2 endpoint omits this step entirely, so any key with an object_permission_id will return incomplete data through v2 — silently and with no error.
| try: | |
| k = k.model_dump() # noqa | |
| k_dict = k.model_dump() | |
| except Exception: | |
| # if using pydantic v1 | |
| k = k.dict() | |
| filtered_key_info.append(k) | |
| k_dict = k.dict() | |
| k_dict.pop("token", None) | |
| filtered_key_info.append(k_dict) | |
| try: | |
| k_dict = k.model_dump() | |
| except Exception: | |
| k_dict = k.dict() | |
| k_dict.pop("token", None) | |
| k_dict = await attach_object_permission_to_dict(k_dict, prisma_client) | |
| filtered_key_info.append(k_dict) |
| alias_rows = await prisma_client.db.litellm_verificationtoken.find_many( | ||
| where={"key_alias": {"in": data.key_aliases}}, | ||
| include={"litellm_budget_table": True}, | ||
| ) |
There was a problem hiding this comment.
Unnecessary
include in alias resolution query
Only row.token is used from alias_rows — the joined litellm_budget_table data is immediately discarded. The include causes an extra DB join on every alias-path request with no benefit.
| alias_rows = await prisma_client.db.litellm_verificationtoken.find_many( | |
| where={"key_alias": {"in": data.key_aliases}}, | |
| include={"litellm_budget_table": True}, | |
| ) | |
| alias_rows = await prisma_client.db.litellm_verificationtoken.find_many( | |
| where={"key_alias": {"in": data.key_aliases}}, | |
| ) |
| k_dict = k.dict() | ||
| k_dict.pop("token", None) | ||
| filtered_key_info.append(k_dict) | ||
| return {"key": data.keys, "info": filtered_key_info} |
There was a problem hiding this comment.
"key": null in response for alias-only requests
When the request body contains only key_aliases (no keys), data.keys is None, producing {"key": null, "info": [...]}. Callers querying by alias can't correlate which info objects map to which aliases, and the null field creates an inconsistent response shape vs. v1. Consider reflecting the requested identifiers:
| return {"key": data.keys, "info": filtered_key_info} | |
| return {"key": data.keys or data.key_aliases, "info": filtered_key_info} |
Summary
The
/v2/key/infoendpoint was missing response filtering that the v1/key/infoendpoint already had. This aligns the two endpoints so v2 applies the same per-key permission checks (_can_user_query_key_info) and strips internal fields from the response.Changes
_can_user_query_key_info()to each key returned by/v2/key/info, matching v1 behaviortokenfield from response objects, matching v1 behaviorkey_aliasesto tokens before querying, so the alias path doesn't skip the token filterType
🧹 Refactoring