fix(mesh): hide node and stack management controls from non-admins (#1284)

* fix(mesh): hide node and stack management controls from non-admins

The Routing tab rendered the per-node mesh enable/disable toggle and the
stack opt-in/opt-out controls for any Admiral-tier user, but those backend
routes require the admin role. A non-admin viewer on an Admiral instance
saw controls that returned 403.

Thread a canManage flag (true only for admins) from the Fleet view into
the Routing tab, its node cards, and the opt-in sheet so non-admins get a
read-only Routing tab: the enable/disable toggle, add-stack, and
opt-in/opt-out controls are hidden, while status, aliases, topology,
activity, diagnostics, and the alias test probe stay available. This
mirrors the Federation tab's existing read-only treatment for non-admins.

Add backend route-gating tests covering the tier and admin-role guards on
every mesh route, and frontend render-gate tests for the node card and the
opt-in sheet in both density layouts.

* refactor(mesh): require canManage on the routing-node-card primitive

Remove the permissive `canManage = true` default on the shared
routing-node-card primitive so a new call site cannot render the
management controls without an explicit decision. Every current caller
already passes the flag; the type now enforces it. Drop the omitted-prop
test, which covered a state the compiler now prevents.
This commit is contained in:
Anso
2026-06-02 16:09:25 -04:00
committed by GitHub
parent 02f98ab90a
commit c82a39c65a
8 changed files with 417 additions and 34 deletions
@@ -14,9 +14,11 @@ interface Props {
nodeName: string;
onChanged: () => void;
onViewTopology?: (stack: string) => void;
/** Opt-in/out is admin-only on the backend; non-admins see the list read-only. */
canManage: boolean;
}
export function MeshOptInSheet({ open, onOpenChange, nodeId, nodeName, onChanged, onViewTopology }: Props) {
export function MeshOptInSheet({ open, onOpenChange, nodeId, nodeName, onChanged, onViewTopology, canManage }: Props) {
const [stacks, setStacks] = useState<MeshStackEntry[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -93,6 +95,7 @@ export function MeshOptInSheet({ open, onOpenChange, nodeId, nodeName, onChanged
<p className="text-sm text-stat-subtitle leading-snug">
Adding a stack lets its services be reached from other meshed stacks by hostname.
Toggling a stack triggers a redeploy on its node so the routing override applies.
{!canManage && ' Changing mesh membership requires an administrator.'}
</p>
{loading && (
@@ -132,13 +135,15 @@ export function MeshOptInSheet({ open, onOpenChange, nodeId, nodeName, onChanged
<Workflow className="w-3 h-3 mr-1" /> Topology
</Button>
)}
<Button
size="sm"
variant={stack.optedIn ? 'outline' : 'default'}
onClick={() => setConfirmStack(stack)}
>
{stack.optedIn ? 'Remove from mesh' : 'Add to mesh'}
</Button>
{canManage && (
<Button
size="sm"
variant={stack.optedIn ? 'outline' : 'default'}
onClick={() => setConfirmStack(stack)}
>
{stack.optedIn ? 'Remove from mesh' : 'Add to mesh'}
</Button>
)}
</div>
)}
</div>