litellm/tests/unit/proxy/management/users/test_endpoints.py
yuneng-jiang 4bafebbcdc
fix(ui): take the dashboard's user role and identity from the server (#47025)
* fix(ui): take the dashboard's user role and identity from the server

Adds an internal GET /management/v1/users/me that returns the user id, role
and email the proxy resolved for the calling key. The dashboard now reads
identity from it instead of the session cookie's claims, so its navigation and
role gating always match what the backend authorizes. The layout waits on it
behind the existing loading screen, and a failed lookup clears the session and
returns to login. Sessions that must change their password can also read it

* fix(ui): sign out when the current user lookup fails and wait for it on chat

The dashboard layout now holds the loading screen and signs out when
/management/v1/users/me fails, instead of mounting the shell and looping back
to loading as each consumer refetched the failed query. The chat layout also
waits for the identity so its panels never query without a user id. The
lookup goes through the typed client with the regenerated schema.d.ts

* fix(ui): send the session's auth header on the first user lookup and keep chat closed until authorized

The first lookup runs before AuthProvider registers the custom header name, so it now reads auth_header_name from the session cookie. Chat also waits for isAuthorized, so a failed lookup no longer mounts the shell with an empty user id

* fix(ui): type the dashboard callers of the now typed user id

useAuthorized used to hand back the cookie's user id untyped, which hid six strict type errors in its callers. The id is now string | null, so the production build failed. Callers handle the null case, and query key filters accept the boolean blocked filter

* fix(ui): look up the current user only after the proxy URL is discovered

Pages outside the dashboard shell, like chat and connect, could start the lookup before UI config discovery set the proxy base URL. On a split-origin deployment both attempts then hit the UI origin and a valid session was signed out

* fix(ui): keep a loaded identity while UI config discovery reruns

Pausing the lookup by passing a null token switched the query key, so on the dashboard the identity the layout had already loaded vanished until the shell's own UI config request finished. The key now stays the same and only the fetch waits for discovery
2026-10-17 14:39:33 -07:00

67 lines
2.3 KiB
Python

from collections.abc import Iterator
from typing import Final
import pytest
from fastapi import FastAPI, Request
from fastapi.testclient import TestClient
from litellm.proxy._types import LitellmUserRoles
from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth
from litellm.proxy.list_api.common import ManagementProblem, problem_response
from litellm.proxy.management_endpoints.management_v1 import router
app: Final = FastAPI()
@app.exception_handler(ManagementProblem)
async def management_problem_exception_handler(request: Request, exc: ManagementProblem):
return problem_response(exc.problem)
app.include_router(router)
client: Final = TestClient(app)
USERS_ME_PATH: Final = "/management/v1/users/me"
@pytest.fixture
def caller() -> Iterator[None]:
yield
app.dependency_overrides.clear()
def authenticate_as(resolved: UserAPIKeyAuth) -> None:
app.dependency_overrides[user_api_key_auth] = lambda: resolved
@pytest.mark.parametrize(
"role",
[LitellmUserRoles.INTERNAL_USER, LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY],
)
def test_returns_the_identity_auth_resolved_for_the_caller(caller, role):
authenticate_as(UserAPIKeyAuth(user_id="u-1", user_role=role, user_email="u1@example.com"))
response = client.get(USERS_ME_PATH, headers={"Authorization": "Bearer k"})
assert response.status_code == 200
assert response.json() == {"user_id": "u-1", "user_role": role.value, "user_email": "u1@example.com"}
def test_key_without_a_user_gets_a_404_problem_document(caller):
authenticate_as(UserAPIKeyAuth(team_id="team-1", user_role=LitellmUserRoles.INTERNAL_USER))
response = client.get(USERS_ME_PATH, headers={"Authorization": "Bearer k"})
assert response.status_code == 404
assert response.headers["content-type"] == "application/problem+json"
assert response.json()["type"] == "urn:litellm:error:user-not-found"
assert response.json()["status"] == 404
def test_route_is_served_but_hidden_from_the_openapi_schema(caller):
authenticate_as(UserAPIKeyAuth(user_id="u-1", user_role=LitellmUserRoles.INTERNAL_USER))
paths: Final = app.openapi()["paths"]
assert client.get(USERS_ME_PATH).status_code == 200
assert "/management/v1/users/bulk" in paths
assert USERS_ME_PATH not in paths