fix(users): grant manager Review Queue access, restrict Webhooks to admin
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
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.
This commit is contained in:
@@ -254,7 +254,7 @@ def review_technique(
|
|||||||
# Entry: repo
|
# Entry: repo
|
||||||
repo: SATechniqueRepository = Depends(get_technique_repository),
|
repo: SATechniqueRepository = Depends(get_technique_repository),
|
||||||
# Entry: current_user
|
# 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:
|
) -> TechniqueOut:
|
||||||
"""Mark a technique as reviewed.
|
"""Mark a technique as reviewed.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -59,7 +59,7 @@ export default function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="/techniques/review-queue"
|
path="/techniques/review-queue"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute roles={["admin", "red_lead", "blue_lead"]}>
|
<ProtectedRoute roles={["admin", "red_lead", "blue_lead", "manager"]}>
|
||||||
<Suspense fallback={<LoadingSpinner text="Loading…" />}><ReviewQueuePage /></Suspense>
|
<Suspense fallback={<LoadingSpinner text="Loading…" />}><ReviewQueuePage /></Suspense>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const mainLinks: NavItem[] = [
|
|||||||
icon: Grid3X3,
|
icon: Grid3X3,
|
||||||
children: [
|
children: [
|
||||||
{ to: "/matrix", label: "Coverage Matrix", icon: Grid3X3 },
|
{ 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 isAdmin = role === "admin";
|
||||||
|
|
||||||
const canSeeReviewQueue =
|
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)
|
// Fetch review queue count for the badge (only for roles that can see it)
|
||||||
const { data: reviewQueue } = useQuery({
|
const { data: reviewQueue } = useQuery({
|
||||||
|
|||||||
@@ -336,7 +336,8 @@ export default function ReviewQueuePage() {
|
|||||||
const canReview =
|
const canReview =
|
||||||
user?.role === "admin" ||
|
user?.role === "admin" ||
|
||||||
user?.role === "red_lead" ||
|
user?.role === "red_lead" ||
|
||||||
user?.role === "blue_lead";
|
user?.role === "blue_lead" ||
|
||||||
|
user?.role === "manager";
|
||||||
|
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -1693,7 +1693,6 @@ export default function SettingsPage() {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const role = user?.role ?? "viewer";
|
const role = user?.role ?? "viewer";
|
||||||
const isAdmin = role === "admin";
|
const isAdmin = role === "admin";
|
||||||
const isLead = ["admin", "red_lead", "blue_lead"].includes(role);
|
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<Tab>("profile");
|
const [activeTab, setActiveTab] = useState<Tab>("profile");
|
||||||
|
|
||||||
@@ -1705,7 +1704,7 @@ export default function SettingsPage() {
|
|||||||
id: "webhooks",
|
id: "webhooks",
|
||||||
label: "Webhooks",
|
label: "Webhooks",
|
||||||
icon: Webhook,
|
icon: Webhook,
|
||||||
show: isLead,
|
show: isAdmin,
|
||||||
},
|
},
|
||||||
{ id: "email", label: "Email / SMTP", icon: Mail, show: isAdmin },
|
{ id: "email", label: "Email / SMTP", icon: Mail, show: isAdmin },
|
||||||
{ id: "jira", label: "Jira", icon: Link2, show: isAdmin },
|
{ id: "jira", label: "Jira", icon: Link2, show: isAdmin },
|
||||||
@@ -1760,7 +1759,7 @@ export default function SettingsPage() {
|
|||||||
<NotificationSection />
|
<NotificationSection />
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
{activeTab === "webhooks" && isLead && (
|
{activeTab === "webhooks" && isAdmin && (
|
||||||
<Section title="Webhook Configuration" icon={Webhook}>
|
<Section title="Webhook Configuration" icon={Webhook}>
|
||||||
<WebhooksSection />
|
<WebhooksSection />
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
Reference in New Issue
Block a user