fix(ui): block Playground page for viewer roles on direct URL access (#35676)

This commit is contained in:
ryan-crabbe-berri 2026-08-03 13:03:46 -07:00 committed by GitHub
parent 46b6eae799
commit b13e4eee5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 77 additions and 11 deletions

View file

@ -22,7 +22,7 @@ import {
UserOutlined,
} from "@ant-design/icons";
import { Card, Text, TextInput, Title, Button as TremorButton } from "@tremor/react";
import { Button, Input, Modal, Popover, Select, Spin, Tooltip, Typography, Upload } from "antd";
import { Button, Input, Modal, Popover, Select, Spin, Tooltip, Upload } from "antd";
import React, { useEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
@ -1016,16 +1016,6 @@ const ChatUI: React.FC<ChatUIProps> = ({
NotificationsManager.success("Chat history cleared.");
};
if (userRole && userRole === "Admin Viewer") {
const { Title, Paragraph } = Typography;
return (
<div>
<Title level={1}>Access Denied</Title>
<Paragraph>Ask your proxy admin for access to test models</Paragraph>
</div>
);
}
const onModelChange = (value: string) => {
setSelectedModel(value);

View file

@ -0,0 +1,62 @@
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import PlaygroundPage from "./page";
const authState = { userRole: "Admin" };
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => ({
token: "token-1",
accessToken: "sk-test",
userId: "user-1",
userRole: authState.userRole,
disabledPersonalKeyCreation: false,
}),
}));
vi.mock("@/utils/proxyUtils", () => ({
fetchProxySettings: vi.fn().mockResolvedValue(null),
}));
vi.mock("@/app/(dashboard)/playground/components/chat_ui/ChatUI", () => ({
default: () => <div data-testid="chat-ui" />,
}));
vi.mock("@/app/(dashboard)/playground/components/compareUI/CompareUI", () => ({
default: () => <div data-testid="compare-ui" />,
}));
vi.mock("@/app/(dashboard)/playground/components/complianceUI/ComplianceUI", () => ({
default: () => <div data-testid="compliance-ui" />,
}));
vi.mock("@/app/(dashboard)/playground/components/chat_ui/AgentBuilderView", () => ({
default: () => <div data-testid="agent-builder" />,
}));
describe("PlaygroundPage role guard", () => {
beforeEach(() => {
authState.userRole = "Admin";
});
it.each(["Internal Viewer", "Admin Viewer"])("blocks the entire playground for %s", (role) => {
authState.userRole = role;
render(<PlaygroundPage />);
expect(screen.getByText("Access Denied")).toBeInTheDocument();
expect(screen.queryByRole("tab")).not.toBeInTheDocument();
expect(screen.queryByTestId("chat-ui")).not.toBeInTheDocument();
expect(screen.queryByTestId("compare-ui")).not.toBeInTheDocument();
expect(screen.queryByTestId("compliance-ui")).not.toBeInTheDocument();
expect(screen.queryByTestId("agent-builder")).not.toBeInTheDocument();
});
it.each(["Admin", "Internal User", "Org Admin"])("renders the playground for %s", (role) => {
authState.userRole = role;
render(<PlaygroundPage />);
expect(screen.queryByText("Access Denied")).not.toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Chat" })).toBeInTheDocument();
expect(screen.getByTestId("chat-ui")).toBeInTheDocument();
});
});

View file

@ -9,6 +9,7 @@ import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
import { DeprecationBanner } from "@/components/DeprecationBanner";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { fetchProxySettings } from "@/utils/proxyUtils";
import { isViewOnlyRole } from "@/utils/roles";
interface ProxySettings {
PROXY_BASE_URL?: string;
@ -35,6 +36,17 @@ export default function PlaygroundPage() {
initializeProxySettings();
}, [accessToken]);
if (isViewOnlyRole(userRole)) {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-2 p-8 text-center">
<h1 className="text-2xl font-semibold">Access Denied</h1>
<p className="text-muted-foreground">
Your role does not have access to the Playground. Ask your proxy admin for access to test models.
</p>
</div>
);
}
return (
<div className="h-full w-full flex flex-col">
<TabGroup className="w-full" style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>

View file

@ -13,6 +13,8 @@ export const rolesWithWriteAccess = ["Internal User", "Admin", "proxy_admin"];
// Per the Admin Viewer principle: read parity with Proxy Admin, no writes,
// no cost-incurring actions (Playground stays gated by `rolesWithWriteAccess`).
export const rolesAllowedToViewWriteScopedPages = [...rolesWithWriteAccess, "Admin Viewer", "proxy_admin_viewer"];
export const viewOnlyRoles = ["Admin Viewer", "Internal Viewer"];
export const isViewOnlyRole = (role: string): boolean => viewOnlyRoles.includes(role);
// Helper function to check if a role is in all_admin_roles
export const isAdminRole = (role: string): boolean => {