diff --git a/extensions/googlechat/src/monitor-access.test.ts b/extensions/googlechat/src/monitor-access.test.ts new file mode 100644 index 00000000000..04db80809ac --- /dev/null +++ b/extensions/googlechat/src/monitor-access.test.ts @@ -0,0 +1,233 @@ +import { describe, expect, it, vi } from "vitest"; + +const createChannelPairingController = vi.hoisted(() => vi.fn()); +const evaluateGroupRouteAccessForPolicy = vi.hoisted(() => vi.fn()); +const isDangerousNameMatchingEnabled = vi.hoisted(() => vi.fn()); +const resolveAllowlistProviderRuntimeGroupPolicy = vi.hoisted(() => vi.fn()); +const resolveDefaultGroupPolicy = vi.hoisted(() => vi.fn()); +const resolveDmGroupAccessWithLists = vi.hoisted(() => vi.fn()); +const resolveMentionGatingWithBypass = vi.hoisted(() => vi.fn()); +const resolveSenderScopedGroupPolicy = vi.hoisted(() => vi.fn()); +const warnMissingProviderGroupPolicyFallbackOnce = vi.hoisted(() => vi.fn()); +const sendGoogleChatMessage = vi.hoisted(() => vi.fn()); + +vi.mock("../runtime-api.js", () => ({ + GROUP_POLICY_BLOCKED_LABEL: { space: "space" }, + createChannelPairingController, + evaluateGroupRouteAccessForPolicy, + isDangerousNameMatchingEnabled, + resolveAllowlistProviderRuntimeGroupPolicy, + resolveDefaultGroupPolicy, + resolveDmGroupAccessWithLists, + resolveMentionGatingWithBypass, + resolveSenderScopedGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +})); + +vi.mock("./api.js", () => ({ + sendGoogleChatMessage, +})); + +function createCore() { + return { + channel: { + commands: { + shouldComputeCommandAuthorized: vi.fn(() => false), + resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false), + shouldHandleTextCommands: vi.fn(() => false), + isControlCommandMessage: vi.fn(() => false), + }, + text: { + hasControlCommand: vi.fn(() => false), + }, + }, + }; +} + +function primeCommonDefaults() { + isDangerousNameMatchingEnabled.mockReturnValue(false); + resolveDefaultGroupPolicy.mockReturnValue("allowlist"); + resolveAllowlistProviderRuntimeGroupPolicy.mockReturnValue({ + groupPolicy: "allowlist", + providerMissingFallbackApplied: false, + }); + resolveSenderScopedGroupPolicy.mockImplementation(({ groupPolicy }) => groupPolicy); + evaluateGroupRouteAccessForPolicy.mockReturnValue({ + allowed: true, + }); + warnMissingProviderGroupPolicyFallbackOnce.mockReturnValue(undefined); +} + +describe("googlechat inbound access policy", () => { + it("issues a pairing challenge for unauthorized DMs in pairing mode", async () => { + primeCommonDefaults(); + const issueChallenge = vi.fn(async ({ onCreated, sendPairingReply }) => { + onCreated?.(); + await sendPairingReply("pairing text"); + }); + createChannelPairingController.mockReturnValue({ + readAllowFromStore: vi.fn(async () => []), + issueChallenge, + }); + resolveDmGroupAccessWithLists.mockReturnValue({ + decision: "pairing", + reason: "pairing_required", + effectiveAllowFrom: [], + effectiveGroupAllowFrom: [], + }); + sendGoogleChatMessage.mockResolvedValue({ ok: true }); + + const { applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js"); + const statusSink = vi.fn(); + const logVerbose = vi.fn(); + + await expect( + applyGoogleChatInboundAccessPolicy({ + account: { + accountId: "default", + config: { + dm: { policy: "pairing" }, + }, + } as never, + config: { + channels: { googlechat: {} }, + } as never, + core: createCore() as never, + space: { name: "spaces/AAA", displayName: "DM" } as never, + message: { annotations: [] } as never, + isGroup: false, + senderId: "users/abc", + senderName: "Alice", + senderEmail: "alice@example.com", + rawBody: "hello", + statusSink, + logVerbose, + }), + ).resolves.toEqual({ ok: false }); + + expect(issueChallenge).toHaveBeenCalledTimes(1); + expect(sendGoogleChatMessage).toHaveBeenCalledWith({ + account: expect.anything(), + space: "spaces/AAA", + text: "pairing text", + }); + expect(statusSink).toHaveBeenCalledWith( + expect.objectContaining({ + lastOutboundAt: expect.any(Number), + }), + ); + }); + + it("allows group traffic when sender and mention gates pass", async () => { + primeCommonDefaults(); + createChannelPairingController.mockReturnValue({ + readAllowFromStore: vi.fn(async () => []), + issueChallenge: vi.fn(), + }); + resolveDmGroupAccessWithLists.mockReturnValue({ + decision: "allow", + effectiveAllowFrom: [], + effectiveGroupAllowFrom: ["users/alice"], + }); + resolveMentionGatingWithBypass.mockReturnValue({ + shouldSkip: false, + effectiveWasMentioned: true, + }); + const core = createCore(); + core.channel.commands.shouldComputeCommandAuthorized.mockReturnValue(true); + core.channel.commands.resolveCommandAuthorizedFromAuthorizers.mockReturnValue(true); + + const { applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js"); + + await expect( + applyGoogleChatInboundAccessPolicy({ + account: { + accountId: "default", + config: { + botUser: "users/app-bot", + groups: { + "spaces/AAA": { + users: ["users/alice"], + requireMention: true, + systemPrompt: " group prompt ", + }, + }, + }, + } as never, + config: { + channels: { googlechat: {} }, + commands: { useAccessGroups: true }, + } as never, + core: core as never, + space: { name: "spaces/AAA", displayName: "Team Room" } as never, + message: { + annotations: [ + { + type: "USER_MENTION", + userMention: { user: { name: "users/app-bot" } }, + }, + ], + } as never, + isGroup: true, + senderId: "users/alice", + senderName: "Alice", + senderEmail: "alice@example.com", + rawBody: "hello team", + logVerbose: vi.fn(), + }), + ).resolves.toEqual({ + ok: true, + commandAuthorized: true, + effectiveWasMentioned: true, + groupSystemPrompt: "group prompt", + }); + }); + + it("drops unauthorized group control commands", async () => { + primeCommonDefaults(); + createChannelPairingController.mockReturnValue({ + readAllowFromStore: vi.fn(async () => []), + issueChallenge: vi.fn(), + }); + resolveDmGroupAccessWithLists.mockReturnValue({ + decision: "allow", + effectiveAllowFrom: [], + effectiveGroupAllowFrom: [], + }); + resolveMentionGatingWithBypass.mockReturnValue({ + shouldSkip: false, + effectiveWasMentioned: false, + }); + const core = createCore(); + core.channel.commands.shouldComputeCommandAuthorized.mockReturnValue(true); + core.channel.commands.resolveCommandAuthorizedFromAuthorizers.mockReturnValue(false); + core.channel.commands.isControlCommandMessage.mockReturnValue(true); + const logVerbose = vi.fn(); + + const { applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js"); + + await expect( + applyGoogleChatInboundAccessPolicy({ + account: { + accountId: "default", + config: {}, + } as never, + config: { + channels: { googlechat: {} }, + commands: { useAccessGroups: true }, + } as never, + core: core as never, + space: { name: "spaces/AAA", displayName: "Team Room" } as never, + message: { annotations: [] } as never, + isGroup: true, + senderId: "users/alice", + senderName: "Alice", + senderEmail: "alice@example.com", + rawBody: "/admin", + logVerbose, + }), + ).resolves.toEqual({ ok: false }); + + expect(logVerbose).toHaveBeenCalledWith("googlechat: drop control command from users/alice"); + }); +});