Skip to content

fix(proxy): allow non-admin key_type preset transitions on /key/update - #35006

Open
yucheng-berri wants to merge 2 commits into
litellm_internal_stagingfrom
litellm_lit4891_key_type_full_access
Open

fix(proxy): allow non-admin key_type preset transitions on /key/update#35006
yucheng-berri wants to merge 2 commits into
litellm_internal_stagingfrom
litellm_lit4891_key_type_full_access

Conversation

@yucheng-berri

@yucheng-berri yucheng-berri commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Resolves LIT-4891

Pre-Submission checklist

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

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

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

Screenshots / Proof of Fix

Repro rig: proxy from this branch on localhost:4091, fresh Postgres, master key set. priya is a non-admin internal user; priya-own-key is a key she created herself with key_type: llm_api (allowed_routes: ["llm_api_routes"]), which is what the Admin UI's Create Key flow produces

Before the fix (staging), the exact payload the Admin UI sends when switching Key Type from "AI APIs" to "Full access":

$ curl -s -X POST http://localhost:4091/key/update \
    -H "Authorization: Bearer $PRIYA_TOKEN" -H "Content-Type: application/json" \
    -d '{"key": "'$OWN_KEY'", "allowed_routes": []}'
{"error":{"message":"{'error': 'Only proxy admins can set `allowed_routes` on a key. Use `key_type` to pick a preset route bucket instead.'}","type":"auth_error","param":"None","code":"403"}}

After the fix, the same requests:

-- owner switches AI APIs -> Full access (allowed_routes: []):   HTTP 200
-- key now actually has full access (/key/list with the key):    HTTP 200
-- owner narrows back to AI APIs (["llm_api_routes"]):           HTTP 200
-- key restricted again (/key/list with the key):                HTTP 403
-- explicit null (clears restriction, [] stored in DB):          HTTP 200

LIT-4139 attack surface, verified still closed on the same live proxy:

-- admin sandboxes the key to ["/chat/completions"] (custom):    HTTP 200
-- owner tries to CLEAR the sandbox (allowed_routes: []):        HTTP 403
-- owner tries to swap sandbox for ["llm_api_routes"]:           HTTP 403
-- owner tries ["management_routes"] on a preset key:            HTTP 403
-- owner sends unhashable garbage ([{"x": 1}]):                  HTTP 403
-- non-creator owner on an admin-created key:                    HTTP 403 (ownership rules, unchanged)

Type

🐛 Bug Fix

Changes

