fix(permissions): hide action buttons for unauthorized roles
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -158,6 +158,10 @@ export default function TechniqueDetailPage() {
|
|||||||
const canReview =
|
const canReview =
|
||||||
user?.role === "admin" || user?.role === "red_lead" || user?.role === "blue_lead";
|
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 {
|
const {
|
||||||
data: technique,
|
data: technique,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -584,7 +588,7 @@ export default function TechniqueDetailPage() {
|
|||||||
<ExternalLink className="h-3.5 w-3.5" />
|
<ExternalLink className="h-3.5 w-3.5" />
|
||||||
View test
|
View test
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : canRunTemplate ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => setTemplateFormId(tpl.id)}
|
onClick={() => setTemplateFormId(tpl.id)}
|
||||||
className={`flex items-center gap-1 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors ${
|
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" />
|
<FlaskConical className="h-3.5 w-3.5" />
|
||||||
{needsReRun ? "Run This Test" : latestValidated ? "Re-run" : "Run This Test"}
|
{needsReRun ? "Run This Test" : latestValidated ? "Re-run" : "Run This Test"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { getTemplates } from "../api/test-templates";
|
import { getTemplates } from "../api/test-templates";
|
||||||
import TestFromTemplateForm from "../components/TestFromTemplateForm";
|
import TestFromTemplateForm from "../components/TestFromTemplateForm";
|
||||||
import type { TestTemplateSummary } from "../types/models";
|
import type { TestTemplateSummary } from "../types/models";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
|
||||||
// ── Constants ──────────────────────────────────────────────────────
|
// ── Constants ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -75,6 +76,13 @@ export default function TestCatalogPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { templateId } = useParams<{ templateId: string }>();
|
const { templateId } = useParams<{ templateId: string }>();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
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 [search, setSearch] = useState(searchParams.get("search") || "");
|
||||||
const [source, setSource] = useState(searchParams.get("source") || "");
|
const [source, setSource] = useState(searchParams.get("source") || "");
|
||||||
@@ -236,6 +244,7 @@ export default function TestCatalogPage() {
|
|||||||
<TemplateCard
|
<TemplateCard
|
||||||
key={tpl.id}
|
key={tpl.id}
|
||||||
template={tpl}
|
template={tpl}
|
||||||
|
canUse={canUseTemplate}
|
||||||
onUse={() => navigate(`/test-catalog/${tpl.id}/use`)}
|
onUse={() => navigate(`/test-catalog/${tpl.id}/use`)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -286,9 +295,11 @@ export default function TestCatalogPage() {
|
|||||||
|
|
||||||
function TemplateCard({
|
function TemplateCard({
|
||||||
template,
|
template,
|
||||||
|
canUse,
|
||||||
onUse,
|
onUse,
|
||||||
}: {
|
}: {
|
||||||
template: TestTemplateSummary;
|
template: TestTemplateSummary;
|
||||||
|
canUse: boolean;
|
||||||
onUse: () => void;
|
onUse: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -341,14 +352,16 @@ function TemplateCard({
|
|||||||
{/* Spacer */}
|
{/* Spacer */}
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
{/* Action */}
|
{/* Action — only visible to users who can create tests */}
|
||||||
<button
|
{canUse && (
|
||||||
onClick={onUse}
|
<button
|
||||||
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"
|
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"
|
||||||
<FlaskConical className="h-4 w-4" />
|
>
|
||||||
Use Template
|
<FlaskConical className="h-4 w-4" />
|
||||||
</button>
|
Use Template
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user