diff --git a/extensions/slack/src/monitor/dm-auth.test.ts b/extensions/slack/src/monitor/dm-auth.test.ts new file mode 100644 index 00000000000..3987b974f04 --- /dev/null +++ b/extensions/slack/src/monitor/dm-auth.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, vi } from "vitest"; +import type { SlackMonitorContext } from "./context.js"; +import { authorizeSlackDirectMessage } from "./dm-auth.js"; + +function makeCtx(dmPolicy: SlackMonitorContext["dmPolicy"]): SlackMonitorContext { + return { + allowNameMatching: false, + dmEnabled: true, + dmPolicy, + } as SlackMonitorContext; +} + +function makeParams( + dmPolicy: SlackMonitorContext["dmPolicy"], +): Parameters[0] { + return { + ctx: makeCtx(dmPolicy), + accountId: "workspace", + senderId: "U123", + allowFromLower: [], + resolveSenderName: vi.fn(async () => ({ name: "Alice" })), + sendPairingReply: vi.fn(), + onDisabled: vi.fn(), + onUnauthorized: vi.fn(), + log: vi.fn(), + }; +} + +describe("authorizeSlackDirectMessage", () => { + it("allows open DM policy when effective allowFrom includes wildcard", async () => { + const params = makeParams("open"); + params.allowFromLower = ["*"]; + params.resolveSenderName = vi.fn(async () => { + throw new Error("users.info failed"); + }); + + await expect(authorizeSlackDirectMessage(params)).resolves.toBe(true); + + expect(params.onUnauthorized).not.toHaveBeenCalled(); + expect(params.resolveSenderName).not.toHaveBeenCalled(); + }); + + it("rejects open DM policy when effective allowFrom lacks wildcard", async () => { + const params = makeParams("open"); + + await expect(authorizeSlackDirectMessage(params)).resolves.toBe(false); + + expect(params.onUnauthorized).toHaveBeenCalledWith({ + allowMatchMeta: "matchKey=none matchSource=none", + senderName: "Alice", + }); + }); + + it("keeps allowlist DM policy gated by allowFrom", async () => { + const params = makeParams("allowlist"); + + await expect(authorizeSlackDirectMessage(params)).resolves.toBe(false); + + expect(params.onUnauthorized).toHaveBeenCalledWith({ + allowMatchMeta: "matchKey=none matchSource=none", + senderName: "Alice", + }); + }); +}); diff --git a/extensions/slack/src/monitor/dm-auth.ts b/extensions/slack/src/monitor/dm-auth.ts index 7b16f9098ec..2665b149cb1 100644 --- a/extensions/slack/src/monitor/dm-auth.ts +++ b/extensions/slack/src/monitor/dm-auth.ts @@ -21,6 +21,10 @@ export async function authorizeSlackDirectMessage(params: { return false; } + if (params.ctx.dmPolicy === "open" && params.allowFromLower.includes("*")) { + return true; + } + const sender = await params.resolveSenderName(params.senderId); const senderName = sender?.name ?? undefined; const allowMatch = resolveSlackAllowListMatch({ diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index 683e2d6f07c..b0600a5ece2 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -167,6 +167,42 @@ describe("slack prepareSlackMessage inbound contract", () => { }); }); + it("prepares wildcard open-policy account DMs", async () => { + const ctx = createInboundSlackCtx({ + cfg: { + channels: { + slack: { + enabled: true, + accounts: { + soltea: { + dmPolicy: "open", + dm: { enabled: true, policy: "open" }, + }, + }, + }, + }, + } as OpenClawConfig, + }); + ctx.accountId = "soltea"; + ctx.allowFrom = ["*"]; + ctx.dmPolicy = "open"; + ctx.resolveUserName = async () => ({ name: "External User" }) as any; + + const prepared = await prepareSlackMessage({ + ctx, + account: createSlackAccount({ + dmPolicy: "open", + dm: { enabled: true, policy: "open" }, + }), + message: createSlackMessage({ channel: "D999", user: "U123", text: "hello" }), + opts: { source: "message" }, + }); + + assertPrepared(prepared, "open-policy Slack DM"); + expect(prepared.ctxPayload.RawBody).toContain("hello"); + expect(prepared.ctxPayload.From).toBe("slack:U123"); + }); + it("keeps Slack assistant DM threads in a thread-scoped session with assistant context", async () => { const ctx = createDefaultSlackCtx(); ctx.saveSlackAssistantThreadContext({