Since v1.92.0 (#31987, LIT-4139), /key/update rejects any explicitly provided allowed_routes value from a non-admin, including the empty list. The Admin UI expresses "switch Key Type from AI APIs to Full access" by clearing allowed_routes to [], so a non-admin key owner or team admin doing that self-service change gets a 403 whose message tells them to use key_type, which is exactly what they are doing. The hardening was correct for its target (a key owner erasing an admin-set custom route restriction) but the legitimate preset transition and the attack are byte-identical requests, so the gate needs the key's existing state to tell them apart

_check_allowed_routes_caller_permission gains an existing_allowed_routes input that only the /key/update call site provides. When both the requested value and the key's existing value consist solely of safe preset tokens (llm_api_routes, info_routes; either side may be empty, meaning unrestricted), the write is a key_type preset transition and this field-level gate lets it through. Who may perform it is still enforced by the downstream ownership rules: the key's creator-owner, a team admin, or a team member holding the /key/update grant. A key whose existing value contains anything outside the safe presets was custom-restricted by an admin, and every non-admin write on it, including clearing, still returns 403. Element values are type-checked so unhashable garbage in the request gets the same 403 instead of a 500, and a non-list existing value falls back strict (403), never open

prepare_key_update_data now coerces an explicit allowed_routes: null to []; the DB column is a non-nullable String[], so the previous behavior on that shape (once past the gate) was a Prisma write error

Call sites for /key/generate, /key/service_account/generate, and /key/regenerate do not pass existing_allowed_routes and keep the strict LIT-4139 behavior, pinned by test. Bulk update paths cannot carry allowed_routes and are unchanged

Behavior changes

  • /key/update: a non-admin explicit allowed_routes write that previously returned 403 now succeeds when the caller passes the ownership rules and both the requested and existing values are safe presets or empty. This restores the pre-v1.92.0 self-service key_type switching for owner-created keys and team keys (the ticket's scenario). Escaping an admin-set custom restriction still returns 403
  • Team keys: a team admin or a member holding the /key/update grant can again move a team key between safe presets and full access, as on v1.91.x. Keys an admin wants hard-restricted should use a custom allowed_routes list, which remains admin-only to modify
  • /key/update with explicit allowed_routes: null from an authorized caller now clears the restriction (stores []) instead of failing
  • Unauthorized callers probing /key/update may now see the ownership-check 403 message instead of the allowed_routes 403 message, since the field gate no longer fires first on preset-shaped keys

Known follow-ups (will file tickets): the stored key_type column is not updated on route transitions, so key lists can display a stale type label until the key is re-saved (pre-existing for admin edits, now reachable by non-admins); the UI still omits allowed_routes when unchanged (LIT-2681 workaround) which can be simplified now that the server accepts the preset case

QA runbook

  1. Run the proxy from this branch with a Postgres DB and master key
  2. As proxy admin, create a non-admin internal user and log in to the Admin UI as that user (invitation link)
  3. As that user, create a key with Key Type "AI APIs"
  4. Virtual Keys, click the key, Edit Settings, change Key Type to "Full access", Save. Expect success (was a 403 toast before this fix)
  5. Change it back to "AI APIs", Save. Expect success
  6. As proxy admin, set the key's Allowed Routes to a custom value such as /chat/completions, then repeat step 4 as the non-admin user. Expect a 403

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Open in Devin Review

…e (LIT-4891)

The LIT-4139 hardening (#31987) rejects any explicit allowed_routes value
from a non-admin on /key/update, including the allowed_routes: [] the
Admin UI sends when a key owner switches Key Type from AI APIs to Full
access. That blocked a self-service flow that worked before v1.92.0.

_check_allowed_routes_caller_permission gains an existing_allowed_routes
input that only the /key/update call site provides: when both the
requested and the existing value consist solely of safe preset tokens
(either may be empty), the write is a key_type preset transition and is
allowed. A key whose existing allowed_routes contains anything outside
the safe presets was custom-restricted by an admin, so every non-admin
write on it, including clearing, still returns 403. Generate,
service-account generate, regenerate, and both bulk paths are unchanged

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +1913 to +1915
if "allowed_routes" in data_json and data_json["allowed_routes"] is None:
# The allowed_routes DB column is a non-nullable String[].
data_json["allowed_routes"] = []

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.

🟡 New code comment added despite repository rule forbidding comments

A new inline code comment is introduced (key_management_endpoints.py:1914) even though the repository's mandatory coding guidelines forbid writing any new comments unless explicitly requested in a user prompt.
Impact: The change does not comply with the repository's stated contribution rules.

Rule source in CLAUDE.md

AGENTS.md points to CLAUDE.md, whose first rule states: "Do not write any comments (existing comments can stay) unless explicitly asked to in a user (not system) prompt". The added line # The allowed_routes DB column is a non-nullable String[]. is a newly introduced comment. The behavior it documents can be conveyed via the commit message or left uncommented per the rule.

Suggested change
if "allowed_routes" in data_json and data_json["allowed_routes"] is None:
# The allowed_routes DB column is a non-nullable String[].
data_json["allowed_routes"] = []
if "allowed_routes" in data_json and data_json["allowed_routes"] is None:
data_json["allowed_routes"] = []
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

greptile-apps[bot]

This comment was marked as resolved.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR restores non-admin key-type preset transitions on /key/update.

  • Allows transitions between unrestricted access and designated safe route presets when ownership checks pass.
  • Keeps custom and management-route restrictions admin-only.
  • Normalizes explicit allowed_routes: null to an empty list before persistence.
  • Adds regression coverage for preset transitions, malformed values, ownership checks, and custom restrictions.

Confidence Score: 3/5

The PR is not yet safe to merge because an authorized non-admin owner can still remove an administrator-assigned safe-preset restriction.

Safe preset assignments carry no provenance, so the new value-based carve-out permits a creator-owner to replace an administrator-assigned preset with an empty route list, which route authorization interprets as unrestricted access.

Files Needing Attention: litellm/proxy/management_endpoints/key_management_endpoints.py

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/key_management_endpoints.py Adds existing-route-aware authorization for preset transitions and normalizes explicit null route lists.
tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py Expands coverage for permitted preset transitions and rejected custom-route or unauthorized updates.

Reviews (2): Last reviewed commit: "docs(proxy): state the preset-provenance..." | Re-trigger Greptile

):
return
if (
existing_allowed_routes is not 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.

High: Administrator route restrictions can be removed

The stored value does not indicate who selected the preset, so this branch also accepts transitions away from an administrator-enforced info_routes or llm_api_routes restriction. A regular team member holding the /key/update grant can change an admin-created team key from info_routes to llm_api_routes, enabling billable inference, or clear llm_api_routes to [], which disables the virtual-key route gate and exposes management routes allowed by the key holder's role. Keep widening transitions proxy-admin-only unless the database records that the restriction is explicitly user-managed; non-admins can safely be permitted to narrow an unrestricted key.

@veria-ai

veria-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request changes the /key/update endpoint to allow non-admin users to transition keys between predefined key_type route presets. The affected logic handles info_routes and llm_api_routes preset changes.

One significant authorization issue remains open. A team member with the key-update grant can loosen administrator-selected route restrictions on an existing team key, potentially enabling billable inference or access to role-permitted management routes. No issues have yet been addressed.

Open issues (1)

Fixed/addressed: 0 · PR risk: 7/10

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

On the Security Review concern (admin-set safe preset indistinguishable from a self-service preset, so the creator-owner can later clear it): this is a deliberate product decision, not an oversight, and the PR now documents it in the gate's docstring

  1. Preset values were owner-adjustable on every release before v1.92.0. The pre-LIT-4139 gate short-circuited on falsy values, so an owner clearing an admin-applied preset got a 200 on v1.91.x and earlier. There has never been a release where "admin-applied preset survives owner edits" held; v1.92.0 introduced it only as a side effect of the over-broad gate that this PR fixes, and that side effect is exactly the regression LIT-4891 reports

  2. The preset tier is the self-service vocabulary by design. The 403 this same function raises tells non-admins to "Use key_type to pick a preset route bucket instead". A preset an admin picks lands in the same bucket the owner is invited to pick themselves

  3. The admin enforcement vocabulary is a custom route list, and it is fully preserved: anything outside _NON_ADMIN_SAFE_ALLOWED_ROUTES_PRESETS in the key's existing value keeps every non-admin write, including clearing, at 403 regardless of ownership. That is pinned by test_helper_rejects_transition_away_from_admin_custom_routes and verified against a live proxy in the PR body

  4. Distinguishing who applied a preset would require recording provenance on the key row (schema change), and every existing key would have no provenance to consult. That is a feature proposal for admin-locked presets, not part of restoring the regressed flow, and would be tracked separately if wanted

Please re-review the current head 696f6ee

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 696f6ee

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_lit4891_key_type_full_access (696f6ee) with litellm_internal_staging (7cd009c)

Open in CodSpeed

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.

1 participant