diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 92862ed9dc..1ddb7b47c1 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -1502,6 +1502,10 @@ async def get_users( sort_order: str = fastapi.Query( default="asc", description="Sort order ('asc' or 'desc')" ), + organization_id: Optional[str] = fastapi.Query( + default=None, + description="Filter users by organization membership. Comma-separated for multiple orgs.", + ), ): """ Get a paginated list of users with filtering and sorting options. @@ -1576,6 +1580,14 @@ async def get_users( "in": sso_id_list, } + if organization_id is not None and isinstance(organization_id, str): + org_id_list = [ + oid.strip() for oid in organization_id.split(",") if oid.strip() + ] + where_conditions["organization_memberships"] = { + "some": {"organization_id": {"in": org_id_list}} + } + ## Filter any none fastapi.Query params - e.g. where_conditions: {'user_email': {'contains': Query(None), 'mode': 'insensitive'}, 'teams': {'has': Query(None)}} where_conditions = {k: v for k, v in where_conditions.items() if v is not None} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx index 5ab6920b28..ae7a0b9767 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/page.tsx @@ -3,13 +3,32 @@ import ViewUserDashboard from "@/components/view_users"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import useTeams from "@/app/(dashboard)/hooks/useTeams"; -import { useState } from "react"; +import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; +import { isProxyAdminRole } from "@/utils/roles"; +import { useState, useMemo } from "react"; +import { Organization } from "@/components/networking"; const UsersPage = () => { const { accessToken, userRole, userId, token } = useAuthorized(); const [keys, setKeys] = useState([]); const { teams } = useTeams(); + const { data: organizations } = useOrganizations(); + + // Compute org IDs where the user is an org_admin, but only if they're NOT a proxy admin + const orgAdminOrgIds = useMemo(() => { + if (!userId || !organizations || !userRole) return null; + // Proxy admins see all users — no org filtering + if (isProxyAdminRole(userRole)) return null; + + const adminOrgIds = organizations + .filter((org: Organization) => + org.members?.some((member) => member.user_id === userId && member.user_role === "org_admin") + ) + .map((org: Organization) => org.organization_id); + + return adminOrgIds.length > 0 ? adminOrgIds : null; + }, [userId, organizations, userRole]); return ( { userID={userId} teams={teams as any} setKeys={setKeys} + orgAdminOrgIds={orgAdminOrgIds} /> ); }; diff --git a/ui/litellm-dashboard/src/components/CreateUserButton.tsx b/ui/litellm-dashboard/src/components/CreateUserButton.tsx index d463ced08f..09978a1c93 100644 --- a/ui/litellm-dashboard/src/components/CreateUserButton.tsx +++ b/ui/litellm-dashboard/src/components/CreateUserButton.tsx @@ -19,6 +19,7 @@ import { getProxyUISettings, invitationCreateCall, modelAvailableCall, + organizationMemberAddCall, userCreateCall, } from "./networking"; import OnboardingModal, { InvitationLink } from "./onboarding_link"; @@ -44,6 +45,7 @@ interface CreateuserProps { possibleUIRoles: null | Record>; onUserCreated?: (userId: string) => void; isEmbedded?: boolean; + organizationId?: string | null; } // Define an interface for the UI settings @@ -55,7 +57,7 @@ interface UISettings { } export const CreateUserButton: React.FC = ({ - userID, accessToken, teams, possibleUIRoles, onUserCreated, isEmbedded = false }) => { + userID, accessToken, teams, possibleUIRoles, onUserCreated, isEmbedded = false, organizationId }) => { const queryClient = useQueryClient(); const [uiSettings, setUISettings] = useState(null); const [form] = Form.useForm(); @@ -112,6 +114,18 @@ export const CreateUserButton: React.FC = ({ setApiuser(true); const user_id = response.data?.user_id || response.user_id; + // Auto-add user to the org admin's organization + if (organizationId && user_id) { + try { + await organizationMemberAddCall(accessToken, organizationId, { + role: "internal_user", + user_id: user_id, + }); + } catch (orgError) { + console.error("Failed to add user to organization:", orgError); + } + } + if (onUserCreated && isEmbedded) { onUserCreated(user_id); form.resetFields(); diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index fa35a566de..d01fc06bc0 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -443,8 +443,8 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse children: item.children ? filterItemsByRole(item.children) : undefined, })) .filter((item) => { - // Special handling for organizations menu item - allow org_admins - if (item.key === "organizations") { + // Special handling for organizations and users menu items - allow org_admins + if (item.key === "organizations" || item.key === "users") { const hasRoleAccess = !item.roles || item.roles.includes(userRole) || isOrgAdmin; if (!hasRoleAccess) return false; diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 91454d8d8b..1dd9afc698 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -1104,6 +1104,7 @@ export const userListCall = async ( sso_user_id: string | null = null, sortBy: string | null = null, sortOrder: "asc" | "desc" | null = null, + organizationIds: string[] | null = null, ) => { /** * Get all available teams on proxy @@ -1151,6 +1152,10 @@ export const userListCall = async ( queryParams.append("sort_order", sortOrder); } + if (organizationIds && organizationIds.length > 0) { + queryParams.append("organization_id", organizationIds.join(",")); + } + const queryString = queryParams.toString(); if (queryString) { url += `?${queryString}`; diff --git a/ui/litellm-dashboard/src/components/view_users.tsx b/ui/litellm-dashboard/src/components/view_users.tsx index 576a1a84be..49a55f393f 100644 --- a/ui/litellm-dashboard/src/components/view_users.tsx +++ b/ui/litellm-dashboard/src/components/view_users.tsx @@ -16,7 +16,7 @@ import { import OnboardingModal, { InvitationLink } from "./onboarding_link"; import { updateExistingKeys } from "@/utils/dataUtils"; -import { isAdminRole } from "@/utils/roles"; +import { isAdminRole, isProxyAdminRole } from "@/utils/roles"; import { useDebouncedState } from "@tanstack/react-pacer/debouncer"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Typography } from "antd"; @@ -39,6 +39,7 @@ interface ViewUserDashboardProps { userID: string | null; teams: any[] | null; setKeys: React.Dispatch>; + orgAdminOrgIds?: string[] | null; } interface FilterState { @@ -69,7 +70,8 @@ const initialFilters: FilterState = { sort_order: "desc", }; -const ViewUserDashboard: React.FC = ({ accessToken, token, userRole, userID, teams }) => { +const ViewUserDashboard: React.FC = ({ accessToken, token, userRole, userID, teams, orgAdminOrgIds }) => { + const isProxyAdmin = userRole ? isProxyAdminRole(userRole) : false; const queryClient = useQueryClient(); const [currentPage, setCurrentPage] = useState(1); const [editModalVisible, setEditModalVisible] = useState(false); @@ -245,7 +247,7 @@ const ViewUserDashboard: React.FC = ({ accessToken, toke }; const userListQuery = useQuery({ - queryKey: ["userList", { debouncedFilter: debouncedFilters, currentPage }], + queryKey: ["userList", { debouncedFilter: debouncedFilters, currentPage, orgAdminOrgIds }], queryFn: async () => { if (!accessToken) throw new Error("Access token required"); @@ -260,6 +262,7 @@ const ViewUserDashboard: React.FC = ({ accessToken, toke debouncedFilters.sso_user_id || null, debouncedFilters.sort_by, debouncedFilters.sort_order, + orgAdminOrgIds ?? null, ); }, enabled: Boolean(accessToken && token && userRole && userID), @@ -301,17 +304,19 @@ const ViewUserDashboard: React.FC = ({ accessToken, toke ) : userID && accessToken ? ( <> - + - + {isProxyAdmin && ( + + )} - {selectionMode && ( + {isProxyAdmin && selectionMode && ( @@ -321,61 +326,93 @@ const ViewUserDashboard: React.FC = ({ accessToken, toke - setActiveTab(index === 0 ? "users" : "settings")}> - - Users - Default User Settings - + {isProxyAdmin ? ( + setActiveTab(index === 0 ? "users" : "settings")}> + + Users + Default User Settings + - - - { - setSelectedUser(user); - setEditModalVisible(true); - }} - handleDelete={handleDelete} - handleResetPassword={handleResetPassword} - enableSelection={selectionMode} - selectedUsers={selectedUsers} - onSelectionChange={handleSelectionChange} - filters={filters} - updateFilters={updateFilters} - initialFilters={initialFilters} - teams={teams} - userListResponse={userListResponse} - currentPage={currentPage} - handlePageChange={handlePageChange} - /> - - - - {!userID || !userRole || !accessToken ? ( -
- -
- ) : ( - + + { + setSelectedUser(user); + setEditModalVisible(true); + }} + handleDelete={handleDelete} + handleResetPassword={handleResetPassword} + enableSelection={selectionMode} + selectedUsers={selectedUsers} + onSelectionChange={handleSelectionChange} + filters={filters} + updateFilters={updateFilters} + initialFilters={initialFilters} + teams={teams} + userListResponse={userListResponse} + currentPage={currentPage} + handlePageChange={handlePageChange} /> - )} - -
-
+ + + + {!userID || !userRole || !accessToken ? ( +
+ +
+ ) : ( + + )} +
+ +
+ ) : ( + { + setSelectedUser(user); + setEditModalVisible(true); + }} + handleDelete={handleDelete} + handleResetPassword={handleResetPassword} + enableSelection={false} + selectedUsers={[]} + onSelectionChange={handleSelectionChange} + filters={filters} + updateFilters={updateFilters} + initialFilters={initialFilters} + teams={teams} + userListResponse={userListResponse} + currentPage={currentPage} + handlePageChange={handlePageChange} + /> + )} {/* Existing Modals */}