From b034de58f32993435ddc0a0aaa44242a9805e8b9 Mon Sep 17 00:00:00 2001 From: Anso Date: Thu, 28 May 2026 15:27:40 -0400 Subject: [PATCH] fix(auto-heal): gate panel write controls on admin role (#1245) The Auto-Heal panel rendered its add-policy form, the per-policy enable toggle, and the per-policy delete button without any admin guard, but POST/PATCH/DELETE on /api/auto-heal/policies enforce requireAdmin + requirePaid. Paid non-admin users could see the controls and would hit a 403 on click. Same drift class as the Schedule task gate already moved in the prior sidebar parity pass. Gate the add-new-policy section, the enable toggle, and the delete button on the admin role. Reading existing policies and viewing history stay available to every paid user, matching the backend GET routes which are requirePaid only. Docs updated to call out the admin requirement on Auto-Heal write actions and to add a troubleshooting accordion for non-admin operators who see the panel without the configuration controls. --- docs/features/sidebar.mdx | 5 ++- frontend/src/components/StackAlertSheet.tsx | 43 +++++++++++++-------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/docs/features/sidebar.mdx b/docs/features/sidebar.mdx index f42fc198..ab32a810 100644 --- a/docs/features/sidebar.mdx +++ b/docs/features/sidebar.mdx @@ -85,7 +85,7 @@ Right-click any stack (or open the kebab that appears on hover) for its context - **Destructive**: **Delete**. - **Auto-Heal** requires a **Skipper** or **Admiral** license. **Schedule task** requires a **Skipper** or **Admiral** license and an **admin** role. + **Auto-Heal** requires a **Skipper** or **Admiral** license; configuring policies (add, toggle, delete) also requires an **admin** role. **Schedule task** requires a **Skipper** or **Admiral** license and an **admin** role. @@ -146,4 +146,7 @@ The footer surfaces the most recent stack lifecycle event on the node. Each tick Scheduling requires a **Skipper** or **Admiral** license and an **admin** role. Ask an admin on this node to schedule the task for you, or sign in with an admin account. + + Reading existing Auto-Heal policies only needs a **Skipper** or **Admiral** license, so the panel still opens for non-admin operators on paid tiers. Adding, enabling, disabling, or deleting policies also requires the **admin** role. Sign in with an admin account, or ask an admin on this node to make the change. + diff --git a/frontend/src/components/StackAlertSheet.tsx b/frontend/src/components/StackAlertSheet.tsx index fb3581ca..b3f2a376 100644 --- a/frontend/src/components/StackAlertSheet.tsx +++ b/frontend/src/components/StackAlertSheet.tsx @@ -482,6 +482,7 @@ function AlertsTab({ stackName }: { stackName: string }) { } function AutoHealTab({ stackName, open }: { stackName: string; open: boolean }) { + const { isAdmin } = useAuth(); const [policies, setPolicies] = useState([]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); @@ -617,12 +618,14 @@ function AutoHealTab({ stackName, open }: { stackName: string; open: boolean }) onToggle={handleToggle} deleting={deleting} saving={saving} + isAdmin={isAdmin} /> ))} )} + {isAdmin && (
@@ -700,6 +703,7 @@ function AutoHealTab({ stackName, open }: { stackName: string; open: boolean })
+ )} ); } @@ -710,9 +714,10 @@ interface PolicyRowProps { onToggle: (id: number, enabled: boolean) => void; deleting: boolean; saving: boolean; + isAdmin: boolean; } -function PolicyRow({ policy, onDelete, onToggle, deleting, saving }: PolicyRowProps) { +function PolicyRow({ policy, onDelete, onToggle, deleting, saving, isAdmin }: PolicyRowProps) { const [historyOpen, setHistoryOpen] = useState(false); const [history, setHistory] = useState([]); const [loadingHistory, setLoadingHistory] = useState(false); @@ -760,12 +765,14 @@ function PolicyRow({ policy, onDelete, onToggle, deleting, saving }: PolicyRowPr )}
- policy.id != null && onToggle(policy.id, checked)} - disabled={saving} - aria-label={`Toggle policy for ${policy.service_name ?? 'all services'}`} - /> + {isAdmin && ( + policy.id != null && onToggle(policy.id, checked)} + disabled={saving} + aria-label={`Toggle policy for ${policy.service_name ?? 'all services'}`} + /> + )} - + {isAdmin && ( + + )}