mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-07 09:24:09 +00:00
fix: gate cross-node HTTP and stop-by-label on remote RBAC capability (#1509)
An older remote node ignores the forwarded actor-role header (running proxied requests as admin) and ignores the stop-by-label stack allowlist (stopping every label-matched stack). The control could neither detect nor prevent this on a mixed-version fleet. Instances now advertise a cross-node-rbac capability, and the control refuses to act when a remote lacks it: - HTTP proxy: a non-admin user's request is not forwarded to a remote that does not advertise the capability (fails closed when it cannot be determined). Admins are unaffected. - Stop-by-label: a real stop bound to a confirmed stack set is not sent to a remote lacking the capability; the node is reported as needing an upgrade. As defense in depth, a node whose results name stacks outside the confirmed set is failed rather than rendered as a clean stop. Separately, the stop's lock-contention path now reports every confirmed stack as a contention failure (including one that lost its label), so a confirmed stack is never silently dropped and the result is never empty.
This commit is contained in:
@@ -4,6 +4,7 @@ import { NodeRegistry } from '../services/NodeRegistry';
|
||||
import { PROXY_TIER_HEADER, PROXY_ROLE_HEADER } from '../services/license-headers';
|
||||
import { LicenseService } from '../services/LicenseService';
|
||||
import { isProxyExemptPath } from '../helpers/proxyExemptPaths';
|
||||
import { remoteSupportsCrossNodeRbac } from '../helpers/remoteCapabilities';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { DatabaseService } from '../services/DatabaseService';
|
||||
import { redactSensitiveText } from '../utils/safeLog';
|
||||
@@ -145,6 +146,30 @@ export function createRemoteProxyMiddleware(): RequestHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mixed-version RBAC gate. The forwarded actor role is enforced only by a
|
||||
// remote that advertises cross-node-rbac; an older remote ignores the
|
||||
// header and runs the proxied request as admin. So a non-admin must not be
|
||||
// forwarded to a remote that does not advertise the capability. Admins are
|
||||
// unaffected (they are admin on the remote regardless), and the check is
|
||||
// skipped for them so it never adds latency to the admin path. Fails closed
|
||||
// when the capability cannot be determined. Using `?.` so an unresolved user
|
||||
// (not reachable past authGate, but defensive) is gated, never waved through.
|
||||
if (req.user?.role !== 'admin') {
|
||||
remoteSupportsCrossNodeRbac(req.nodeId)
|
||||
.then((supported) => {
|
||||
if (!supported) {
|
||||
res.status(403).json({
|
||||
error: `Remote node "${node.name}" is running a version that does not enforce per-user permissions. Upgrade it before non-admin users can act on it.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
req.proxyTarget = target;
|
||||
proxy(req, res, next);
|
||||
})
|
||||
.catch(next);
|
||||
return;
|
||||
}
|
||||
|
||||
req.proxyTarget = target;
|
||||
proxy(req, res, next);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user