fix(permissions): hide non-actionable UI + fix viewer route access
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

1. /executive-dashboard: add 'viewer' to ProtectedRoute roles — sidebar
   showed the link to viewers but the route redirected them to /dashboard.
2. /comparison: same fix — viewer was in sidebar roles but not in route.
3. /techniques/review-queue: add ProtectedRoute (leads+admin) — the page
   had no route-level protection, any authenticated user could access it.
4. TechniqueDetailPage review banner: hide from users who can't act on it.
   Previously shown to everyone with a 'Leads only' badge; now only shown
   to canReview users (admin/red_lead/blue_lead). Non-leads don't need to
   see alerts about changes they cannot acknowledge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-29 15:25:36 +02:00
parent 8a542f912d
commit f590a00006
2 changed files with 16 additions and 14 deletions

View File

@@ -53,13 +53,20 @@ export default function App() {
<Route path="/techniques/:mitreId" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><TechniqueDetailPage /></Suspense>} /> <Route path="/techniques/:mitreId" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><TechniqueDetailPage /></Suspense>} />
<Route path="/matrix" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><MatrixPage /></Suspense>} /> <Route path="/matrix" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><MatrixPage /></Suspense>} />
<Route path="/techniques/review-queue" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><ReviewQueuePage /></Suspense>} /> <Route
path="/techniques/review-queue"
element={
<ProtectedRoute roles={["admin", "red_lead", "blue_lead"]}>
<Suspense fallback={<LoadingSpinner text="Loading…" />}><ReviewQueuePage /></Suspense>
</ProtectedRoute>
}
/>
{/* ── Executive Dashboard (leads + admin) ──────────────── */} {/* ── Executive Dashboard (leads + admin + viewer) ──────── */}
<Route <Route
path="/executive-dashboard" path="/executive-dashboard"
element={ element={
<ProtectedRoute roles={["admin", "red_lead", "blue_lead"]}> <ProtectedRoute roles={["admin", "red_lead", "blue_lead", "viewer"]}>
<Suspense fallback={<LoadingSpinner text="Loading…" />}><ExecutiveDashboardPage /></Suspense> <Suspense fallback={<LoadingSpinner text="Loading…" />}><ExecutiveDashboardPage /></Suspense>
</ProtectedRoute> </ProtectedRoute>
} }
@@ -84,11 +91,11 @@ export default function App() {
{/* ── Compliance ───────────────────────────────────────── */} {/* ── Compliance ───────────────────────────────────────── */}
<Route path="/compliance" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><CompliancePage /></Suspense>} /> <Route path="/compliance" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><CompliancePage /></Suspense>} />
{/* ── Comparison (leads + admin) ───────────────────────── */} {/* ── Comparison (leads + admin + viewer) ──────────────── */}
<Route <Route
path="/comparison" path="/comparison"
element={ element={
<ProtectedRoute roles={["admin", "red_lead", "blue_lead"]}> <ProtectedRoute roles={["admin", "red_lead", "blue_lead", "viewer"]}>
<Suspense fallback={<LoadingSpinner text="Loading…" />}><ComparisonPage /></Suspense> <Suspense fallback={<LoadingSpinner text="Loading…" />}><ComparisonPage /></Suspense>
</ProtectedRoute> </ProtectedRoute>
} }

View File

@@ -273,8 +273,8 @@ export default function TechniqueDetailPage() {
</div> </div>
</div> </div>
{/* Review required banner */} {/* Review required banner — only shown to users who can act on it */}
{technique.review_required && ( {technique.review_required && canReview && (
<div className="flex items-start gap-3 rounded-xl border border-amber-500/30 bg-amber-500/5 p-4"> <div className="flex items-start gap-3 rounded-xl border border-amber-500/30 bg-amber-500/5 p-4">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-amber-400" /> <AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-amber-400" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -286,15 +286,10 @@ export default function TechniqueDetailPage() {
{technique.mitre_last_modified && ( {technique.mitre_last_modified && (
<> Last modified in ATT&CK: <span className="font-mono">{technique.mitre_last_modified.slice(0, 10)}</span>.</> <> Last modified in ATT&CK: <span className="font-mono">{technique.mitre_last_modified.slice(0, 10)}</span>.</>
)} )}
{" "}A lead or admin should review the changes and click{" "} {" "}Click{" "}
<span className="font-semibold">Mark as Reviewed</span> to acknowledge them. <span className="font-semibold">Mark as Reviewed</span> to acknowledge the changes.
</p> </p>
</div> </div>
{!canReview && (
<span className="shrink-0 rounded-full border border-amber-500/20 bg-amber-500/10 px-2 py-0.5 text-[10px] text-amber-400">
Leads only
</span>
)}
</div> </div>
)} )}