fix(permissions): hide action buttons for unauthorized roles
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

TestCatalogPage: 'Use Template' button had no role check — any user
(including viewer/blue_tech/red_tech) could see and click it, which
would fail at the backend (POST /tests/from-template requires
red_lead|blue_lead). Added canUseTemplate check; button hidden for
viewer, blue_tech, red_tech.

TechniqueDetailPage: 'Run This Test' / 'Re-run' buttons in the
Available Templates section also had no role check. Added canRunTemplate
(same criteria: admin|red_lead|blue_lead). The 'View test' button for
active tests remains visible to everyone (read-only navigation).

Principle: if a user cannot perform the action, the button does not
appear — no permission error messages, just absence of the control.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-29 15:47:08 +02:00
parent f590a00006
commit 07c6164ceb
2 changed files with 27 additions and 10 deletions

View File

@@ -158,6 +158,10 @@ export default function TechniqueDetailPage() {
const canReview =
user?.role === "admin" || user?.role === "red_lead" || user?.role === "blue_lead";
// Same roles that can create tests (mirrors backend POST /tests/from-template)
const canRunTemplate =
user?.role === "admin" || user?.role === "red_lead" || user?.role === "blue_lead";
const {
data: technique,
isLoading,
@@ -584,7 +588,7 @@ export default function TechniqueDetailPage() {
<ExternalLink className="h-3.5 w-3.5" />
View test
</button>
) : (
) : canRunTemplate ? (
<button
onClick={() => setTemplateFormId(tpl.id)}
className={`flex items-center gap-1 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors ${
@@ -596,7 +600,7 @@ export default function TechniqueDetailPage() {
<FlaskConical className="h-3.5 w-3.5" />
{needsReRun ? "Run This Test" : latestValidated ? "Re-run" : "Run This Test"}
</button>
)}
) : null}
</div>
</div>
);

View File

@@ -14,6 +14,7 @@ import {
import { getTemplates } from "../api/test-templates";
import TestFromTemplateForm from "../components/TestFromTemplateForm";
import type { TestTemplateSummary } from "../types/models";
import { useAuth } from "../context/AuthContext";
// ── Constants ──────────────────────────────────────────────────────
@@ -75,6 +76,13 @@ export default function TestCatalogPage() {
const navigate = useNavigate();
const { templateId } = useParams<{ templateId: string }>();
const [searchParams, setSearchParams] = useSearchParams();
const { user } = useAuth();
// Only leads and admins can create tests from templates
const canUseTemplate =
user?.role === "admin" ||
user?.role === "red_lead" ||
user?.role === "blue_lead";
const [search, setSearch] = useState(searchParams.get("search") || "");
const [source, setSource] = useState(searchParams.get("source") || "");
@@ -236,6 +244,7 @@ export default function TestCatalogPage() {
<TemplateCard
key={tpl.id}
template={tpl}
canUse={canUseTemplate}
onUse={() => navigate(`/test-catalog/${tpl.id}/use`)}
/>
))}
@@ -286,9 +295,11 @@ export default function TestCatalogPage() {
function TemplateCard({
template,
canUse,
onUse,
}: {
template: TestTemplateSummary;
canUse: boolean;
onUse: () => void;
}) {
return (
@@ -341,7 +352,8 @@ function TemplateCard({
{/* Spacer */}
<div className="flex-1" />
{/* Action */}
{/* Action — only visible to users who can create tests */}
{canUse && (
<button
onClick={onUse}
className="mt-4 flex w-full items-center justify-center gap-1.5 rounded-lg border border-cyan-500/30 bg-cyan-500/10 py-2 text-sm font-medium text-cyan-400 hover:bg-cyan-500/20 transition-colors"
@@ -349,6 +361,7 @@ function TemplateCard({
<FlaskConical className="h-4 w-4" />
Use Template
</button>
)}
</div>
);
}