From 62354dff9ca4be5c3f8336d253b3550e5e65a8c9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 01:49:13 +0000 Subject: [PATCH] refactor: share allowlist match metadata Co-authored-by: thewilloftheshadow --- .../matrix/src/matrix/monitor/allowlist.ts | 10 +++--- extensions/matrix/src/matrix/monitor/index.ts | 13 +++---- .../src/monitor-handler/message-handler.ts | 31 ++++++++-------- extensions/msteams/src/policy.ts | 35 ++++++++++++++----- src/channels/allowlist-match.ts | 21 +++++++++++ src/channels/plugins/allowlist-match.ts | 2 ++ src/channels/plugins/index.ts | 5 +++ src/discord/monitor/allow-list.ts | 7 ++-- src/slack/monitor/allow-list.ts | 17 +++------ src/telegram/bot-access.ts | 8 ++--- 10 files changed, 94 insertions(+), 55 deletions(-) create mode 100644 src/channels/allowlist-match.ts create mode 100644 src/channels/plugins/allowlist-match.ts diff --git a/extensions/matrix/src/matrix/monitor/allowlist.ts b/extensions/matrix/src/matrix/monitor/allowlist.ts index 031e05d11..4b4138d4c 100644 --- a/extensions/matrix/src/matrix/monitor/allowlist.ts +++ b/extensions/matrix/src/matrix/monitor/allowlist.ts @@ -1,3 +1,5 @@ +import type { AllowlistMatch } from "../../../../../src/channels/plugins/allowlist-match.js"; + function normalizeAllowList(list?: Array) { return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean); } @@ -10,11 +12,9 @@ function normalizeMatrixUser(raw?: string | null): string { return (raw ?? "").trim().toLowerCase(); } -export type MatrixAllowListMatch = { - allowed: boolean; - matchKey?: string; - matchSource?: "wildcard" | "id" | "prefixed-id" | "prefixed-user" | "name" | "localpart"; -}; +export type MatrixAllowListMatch = AllowlistMatch< + "wildcard" | "id" | "prefixed-id" | "prefixed-user" | "name" | "localpart" +>; export function resolveMatrixAllowListMatch(params: { allowList: string[]; diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index d4781ecfe..60880d64a 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -16,6 +16,7 @@ import { import { createReplyDispatcherWithTyping } from "../../../../../src/auto-reply/reply/reply-dispatcher.js"; import type { ReplyPayload } from "../../../../../src/auto-reply/types.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../../../../src/channels/command-gating.js"; +import { formatAllowlistMatchMeta } from "../../../../../src/channels/plugins/allowlist-match.js"; import { loadConfig } from "../../../../../src/config/config.js"; import { resolveStorePath, updateLastRoute } from "../../../../../src/config/sessions.js"; import { danger, logVerbose, shouldLogVerbose } from "../../../../../src/globals.js"; @@ -326,9 +327,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi userId: senderId, userName: senderName, }); - const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${ - allowMatch.matchSource ?? "none" - }`; + const allowMatchMeta = formatAllowlistMatchMeta(allowMatch); if (!allowMatch.allowed) { if (dmPolicy === "pairing") { const { code, created } = await upsertChannelPairingRequest({ @@ -369,14 +368,16 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi } if (isRoom && roomConfigInfo.config?.users?.length) { - const userAllowed = resolveMatrixAllowListMatches({ + const userMatch = resolveMatrixAllowListMatch({ allowList: normalizeAllowListLower(roomConfigInfo.config.users), userId: senderId, userName: senderName, }); - if (!userAllowed) { + if (!userMatch.allowed) { logVerbose( - `matrix: blocked sender ${senderId} (room users allowlist, ${roomMatchMeta})`, + `matrix: blocked sender ${senderId} (room users allowlist, ${roomMatchMeta}, ${formatAllowlistMatchMeta( + userMatch, + )})`, ); return; } diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index a5e5415d2..0e5b6dc13 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -15,6 +15,7 @@ import { } from "../../../../src/auto-reply/reply/history.js"; import { resolveMentionGating } from "../../../../src/channels/mention-gating.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../../../src/channels/command-gating.js"; +import { formatAllowlistMatchMeta } from "../../../../src/channels/plugins/allowlist-match.js"; import { danger, logVerbose, shouldLogVerbose } from "../../../../src/globals.js"; import { enqueueSystemEvent } from "../../../../src/infra/system-events.js"; import { @@ -41,6 +42,7 @@ import { import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js"; import { isMSTeamsGroupAllowed, + resolveMSTeamsAllowlistMatch, resolveMSTeamsReplyPolicy, resolveMSTeamsRouteConfig, } from "../policy.js"; @@ -141,19 +143,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { } if (dmPolicy !== "open") { - const effectiveAllowFrom = [ - ...allowFrom.map((v) => String(v).toLowerCase()), - ...storedAllowFrom, - ]; + const effectiveAllowFrom = [...allowFrom.map((v) => String(v)), ...storedAllowFrom]; + const allowMatch = resolveMSTeamsAllowlistMatch({ + allowFrom: effectiveAllowFrom, + senderId, + senderName, + }); - const senderLower = senderId.toLowerCase(); - const senderNameLower = senderName.toLowerCase(); - const allowed = - effectiveAllowFrom.includes("*") || - effectiveAllowFrom.includes(senderLower) || - effectiveAllowFrom.includes(senderNameLower); - - if (!allowed) { + if (!allowMatch.allowed) { if (dmPolicy === "pairing") { const request = await upsertChannelPairingRequest({ channel: "msteams", @@ -170,6 +167,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { log.debug("dropping dm (not allowlisted)", { sender: senderId, label: senderName, + allowlistMatch: formatAllowlistMatchMeta(allowMatch), }); return; } @@ -213,6 +211,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { if (channelGate.allowlistConfigured && !channelGate.allowed) { log.debug("dropping group message (not in team/channel allowlist)", { conversationId, + teamKey: channelGate.teamKey ?? "none", + channelKey: channelGate.channelKey ?? "none", + channelMatchKey: channelGate.channelMatchKey ?? "none", + channelMatchSource: channelGate.channelMatchSource ?? "none", }); return; } @@ -223,16 +225,17 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { return; } if (effectiveGroupAllowFrom.length > 0) { - const allowed = isMSTeamsGroupAllowed({ + const allowMatch = resolveMSTeamsAllowlistMatch({ groupPolicy, allowFrom: effectiveGroupAllowFrom, senderId, senderName, }); - if (!allowed) { + if (!allowMatch.allowed) { log.debug("dropping group message (not in groupAllowFrom)", { sender: senderId, label: senderName, + allowlistMatch: formatAllowlistMatchMeta(allowMatch), }); return; } diff --git a/extensions/msteams/src/policy.ts b/extensions/msteams/src/policy.ts index 00fb08091..1762cb537 100644 --- a/extensions/msteams/src/policy.ts +++ b/extensions/msteams/src/policy.ts @@ -11,6 +11,7 @@ import { resolveChannelEntryMatchWithFallback, resolveNestedAllowlistDecision, } from "../../../src/channels/plugins/channel-config.js"; +import type { AllowlistMatch } from "../../../src/channels/plugins/allowlist-match.js"; export type MSTeamsResolvedRouteConfig = { teamConfig?: MSTeamsTeamConfig; @@ -90,6 +91,31 @@ export type MSTeamsReplyPolicy = { replyStyle: MSTeamsReplyStyle; }; +export type MSTeamsAllowlistMatch = AllowlistMatch<"wildcard" | "id" | "name">; + +export function resolveMSTeamsAllowlistMatch(params: { + allowFrom: Array; + senderId: string; + senderName?: string | null; +}): MSTeamsAllowlistMatch { + const allowFrom = params.allowFrom + .map((entry) => String(entry).trim().toLowerCase()) + .filter(Boolean); + if (allowFrom.length === 0) return { allowed: false }; + if (allowFrom.includes("*")) { + return { allowed: true, matchKey: "*", matchSource: "wildcard" }; + } + const senderId = params.senderId.toLowerCase(); + if (allowFrom.includes(senderId)) { + return { allowed: true, matchKey: senderId, matchSource: "id" }; + } + const senderName = params.senderName?.toLowerCase(); + if (senderName && allowFrom.includes(senderName)) { + return { allowed: true, matchKey: senderName, matchSource: "name" }; + } + return { allowed: false }; +} + export function resolveMSTeamsReplyPolicy(params: { isDirectMessage: boolean; globalConfig?: MSTeamsConfig; @@ -126,12 +152,5 @@ export function isMSTeamsGroupAllowed(params: { const { groupPolicy } = params; if (groupPolicy === "disabled") return false; if (groupPolicy === "open") return true; - const allowFrom = params.allowFrom - .map((entry) => String(entry).trim().toLowerCase()) - .filter(Boolean); - if (allowFrom.length === 0) return false; - if (allowFrom.includes("*")) return true; - const senderId = params.senderId.toLowerCase(); - const senderName = params.senderName?.toLowerCase(); - return allowFrom.includes(senderId) || (senderName ? allowFrom.includes(senderName) : false); + return resolveMSTeamsAllowlistMatch(params).allowed; } diff --git a/src/channels/allowlist-match.ts b/src/channels/allowlist-match.ts new file mode 100644 index 000000000..69e797ed9 --- /dev/null +++ b/src/channels/allowlist-match.ts @@ -0,0 +1,21 @@ +export type AllowlistMatchSource = + | "wildcard" + | "id" + | "name" + | "tag" + | "username" + | "prefixed-id" + | "prefixed-user" + | "prefixed-name" + | "slug" + | "localpart"; + +export type AllowlistMatch = { + allowed: boolean; + matchKey?: string; + matchSource?: TSource; +}; + +export function formatAllowlistMatchMeta(match?: AllowlistMatch | null): string { + return `matchKey=${match?.matchKey ?? "none"} matchSource=${match?.matchSource ?? "none"}`; +} diff --git a/src/channels/plugins/allowlist-match.ts b/src/channels/plugins/allowlist-match.ts new file mode 100644 index 000000000..fdf4d1d7a --- /dev/null +++ b/src/channels/plugins/allowlist-match.ts @@ -0,0 +1,2 @@ +export type { AllowlistMatch, AllowlistMatchSource } from "../allowlist-match.js"; +export { formatAllowlistMatchMeta } from "../allowlist-match.js"; diff --git a/src/channels/plugins/index.ts b/src/channels/plugins/index.ts index b75ad9c19..eb1c8eb0e 100644 --- a/src/channels/plugins/index.ts +++ b/src/channels/plugins/index.ts @@ -93,4 +93,9 @@ export { type ChannelEntryMatch, type ChannelMatchSource, } from "./channel-config.js"; +export { + formatAllowlistMatchMeta, + type AllowlistMatch, + type AllowlistMatchSource, +} from "./allowlist-match.js"; export type { ChannelId, ChannelPlugin } from "./types.js"; diff --git a/src/discord/monitor/allow-list.ts b/src/discord/monitor/allow-list.ts index 7e00a984e..0b4914e1c 100644 --- a/src/discord/monitor/allow-list.ts +++ b/src/discord/monitor/allow-list.ts @@ -4,6 +4,7 @@ import { buildChannelKeyCandidates, resolveChannelEntryMatchWithFallback, } from "../../channels/channel-config.js"; +import type { AllowlistMatch } from "../../channels/allowlist-match.js"; import { formatDiscordUserTag } from "./format.js"; export type DiscordAllowList = { @@ -12,11 +13,7 @@ export type DiscordAllowList = { names: Set; }; -export type DiscordAllowListMatch = { - allowed: boolean; - matchKey?: string; - matchSource?: "wildcard" | "id" | "name" | "tag"; -}; +export type DiscordAllowListMatch = AllowlistMatch<"wildcard" | "id" | "name" | "tag">; export type DiscordGuildEntryResolved = { id?: string; diff --git a/src/slack/monitor/allow-list.ts b/src/slack/monitor/allow-list.ts index 92b22d18c..42b31b31d 100644 --- a/src/slack/monitor/allow-list.ts +++ b/src/slack/monitor/allow-list.ts @@ -1,3 +1,5 @@ +import type { AllowlistMatch } from "../../channels/allowlist-match.js"; + export function normalizeSlackSlug(raw?: string) { const trimmed = raw?.trim().toLowerCase() ?? ""; if (!trimmed) return ""; @@ -14,18 +16,9 @@ export function normalizeAllowListLower(list?: Array) { return normalizeAllowList(list).map((entry) => entry.toLowerCase()); } -export type SlackAllowListMatch = { - allowed: boolean; - matchKey?: string; - matchSource?: - | "wildcard" - | "id" - | "prefixed-id" - | "prefixed-user" - | "name" - | "prefixed-name" - | "slug"; -}; +export type SlackAllowListMatch = AllowlistMatch< + "wildcard" | "id" | "prefixed-id" | "prefixed-user" | "name" | "prefixed-name" | "slug" +>; export function resolveSlackAllowListMatch(params: { allowList: string[]; diff --git a/src/telegram/bot-access.ts b/src/telegram/bot-access.ts index 00ac27347..d135a6479 100644 --- a/src/telegram/bot-access.ts +++ b/src/telegram/bot-access.ts @@ -1,3 +1,5 @@ +import type { AllowlistMatch } from "../channels/allowlist-match.js"; + export type NormalizedAllowFrom = { entries: string[]; entriesLower: string[]; @@ -5,11 +7,7 @@ export type NormalizedAllowFrom = { hasEntries: boolean; }; -export type AllowFromMatch = { - allowed: boolean; - matchKey?: string; - matchSource?: "wildcard" | "id" | "username"; -}; +export type AllowFromMatch = AllowlistMatch<"wildcard" | "id" | "username">; export const normalizeAllowFrom = (list?: Array): NormalizedAllowFrom => { const entries = (list ?? []).map((value) => String(value).trim()).filter(Boolean);