mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-14 11:30:41 +00:00
290 lines
8.4 KiB
TypeScript
290 lines
8.4 KiB
TypeScript
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { FeishuMessageEvent } from "./bot.js";
|
|
import { handleFeishuMessage } from "./bot.js";
|
|
import { setFeishuRuntime } from "./runtime.js";
|
|
|
|
const { mockCreateFeishuReplyDispatcher, mockSendMessageFeishu, mockGetMessageFeishu } = vi.hoisted(
|
|
() => ({
|
|
mockCreateFeishuReplyDispatcher: vi.fn(() => ({
|
|
dispatcher: vi.fn(),
|
|
replyOptions: {},
|
|
markDispatchIdle: vi.fn(),
|
|
})),
|
|
mockSendMessageFeishu: vi.fn().mockResolvedValue({ messageId: "pairing-msg", chatId: "oc-dm" }),
|
|
mockGetMessageFeishu: vi.fn().mockResolvedValue(null),
|
|
}),
|
|
);
|
|
|
|
vi.mock("./reply-dispatcher.js", () => ({
|
|
createFeishuReplyDispatcher: mockCreateFeishuReplyDispatcher,
|
|
}));
|
|
|
|
vi.mock("./send.js", () => ({
|
|
sendMessageFeishu: mockSendMessageFeishu,
|
|
getMessageFeishu: mockGetMessageFeishu,
|
|
}));
|
|
|
|
describe("handleFeishuMessage command authorization", () => {
|
|
const mockFinalizeInboundContext = vi.fn((ctx: unknown) => ctx);
|
|
const mockDispatchReplyFromConfig = vi
|
|
.fn()
|
|
.mockResolvedValue({ queuedFinal: false, counts: { final: 1 } });
|
|
const mockResolveCommandAuthorizedFromAuthorizers = vi.fn(() => false);
|
|
const mockShouldComputeCommandAuthorized = vi.fn(() => true);
|
|
const mockReadAllowFromStore = vi.fn().mockResolvedValue([]);
|
|
const mockUpsertPairingRequest = vi.fn().mockResolvedValue({ code: "ABCDEFGH", created: false });
|
|
const mockBuildPairingReply = vi.fn(() => "Pairing response");
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
setFeishuRuntime({
|
|
system: {
|
|
enqueueSystemEvent: vi.fn(),
|
|
},
|
|
channel: {
|
|
routing: {
|
|
resolveAgentRoute: vi.fn(() => ({
|
|
agentId: "main",
|
|
accountId: "default",
|
|
sessionKey: "agent:main:feishu:dm:ou-attacker",
|
|
matchedBy: "default",
|
|
})),
|
|
},
|
|
reply: {
|
|
resolveEnvelopeFormatOptions: vi.fn(() => ({ template: "channel+name+time" })),
|
|
formatAgentEnvelope: vi.fn((params: { body: string }) => params.body),
|
|
finalizeInboundContext: mockFinalizeInboundContext,
|
|
dispatchReplyFromConfig: mockDispatchReplyFromConfig,
|
|
},
|
|
commands: {
|
|
shouldComputeCommandAuthorized: mockShouldComputeCommandAuthorized,
|
|
resolveCommandAuthorizedFromAuthorizers: mockResolveCommandAuthorizedFromAuthorizers,
|
|
},
|
|
pairing: {
|
|
readAllowFromStore: mockReadAllowFromStore,
|
|
upsertPairingRequest: mockUpsertPairingRequest,
|
|
buildPairingReply: mockBuildPairingReply,
|
|
},
|
|
},
|
|
} as unknown as PluginRuntime);
|
|
});
|
|
|
|
it("uses authorizer resolution instead of hardcoded CommandAuthorized=true", async () => {
|
|
const cfg: ClawdbotConfig = {
|
|
commands: { useAccessGroups: true },
|
|
channels: {
|
|
feishu: {
|
|
dmPolicy: "open",
|
|
allowFrom: ["ou-admin"],
|
|
},
|
|
},
|
|
} as ClawdbotConfig;
|
|
|
|
const event: FeishuMessageEvent = {
|
|
sender: {
|
|
sender_id: {
|
|
open_id: "ou-attacker",
|
|
},
|
|
},
|
|
message: {
|
|
message_id: "msg-auth-bypass-regression",
|
|
chat_id: "oc-dm",
|
|
chat_type: "p2p",
|
|
message_type: "text",
|
|
content: JSON.stringify({ text: "/status" }),
|
|
},
|
|
};
|
|
|
|
await handleFeishuMessage({
|
|
cfg,
|
|
event,
|
|
runtime: {
|
|
log: vi.fn(),
|
|
error: vi.fn(),
|
|
exit: vi.fn((code: number): never => {
|
|
throw new Error(`exit ${code}`);
|
|
}),
|
|
} as RuntimeEnv,
|
|
});
|
|
|
|
expect(mockResolveCommandAuthorizedFromAuthorizers).toHaveBeenCalledWith({
|
|
useAccessGroups: true,
|
|
authorizers: [{ configured: true, allowed: false }],
|
|
});
|
|
expect(mockFinalizeInboundContext).toHaveBeenCalledTimes(1);
|
|
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
CommandAuthorized: false,
|
|
SenderId: "ou-attacker",
|
|
Surface: "feishu",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("reads pairing allow store for non-command DMs when dmPolicy is pairing", async () => {
|
|
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
mockReadAllowFromStore.mockResolvedValue(["ou-attacker"]);
|
|
|
|
const cfg: ClawdbotConfig = {
|
|
commands: { useAccessGroups: true },
|
|
channels: {
|
|
feishu: {
|
|
dmPolicy: "pairing",
|
|
allowFrom: [],
|
|
},
|
|
},
|
|
} as ClawdbotConfig;
|
|
|
|
const event: FeishuMessageEvent = {
|
|
sender: {
|
|
sender_id: {
|
|
open_id: "ou-attacker",
|
|
},
|
|
},
|
|
message: {
|
|
message_id: "msg-read-store-non-command",
|
|
chat_id: "oc-dm",
|
|
chat_type: "p2p",
|
|
message_type: "text",
|
|
content: JSON.stringify({ text: "hello there" }),
|
|
},
|
|
};
|
|
|
|
await handleFeishuMessage({
|
|
cfg,
|
|
event,
|
|
runtime: {
|
|
log: vi.fn(),
|
|
error: vi.fn(),
|
|
exit: vi.fn((code: number): never => {
|
|
throw new Error(`exit ${code}`);
|
|
}),
|
|
} as RuntimeEnv,
|
|
});
|
|
|
|
expect(mockReadAllowFromStore).toHaveBeenCalledWith("feishu");
|
|
expect(mockResolveCommandAuthorizedFromAuthorizers).not.toHaveBeenCalled();
|
|
expect(mockFinalizeInboundContext).toHaveBeenCalledTimes(1);
|
|
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("creates pairing request and drops unauthorized DMs in pairing mode", async () => {
|
|
mockShouldComputeCommandAuthorized.mockReturnValue(false);
|
|
mockReadAllowFromStore.mockResolvedValue([]);
|
|
mockUpsertPairingRequest.mockResolvedValue({ code: "ABCDEFGH", created: true });
|
|
|
|
const cfg: ClawdbotConfig = {
|
|
channels: {
|
|
feishu: {
|
|
dmPolicy: "pairing",
|
|
allowFrom: [],
|
|
},
|
|
},
|
|
} as ClawdbotConfig;
|
|
|
|
const event: FeishuMessageEvent = {
|
|
sender: {
|
|
sender_id: {
|
|
open_id: "ou-unapproved",
|
|
},
|
|
},
|
|
message: {
|
|
message_id: "msg-pairing-flow",
|
|
chat_id: "oc-dm",
|
|
chat_type: "p2p",
|
|
message_type: "text",
|
|
content: JSON.stringify({ text: "hello" }),
|
|
},
|
|
};
|
|
|
|
await handleFeishuMessage({
|
|
cfg,
|
|
event,
|
|
runtime: {
|
|
log: vi.fn(),
|
|
error: vi.fn(),
|
|
exit: vi.fn((code: number): never => {
|
|
throw new Error(`exit ${code}`);
|
|
}),
|
|
} as RuntimeEnv,
|
|
});
|
|
|
|
expect(mockUpsertPairingRequest).toHaveBeenCalledWith({
|
|
channel: "feishu",
|
|
id: "ou-unapproved",
|
|
meta: { name: undefined },
|
|
});
|
|
expect(mockBuildPairingReply).toHaveBeenCalledWith({
|
|
channel: "feishu",
|
|
idLine: "Your Feishu user id: ou-unapproved",
|
|
code: "ABCDEFGH",
|
|
});
|
|
expect(mockSendMessageFeishu).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
to: "user:ou-unapproved",
|
|
accountId: "default",
|
|
}),
|
|
);
|
|
expect(mockFinalizeInboundContext).not.toHaveBeenCalled();
|
|
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("computes group command authorization from group allowFrom", async () => {
|
|
mockShouldComputeCommandAuthorized.mockReturnValue(true);
|
|
mockResolveCommandAuthorizedFromAuthorizers.mockReturnValue(false);
|
|
|
|
const cfg: ClawdbotConfig = {
|
|
commands: { useAccessGroups: true },
|
|
channels: {
|
|
feishu: {
|
|
groups: {
|
|
"oc-group": {
|
|
requireMention: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as ClawdbotConfig;
|
|
|
|
const event: FeishuMessageEvent = {
|
|
sender: {
|
|
sender_id: {
|
|
open_id: "ou-attacker",
|
|
},
|
|
},
|
|
message: {
|
|
message_id: "msg-group-command-auth",
|
|
chat_id: "oc-group",
|
|
chat_type: "group",
|
|
message_type: "text",
|
|
content: JSON.stringify({ text: "/status" }),
|
|
},
|
|
};
|
|
|
|
await handleFeishuMessage({
|
|
cfg,
|
|
event,
|
|
runtime: {
|
|
log: vi.fn(),
|
|
error: vi.fn(),
|
|
exit: vi.fn((code: number): never => {
|
|
throw new Error(`exit ${code}`);
|
|
}),
|
|
} as RuntimeEnv,
|
|
});
|
|
|
|
expect(mockResolveCommandAuthorizedFromAuthorizers).toHaveBeenCalledWith({
|
|
useAccessGroups: true,
|
|
authorizers: [{ configured: false, allowed: false }],
|
|
});
|
|
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
ChatType: "group",
|
|
CommandAuthorized: false,
|
|
SenderId: "ou-attacker",
|
|
}),
|
|
);
|
|
});
|
|
});
|