From 0b6053167702ac0fff5d256bcb45d319b54c9eb2 Mon Sep 17 00:00:00 2001 From: kitos Date: Thu, 16 Jul 2026 10:31:21 +0200 Subject: [PATCH] fix(users): grant manager Review Queue access, restrict Webhooks to admin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manager could not see the Review Queue nav item, load the page, or call mark-reviewed (red_lead/blue_lead/admin only) — added to the route guard, sidebar visibility, badge query, and the backend endpoint's role check. Webhook Configuration was shown to red_lead/blue_lead in Settings even though the backend already restricted every webhook endpoint to admin only — the tab is now admin-only in the UI too, matching what was already enforced server-side. --- backend/app/routers/techniques.py | 2 +- .../tests/test_review_queue_manager_access.py | 25 +++++++++++++++++++ frontend/src/App.tsx | 2 +- frontend/src/components/Sidebar.tsx | 4 +-- frontend/src/pages/ReviewQueuePage.tsx | 3 ++- frontend/src/pages/SettingsPage.tsx | 5 ++-- 6 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 backend/tests/test_review_queue_manager_access.py diff --git a/backend/app/routers/techniques.py b/backend/app/routers/techniques.py index 805eee7..bd53dab 100644 --- a/backend/app/routers/techniques.py +++ b/backend/app/routers/techniques.py @@ -254,7 +254,7 @@ def review_technique( # Entry: repo repo: SATechniqueRepository = Depends(get_technique_repository), # Entry: current_user - current_user: User = Depends(require_any_role("red_lead", "blue_lead")), + current_user: User = Depends(require_any_role("red_lead", "blue_lead", "manager")), ) -> TechniqueOut: """Mark a technique as reviewed. diff --git a/backend/tests/test_review_queue_manager_access.py b/backend/tests/test_review_queue_manager_access.py new file mode 100644 index 0000000..9e4d07a --- /dev/null +++ b/backend/tests/test_review_queue_manager_access.py @@ -0,0 +1,25 @@ +"""Manager must have the same Review Queue access as red_lead/blue_lead — +mark_reviewed was previously red_lead/blue_lead (and admin) only.""" + + +def test_manager_can_mark_technique_reviewed(client, db, api, auth_headers, manager_headers): + resp = api( + "post", "/api/v1/techniques", auth_headers, + json={"mitre_id": "T1059.400", "name": "Manager Review Access Technique"}, + ) + assert resp.status_code == 201, resp.text + + resp = api("patch", "/api/v1/techniques/T1059.400/review", manager_headers) + assert resp.status_code == 200, resp.text + assert resp.json()["review_required"] is False + + +def test_red_tech_cannot_mark_technique_reviewed(client, db, api, auth_headers, red_tech_headers): + resp = api( + "post", "/api/v1/techniques", auth_headers, + json={"mitre_id": "T1059.401", "name": "Red Tech No Access Technique"}, + ) + assert resp.status_code == 201, resp.text + + resp = api("patch", "/api/v1/techniques/T1059.401/review", red_tech_headers) + assert resp.status_code == 403 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5256c3a..6f5627c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -59,7 +59,7 @@ export default function App() { + }> } diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 9169486..bd27f8a 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -43,7 +43,7 @@ const mainLinks: NavItem[] = [ icon: Grid3X3, children: [ { to: "/matrix", label: "Coverage Matrix", icon: Grid3X3 }, - { to: "/techniques/review-queue", label: "Review Queue", icon: ClipboardCheck, roles: ["admin", "red_lead", "blue_lead"] }, + { to: "/techniques/review-queue", label: "Review Queue", icon: ClipboardCheck, roles: ["admin", "red_lead", "blue_lead", "manager"] }, ], }, { @@ -159,7 +159,7 @@ export default function Sidebar() { const isAdmin = role === "admin"; const canSeeReviewQueue = - isAdmin || role === "red_lead" || role === "blue_lead"; + isAdmin || role === "red_lead" || role === "blue_lead" || role === "manager"; // Fetch review queue count for the badge (only for roles that can see it) const { data: reviewQueue } = useQuery({ diff --git a/frontend/src/pages/ReviewQueuePage.tsx b/frontend/src/pages/ReviewQueuePage.tsx index d70e18f..c0fd8d8 100644 --- a/frontend/src/pages/ReviewQueuePage.tsx +++ b/frontend/src/pages/ReviewQueuePage.tsx @@ -336,7 +336,8 @@ export default function ReviewQueuePage() { const canReview = user?.role === "admin" || user?.role === "red_lead" || - user?.role === "blue_lead"; + user?.role === "blue_lead" || + user?.role === "manager"; const [expandedId, setExpandedId] = useState(null); diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index f381d3b..c3c6ce5 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1693,7 +1693,6 @@ export default function SettingsPage() { const { user } = useAuth(); const role = user?.role ?? "viewer"; const isAdmin = role === "admin"; - const isLead = ["admin", "red_lead", "blue_lead"].includes(role); const [activeTab, setActiveTab] = useState("profile"); @@ -1705,7 +1704,7 @@ export default function SettingsPage() { id: "webhooks", label: "Webhooks", icon: Webhook, - show: isLead, + show: isAdmin, }, { id: "email", label: "Email / SMTP", icon: Mail, show: isAdmin }, { id: "jira", label: "Jira", icon: Link2, show: isAdmin }, @@ -1760,7 +1759,7 @@ export default function SettingsPage() { )} - {activeTab === "webhooks" && isLead && ( + {activeTab === "webhooks" && isAdmin && (