345 lines
12 KiB
Python
345 lines
12 KiB
Python
"""
|
|
Login utilities for handling user authentication in the proxy server.
|
|
|
|
This module contains the core login logic that can be reused across different
|
|
login endpoints (e.g., /login and /v2/login).
|
|
"""
|
|
|
|
import os
|
|
import secrets
|
|
from typing import Literal, Optional, cast
|
|
|
|
from fastapi import HTTPException
|
|
|
|
import litellm
|
|
from litellm.constants import LITELLM_PROXY_ADMIN_NAME, LITELLM_UI_SESSION_DURATION
|
|
from litellm.proxy._types import (
|
|
LiteLLM_UserTable,
|
|
LitellmUserRoles,
|
|
ProxyErrorTypes,
|
|
ProxyException,
|
|
UpdateUserRequest,
|
|
UserAPIKeyAuth,
|
|
hash_token,
|
|
)
|
|
from litellm.proxy.management_endpoints.internal_user_endpoints import user_update
|
|
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
|
generate_key_helper_fn,
|
|
)
|
|
from litellm.proxy.management_endpoints.ui_sso import (
|
|
get_disabled_non_admin_personal_key_creation,
|
|
)
|
|
from litellm.proxy.utils import PrismaClient, get_server_root_path
|
|
from litellm.secret_managers.main import get_secret_bool
|
|
from litellm.types.proxy.ui_sso import ReturnedUITokenObject
|
|
|
|
|
|
def get_ui_credentials(master_key: Optional[str]) -> tuple[str, str]:
|
|
"""
|
|
Get UI username and password from environment variables or master key.
|
|
|
|
Args:
|
|
master_key: Master key for the proxy (used as fallback for password)
|
|
|
|
Returns:
|
|
tuple[str, str]: A tuple containing (ui_username, ui_password)
|
|
|
|
Raises:
|
|
ProxyException: If neither UI_PASSWORD nor master_key is available
|
|
"""
|
|
ui_username = os.getenv("UI_USERNAME", "admin")
|
|
ui_password = os.getenv("UI_PASSWORD", None)
|
|
if ui_password is None:
|
|
ui_password = str(master_key) if master_key is not None else None
|
|
if ui_password is None:
|
|
raise ProxyException(
|
|
message="set Proxy master key to use UI. https://docs.litellm.ai/docs/proxy/virtual_keys. If set, use `--detailed_debug` to debug issue.",
|
|
type=ProxyErrorTypes.auth_error,
|
|
param="UI_PASSWORD",
|
|
code=500,
|
|
)
|
|
return ui_username, ui_password
|
|
|
|
|
|
class LoginResult:
|
|
"""Result object containing authentication data from login."""
|
|
|
|
user_id: str
|
|
key: str
|
|
user_email: Optional[str]
|
|
user_role: str
|
|
login_method: Literal["sso", "username_password"]
|
|
|
|
def __init__(
|
|
self,
|
|
user_id: str,
|
|
key: str,
|
|
user_email: Optional[str],
|
|
user_role: str,
|
|
login_method: Literal["sso", "username_password"] = "username_password",
|
|
):
|
|
self.user_id = user_id
|
|
self.key = key
|
|
self.user_email = user_email
|
|
self.user_role = user_role
|
|
self.login_method = login_method
|
|
|
|
|
|
async def authenticate_user( # noqa: PLR0915
|
|
username: str,
|
|
password: str,
|
|
master_key: Optional[str],
|
|
prisma_client: Optional[PrismaClient],
|
|
) -> LoginResult:
|
|
"""
|
|
Authenticate a user and generate an API key for UI access.
|
|
|
|
This function handles two login scenarios:
|
|
1. Admin login using UI_USERNAME and UI_PASSWORD
|
|
2. User login using email and password from database
|
|
|
|
Args:
|
|
username: Username or email from the login form
|
|
password: Password from the login form
|
|
master_key: Master key for the proxy (required)
|
|
prisma_client: Prisma database client (optional)
|
|
|
|
Returns:
|
|
LoginResult: Object containing authentication data
|
|
|
|
Raises:
|
|
ProxyException: If authentication fails or required configuration is missing
|
|
"""
|
|
if master_key is None:
|
|
raise ProxyException(
|
|
message="Master Key not set for Proxy. Please set Master Key to use Admin UI. Set `LITELLM_MASTER_KEY` in .env or set general_settings:master_key in config.yaml. https://docs.litellm.ai/docs/proxy/virtual_keys. If set, use `--detailed_debug` to debug issue.",
|
|
type=ProxyErrorTypes.auth_error,
|
|
param="master_key",
|
|
code=500,
|
|
)
|
|
|
|
ui_username, ui_password = get_ui_credentials(master_key)
|
|
|
|
# Check if we can find the `username` in the db. On the UI, users can enter username=their email
|
|
_user_row: Optional[LiteLLM_UserTable] = None
|
|
user_role: Optional[
|
|
Literal[
|
|
LitellmUserRoles.PROXY_ADMIN,
|
|
LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY,
|
|
LitellmUserRoles.INTERNAL_USER,
|
|
LitellmUserRoles.INTERNAL_USER_VIEW_ONLY,
|
|
]
|
|
] = None
|
|
|
|
if prisma_client is not None:
|
|
_user_row = cast(
|
|
Optional[LiteLLM_UserTable],
|
|
await prisma_client.db.litellm_usertable.find_first(
|
|
where={"user_email": {"equals": username, "mode": "insensitive"}}
|
|
),
|
|
)
|
|
|
|
"""
|
|
To login to Admin UI, we support the following
|
|
- Login with UI_USERNAME and UI_PASSWORD
|
|
- Login with Invite Link `user_email` and `password` combination
|
|
"""
|
|
if secrets.compare_digest(
|
|
username.encode("utf-8"), ui_username.encode("utf-8")
|
|
) and secrets.compare_digest(password.encode("utf-8"), ui_password.encode("utf-8")):
|
|
# Non SSO -> If user is using UI_USERNAME and UI_PASSWORD they are Proxy admin
|
|
user_role = LitellmUserRoles.PROXY_ADMIN
|
|
user_id = LITELLM_PROXY_ADMIN_NAME
|
|
|
|
# we want the key created to have PROXY_ADMIN_PERMISSIONS
|
|
key_user_id = LITELLM_PROXY_ADMIN_NAME
|
|
if (
|
|
os.getenv("PROXY_ADMIN_ID", None) is not None
|
|
and os.environ["PROXY_ADMIN_ID"] == user_id
|
|
) or user_id == LITELLM_PROXY_ADMIN_NAME:
|
|
# checks if user is admin
|
|
key_user_id = os.getenv("PROXY_ADMIN_ID", LITELLM_PROXY_ADMIN_NAME)
|
|
|
|
# Admin is Authe'd in - generate key for the UI to access Proxy
|
|
|
|
# ensure this user is set as the proxy admin, in this route there is no sso, we can assume this user is only the admin
|
|
await user_update(
|
|
data=UpdateUserRequest(
|
|
user_id=key_user_id,
|
|
user_role=user_role,
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
),
|
|
)
|
|
|
|
if os.getenv("DATABASE_URL") is not None:
|
|
response = await generate_key_helper_fn(
|
|
request_type="key",
|
|
**{
|
|
"user_role": LitellmUserRoles.PROXY_ADMIN,
|
|
"duration": LITELLM_UI_SESSION_DURATION,
|
|
"key_max_budget": litellm.max_ui_session_budget,
|
|
"models": [],
|
|
"aliases": {},
|
|
"config": {},
|
|
"spend": 0,
|
|
"user_id": key_user_id,
|
|
"team_id": "litellm-dashboard",
|
|
}, # type: ignore
|
|
)
|
|
else:
|
|
raise ProxyException(
|
|
message="No Database connected. Set DATABASE_URL in .env. If set, use `--detailed_debug` to debug issue.",
|
|
type=ProxyErrorTypes.auth_error,
|
|
param="DATABASE_URL",
|
|
code=500,
|
|
)
|
|
|
|
key = response["token"] # type: ignore
|
|
|
|
if get_secret_bool("EXPERIMENTAL_UI_LOGIN"):
|
|
from litellm.proxy.auth.auth_checks import ExperimentalUIJWTToken
|
|
|
|
user_info: Optional[LiteLLM_UserTable] = None
|
|
if _user_row is not None:
|
|
user_info = _user_row
|
|
elif (
|
|
user_id is not None
|
|
): # if user_id is not None, we are using the UI_USERNAME and UI_PASSWORD
|
|
user_info = LiteLLM_UserTable(
|
|
user_id=user_id,
|
|
user_role=user_role,
|
|
models=[],
|
|
max_budget=litellm.max_ui_session_budget,
|
|
)
|
|
if user_info is None:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={
|
|
"error": "User Information is required for experimental UI login"
|
|
},
|
|
)
|
|
|
|
key = ExperimentalUIJWTToken.get_experimental_ui_login_jwt_auth_token(
|
|
user_info
|
|
)
|
|
|
|
return LoginResult(
|
|
user_id=user_id,
|
|
key=key,
|
|
user_email=None,
|
|
user_role=user_role,
|
|
login_method="username_password",
|
|
)
|
|
|
|
elif _user_row is not None:
|
|
"""
|
|
When sharing invite links
|
|
|
|
-> if the user has no role in the DB assume they are only a viewer
|
|
"""
|
|
user_id = getattr(_user_row, "user_id", "unknown")
|
|
user_role = getattr(
|
|
_user_row, "user_role", LitellmUserRoles.INTERNAL_USER_VIEW_ONLY
|
|
)
|
|
user_email = getattr(_user_row, "user_email", "unknown")
|
|
_password = getattr(_user_row, "password", "unknown")
|
|
|
|
if _password is None:
|
|
raise ProxyException(
|
|
message="User has no password set. Please set a password for the user via `/user/update`.",
|
|
type=ProxyErrorTypes.auth_error,
|
|
param="password",
|
|
code=401,
|
|
)
|
|
|
|
# check if password == _user_row.password
|
|
hash_password = hash_token(token=password)
|
|
if secrets.compare_digest(
|
|
password.encode("utf-8"), _password.encode("utf-8")
|
|
) or secrets.compare_digest(
|
|
hash_password.encode("utf-8"), _password.encode("utf-8")
|
|
):
|
|
if os.getenv("DATABASE_URL") is not None:
|
|
response = await generate_key_helper_fn(
|
|
request_type="key",
|
|
**{ # type: ignore
|
|
"user_role": user_role,
|
|
"duration": LITELLM_UI_SESSION_DURATION,
|
|
"key_max_budget": litellm.max_ui_session_budget,
|
|
"models": [],
|
|
"aliases": {},
|
|
"config": {},
|
|
"spend": 0,
|
|
"user_id": user_id,
|
|
"team_id": "litellm-dashboard",
|
|
},
|
|
)
|
|
else:
|
|
raise ProxyException(
|
|
message="No Database connected. Set DATABASE_URL in .env. If set, use `--detailed_debug` to debug issue.",
|
|
type=ProxyErrorTypes.auth_error,
|
|
param="DATABASE_URL",
|
|
code=500,
|
|
)
|
|
|
|
key = response["token"] # type: ignore
|
|
|
|
return LoginResult(
|
|
user_id=user_id,
|
|
key=key,
|
|
user_email=user_email,
|
|
user_role=cast(str, user_role),
|
|
login_method="username_password",
|
|
)
|
|
else:
|
|
raise ProxyException(
|
|
message=f"Invalid credentials used to access UI.\nNot valid credentials for {username}",
|
|
type=ProxyErrorTypes.auth_error,
|
|
param="invalid_credentials",
|
|
code=401,
|
|
)
|
|
else:
|
|
raise ProxyException(
|
|
message="Invalid credentials used to access UI.\nCheck 'UI_USERNAME', 'UI_PASSWORD' in .env file",
|
|
type=ProxyErrorTypes.auth_error,
|
|
param="invalid_credentials",
|
|
code=401,
|
|
)
|
|
|
|
|
|
def create_ui_token_object(
|
|
login_result: LoginResult,
|
|
general_settings: dict,
|
|
premium_user: bool,
|
|
) -> ReturnedUITokenObject:
|
|
"""
|
|
Create a ReturnedUITokenObject from a LoginResult.
|
|
|
|
Args:
|
|
login_result: The result from authenticate_user
|
|
general_settings: General proxy settings dictionary
|
|
premium_user: Whether premium features are enabled
|
|
|
|
Returns:
|
|
ReturnedUITokenObject: Token object ready for JWT encoding
|
|
"""
|
|
disabled_non_admin_personal_key_creation = (
|
|
get_disabled_non_admin_personal_key_creation()
|
|
)
|
|
|
|
return ReturnedUITokenObject(
|
|
user_id=login_result.user_id,
|
|
key=login_result.key,
|
|
user_email=login_result.user_email,
|
|
user_role=login_result.user_role,
|
|
login_method=login_result.login_method,
|
|
premium_user=premium_user,
|
|
auth_header_name=general_settings.get(
|
|
"litellm_key_header_name", "Authorization"
|
|
),
|
|
disabled_non_admin_personal_key_creation=disabled_non_admin_personal_key_creation,
|
|
server_root_path=get_server_root_path(),
|
|
)
|