feat(rbac): make stack-scoped grants node-specific (#1727)

* feat(rbac): make stack-scoped grants node-specific

Qualify stack role assignments as (nodeId, stackName), migrate legacy rows to the default node, and forward bound multi-action evidence on Proxy/Pilot hops so scoped users keep least-privilege remote access without shipping the full grant table.

* fix: mirror scoped-stack-auth-evidence capability to frontend, sanitize node id in role assignment log

Backend added the scoped-stack-auth-evidence capability without the
matching frontend entry, failing the capability parity test. The role
assignment log also interpolated the node id without sanitizeForLog,
unlike the rest of the line.

* fix(rbac): honor node-wide scopes and fix proxied DELETE cleanup

Node-scoped grants now authorize that role's stack actions on the same node in the backend resolver, frontend can(), and remote evidence. Proxied DELETE cleanup uses the gate-stashed route because pathRewrite mutates req.path before proxyRes. Add proxy integration coverage and drop the stale scoped-permissions screenshot.

* fix(rbac): preserve node-qualified grants during repair
This commit is contained in:
Anso
2026-07-29 09:42:14 -04:00
committed by GitHub
parent d698eb46f9
commit 9922d8e765
37 changed files with 2487 additions and 143 deletions
+17
View File
@@ -1,5 +1,6 @@
import type { UserRole, ApiTokenScope, ApiToken } from '../services/DatabaseService';
import type { LicenseTier } from '../services/license-types';
import type { PermissionAction } from '../middleware/permissions';
// Extend Express Request type for user and node context.
// This file is imported for its side effects only (ambient declaration).
@@ -27,6 +28,22 @@ declare global {
deployContext?: import('../services/network/missingExternalNetworksError').DeployInvocationContext;
/** Verified JWT scope for machine credentials (`node_proxy` / `pilot_tunnel`). */
machineAuthScope?: 'node_proxy' | 'pilot_tunnel';
/**
* Hub-bound stack-scoped action evidence, trusted only when set under
* machine auth (`node_proxy` / `pilot_tunnel`). Never set from browser sessions.
*/
scopedStackEvidence?: { stackName: string; actions: ReadonlySet<PermissionAction> };
/**
* Hub-side pending evidence to attach on the outbound proxy hop when
* the caller's global role alone would not grant the primary action.
*/
proxyScopedStackEvidence?: { stackName: string; actions: readonly PermissionAction[] };
/**
* Named-stack classification from the hub gate. Stashed because
* http-proxy pathRewrite mutates req.url before proxyRes, so
* re-classifying req.path there would miss DELETE cleanup.
*/
proxyNamedStackRoute?: { stackName: string; action: PermissionAction };
}
}
}