























@@ -1,5 +1,6 @@
11import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
22import { beforeEach, describe, expect, it, vi } from "vitest";
3+import { createSdkAccessAdapter } from "../../bridge/sdk-adapter.js";
34import { registerPlatformAdapter, type PlatformAdapter } from "../adapter/index.js";
45import type { InteractionEvent } from "../types.js";
56import { createInteractionHandler } from "./interaction-handler.js";
@@ -17,13 +18,17 @@ vi.mock("../messaging/sender.js", () => ({
17181819const resolveApprovalMock = vi.fn(async () => true);
192020-const account: GatewayAccount = {
21-accountId: "default",
22-appId: "app",
23-clientSecret: "secret",
24-markdownSupport: false,
25-config: {},
26-};
21+function makeAccount(config: GatewayAccount["config"] = {}): GatewayAccount {
22+return {
23+accountId: "default",
24+appId: "app",
25+clientSecret: "secret",
26+markdownSupport: false,
27+ config,
28+};
29+}
30+31+const account = makeAccount();
27322833const runtime = {} as GatewayPluginRuntime;
2934@@ -42,12 +47,13 @@ function makeRestrictedCfg(approvers: string[]): OpenClawConfig {
4247} as OpenClawConfig;
4348}
444945-function makeUnrestrictedCfg(): OpenClawConfig {
50+function makeCommandAuthorizedFallbackCfg(): OpenClawConfig {
4651return {
4752channels: {
4853qqbot: {
4954appId: "app",
5055clientSecret: "secret",
56+allowFrom: ["ATTACKER_OPENID"],
5157},
5258},
5359} as OpenClawConfig;
@@ -172,9 +178,40 @@ describe("createInteractionHandler approval buttons", () => {
172178);
173179});
174180175-it("allows approval button clicks when exec approvals are not configured", async () => {
181+it("resolves fallback approval buttons from explicit command-authorized senders", async () => {
182+const handler = createInteractionHandler(account, runtime, undefined, {
183+getActiveCfg: () => makeCommandAuthorizedFallbackCfg(),
184+});
185+186+handler(makeApprovalEvent());
187+188+await vi.waitFor(() =>
189+expect(resolveApprovalMock).toHaveBeenCalledWith("exec:abc12345", "allow-once"),
190+);
191+});
192+193+it("delegates fallback approval button auth to the gateway command resolver", async () => {
194+const access = createSdkAccessAdapter();
176195const handler = createInteractionHandler(account, runtime, undefined, {
177-getActiveCfg: () => makeUnrestrictedCfg(),
196+getActiveCfg: () =>
197+({
198+accessGroups: {
199+operators: {
200+type: "message.senders",
201+members: {
202+qqbot: ["ATTACKER_OPENID"],
203+},
204+},
205+},
206+channels: {
207+qqbot: {
208+appId: "app",
209+clientSecret: "secret",
210+allowFrom: ["accessGroup:operators"],
211+},
212+},
213+}) as OpenClawConfig,
214+resolveCommandAuthorized: (params) => access.resolveSlashCommandAuthorization(params),
178215});
179216180217handler(makeApprovalEvent());
@@ -184,6 +221,121 @@ describe("createInteractionHandler approval buttons", () => {
184221);
185222});
186223224+it("uses merged account config for fallback button command auth", async () => {
225+const handler = createInteractionHandler(account, runtime, undefined, {
226+getActiveCfg: () =>
227+({
228+channels: {
229+qqbot: {
230+appId: "app",
231+clientSecret: "secret",
232+accounts: {
233+default: {
234+allowFrom: ["ATTACKER_OPENID"],
235+},
236+},
237+},
238+},
239+}) as OpenClawConfig,
240+});
241+242+handler(makeApprovalEvent());
243+244+await vi.waitFor(() =>
245+expect(resolveApprovalMock).toHaveBeenCalledWith("exec:abc12345", "allow-once"),
246+);
247+});
248+249+it("rejects fallback approval buttons from senders without explicit command auth", async () => {
250+const handler = createInteractionHandler(account, runtime, undefined, {
251+getActiveCfg: () =>
252+({
253+channels: {
254+qqbot: {
255+appId: "app",
256+clientSecret: "secret",
257+allowFrom: ["OWNER_OPENID"],
258+},
259+},
260+}) as OpenClawConfig,
261+});
262+263+handler(makeApprovalEvent());
264+265+await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
266+267+expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
268+{ appId: "app", clientSecret: "secret" },
269+"interaction-1",
270+0,
271+{ content: "You are not authorized to approve this request." },
272+);
273+expect(resolveApprovalMock).not.toHaveBeenCalled();
274+});
275+276+it.each([
277+[
278+"no allowlist",
279+{
280+channels: {
281+qqbot: {
282+appId: "app",
283+clientSecret: "secret",
284+},
285+},
286+},
287+],
288+[
289+"wildcard allowlist",
290+{
291+channels: {
292+qqbot: {
293+appId: "app",
294+clientSecret: "secret",
295+allowFrom: ["*"],
296+},
297+},
298+},
299+],
300+] satisfies Array<[string, OpenClawConfig]>)(
301+"rejects fallback approval buttons when %s does not grant command auth",
302+async (_name, cfg) => {
303+const handler = createInteractionHandler(account, runtime, undefined, {
304+getActiveCfg: () => cfg,
305+});
306+307+handler(makeApprovalEvent());
308+309+await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
310+311+expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
312+{ appId: "app", clientSecret: "secret" },
313+"interaction-1",
314+0,
315+{ content: "You are not authorized to approve this request." },
316+);
317+expect(resolveApprovalMock).not.toHaveBeenCalled();
318+},
319+);
320+321+it("rejects fallback approval buttons without a trusted actor id", async () => {
322+const handler = createInteractionHandler(account, runtime, undefined, {
323+getActiveCfg: () => makeCommandAuthorizedFallbackCfg(),
324+});
325+326+handler(makeApprovalEvent({ group_member_openid: undefined, user_openid: undefined }));
327+328+await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
329+330+expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
331+{ appId: "app", clientSecret: "secret" },
332+"interaction-1",
333+0,
334+{ content: "You are not authorized to approve this request." },
335+);
336+expect(resolveApprovalMock).not.toHaveBeenCalled();
337+});
338+187339it("rejects approval button clicks when active config cannot be loaded", async () => {
188340const handler = createInteractionHandler(account, runtime, undefined, {
189341getActiveCfg: () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。