fix(ui): hide guardrail review buttons from non-admin users

The team guardrail submissions list rendered Approve/Reject buttons for
non-admin users even though the backend correctly rejected the calls.
Thread userRole from the page through GuardrailsPanel into
TeamGuardrailsTab and gate the row-card and detail-panel review buttons
on isAdmin so the UI matches the backend authorization.

Defense in depth only — the backend remains the source of truth and is
double-gated at both the route admin check and the explicit endpoint
role check.

Refs LIT-2494
This commit is contained in:
Ryan Crabbe 2026-05-09 09:21:28 -07:00
parent b5d3a5fc85
commit fe240e608e
No known key found for this signature in database
4 changed files with 95 additions and 6 deletions

View file

@ -4,9 +4,9 @@ import GuardrailsPanel from "@/components/guardrails";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const GuardrailsPage = () => {
const { accessToken } = useAuthorized();
const { accessToken, userRole } = useAuthorized();
return <GuardrailsPanel accessToken={accessToken} />;
return <GuardrailsPanel accessToken={accessToken} userRole={userRole} />;
};
export default GuardrailsPage;

View file

@ -252,7 +252,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
{
key: "submitted",
label: "Submitted Guardrails",
children: <TeamGuardrailsTab accessToken={accessToken} />,
children: <TeamGuardrailsTab accessToken={accessToken} userRole={userRole} />,
},
]}
/>

View file

@ -25,6 +25,7 @@ import {
import NotificationsManager from "@/components/molecules/notifications_manager";
import TeamDropdown from "@/components/common_components/team_dropdown";
import { useRegisterGuardrail } from "@/app/(dashboard)/hooks/guardrails/useRegisterGuardrail";
import { isAdminRole } from "@/utils/roles";
type GuardrailStatus = "active" | "pending" | "rejected";
@ -229,6 +230,7 @@ type GuardrailCardProps = {
guardrail: TeamGuardrail;
isSelected: boolean;
isHeadersExpanded: boolean;
isAdmin: boolean;
onSelect: () => void;
onToggleForwardKey: () => void;
onToggleHeaders: () => void;
@ -240,6 +242,7 @@ function GuardrailCard({
guardrail: g,
isSelected,
isHeadersExpanded,
isAdmin,
onSelect,
onToggleForwardKey,
onToggleHeaders,
@ -304,7 +307,7 @@ function GuardrailCard({
>
{isSelected ? "Close" : "Review"}
</button>
{g.status === "pending" && (
{isAdmin && g.status === "pending" && (
<>
<button
type="button"
@ -391,6 +394,7 @@ function ConfigRow({
type DetailPanelProps = {
guardrail: TeamGuardrail;
isAdmin: boolean;
onClose: () => void;
onApprove: () => void;
onReject: () => void;
@ -403,6 +407,7 @@ type DetailPanelProps = {
function DetailPanel({
guardrail: g,
isAdmin,
onClose,
onApprove,
onReject,
@ -709,7 +714,7 @@ function DetailPanel({
<ExternalLinkIcon className="h-4 w-4" />
Test Endpoint
</button>
{g.status === "pending" && (
{isAdmin && g.status === "pending" && (
<div className="flex gap-2">
<button
type="button"
@ -800,9 +805,11 @@ function ConfirmDialog({
interface TeamGuardrailsTabProps {
accessToken: string | null;
userRole?: string;
}
export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
export function TeamGuardrailsTab({ accessToken, userRole }: TeamGuardrailsTabProps) {
const isAdmin = userRole ? isAdminRole(userRole) : false;
const [guardrails, setGuardrails] = useState<TeamGuardrail[]>([]);
const [summary, setSummary] = useState({
total: 0,
@ -1041,6 +1048,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
guardrail={g}
isSelected={selectedId === g.id}
isHeadersExpanded={expandedHeaders.has(g.id)}
isAdmin={isAdmin}
onSelect={() => setSelectedId(selectedId === g.id ? null : g.id)}
onToggleForwardKey={() => toggleForwardKey(g.id)}
onToggleHeaders={() => toggleHeaders(g.id)}
@ -1053,6 +1061,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
{selected && (
<DetailPanel
guardrail={selected}
isAdmin={isAdmin}
onClose={() => setSelectedId(null)}
onApprove={() =>
setConfirmAction({ id: selected.id, action: "approve" })

View file

@ -0,0 +1,80 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderWithProviders, screen } from "./test-utils";
import { TeamGuardrailsTab } from "../src/components/guardrails/TeamGuardrailsTab";
vi.mock("../src/components/networking", () => ({
listGuardrailSubmissions: vi.fn(),
approveGuardrailSubmission: vi.fn(),
rejectGuardrailSubmission: vi.fn(),
updateGuardrailCall: vi.fn(),
}));
vi.mock("../src/app/(dashboard)/hooks/guardrails/useRegisterGuardrail", () => ({
useRegisterGuardrail: () => ({
mutateAsync: vi.fn(),
isPending: false,
}),
}));
vi.mock("../src/components/common_components/team_dropdown", () => ({
default: () => null,
}));
import { listGuardrailSubmissions } from "../src/components/networking";
const pendingSubmission = {
guardrail_id: "guard-1",
guardrail_name: "test-pending-guardrail",
status: "pending_review",
team_id: "team-1",
team_guardrail: true,
litellm_params: {
guardrail: "generic_guardrail_api",
mode: "pre_call",
api_base: "https://example.com/guard",
},
guardrail_info: {},
submitted_at: "2026-05-09T00:00:00Z",
};
describe("TeamGuardrailsTab — approve/reject role gate", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(listGuardrailSubmissions).mockResolvedValue({
submissions: [pendingSubmission],
summary: { total: 1, pending_review: 1, active: 0, rejected: 0 },
});
});
it("hides Approve and Reject buttons for an internal user on a pending submission", async () => {
renderWithProviders(
<TeamGuardrailsTab accessToken="test-token" userRole="Internal User" />
);
// Wait for the submission row to render
await screen.findByText("test-pending-guardrail");
expect(screen.queryByRole("button", { name: /approve/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /reject/i })).not.toBeInTheDocument();
});
it("shows Approve and Reject buttons for an admin on a pending submission", async () => {
renderWithProviders(
<TeamGuardrailsTab accessToken="test-token" userRole="Admin" />
);
await screen.findByText("test-pending-guardrail");
expect(screen.getByRole("button", { name: /approve/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /reject/i })).toBeInTheDocument();
});
it("hides Approve and Reject buttons when userRole is undefined (defaults to non-admin)", async () => {
renderWithProviders(<TeamGuardrailsTab accessToken="test-token" />);
await screen.findByText("test-pending-guardrail");
expect(screen.queryByRole("button", { name: /approve/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /reject/i })).not.toBeInTheDocument();
});
});