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 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([
|
||||
"node.pair.request",
|
||||
"node.pair.list",
|
||||
@ -98,9 +107,16 @@ function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["c
|
||||
if (role === "node") return null;
|
||||
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
|
||||
}
|
||||
|
||||
if (role === "node") {
|
||||
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") {
|
||||
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
onceMessage,
|
||||
startGatewayServer,
|
||||
startServerWithClient,
|
||||
rpcReq,
|
||||
testState,
|
||||
} from "./test-helpers.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", () => {
|
||||
let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
||||
let port: number;
|
||||
|
||||
@ -333,7 +333,10 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
}
|
||||
|
||||
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) {
|
||||
setHandshakeState("failed");
|
||||
setCloseCause("invalid-role", {
|
||||
|
||||
@ -120,12 +120,17 @@ export function connectGateway(host: GatewayHost) {
|
||||
const clientName =
|
||||
host.tab === "public-chat" ? GATEWAY_CLIENT_NAMES.WEBCHAT_UI : "moltbot-control-ui";
|
||||
|
||||
const role = host.tab === "public-chat" ? "webchat" : undefined;
|
||||
|
||||
host.client = new GatewayBrowserClient({
|
||||
url: host.settings.gatewayUrl,
|
||||
token: host.settings.token.trim() ? host.settings.token : undefined,
|
||||
password: host.password.trim() ? host.password : undefined,
|
||||
clientName,
|
||||
mode: "webchat",
|
||||
role,
|
||||
// Leave scopes undefined for operator (gateway will default). For webchat we want no scopes.
|
||||
scopes: role === "webchat" ? [] : undefined,
|
||||
onHello: (hello) => {
|
||||
host.connected = true;
|
||||
host.lastError = null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user