feat(auth): list and revoke lite login CLI sessions from the Admin UI - #36846
feat(auth): list and revoke lite login CLI sessions from the Admin UI#36846yassin-berriai wants to merge 3 commits into
Conversation
|
|
Greptile SummaryThe PR adds persistent, individually revocable sessions for classic
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/proxy/auth/cli_session_registry.py | Implements session registration, cached revocation checks, pagination, and idempotent revocation. |
| litellm/proxy/auth/user_api_key_auth.py | Integrates CLI revocation enforcement into self-contained session-token authentication. |
| litellm/proxy/management_endpoints/cli_session_endpoints.py | Adds administrator-authorized session listing and revocation endpoints. |
| litellm/proxy/management_endpoints/ui_sso.py | Registers newly minted classic CLI sessions before releasing credentials to the polling client. |
| litellm-proxy-extras/litellm_proxy_extras/migrations/20260813210000_add_cli_session_table/migration.sql | Creates the CLI session registry with lookup and expiration indexes. |
| ui/litellm-dashboard/src/components/CLISessionsPage/CLISessionsPage.tsx | Adds the admin session-management view with view-only-aware revoke controls. |
| ui/litellm-dashboard/src/components/CLISessionsPage/CLISessionsTable/CLISessionsTableColumns.tsx | Renders session identity, lifetime, status, and authorized revoke actions. |
| ui/litellm-dashboard/src/app/(dashboard)/hooks/cliSessions/useCLISessions.ts | Adds role-gated list and revoke queries with cache refresh after mutation. |
Reviews (6): Last reviewed commit: "test(cli-sessions): drop the cwd-relativ..." | Re-trigger Greptile
PR overviewThis pull request adds Admin UI support for listing and revoking LiteLLM CLI login sessions, backed by changes to the CLI session registry and authentication flow. One issue has already been addressed, but a fail-open path remains for revoked proxy-admin CLI sessions when the database client is unavailable. An attacker who already possesses a revoked admin credential could continue authenticating during that condition, so revocation is not reliably enforced until the configured database-unavailability policy is applied. Open issues (1)
Fixed/addressed: 1 · PR risk: 4/10 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
75e9fe1 to
5fa1184
Compare
|
@greptileai please re-review at 5fa1184. Both findings are taken: DB failures now follow allow_requests_on_db_unavailable, and Revoke renders for proxy admin only. |
5fa1184 to
d8a4cad
Compare
|
@greptileai please re-review at d8a4cad. The viewer gate now uses isViewOnly, since effectiveSessionRole normalizes proxy_admin_viewer to Admin. Other findings answered in the description. |
d8a4cad to
6dd0fa5
Compare
|
@greptileai please re-review at 6dd0fa5. Rebased onto staging after LIT-4757 landed; only the docstring overlapped and test fixtures were updated. |
6dd0fa5 to
b09975b
Compare
|
@greptileai please re-review at b09975b. Fixed the CI lint failure: a type-discipline budget regression, suppressed via the existing prisma-payload convention. |
`lite login` mints a self-contained encrypted UserAPIKeyAuth blob rather than a virtual key, so nothing about the session was stored server side and the auth path authenticated it by decrypting it. An admin had no way to see who held a live CLI credential and no way to cut one off before it expired. Every login now registers a row in LiteLLM_CLISessionTable, keyed by the sha256 of the session token, and the auth path refuses a session whose row is revoked. The lookup is cached in the shared auth DualCache for DEFAULT_IN_MEMORY_TTL, so a session costs one read per cache interval per replica and a revoke converges on every replica within that same interval. A CLI Sessions page in the dashboard lists the sessions with their user, issue time and expiry, and revokes one. Registration is fail-closed: a session that cannot be recorded is never handed to the CLI, since it could never be revoked. Sessions minted before this change have no row and keep working until they expire. A lookup that cannot reach the database follows the proxy-wide allow_requests_on_db_unavailable posture rather than inventing its own, so the credential's availability during an outage is whatever the operator already chose for every other DB-backed auth read.
b09975b to
1acd0a2
Compare
The two new CLI-session test files were added before #37802 swept the suite's sys.path.insert(0, os.path.abspath(...)) calls, so rebasing onto staging put them over the TQ003 test-quality-budget ceiling and failed lint.
|
@greptileai please re-review at 4cbcc2b. Fixed the rebase-induced lint failure: two new test files still had the cwd-relative sys.path.insert hack that #37802 swept from the rest of the suite, which pushed TQ003 over budget. |
Every lite login inserted a row that was never removed, so the registry grew without bound as logins accumulated. Registration now deletes rows whose expires_at has passed, bounding the table by the number of live sessions. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| operator who opted into serving during an outage keeps serving CLI sessions, | ||
| and one who did not gets the same failure every other DB-backed auth read gives. | ||
| """ | ||
| if prisma_client is None: |
There was a problem hiding this comment.
Low: Revoked admin sessions fail open without a database client
An attacker holding a revoked proxy-admin CLI credential can continue authenticating when prisma_client is unavailable because this branch reports the session as unrevoked, and admin session tokens return before the auth path's standard no-database check. Apply the configured allow_requests_on_db_unavailable policy here and fail closed when that setting is disabled.
|
Ideally, lite login should not be provisioning a new identity. |
TLDR
Problem this solves:
lite logincredentials are invisible to adminsHow it solves it:
User Flow
Before: an admin who needs to cut off a contractor's CLI access has no action to take, so the credential keeps working until it expires
lite loginagainst https://litellm-domain and gets a working credentialAfter: the admin sees the login in the dashboard and revokes it, and the contractor's next request is refused
lite loginagainst https://litellm-domain and gets a working credentiallite loginto start a new one."Another user with their own
lite logincredential is unaffected: only the revoked login stops working, and the contractor can no longer reach any route on the proxy with it, including the management routes their role would otherwise allowRelevant issues
Linear ticket
Resolves LIT-5518
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito 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
Both runs hit a live proxy on a real Postgres against the real Anthropic API.
lite loginwas driven for real, generic OIDC pointed at a throwaway local IdP, and the browser half of the device flow was followed with curl the way a browser would.Before, at a4ab511
After, at 4cbcc2b (re-run against this exact head)
The proof above stayed valid through the previous rebase, but a further rebase onto current
litellm_internal_stagingand a follow-up commit that dropped two now-bannedsys.path.inserttest hacks (4cbcc2b082) landed since it was captured, so this section is a fresh run against the current tip rather than an identity argument. Same live proxy, same real Postgres, same real Anthropic API.UI
The dashboard half is a new CLI Sessions entry in the AI Gateway section of the left nav, admin only, at
?page=cli-sessions. Chrome automation was unavailable on the capture box; see "Manual UI verification steps" below for the exact clicks.Manual UI verification steps
python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --reload --use_v2_migration_resolvernpm run devinui/litellm-dashboardlite loginfrom a terminal pointed at the running proxy, so a real CLI session gets registeredhttp://localhost:4000/ui/?page=cli-sessionsas a proxy admin, and expect a row for that login showing the user id, issued time, expiry, and an Active badgelitellm models list) using the same login and expect it to fail, telling you to runlite loginagainType
🆕 New Feature
Caveats (if any)
DEFAULT_IN_MEMORY_TTLReview notes
Both Greptile findings were real and both were taken. The first, that a valid CLI credential could fail whenever its revocation cache missed during a Prisma outage, was the sharper one: this credential previously authenticated with no database read at all on the admin path, so a lookup that raises is a new failure mode. The lookup now routes any failure through
PrismaDBExceptionHandler.handle_db_exception, which is the proxy's existing seam for exactly this decision, so an operator who setallow_requests_on_db_unavailablekeeps serving CLI sessions during an outage and one who did not gets the same failure every other DB-backed auth read already gives, rather than a private policy invented here. The second, that the page offered a Revoke control to roles the backend refuses, took two passes and the review was right to reject the first one.effectiveSessionRolenormalizesproxy_admin_viewertoAdminso read parity works, which means a role-only check cannot tell a revoker from a reader; the gate now also requires!isViewOnly, the signal the rest of the dashboard already uses for exactly this. The page, the list query and the nav entry separately moved offall_admin_roles, which carries org admin, whom the proxy refuses on tenant-wide reads.Two findings were not taken. The docs one asks for the revocation guidance to move to litellm-docs; the sentence I changed is a pre-existing in-repo claim that this PR makes false, quoted verbatim in the ticket as evidence, so correcting it in place is the smaller and more honest change and relocating the surrounding product docs is its own piece of work. The unbounded-registry one is real but not a new exposure: a row is only written by a completed SSO login, the same act that already mints a credential, and rows are bounded by that same authenticated flow. A retention sweep for expired sessions is worth doing and is listed as a caveat rather than smuggled into this PR.
The revocation lookup is cached in the shared auth
DualCacherather than read per request, so a CLI session costs one indexed read per cache interval per replica instead of one per request. The pod that serves the revoke publishes the revoked state into that cache directly, which is why the capture shows the first 401 arriving 0.15s after the revoke rather than at the TTL boundary; a replica that did not serve the revoke picks it up from Redis once its own entry lapses, which is theDEFAULT_IN_MEMORY_TTLbound quoted above. There is no second cache to invalidate: a CLI credential never enters the key cache, because the only write-back on that path fires after a database load and a self-contained blob never gets one.Registration is deliberately fail-closed and revocation deliberately fail-open on a missing row. A login that cannot be recorded is never handed to the CLI, since it could never be revoked; a session with no row at all predates this change and keeps authenticating until it expires, so upgrading does not sign every CLI user out.
Final Attestation