fix(nodes): gate node-management actions by role and release pilot tunnels on delete (#1280)

* fix(nodes): gate node-management actions by role and release pilot tunnels on delete

The node-management write actions in the Nodes panel (add, edit, delete,
generate node token, reset fleet-sync anchor) rendered for every signed-in role,
but the API enforces the manage-nodes permission on them, so lower-privilege
roles saw buttons that returned 403. The panel now renders each action against
the same permission its route enforces; the read-only node table stays visible
to every role.

Deleting a node now also tears down its live pilot-agent tunnel (and any mesh
bridge) immediately, releasing the loopback server, heartbeat timer, and open
streams instead of leaving them until the agent next disconnects, matching the
cleanup the re-enrollment path already performed.

Adds backend route tests for the permission boundaries and tunnel teardown, and
a Nodes panel render test covering the viewer, admin, and node-admin views.

* fix(nodes): close proxy mesh bridges via the dialer on node delete to skip a redial

Deleting a node closed any active mesh bridge through PilotTunnelManager, but a
proxy-mode bridge is owned by the mesh dialer, whose close listener then treated
the close as unexpected and scheduled a reactive redial against the node being
removed. The delete handler now closes a proxy bridge through the dialer's
intentional-close path first (which suppresses the redial), then closes a
pilot-agent tunnel as before. Adds a backend test that primes a live proxy
bridge and asserts deletion closes it without scheduling a redial.
This commit is contained in:
Anso
2026-06-02 09:51:42 -04:00
committed by GitHub
parent 2dd0660491
commit 35a1182890
5 changed files with 402 additions and 42 deletions
+9
View File
@@ -313,6 +313,15 @@ nodesRouter.delete('/:id', async (req: Request, res: Response) => {
if (!requirePermission(req, res, 'node:manage', 'node', nodeIdParam)) return;
try {
const id = parseInt(nodeIdParam);
// Release any live tunnel or mesh bridge before deleting the record so it is
// freed immediately rather than lingering until the peer disconnects. Close a
// proxy-mode mesh bridge through its dialer FIRST: closeBridge removes the
// bridge before closing it, so the dialer does not schedule a reactive redial
// against a node that is about to disappear. closeTunnel then closes a
// pilot-agent tunnel; both are no-ops when this node has no such bridge (a
// local node, or one with no active connection). Mirrors the re-enroll path.
MeshProxyTunnelDialer.getInstance().closeBridge(id, 'node deleted');
PilotTunnelManager.getInstance().closeTunnel(id, PilotCloseCode.NormalClosure, 'node deleted');
DatabaseService.getInstance().deleteNode(id);
NodeRegistry.getInstance().evictConnection(id);
NodeRegistry.getInstance().notifyNodeRemoved(id);