diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logging-and-alerts/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logging-and-alerts/page.tsx
index 8232e391259..ea6f2dcdfd1 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/logging-and-alerts/page.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/logging-and-alerts/page.tsx
@@ -4,6 +4,14 @@ import Settings from "@/components/settings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function LoggingAndAlerts() {
- const { accessToken, userRole, userId, premiumUser } = useAuthorized();
- return ;
+ const { accessToken, userRole, userId, premiumUser, isViewOnly } = useAuthorized();
+ return (
+
+ );
}
diff --git a/ui/litellm-dashboard/src/components/settings.test.tsx b/ui/litellm-dashboard/src/components/settings.test.tsx
index c8b7418d45d..e4a20bedd66 100644
--- a/ui/litellm-dashboard/src/components/settings.test.tsx
+++ b/ui/litellm-dashboard/src/components/settings.test.tsx
@@ -387,3 +387,45 @@ describe("Add Callback dropdown", () => {
expect(screen.getByText("Arize (scoped destination)")).toBeInTheDocument();
});
});
+
+describe("Settings read-only admin", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ credentialsFixture = { credentials: [] };
+ vi.mocked(getCallbacksCall).mockResolvedValue({ callbacks: [], available_callbacks: [], alerts: [] });
+ vi.mocked(getCallbackConfigsCall).mockResolvedValue([]);
+ vi.mocked(alertingSettingsCall).mockResolvedValue([]);
+ });
+
+ it("gives a view-only session no write affordances even though its role reads as Admin", async () => {
+ // Regression: proxy_admin_viewer is mapped to the effective role "Admin" so it gets
+ // read parity, and the write restriction travels separately on isViewOnly. Deriving
+ // write access from the role alone handed the viewer Add, Edit scope and Delete.
+ const { queryByText } = renderSettings({
+ accessToken: "token",
+ userRole: "Admin",
+ userID: "viewer-1",
+ premiumUser: false,
+ isViewOnly: true,
+ } as never);
+
+ await waitFor(() => {
+ expect(queryByText("Active Logging Callbacks")).toBeInTheDocument();
+ });
+ expect(queryByText("Add Callback")).not.toBeInTheDocument();
+ });
+
+ it("keeps the write affordances for a real proxy admin", async () => {
+ const { queryByText } = renderSettings({
+ accessToken: "token",
+ userRole: "Admin",
+ userID: "admin-1",
+ premiumUser: false,
+ });
+
+ await waitFor(() => {
+ expect(queryByText("Active Logging Callbacks")).toBeInTheDocument();
+ });
+ expect(queryByText("Add Callback")).toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx
index 9def17ede04..dacba0524bc 100644
--- a/ui/litellm-dashboard/src/components/settings.tsx
+++ b/ui/litellm-dashboard/src/components/settings.tsx
@@ -57,6 +57,7 @@ interface SettingsPageProps {
userRole: string | null;
userID: string | null;
premiumUser: boolean;
+ isViewOnly?: boolean;
}
const assetsLogoFolder = "/ui/assets/logos/";
@@ -215,7 +216,7 @@ const buildCallbackPayload = (formValues: Record, callbackName: str
};
};
-const Settings: React.FC = ({ accessToken, userRole, userID, premiumUser }) => {
+const Settings: React.FC = ({ accessToken, userRole, userID, premiumUser, isViewOnly = false }) => {
const [callbacks, setCallbacks] = useState([]);
const [isLoadingCallbacks, setIsLoadingCallbacks] = useState(true);
const [alerts, setAlerts] = useState([]);
@@ -253,7 +254,7 @@ const Settings: React.FC = ({ accessToken, userRole, userID,
// credential_type=logging; they share the one Active Logging Callbacks table as
// rows alongside config callbacks. Only a proxy admin (or admin-viewer, read-only)
// may read them, so non-admins skip the fetch entirely.
- const isProxyAdmin = userRole != null && isProxyAdminRole(userRole);
+ const isProxyAdmin = !isViewOnly && userRole != null && isProxyAdminRole(userRole);
const { data: credentialData, refetch: refetchCredentials } = useCredentials(canReadCredentialsRole(userRole));
const { data: teamsData } = useTeams();
const { data: orgsData } = useOrganizations();