mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-16 19:17:55 +00:00
fix(gateway): allow node chat and session rpc methods
This commit is contained in:
79
src/gateway/server-methods.authorization.test.ts
Normal file
79
src/gateway/server-methods.authorization.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ErrorCodes, type ErrorShape } from "./protocol/index.js";
|
||||
import { handleGatewayRequest } from "./server-methods.js";
|
||||
|
||||
type AuthResult = {
|
||||
ok: boolean | null;
|
||||
error?: ErrorShape;
|
||||
called: boolean;
|
||||
};
|
||||
|
||||
async function runMethod(method: string, role: "node" | "operator", scopes: string[] = []) {
|
||||
let called = false;
|
||||
let ok: boolean | null = null;
|
||||
let error: ErrorShape | undefined;
|
||||
|
||||
await handleGatewayRequest({
|
||||
req: {
|
||||
type: "req",
|
||||
id: "test-1",
|
||||
method,
|
||||
params: {},
|
||||
} as never,
|
||||
client: {
|
||||
connect: {
|
||||
role,
|
||||
scopes,
|
||||
},
|
||||
} as never,
|
||||
isWebchatConnect: () => false,
|
||||
respond: (nextOK, _payload, nextError) => {
|
||||
ok = nextOK;
|
||||
error = nextError;
|
||||
},
|
||||
context: {} as never,
|
||||
extraHandlers: {
|
||||
[method]: async () => {
|
||||
called = true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ok,
|
||||
error,
|
||||
called,
|
||||
} satisfies AuthResult;
|
||||
}
|
||||
|
||||
describe("gateway method authorization", () => {
|
||||
it("allows node role to use chat/session methods needed by mobile chat UI", async () => {
|
||||
const chatHistory = await runMethod("chat.history", "node");
|
||||
expect(chatHistory.called).toBe(true);
|
||||
expect(chatHistory.ok).toBe(null);
|
||||
|
||||
const chatSend = await runMethod("chat.send", "node");
|
||||
expect(chatSend.called).toBe(true);
|
||||
expect(chatSend.ok).toBe(null);
|
||||
|
||||
const sessionsList = await runMethod("sessions.list", "node");
|
||||
expect(sessionsList.called).toBe(true);
|
||||
expect(sessionsList.ok).toBe(null);
|
||||
});
|
||||
|
||||
it("still blocks non-allowed methods for node role", async () => {
|
||||
const result = await runMethod("status", "node");
|
||||
expect(result.called).toBe(false);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error?.code).toBe(ErrorCodes.INVALID_REQUEST);
|
||||
expect(result.error?.message).toContain("unauthorized role: node");
|
||||
});
|
||||
|
||||
it("keeps node-only methods restricted from operator role", async () => {
|
||||
const result = await runMethod("node.event", "operator", ["operator.admin"]);
|
||||
expect(result.called).toBe(false);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error?.code).toBe(ErrorCodes.INVALID_REQUEST);
|
||||
expect(result.error?.message).toContain("unauthorized role: operator");
|
||||
});
|
||||
});
|
||||
@@ -33,7 +33,16 @@ const APPROVALS_SCOPE = "operator.approvals";
|
||||
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"]);
|
||||
const NODE_ONLY_METHODS = new Set(["node.invoke.result", "node.event", "skills.bins"]);
|
||||
// Node clients (iOS/Android) embed chat UI and need these RPCs.
|
||||
const NODE_ALLOWED_METHODS = new Set([
|
||||
...NODE_ONLY_METHODS,
|
||||
"health",
|
||||
"sessions.list",
|
||||
"chat.history",
|
||||
"chat.send",
|
||||
"chat.abort",
|
||||
]);
|
||||
const PAIRING_METHODS = new Set([
|
||||
"node.pair.request",
|
||||
"node.pair.list",
|
||||
@@ -96,13 +105,16 @@ function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["c
|
||||
}
|
||||
const role = client.connect.role ?? "operator";
|
||||
const scopes = client.connect.scopes ?? [];
|
||||
if (NODE_ROLE_METHODS.has(method)) {
|
||||
if (NODE_ONLY_METHODS.has(method)) {
|
||||
if (role === "node") {
|
||||
return null;
|
||||
}
|
||||
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
|
||||
}
|
||||
if (role === "node") {
|
||||
if (NODE_ALLOWED_METHODS.has(method)) {
|
||||
return null;
|
||||
}
|
||||
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
|
||||
}
|
||||
if (role !== "operator") {
|
||||
|
||||
@@ -148,6 +148,23 @@ describe("gateway role enforcement", () => {
|
||||
expect(binsRes.ok).toBe(true);
|
||||
expect(Array.isArray(binsRes.payload?.bins)).toBe(true);
|
||||
|
||||
const healthRes = await rpcReq<{ ok?: boolean }>(nodeWs, "health", {});
|
||||
expect(healthRes.ok).toBe(true);
|
||||
expect(healthRes.payload?.ok).toBe(true);
|
||||
|
||||
const historyRes = await rpcReq<{ messages?: unknown[] }>(nodeWs, "chat.history", {
|
||||
sessionKey: "main",
|
||||
});
|
||||
expect(historyRes.ok).toBe(true);
|
||||
expect(Array.isArray(historyRes.payload?.messages)).toBe(true);
|
||||
|
||||
const sessionsRes = await rpcReq<{ sessions?: unknown[] }>(nodeWs, "sessions.list", {
|
||||
includeGlobal: true,
|
||||
includeUnknown: false,
|
||||
});
|
||||
expect(sessionsRes.ok).toBe(true);
|
||||
expect(Array.isArray(sessionsRes.payload?.sessions)).toBe(true);
|
||||
|
||||
const statusRes = await rpcReq(nodeWs, "status", {});
|
||||
expect(statusRes.ok).toBe(false);
|
||||
expect(statusRes.error?.message ?? "").toContain("unauthorized role");
|
||||
|
||||
Reference in New Issue
Block a user