feat(manager): allow manager to create auto-approved campaigns and tests from templates
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
A manager organizes and validates work, so their own campaigns skip the draft -> submit -> pending_approval queue and go straight to active with the start_date they provide (they're the same role that would otherwise approve it). Manager can also now create tests from the catalog, same as red_lead/blue_lead.
This commit is contained in:
@@ -73,6 +73,9 @@ export interface CampaignCreatePayload {
|
||||
target_platform?: string;
|
||||
tags?: string[];
|
||||
scheduled_at?: string;
|
||||
// Only honored when the creator is a manager — auto-approves the
|
||||
// campaign immediately instead of queuing it for later approval.
|
||||
start_date?: string;
|
||||
}
|
||||
|
||||
export interface AddTestPayload {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { listCampaigns, createCampaign, generateCampaignFromThreatActor, type Ca
|
||||
import { getThreatActors, type ThreatActorSummary } from "../api/threat-actors";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import MarkdownText from "../components/MarkdownText";
|
||||
import { datetimeLocalToIso } from "../utils/datetime";
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
draft: "bg-gray-800/50 text-gray-400 border-gray-600/30",
|
||||
@@ -56,9 +57,18 @@ export default function CampaignsPage() {
|
||||
description: "",
|
||||
type: "custom",
|
||||
target_platform: "",
|
||||
start_date: "",
|
||||
});
|
||||
|
||||
const canCreate = user?.role === "admin" || user?.role === "red_lead" || user?.role === "blue_lead";
|
||||
const canCreate =
|
||||
user?.role === "admin" ||
|
||||
user?.role === "red_lead" ||
|
||||
user?.role === "blue_lead" ||
|
||||
user?.role === "manager";
|
||||
// A manager's campaign is auto-approved on creation instead of going
|
||||
// through the draft -> submit -> pending_approval queue, so they must
|
||||
// pick the start_date up front rather than via a later /approve call.
|
||||
const isManagerCreate = user?.role === "manager";
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["campaigns", filters],
|
||||
@@ -71,11 +81,15 @@ export default function CampaignsPage() {
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => createCampaign(newCampaign),
|
||||
mutationFn: () =>
|
||||
createCampaign({
|
||||
...newCampaign,
|
||||
start_date: isManagerCreate ? datetimeLocalToIso(newCampaign.start_date) : undefined,
|
||||
}),
|
||||
onSuccess: (campaign) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["campaigns"] });
|
||||
setShowCreateForm(false);
|
||||
setNewCampaign({ name: "", description: "", type: "custom", target_platform: "" });
|
||||
setNewCampaign({ name: "", description: "", type: "custom", target_platform: "", start_date: "" });
|
||||
navigate(`/campaigns/${campaign.id}`);
|
||||
},
|
||||
});
|
||||
@@ -227,6 +241,23 @@ export default function CampaignsPage() {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{isManagerCreate && (
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-medium text-gray-300">
|
||||
Start Date <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
required
|
||||
value={newCampaign.start_date}
|
||||
onChange={(e) => setNewCampaign((c) => ({ ...c, start_date: e.target.value }))}
|
||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-cyan-500 focus:outline-none"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
This campaign will be auto-approved and go active immediately — no manager review queue needed.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
@@ -237,7 +268,7 @@ export default function CampaignsPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => createMutation.mutate()}
|
||||
disabled={!newCampaign.name || createMutation.isPending}
|
||||
disabled={!newCampaign.name || (isManagerCreate && !newCampaign.start_date) || createMutation.isPending}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-cyan-600 px-4 py-2 text-sm font-medium text-white hover:bg-cyan-500 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{createMutation.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
|
||||
@@ -90,11 +90,12 @@ export default function TestCatalogPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { user } = useAuth();
|
||||
|
||||
// Only leads can create tests from templates — admin administers the
|
||||
// site, not test content.
|
||||
// Leads and managers can create tests from templates — admin administers
|
||||
// the site, not test content.
|
||||
const canUseTemplate =
|
||||
user?.role === "red_lead" ||
|
||||
user?.role === "blue_lead";
|
||||
user?.role === "blue_lead" ||
|
||||
user?.role === "manager";
|
||||
|
||||
// Leads create templates straight into the catalog; operators get the
|
||||
// same button but their submission goes through lead review first.
|
||||
|
||||
Reference in New Issue
Block a user