gateway: add webchat role with strict method allowlist
This commit is contained in:
parent
614aabfa22
commit
5fa7d31d87
@ -34,6 +34,15 @@ const PAIRING_SCOPE = "operator.pairing";
|
|||||||
|
|
||||||
const APPROVAL_METHODS = new Set(["exec.approval.request", "exec.approval.resolve"]);
|
const APPROVAL_METHODS = new Set(["exec.approval.request", "exec.approval.resolve"]);
|
||||||
const NODE_ROLE_METHODS = new Set(["node.invoke.result", "node.event", "skills.bins"]);
|
const NODE_ROLE_METHODS = new Set(["node.invoke.result", "node.event", "skills.bins"]);
|
||||||
|
|
||||||
|
// Minimal method surface for public webchat clients.
|
||||||
|
// This is intentionally tiny; expand only when a concrete UI need is proven.
|
||||||
|
const WEBCHAT_ROLE_METHODS = new Set([
|
||||||
|
"agent.identity.get",
|
||||||
|
"chat.history",
|
||||||
|
"chat.send",
|
||||||
|
"chat.abort",
|
||||||
|
]);
|
||||||
const PAIRING_METHODS = new Set([
|
const PAIRING_METHODS = new Set([
|
||||||
"node.pair.request",
|
"node.pair.request",
|
||||||
"node.pair.list",
|
"node.pair.list",
|
||||||
@ -98,9 +107,16 @@ function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["c
|
|||||||
if (role === "node") return null;
|
if (role === "node") return null;
|
||||||
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
|
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role === "node") {
|
if (role === "node") {
|
||||||
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
|
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (role === "webchat") {
|
||||||
|
if (WEBCHAT_ROLE_METHODS.has(method)) return null;
|
||||||
|
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized method for role webchat: ${method}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (role !== "operator") {
|
if (role !== "operator") {
|
||||||
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
|
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
onceMessage,
|
onceMessage,
|
||||||
startGatewayServer,
|
startGatewayServer,
|
||||||
startServerWithClient,
|
startServerWithClient,
|
||||||
|
rpcReq,
|
||||||
testState,
|
testState,
|
||||||
} from "./test-helpers.js";
|
} from "./test-helpers.js";
|
||||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||||
@ -214,6 +215,48 @@ describe("gateway server auth/connect", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("webchat role authz", () => {
|
||||||
|
let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
||||||
|
let port: number;
|
||||||
|
let prevToken: string | undefined;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||||
|
process.env.CLAWDBOT_GATEWAY_TOKEN = "secret";
|
||||||
|
port = await getFreePort();
|
||||||
|
server = await startGatewayServer(port);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await server.close();
|
||||||
|
if (prevToken === undefined) {
|
||||||
|
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||||
|
} else {
|
||||||
|
process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("webchat role can connect but is restricted to chat methods", async () => {
|
||||||
|
const ws = await openWs(port);
|
||||||
|
const res = await connectReq(ws, {
|
||||||
|
token: "secret",
|
||||||
|
role: "webchat",
|
||||||
|
client: {
|
||||||
|
id: GATEWAY_CLIENT_NAMES.WEBCHAT_UI,
|
||||||
|
version: "1.0.0",
|
||||||
|
platform: "test",
|
||||||
|
mode: GATEWAY_CLIENT_MODES.WEBCHAT,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
|
||||||
|
const deny = await rpcReq(ws, "sessions.list", { limit: 1 });
|
||||||
|
expect(deny.ok).toBe(false);
|
||||||
|
|
||||||
|
ws.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("token auth", () => {
|
describe("token auth", () => {
|
||||||
let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
||||||
let port: number;
|
let port: number;
|
||||||
|
|||||||
@ -333,7 +333,10 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const roleRaw = connectParams.role ?? "operator";
|
const roleRaw = connectParams.role ?? "operator";
|
||||||
const role = roleRaw === "operator" || roleRaw === "node" ? roleRaw : null;
|
const role =
|
||||||
|
roleRaw === "operator" || roleRaw === "node" || roleRaw === "webchat"
|
||||||
|
? roleRaw
|
||||||
|
: null;
|
||||||
if (!role) {
|
if (!role) {
|
||||||
setHandshakeState("failed");
|
setHandshakeState("failed");
|
||||||
setCloseCause("invalid-role", {
|
setCloseCause("invalid-role", {
|
||||||
|
|||||||
@ -120,12 +120,17 @@ export function connectGateway(host: GatewayHost) {
|
|||||||
const clientName =
|
const clientName =
|
||||||
host.tab === "public-chat" ? GATEWAY_CLIENT_NAMES.WEBCHAT_UI : "moltbot-control-ui";
|
host.tab === "public-chat" ? GATEWAY_CLIENT_NAMES.WEBCHAT_UI : "moltbot-control-ui";
|
||||||
|
|
||||||
|
const role = host.tab === "public-chat" ? "webchat" : undefined;
|
||||||
|
|
||||||
host.client = new GatewayBrowserClient({
|
host.client = new GatewayBrowserClient({
|
||||||
url: host.settings.gatewayUrl,
|
url: host.settings.gatewayUrl,
|
||||||
token: host.settings.token.trim() ? host.settings.token : undefined,
|
token: host.settings.token.trim() ? host.settings.token : undefined,
|
||||||
password: host.password.trim() ? host.password : undefined,
|
password: host.password.trim() ? host.password : undefined,
|
||||||
clientName,
|
clientName,
|
||||||
mode: "webchat",
|
mode: "webchat",
|
||||||
|
role,
|
||||||
|
// Leave scopes undefined for operator (gateway will default). For webchat we want no scopes.
|
||||||
|
scopes: role === "webchat" ? [] : undefined,
|
||||||
onHello: (hello) => {
|
onHello: (hello) => {
|
||||||
host.connected = true;
|
host.connected = true;
|
||||||
host.lastError = null;
|
host.lastError = null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user