fix: enforce ws3 roles + node allowlist
This commit is contained in:
@@ -24,6 +24,7 @@ import { authorizeGatewayConnect } from "../../auth.js";
|
||||
import { loadConfig } from "../../../config/config.js";
|
||||
import { buildDeviceAuthPayload } from "../../device-auth.js";
|
||||
import { isLoopbackAddress } from "../../net.js";
|
||||
import { resolveNodeCommandAllowlist } from "../../node-command-policy.js";
|
||||
import {
|
||||
type ConnectParams,
|
||||
ErrorCodes,
|
||||
@@ -253,17 +254,55 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
}
|
||||
const authMethod = authResult.method ?? "none";
|
||||
|
||||
const role = connectParams.role ?? "operator";
|
||||
const scopes = Array.isArray(connectParams.scopes)
|
||||
? connectParams.scopes
|
||||
: role === "operator"
|
||||
? ["operator.admin"]
|
||||
: [];
|
||||
const roleRaw = connectParams.role ?? "operator";
|
||||
const role = roleRaw === "operator" || roleRaw === "node" ? roleRaw : null;
|
||||
if (!role) {
|
||||
setHandshakeState("failed");
|
||||
setCloseCause("invalid-role", {
|
||||
role: roleRaw,
|
||||
client: connectParams.client.id,
|
||||
clientDisplayName: connectParams.client.displayName,
|
||||
mode: connectParams.client.mode,
|
||||
version: connectParams.client.version,
|
||||
});
|
||||
send({
|
||||
type: "res",
|
||||
id: frame.id,
|
||||
ok: false,
|
||||
error: errorShape(ErrorCodes.INVALID_REQUEST, "invalid role"),
|
||||
});
|
||||
close(1008, "invalid role");
|
||||
return;
|
||||
}
|
||||
const requestedScopes = Array.isArray(connectParams.scopes) ? connectParams.scopes : [];
|
||||
const scopes =
|
||||
requestedScopes.length > 0
|
||||
? requestedScopes
|
||||
: role === "operator"
|
||||
? ["operator.admin"]
|
||||
: [];
|
||||
connectParams.role = role;
|
||||
connectParams.scopes = scopes;
|
||||
|
||||
const device = connectParams.device;
|
||||
let devicePublicKey: string | null = null;
|
||||
if (!device) {
|
||||
setHandshakeState("failed");
|
||||
setCloseCause("device-required", {
|
||||
client: connectParams.client.id,
|
||||
clientDisplayName: connectParams.client.displayName,
|
||||
mode: connectParams.client.mode,
|
||||
version: connectParams.client.version,
|
||||
});
|
||||
send({
|
||||
type: "res",
|
||||
id: frame.id,
|
||||
ok: false,
|
||||
error: errorShape(ErrorCodes.NOT_PAIRED, "device identity required"),
|
||||
});
|
||||
close(1008, "device identity required");
|
||||
return;
|
||||
}
|
||||
if (device) {
|
||||
const derivedId = deriveDeviceIdFromPublicKey(device.publicKey);
|
||||
if (!derivedId || derivedId !== device.id) {
|
||||
@@ -307,7 +346,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
clientId: connectParams.client.id,
|
||||
clientMode: connectParams.client.mode,
|
||||
role,
|
||||
scopes,
|
||||
scopes: requestedScopes,
|
||||
signedAtMs: signedAt,
|
||||
token: connectParams.auth?.token ?? null,
|
||||
});
|
||||
@@ -347,9 +386,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
}
|
||||
|
||||
if (device && devicePublicKey) {
|
||||
const paired = await getPairedDevice(device.id);
|
||||
const isPaired = paired?.publicKey === devicePublicKey;
|
||||
if (!isPaired) {
|
||||
const requirePairing = async (reason: string, paired?: { deviceId: string }) => {
|
||||
const pairing = await requestDevicePairing({
|
||||
deviceId: device.id,
|
||||
publicKey: devicePublicKey,
|
||||
@@ -360,7 +397,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
role,
|
||||
scopes,
|
||||
remoteIp: remoteAddr,
|
||||
silent: isLoopbackAddress(remoteAddr) && authMethod !== "none",
|
||||
silent: isLoopbackAddress(remoteAddr),
|
||||
});
|
||||
const context = buildRequestContext();
|
||||
if (pairing.request.silent === true) {
|
||||
@@ -385,6 +422,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
setCloseCause("pairing-required", {
|
||||
deviceId: device.id,
|
||||
requestId: pairing.request.requestId,
|
||||
reason,
|
||||
});
|
||||
send({
|
||||
type: "res",
|
||||
@@ -395,9 +433,47 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
}),
|
||||
});
|
||||
close(1008, "pairing required");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const paired = await getPairedDevice(device.id);
|
||||
const isPaired = paired?.publicKey === devicePublicKey;
|
||||
if (!isPaired) {
|
||||
const ok = await requirePairing("not-paired");
|
||||
if (!ok) return;
|
||||
} else {
|
||||
const allowedRoles = new Set(
|
||||
Array.isArray(paired.roles)
|
||||
? paired.roles
|
||||
: paired.role
|
||||
? [paired.role]
|
||||
: [],
|
||||
);
|
||||
if (allowedRoles.size === 0) {
|
||||
const ok = await requirePairing("role-upgrade", paired);
|
||||
if (!ok) return;
|
||||
} else if (!allowedRoles.has(role)) {
|
||||
const ok = await requirePairing("role-upgrade", paired);
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
const pairedScopes = Array.isArray(paired.scopes) ? paired.scopes : [];
|
||||
if (scopes.length > 0) {
|
||||
if (pairedScopes.length === 0) {
|
||||
const ok = await requirePairing("scope-upgrade", paired);
|
||||
if (!ok) return;
|
||||
} else {
|
||||
const allowedScopes = new Set(pairedScopes);
|
||||
const missingScope = scopes.find((scope) => !allowedScopes.has(scope));
|
||||
if (missingScope) {
|
||||
const ok = await requirePairing("scope-upgrade", paired);
|
||||
if (!ok) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await updatePairedDeviceMetadata(device.id, {
|
||||
displayName: connectParams.client.displayName,
|
||||
platform: connectParams.client.platform,
|
||||
@@ -410,10 +486,25 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
}
|
||||
}
|
||||
|
||||
if (role === "node") {
|
||||
const cfg = loadConfig();
|
||||
const allowlist = resolveNodeCommandAllowlist(cfg, {
|
||||
platform: connectParams.client.platform,
|
||||
deviceFamily: connectParams.client.deviceFamily,
|
||||
});
|
||||
const declared = Array.isArray(connectParams.commands) ? connectParams.commands : [];
|
||||
const filtered = declared
|
||||
.map((cmd) => cmd.trim())
|
||||
.filter((cmd) => cmd.length > 0 && allowlist.has(cmd));
|
||||
connectParams.commands = filtered;
|
||||
}
|
||||
|
||||
const shouldTrackPresence = !isGatewayCliClient(connectParams.client);
|
||||
const clientId = connectParams.client.id;
|
||||
const instanceId = connectParams.client.instanceId;
|
||||
const presenceKey = shouldTrackPresence ? (instanceId ?? connId) : undefined;
|
||||
const presenceKey = shouldTrackPresence
|
||||
? (connectParams.device?.id ?? instanceId ?? connId)
|
||||
: undefined;
|
||||
|
||||
logWs("in", "connect", {
|
||||
connId,
|
||||
@@ -441,7 +532,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
deviceFamily: connectParams.client.deviceFamily,
|
||||
modelIdentifier: connectParams.client.modelIdentifier,
|
||||
mode: connectParams.client.mode,
|
||||
instanceId,
|
||||
instanceId: connectParams.device?.id ?? instanceId,
|
||||
reason: "connect",
|
||||
});
|
||||
incrementPresenceVersion();
|
||||
|
||||
Reference in New Issue
Block a user