mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
Merge e3c9304e8d into 3c41392893
This commit is contained in:
commit
f65d6d455e
6 changed files with 82 additions and 8 deletions
|
|
@ -118,6 +118,7 @@ from litellm.proxy.management_endpoints.types import (
|
|||
from litellm.proxy.utils import (
|
||||
PrismaClient,
|
||||
ProxyLogging,
|
||||
get_cookie_path_from_server_root_path,
|
||||
get_custom_url,
|
||||
get_server_root_path,
|
||||
)
|
||||
|
|
@ -3636,7 +3637,7 @@ class SSOAuthenticationHandler:
|
|||
litellm_dashboard_ui += "?login=success"
|
||||
verbose_proxy_logger.info("Redirecting to %s", litellm_dashboard_ui)
|
||||
redirect_response: Final = RedirectResponse(url=litellm_dashboard_ui, status_code=303)
|
||||
redirect_response.set_cookie(key="token", value=jwt_token)
|
||||
redirect_response.set_cookie(key="token", value=jwt_token, path=get_cookie_path_from_server_root_path())
|
||||
return redirect_response
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -624,6 +624,7 @@ from litellm.proxy.utils import (
|
|||
_is_valid_team_configs,
|
||||
evict_config_param,
|
||||
get_config_param,
|
||||
get_cookie_path_from_server_root_path,
|
||||
get_custom_url,
|
||||
get_error_message_str,
|
||||
get_server_root_path,
|
||||
|
|
@ -14932,7 +14933,7 @@ async def login(request: Request):
|
|||
|
||||
# Create redirect response with cookie
|
||||
redirect_response: Final = RedirectResponse(url=litellm_dashboard_ui, status_code=303)
|
||||
redirect_response.set_cookie(key="token", value=jwt_token)
|
||||
redirect_response.set_cookie(key="token", value=jwt_token, path=get_cookie_path_from_server_root_path())
|
||||
if cp_return_to:
|
||||
redirect_response.delete_cookie(key="litellm_cp_return_to")
|
||||
return redirect_response
|
||||
|
|
@ -14975,7 +14976,7 @@ async def login_v2(request: Request):
|
|||
content={"redirect_url": litellm_dashboard_ui, "token": jwt_token},
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
json_response.set_cookie(key="token", value=jwt_token)
|
||||
json_response.set_cookie(key="token", value=jwt_token, path=get_cookie_path_from_server_root_path())
|
||||
return json_response
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception("litellm.proxy.proxy_server.login_v2(): Exception occurred - %s", e)
|
||||
|
|
@ -15120,7 +15121,7 @@ async def login_v3_exchange(request: Request):
|
|||
},
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
json_response.set_cookie(key="token", value=cached_data["token"])
|
||||
json_response.set_cookie(key="token", value=cached_data["token"], path=get_cookie_path_from_server_root_path())
|
||||
return json_response
|
||||
except ProxyException:
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -7014,6 +7014,20 @@ def get_server_root_path() -> str:
|
|||
return os.getenv("SERVER_ROOT_PATH", "")
|
||||
|
||||
|
||||
def get_cookie_path_from_server_root_path() -> str:
|
||||
"""
|
||||
Cookie `path` scoped to SERVER_ROOT_PATH.
|
||||
|
||||
Ensures auth cookies for deployments served under different root paths
|
||||
(e.g. `a.com` vs `a.com/prefix`) do not overwrite each other. Defaults to
|
||||
"/" when SERVER_ROOT_PATH is unset.
|
||||
"""
|
||||
root_path = get_server_root_path()
|
||||
if not root_path or root_path == "/":
|
||||
return "/"
|
||||
return "/" + root_path.strip("/")
|
||||
|
||||
|
||||
def normalize_route_for_root_path(route: str) -> str | None:
|
||||
"""Strip SERVER_ROOT_PATH prefix. Returns de-prefixed route, or None if route is not under root path."""
|
||||
root_path: Final = get_server_root_path()
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from litellm.types.guardrails import GuardrailEventHooks
|
|||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from litellm.proxy.utils import get_custom_url, join_paths
|
||||
from litellm.proxy.utils import get_cookie_path_from_server_root_path, get_custom_url, join_paths
|
||||
|
||||
|
||||
def test_get_custom_url(monkeypatch):
|
||||
|
|
@ -22,6 +22,26 @@ def test_get_custom_url(monkeypatch):
|
|||
assert custom_url == "http://0.0.0.0:4000/litellm/ui/"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"server_root_path, expected",
|
||||
[
|
||||
(None, "/"),
|
||||
("", "/"),
|
||||
("/", "/"),
|
||||
("/litellm", "/litellm"),
|
||||
("litellm", "/litellm"),
|
||||
("/litellm/", "/litellm"),
|
||||
("/team/a", "/team/a"),
|
||||
],
|
||||
)
|
||||
def test_get_cookie_path_from_server_root_path(monkeypatch, server_root_path, expected):
|
||||
if server_root_path is None:
|
||||
monkeypatch.delenv("SERVER_ROOT_PATH", raising=False)
|
||||
else:
|
||||
monkeypatch.setenv("SERVER_ROOT_PATH", server_root_path)
|
||||
assert get_cookie_path_from_server_root_path() == expected
|
||||
|
||||
|
||||
def test_proxy_only_error_true_for_llm_route():
|
||||
proxy_logging_obj = ProxyLogging(user_api_key_cache=DualCache())
|
||||
assert proxy_logging_obj._is_proxy_only_llm_api_error(
|
||||
|
|
|
|||
|
|
@ -128,6 +128,34 @@ describe("cookieUtils", () => {
|
|||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should clear token cookie at the server root path when deployed under SERVER_ROOT_PATH", () => {
|
||||
const originalLocation = window.location;
|
||||
vi.stubGlobal("location", { ...originalLocation, pathname: "/litellm/ui/" });
|
||||
|
||||
const cookieSpy = vi.spyOn(document, "cookie", "set");
|
||||
|
||||
clearTokenCookies();
|
||||
|
||||
expect(cookieSpy).toHaveBeenCalledWith(expect.stringContaining("path=/litellm;"));
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should clear token cookie at the server root path when the root path contains a /ui segment", () => {
|
||||
const originalLocation = window.location;
|
||||
vi.stubGlobal("location", { ...originalLocation, pathname: "/foo/ui/bar/ui/" });
|
||||
|
||||
const cookieSpy = vi.spyOn(document, "cookie", "set");
|
||||
|
||||
clearTokenCookies();
|
||||
|
||||
expect(cookieSpy).toHaveBeenCalledWith(expect.stringContaining("path=/foo/ui/bar;"));
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should clear sessionStorage token", () => {
|
||||
sessionStorage.setItem("token", "stored-token");
|
||||
clearTokenCookies();
|
||||
|
|
|
|||
|
|
@ -14,9 +14,12 @@ function getUiCookiePath(): string {
|
|||
if (typeof window === "undefined") return "/ui";
|
||||
// Match "/ui" only as a full path segment (followed by "/" or end of string)
|
||||
// to avoid false matches like "/my-ui-tool/login" → "/my-ui".
|
||||
const match = window.location.pathname.match(/\/ui(?=\/|$)/);
|
||||
if (match && match.index !== undefined) {
|
||||
return window.location.pathname.substring(0, match.index + 3);
|
||||
// The UI mounts at the last "/ui" segment (SERVER_ROOT_PATH + "/ui"), so use the
|
||||
// last match to stay correct when the root path itself contains a "/ui" segment.
|
||||
const matches = [...window.location.pathname.matchAll(/\/ui(?=\/|$)/g)];
|
||||
const lastMatch = matches[matches.length - 1];
|
||||
if (lastMatch && lastMatch.index !== undefined) {
|
||||
return window.location.pathname.substring(0, lastMatch.index + 3);
|
||||
}
|
||||
return "/ui";
|
||||
}
|
||||
|
|
@ -38,6 +41,13 @@ export function clearTokenCookies() {
|
|||
const uiCookiePath = getUiCookiePath();
|
||||
const paths = ["/", uiCookiePath];
|
||||
|
||||
// Clear at the server root path (e.g. "/litellm") too, since the server-set
|
||||
// auth cookie is scoped there when SERVER_ROOT_PATH is configured.
|
||||
const serverRootPath = uiCookiePath.replace(/\/ui$/, "");
|
||||
if (serverRootPath && !paths.includes(serverRootPath)) {
|
||||
paths.push(serverRootPath);
|
||||
}
|
||||
|
||||
// Add the current path directory if it's different from root and /ui
|
||||
if (currentPath && currentPath !== "/" && !currentPath.startsWith("/ui")) {
|
||||
const dirPath = currentPath.substring(0, currentPath.lastIndexOf("/") + 1);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue