From 2c338296c138f6402ec5ecc80961861f2e2ae370 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 9 Jul 2024 14:46:46 -0700 Subject: [PATCH 1/8] ui - allow setting allowed ip --- .../src/components/admins.tsx | 109 +++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/admins.tsx b/ui/litellm-dashboard/src/components/admins.tsx index 3b012d5b4b2..94722923bf4 100644 --- a/ui/litellm-dashboard/src/components/admins.tsx +++ b/ui/litellm-dashboard/src/components/admins.tsx @@ -73,6 +73,11 @@ const AdminPanel: React.FC = ({ const [isAddSSOModalVisible, setIsAddSSOModalVisible] = useState(false); const [isInstructionsModalVisible, setIsInstructionsModalVisible] = useState(false); + const [isAllowedIPModalVisible, setIsAllowedIPModalVisible] = useState(false); + const [isAddIPModalVisible, setIsAddIPModalVisible] = useState(false); + const [isDeleteIPModalVisible, setIsDeleteIPModalVisible] = useState(false); + const [allowedIPs, setAllowedIPs] = useState([]); + const [ipToDelete, setIPToDelete] = useState(null); const router = useRouter(); const [possibleUIRoles, setPossibleUIRoles] = useState = ({ } nonSssoUrl += "/fallback/login"; + const handleShowAllowedIPs = () => { + // In a real application, you would fetch the allowed IPs from your backend here + setAllowedIPs(['192.168.1.1', '10.0.0.1', '172.16.0.1']); + setIsAllowedIPModalVisible(true); + }; + + const handleAddIP = (values: { ip: string }) => { + setAllowedIPs([...allowedIPs, values.ip]); + setIsAddIPModalVisible(false); + message.success('IP address added successfully'); + }; + + const handleDeleteIP = (ip: string) => { + setIPToDelete(ip); + setIsDeleteIPModalVisible(true); + }; + + const confirmDeleteIP = () => { + if (ipToDelete) { + setAllowedIPs(allowedIPs.filter(ip => ip !== ipToDelete)); + setIsDeleteIPModalVisible(false); + setIPToDelete(null); + message.success('IP address deleted successfully'); + } + }; + + const handleAddSSOOk = () => { setIsAddSSOModalVisible(false); form.resetFields(); @@ -533,9 +565,19 @@ const AdminPanel: React.FC = ({ + + ✨ Security Settings Add SSO + + + + Allowed IP Addresses + + + +
- + = ({ Done
+ setIsAllowedIPModalVisible(false)} + footer={[ + , + + ]} + > + + + + IP Address + Action + + + + {allowedIPs.map((ip, index) => ( + + {ip} + + + + + ))} + +
+
+ + setIsAddIPModalVisible(false)} + footer={null} + > +
+ + + + + + Add IP Address + + +
+
+ + setIsDeleteIPModalVisible(false)} + > +

Are you sure you want to delete the IP address: {ipToDelete}?

+
If you need to login without sso, you can access{" "} From 362c01c21f1a81bbaabae227fb9dc2aee299a2ac Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 9 Jul 2024 15:12:08 -0700 Subject: [PATCH 2/8] ui - add Create, get, delete endpoints for IP Addresses --- .../proxy_setting_endpoints.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py new file mode 100644 index 00000000000..44fadd26ae9 --- /dev/null +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -0,0 +1,113 @@ +#### CRUD ENDPOINTS for UI Settings ##### +from datetime import datetime, timedelta, timezone +from typing import List, Optional + +import fastapi +from fastapi import APIRouter, Depends, Header, HTTPException, Request, status + +import litellm +from litellm._logging import verbose_proxy_logger +from litellm.proxy._types import * +from litellm.proxy.auth.user_api_key_auth import user_api_key_auth + +router = APIRouter() + + +class IPAddress(BaseModel): + ip: str + + +@router.get( + "/get/allowed_ips", + tags=["Budget & Spend Tracking"], + dependencies=[Depends(user_api_key_auth)], + include_in_schema=False, +) +async def get_allowed_ips(): + from litellm.proxy.proxy_server import general_settings + + _allowed_ip = general_settings.get("allowed_ips") + return {"data": _allowed_ip} + + +@router.post( + "/add/allowed_ip", + tags=["Budget & Spend Tracking"], + dependencies=[Depends(user_api_key_auth)], +) +async def add_allowed_ip(ip_address: IPAddress): + from litellm.proxy.proxy_server import ( + general_settings, + prisma_client, + proxy_config, + store_model_in_db, + ) + + _allowed_ips: List = general_settings.get("allowed_ips", []) + if ip_address.ip not in _allowed_ips: + _allowed_ips.append(ip_address.ip) + general_settings["allowed_ips"] = _allowed_ips + else: + raise HTTPException(status_code=400, detail="IP address already exists") + + if prisma_client is None: + raise Exception("No DB Connected") + + if store_model_in_db is not True: + raise HTTPException( + status_code=500, + detail={ + "error": "Set `'STORE_MODEL_IN_DB='True'` in your env to enable this feature." + }, + ) + + # Load existing config + config = await proxy_config.get_config() + verbose_proxy_logger.debug("Loaded config: %s", config) + if "general_settings" not in config: + config["general_settings"] = {} + + if "allowed_ips" not in config["general_settings"]: + config["general_settings"]["allowed_ips"] = [] + + if ip_address.ip not in config["general_settings"]["allowed_ips"]: + config["general_settings"]["allowed_ips"].append(ip_address.ip) + + await proxy_config.save_config(new_config=config) + + return { + "message": f"IP {ip_address.ip} address added successfully", + "status": "success", + } + + +@router.post( + "/delete/allowed_ip", + tags=["Budget & Spend Tracking"], + dependencies=[Depends(user_api_key_auth)], +) +async def delete_allowed_ip(ip_address: IPAddress): + from litellm.proxy.proxy_server import general_settings, proxy_config + + _allowed_ips: List = general_settings.get("allowed_ips", []) + if ip_address.ip in _allowed_ips: + _allowed_ips.remove(ip_address.ip) + general_settings["allowed_ips"] = _allowed_ips + else: + raise HTTPException(status_code=404, detail="IP address not found") + + # Load existing config + config = await proxy_config.get_config() + verbose_proxy_logger.debug("Loaded config: %s", config) + if "general_settings" not in config: + config["general_settings"] = {} + + if "allowed_ips" not in config["general_settings"]: + config["general_settings"]["allowed_ips"] = [] + + if ip_address.ip in config["general_settings"]["allowed_ips"]: + config["general_settings"]["allowed_ips"].remove(ip_address.ip) + + await proxy_config.save_config(new_config=config) + + return {"message": f"IP {ip_address.ip} deleted successfully", "status": "success"} From 22df67edb78bb693bddcaa627e374dd9dd12b01a Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 9 Jul 2024 15:29:41 -0700 Subject: [PATCH 3/8] feat - add mgtm endpoint routes --- litellm/proxy/proxy_server.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 581cd9229d6..6c7153adebb 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -186,6 +186,9 @@ from litellm.proxy.spend_tracking.spend_management_endpoints import ( router as spend_management_router, ) from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload +from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import ( + router as ui_crud_endpoints_router, +) from litellm.proxy.utils import ( DBClient, PrismaClient, @@ -1281,7 +1284,7 @@ class ProxyConfig: return config async def save_config(self, new_config: dict): - global prisma_client, general_settings, user_config_file_path + global prisma_client, general_settings, user_config_file_path, store_model_in_db # Load existing config ## DB - writes valid config to db """ @@ -1290,6 +1293,7 @@ class ProxyConfig: """ if prisma_client is not None and ( general_settings.get("store_model_in_db", False) == True + or store_model_in_db ): # if using - db for config - models are in ModelTable new_config.pop("model_list", None) @@ -9181,3 +9185,4 @@ app.include_router(spend_management_router) app.include_router(caching_router) app.include_router(analytics_router) app.include_router(debugging_endpoints_router) +app.include_router(ui_crud_endpoints_router) From f3dddd234d6d5bdd36e756412bdfd15060660090 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 9 Jul 2024 15:43:44 -0700 Subject: [PATCH 4/8] ui - get, set, delete allowed ip addresses --- .../src/components/admins.tsx | 70 +++++++++++---- .../src/components/networking.tsx | 89 +++++++++++++++++++ 2 files changed, 141 insertions(+), 18 deletions(-) diff --git a/ui/litellm-dashboard/src/components/admins.tsx b/ui/litellm-dashboard/src/components/admins.tsx index 94722923bf4..85b5dc3d4ea 100644 --- a/ui/litellm-dashboard/src/components/admins.tsx +++ b/ui/litellm-dashboard/src/components/admins.tsx @@ -50,6 +50,9 @@ import { setCallbacksCall, invitationCreateCall, getPossibleUserRoles, + addAllowedIP, + getAllowedIPs, + deleteAllowedIP, } from "./networking"; const AdminPanel: React.FC = ({ @@ -98,29 +101,60 @@ const AdminPanel: React.FC = ({ } nonSssoUrl += "/fallback/login"; - const handleShowAllowedIPs = () => { - // In a real application, you would fetch the allowed IPs from your backend here - setAllowedIPs(['192.168.1.1', '10.0.0.1', '172.16.0.1']); - setIsAllowedIPModalVisible(true); + const handleShowAllowedIPs = async () => { + try { + if (accessToken) { + const data = await getAllowedIPs(accessToken); + setAllowedIPs(data.length > 0 ? data : ["All IP Addresses"]); + } else { + setAllowedIPs(["All IP Addresses"]); + } + } catch (error) { + console.error("Error fetching allowed IPs:", error); + message.error("Failed to fetch allowed IPs"); + setAllowedIPs(["All IP Addresses"]); + } finally { + setIsAllowedIPModalVisible(true); + } }; - - const handleAddIP = (values: { ip: string }) => { - setAllowedIPs([...allowedIPs, values.ip]); - setIsAddIPModalVisible(false); - message.success('IP address added successfully'); + + const handleAddIP = async (values: { ip: string }) => { + try { + if (accessToken) { + await addAllowedIP(accessToken, values.ip); + // Fetch the updated list of IPs + const updatedIPs = await getAllowedIPs(accessToken); + setAllowedIPs(updatedIPs); + message.success('IP address added successfully'); + } + } catch (error) { + console.error("Error adding IP:", error); + message.error('Failed to add IP address'); + } finally { + setIsAddIPModalVisible(false); + } }; - - const handleDeleteIP = (ip: string) => { + + const handleDeleteIP = async (ip: string) => { setIPToDelete(ip); setIsDeleteIPModalVisible(true); }; - - const confirmDeleteIP = () => { - if (ipToDelete) { - setAllowedIPs(allowedIPs.filter(ip => ip !== ipToDelete)); - setIsDeleteIPModalVisible(false); - setIPToDelete(null); - message.success('IP address deleted successfully'); + + const confirmDeleteIP = async () => { + if (ipToDelete && accessToken) { + try { + await deleteAllowedIP(accessToken, ipToDelete); + // Fetch the updated list of IPs + const updatedIPs = await getAllowedIPs(accessToken); + setAllowedIPs(updatedIPs.length > 0 ? updatedIPs : ["All IP Addresses"]); + message.success('IP address deleted successfully'); + } catch (error) { + console.error("Error deleting IP:", error); + message.error('Failed to delete IP address'); + } finally { + setIsDeleteIPModalVisible(false); + setIPToDelete(null); + } } }; diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 75819af58c0..e50fc37e894 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -788,6 +788,95 @@ export const modelHubCall = async (accessToken: String) => { } }; +// Function to get allowed IPs +export const getAllowedIPs = async (accessToken: String) => { + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/get/allowed_ips` + : `/get/allowed_ips`; + + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.text(); + throw new Error(`Network response was not ok: ${errorData}`); + } + + const data = await response.json(); + console.log("getAllowedIPs:", data); + return data.data; // Assuming the API returns { data: [...] } + } catch (error) { + console.error("Failed to get allowed IPs:", error); + throw error; + } +}; + +// Function to add an allowed IP +export const addAllowedIP = async (accessToken: String, ip: String) => { + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/add/allowed_ip` + : `/add/allowed_ip`; + + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ ip: ip }), + }); + + if (!response.ok) { + const errorData = await response.text(); + throw new Error(`Network response was not ok: ${errorData}`); + } + + const data = await response.json(); + console.log("addAllowedIP:", data); + return data; + } catch (error) { + console.error("Failed to add allowed IP:", error); + throw error; + } +}; + +// Function to delete an allowed IP +export const deleteAllowedIP = async (accessToken: String, ip: String) => { + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/delete/allowed_ip` + : `/delete/allowed_ip`; + + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ ip: ip }), + }); + + if (!response.ok) { + const errorData = await response.text(); + throw new Error(`Network response was not ok: ${errorData}`); + } + + const data = await response.json(); + console.log("deleteAllowedIP:", data); + return data; + } catch (error) { + console.error("Failed to delete allowed IP:", error); + throw error; + } +}; + export const modelMetricsCall = async ( accessToken: String, userID: String, From b04c4da12eaeae6a1f6573505e5817f9a87fbb81 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 9 Jul 2024 15:57:53 -0700 Subject: [PATCH 5/8] fix allowed ip screen --- .../src/components/admins.tsx | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/ui/litellm-dashboard/src/components/admins.tsx b/ui/litellm-dashboard/src/components/admins.tsx index 85b5dc3d4ea..585111fd491 100644 --- a/ui/litellm-dashboard/src/components/admins.tsx +++ b/ui/litellm-dashboard/src/components/admins.tsx @@ -723,25 +723,25 @@ const AdminPanel: React.FC = ({ ]} > - - - IP Address - Action - - - - {allowedIPs.map((ip, index) => ( - - {ip} - - - - - ))} - -
+ + + IP Address + Action + + + + {allowedIPs.map((ip, index) => ( + + {ip} + + + + + ))} + + = ({ setIsDeleteIPModalVisible(false)} + onOk={confirmDeleteIP} + footer={[ + , + + ]} >

Are you sure you want to delete the IP address: {ipToDelete}?

From e966d9fd0f502ee9e8b4efac59af60de025dea40 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 9 Jul 2024 16:04:03 -0700 Subject: [PATCH 6/8] fix text hierarhcy --- .../src/components/admins.tsx | 19 ++++++++++--------- .../src/components/leftnav.tsx | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ui/litellm-dashboard/src/components/admins.tsx b/ui/litellm-dashboard/src/components/admins.tsx index 585111fd491..cdd5dc55fa9 100644 --- a/ui/litellm-dashboard/src/components/admins.tsx +++ b/ui/litellm-dashboard/src/components/admins.tsx @@ -15,7 +15,7 @@ import { message, } from "antd"; import { CopyToClipboard } from "react-copy-to-clipboard"; -import { Select, SelectItem } from "@tremor/react"; +import { Select, SelectItem, Subtitle } from "@tremor/react"; import { Table, TableBody, @@ -598,16 +598,17 @@ const AdminPanel: React.FC = ({
- + ✨ Security Settings - Add SSO - - - - Allowed IP Addresses - - +
+
+ +
+
+ +
+
diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 1fafedf18b0..6a862c6b276 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -98,7 +98,7 @@ const Sidebar: React.FC = ({ ) : null} {userRole == "Admin" ? ( setPage("admin-panel")}> - Admin + Admin Settings ) : null} setPage("api_ref")}> From f7002ecd08bbe145b8f31bb46ef2dc32336260a5 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 9 Jul 2024 16:15:05 -0700 Subject: [PATCH 7/8] fixes when no ip addresses enabled --- .../src/components/admins.tsx | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/ui/litellm-dashboard/src/components/admins.tsx b/ui/litellm-dashboard/src/components/admins.tsx index cdd5dc55fa9..ad25011653a 100644 --- a/ui/litellm-dashboard/src/components/admins.tsx +++ b/ui/litellm-dashboard/src/components/admins.tsx @@ -93,6 +93,8 @@ const AdminPanel: React.FC = ({ isLocal ? "http://localhost:4000" : "" ); + const all_ip_address_allowed = "All IP Addresses Allowed"; + let nonSssoUrl; try { nonSssoUrl = window.location.origin; @@ -105,14 +107,14 @@ const AdminPanel: React.FC = ({ try { if (accessToken) { const data = await getAllowedIPs(accessToken); - setAllowedIPs(data.length > 0 ? data : ["All IP Addresses"]); + setAllowedIPs(data && data.length > 0 ? data : [all_ip_address_allowed]); } else { - setAllowedIPs(["All IP Addresses"]); + setAllowedIPs([all_ip_address_allowed]); } } catch (error) { console.error("Error fetching allowed IPs:", error); - message.error("Failed to fetch allowed IPs"); - setAllowedIPs(["All IP Addresses"]); + message.error(`Failed to fetch allowed IPs ${error}`); + setAllowedIPs([all_ip_address_allowed]); } finally { setIsAllowedIPModalVisible(true); } @@ -129,7 +131,7 @@ const AdminPanel: React.FC = ({ } } catch (error) { console.error("Error adding IP:", error); - message.error('Failed to add IP address'); + message.error(`Failed to add IP address ${error}`); } finally { setIsAddIPModalVisible(false); } @@ -146,11 +148,11 @@ const AdminPanel: React.FC = ({ await deleteAllowedIP(accessToken, ipToDelete); // Fetch the updated list of IPs const updatedIPs = await getAllowedIPs(accessToken); - setAllowedIPs(updatedIPs.length > 0 ? updatedIPs : ["All IP Addresses"]); + setAllowedIPs(updatedIPs.length > 0 ? updatedIPs : [all_ip_address_allowed]); message.success('IP address deleted successfully'); } catch (error) { console.error("Error deleting IP:", error); - message.error('Failed to delete IP address'); + message.error(`Failed to delete IP address ${error}`); } finally { setIsDeleteIPModalVisible(false); setIPToDelete(null); @@ -731,16 +733,18 @@ const AdminPanel: React.FC = ({ - {allowedIPs.map((ip, index) => ( - - {ip} - - - - - ))} + {allowedIPs.map((ip, index) => ( + + {ip} + + {ip !== all_ip_address_allowed && ( + + )} + + +))} From 8195a4eaccff6b481d6e37d7d2e2d08533f28605 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 9 Jul 2024 16:25:23 -0700 Subject: [PATCH 8/8] check if premium user for sso / allowed ip --- ui/litellm-dashboard/src/app/page.tsx | 1 + ui/litellm-dashboard/src/components/admins.tsx | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 1aaf8cbc3d8..26bdb7af24a 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -221,6 +221,7 @@ const CreateKeyPage = () => { searchParams={searchParams} accessToken={accessToken} showSSOBanner={showSSOBanner} + premiumUser={premiumUser} /> ) : page == "api_ref" ? ( >; showSSOBanner: boolean; + premiumUser: boolean; } import { @@ -59,6 +60,7 @@ const AdminPanel: React.FC = ({ searchParams, accessToken, showSSOBanner, + premiumUser, }) => { const [form] = Form.useForm(); const [memberForm] = Form.useForm(); @@ -105,6 +107,12 @@ const AdminPanel: React.FC = ({ const handleShowAllowedIPs = async () => { try { + if (premiumUser !== true) { + message.error( + "This feature is only available for premium users. Please upgrade your account." + ) + return + } if (accessToken) { const data = await getAllowedIPs(accessToken); setAllowedIPs(data && data.length > 0 ? data : [all_ip_address_allowed]); @@ -116,7 +124,9 @@ const AdminPanel: React.FC = ({ message.error(`Failed to fetch allowed IPs ${error}`); setAllowedIPs([all_ip_address_allowed]); } finally { - setIsAllowedIPModalVisible(true); + if (premiumUser === true) { + setIsAllowedIPModalVisible(true); + } } }; @@ -605,7 +615,7 @@ const AdminPanel: React.FC = ({ ✨ Security Settings
- +