From a5aa48beea237e30b005a435dd62642db45e2c00 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 00:14:41 +0000 Subject: [PATCH] feat: add dm allowlist match metadata logs Co-authored-by: thewilloftheshadow --- .../matrix/src/matrix/monitor/allowlist.ts | 54 ++++++++++++++----- extensions/matrix/src/matrix/monitor/index.ts | 34 ++++++++---- src/discord/monitor/allow-list.ts | 28 ++++++++++ .../monitor/message-handler.preflight.ts | 26 ++++++--- src/slack/monitor/allow-list.ts | 50 ++++++++++++----- src/slack/monitor/message-handler/prepare.ts | 19 ++++--- src/telegram/bot-access.ts | 30 +++++++++++ src/telegram/bot-message-context.ts | 29 ++++++---- 8 files changed, 211 insertions(+), 59 deletions(-) diff --git a/extensions/matrix/src/matrix/monitor/allowlist.ts b/extensions/matrix/src/matrix/monitor/allowlist.ts index a423575e1..031e05d11 100644 --- a/extensions/matrix/src/matrix/monitor/allowlist.ts +++ b/extensions/matrix/src/matrix/monitor/allowlist.ts @@ -10,23 +10,49 @@ 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 function resolveMatrixAllowListMatch(params: { + allowList: string[]; + userId?: string; + userName?: string; +}): MatrixAllowListMatch { + const allowList = params.allowList; + if (allowList.length === 0) return { allowed: false }; + if (allowList.includes("*")) { + return { allowed: true, matchKey: "*", matchSource: "wildcard" }; + } + const userId = normalizeMatrixUser(params.userId); + const userName = normalizeMatrixUser(params.userName); + const localPart = userId.startsWith("@") ? (userId.slice(1).split(":")[0] ?? "") : ""; + const candidates: Array<{ value?: string; source: MatrixAllowListMatch["matchSource"] }> = [ + { value: userId, source: "id" }, + { value: userId ? `matrix:${userId}` : "", source: "prefixed-id" }, + { value: userId ? `user:${userId}` : "", source: "prefixed-user" }, + { value: userName, source: "name" }, + { value: localPart, source: "localpart" }, + ]; + for (const candidate of candidates) { + if (!candidate.value) continue; + if (allowList.includes(candidate.value)) { + return { + allowed: true, + matchKey: candidate.value, + matchSource: candidate.source, + }; + } + } + return { allowed: false }; +} + export function resolveMatrixAllowListMatches(params: { allowList: string[]; userId?: string; userName?: string; }) { - const allowList = params.allowList; - if (allowList.length === 0) return false; - if (allowList.includes("*")) return true; - const userId = normalizeMatrixUser(params.userId); - const userName = normalizeMatrixUser(params.userName); - const localPart = userId.startsWith("@") ? (userId.slice(1).split(":")[0] ?? "") : ""; - const candidates = [ - userId, - userId ? `matrix:${userId}` : "", - userId ? `user:${userId}` : "", - userName, - localPart, - ].filter(Boolean); - return candidates.some((value) => allowList.includes(value)); + return resolveMatrixAllowListMatch(params).allowed; } diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index 0da860e98..3dc7f13a3 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -41,7 +41,11 @@ import { parsePollStartContent, } from "../poll-types.js"; import { reactMatrixMessage, sendMessageMatrix, sendTypingMatrix } from "../send.js"; -import { resolveMatrixAllowListMatches, normalizeAllowListLower } from "./allowlist.js"; +import { + resolveMatrixAllowListMatch, + resolveMatrixAllowListMatches, + normalizeAllowListLower, +} from "./allowlist.js"; import { registerMatrixAutoJoin } from "./auto-join.js"; import { createDirectRoomTracker } from "./direct.js"; import { downloadMatrixMedia } from "./media.js"; @@ -210,14 +214,15 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi if (isDirectMessage) { if (!dmEnabled || dmPolicy === "disabled") return; if (dmPolicy !== "open") { - const permitted = - effectiveAllowFrom.length > 0 && - resolveMatrixAllowListMatches({ - allowList: effectiveAllowFrom, - userId: senderId, - userName: senderName, - }); - if (!permitted) { + const allowMatch = resolveMatrixAllowListMatch({ + allowList: effectiveAllowFrom, + userId: senderId, + userName: senderName, + }); + const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${ + allowMatch.matchSource ?? "none" + }`; + if (!allowMatch.allowed) { if (dmPolicy === "pairing") { const { code, created } = await upsertChannelPairingRequest({ channel: "matrix", @@ -225,6 +230,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi meta: { name: senderName }, }); if (created) { + logVerbose( + `matrix pairing request sender=${senderId} name=${senderName ?? "unknown"} (${allowMatchMeta})`, + ); try { await sendMessageMatrix( `room:${roomId}`, @@ -243,6 +251,11 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi } } } + if (dmPolicy !== "pairing") { + logVerbose( + `matrix: blocked dm sender ${senderId} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`, + ); + } return; } } @@ -261,6 +274,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi return; } } + if (isRoom) { + logVerbose(`matrix: allow room ${roomId} (${roomMatchMeta})`); + } const rawBody = content.body.trim(); let media: { diff --git a/src/discord/monitor/allow-list.ts b/src/discord/monitor/allow-list.ts index 72ecc6757..9fa9a765b 100644 --- a/src/discord/monitor/allow-list.ts +++ b/src/discord/monitor/allow-list.ts @@ -9,6 +9,12 @@ export type DiscordAllowList = { names: Set; }; +export type DiscordAllowListMatch = { + allowed: boolean; + matchKey?: string; + matchSource?: "wildcard" | "id" | "name" | "tag"; +}; + export type DiscordGuildEntryResolved = { id?: string; slug?: string; @@ -92,6 +98,28 @@ export function allowListMatches( return false; } +export function resolveDiscordAllowListMatch(params: { + allowList: DiscordAllowList; + candidate: { id?: string; name?: string; tag?: string }; +}): DiscordAllowListMatch { + const { allowList, candidate } = params; + if (allowList.allowAll) { + return { allowed: true, matchKey: "*", matchSource: "wildcard" }; + } + if (candidate.id && allowList.ids.has(candidate.id)) { + return { allowed: true, matchKey: candidate.id, matchSource: "id" }; + } + const nameSlug = candidate.name ? normalizeDiscordSlug(candidate.name) : ""; + if (nameSlug && allowList.names.has(nameSlug)) { + return { allowed: true, matchKey: nameSlug, matchSource: "name" }; + } + const tagSlug = candidate.tag ? normalizeDiscordSlug(candidate.tag) : ""; + if (tagSlug && allowList.names.has(tagSlug)) { + return { allowed: true, matchKey: tagSlug, matchSource: "tag" }; + } + return { allowed: false }; +} + export function resolveDiscordUserAllowed(params: { allowList?: Array; userId: string; diff --git a/src/discord/monitor/message-handler.preflight.ts b/src/discord/monitor/message-handler.preflight.ts index c4587878c..43fd86afb 100644 --- a/src/discord/monitor/message-handler.preflight.ts +++ b/src/discord/monitor/message-handler.preflight.ts @@ -22,6 +22,7 @@ import { isDiscordGroupAllowedByPolicy, normalizeDiscordAllowList, normalizeDiscordSlug, + resolveDiscordAllowListMatch, resolveDiscordChannelConfigWithFallback, resolveDiscordGuildEntry, resolveDiscordShouldRequireMention, @@ -89,13 +90,20 @@ export async function preflightDiscordMessage( const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []); const effectiveAllowFrom = [...(params.allowFrom ?? []), ...storeAllowFrom]; const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:"]); - const permitted = allowList - ? allowListMatches(allowList, { - id: author.id, - name: author.username, - tag: formatDiscordUserTag(author), + const allowMatch = allowList + ? resolveDiscordAllowListMatch({ + allowList, + candidate: { + id: author.id, + name: author.username, + tag: formatDiscordUserTag(author), + }, }) - : false; + : { allowed: false }; + const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${ + allowMatch.matchSource ?? "none" + }`; + const permitted = allowMatch.allowed; if (!permitted) { commandAuthorized = false; if (dmPolicy === "pairing") { @@ -109,7 +117,7 @@ export async function preflightDiscordMessage( }); if (created) { logVerbose( - `discord pairing request sender=${author.id} tag=${formatDiscordUserTag(author)}`, + `discord pairing request sender=${author.id} tag=${formatDiscordUserTag(author)} (${allowMatchMeta})`, ); try { await sendMessageDiscord( @@ -130,7 +138,9 @@ export async function preflightDiscordMessage( } } } else { - logVerbose(`Blocked unauthorized discord sender ${author.id} (dmPolicy=${dmPolicy})`); + logVerbose( + `Blocked unauthorized discord sender ${author.id} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`, + ); } return null; } diff --git a/src/slack/monitor/allow-list.ts b/src/slack/monitor/allow-list.ts index 0c6394cd9..89d6164d1 100644 --- a/src/slack/monitor/allow-list.ts +++ b/src/slack/monitor/allow-list.ts @@ -14,22 +14,48 @@ export function normalizeAllowListLower(list?: Array) { return normalizeAllowList(list).map((entry) => entry.toLowerCase()); } -export function allowListMatches(params: { allowList: string[]; id?: string; name?: string }) { +export type SlackAllowListMatch = { + allowed: boolean; + matchKey?: string; + matchSource?: "wildcard" | "id" | "prefixed-id" | "prefixed-user" | "name" | "prefixed-name" | "slug"; +}; + +export function resolveSlackAllowListMatch(params: { + allowList: string[]; + id?: string; + name?: string; +}): SlackAllowListMatch { const allowList = params.allowList; - if (allowList.length === 0) return false; - if (allowList.includes("*")) return true; + if (allowList.length === 0) return { allowed: false }; + if (allowList.includes("*")) { + return { allowed: true, matchKey: "*", matchSource: "wildcard" }; + } const id = params.id?.toLowerCase(); const name = params.name?.toLowerCase(); const slug = normalizeSlackSlug(name); - const candidates = [ - id, - id ? `slack:${id}` : undefined, - id ? `user:${id}` : undefined, - name, - name ? `slack:${name}` : undefined, - slug, - ].filter(Boolean) as string[]; - return candidates.some((value) => allowList.includes(value)); + const candidates: Array<{ value?: string; source: SlackAllowListMatch["matchSource"] }> = [ + { value: id, source: "id" }, + { value: id ? `slack:${id}` : undefined, source: "prefixed-id" }, + { value: id ? `user:${id}` : undefined, source: "prefixed-user" }, + { value: name, source: "name" }, + { value: name ? `slack:${name}` : undefined, source: "prefixed-name" }, + { value: slug, source: "slug" }, + ]; + for (const candidate of candidates) { + if (!candidate.value) continue; + if (allowList.includes(candidate.value)) { + return { + allowed: true, + matchKey: candidate.value, + matchSource: candidate.source, + }; + } + } + return { allowed: false }; +} + +export function allowListMatches(params: { allowList: string[]; id?: string; name?: string }) { + return resolveSlackAllowListMatch(params).allowed; } export function resolveSlackUserAllowed(params: { diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index a8d8aa1e8..1b8b8615c 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -27,7 +27,7 @@ import { reactSlackMessage } from "../../actions.js"; import { sendMessageSlack } from "../../send.js"; import type { SlackMessageEvent } from "../../types.js"; -import { allowListMatches, resolveSlackUserAllowed } from "../allow-list.js"; +import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-list.js"; import { resolveSlackEffectiveAllowFrom } from "../auth.js"; import { resolveSlackChannelConfig } from "../channel-config.js"; import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js"; @@ -121,11 +121,14 @@ export async function prepareSlackMessage(params: { return null; } if (ctx.dmPolicy !== "open") { - const permitted = allowListMatches({ + const allowMatch = resolveSlackAllowListMatch({ allowList: allowFromLower, id: directUserId, }); - if (!permitted) { + const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${ + allowMatch.matchSource ?? "none" + }`; + if (!allowMatch.allowed) { if (ctx.dmPolicy === "pairing") { const sender = await ctx.resolveUserName(directUserId); const senderName = sender?.name ?? undefined; @@ -136,7 +139,9 @@ export async function prepareSlackMessage(params: { }); if (created) { logVerbose( - `slack pairing request sender=${directUserId} name=${senderName ?? "unknown"}`, + `slack pairing request sender=${directUserId} name=${ + senderName ?? "unknown" + } (${allowMatchMeta})`, ); try { await sendMessageSlack( @@ -158,7 +163,7 @@ export async function prepareSlackMessage(params: { } } else { logVerbose( - `Blocked unauthorized slack sender ${message.user} (dmPolicy=${ctx.dmPolicy})`, + `Blocked unauthorized slack sender ${message.user} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`, ); } return null; @@ -225,11 +230,11 @@ export async function prepareSlackMessage(params: { surface: "slack", }); - const ownerAuthorized = allowListMatches({ + const ownerAuthorized = resolveSlackAllowListMatch({ allowList: allowFromLower, id: senderId, name: senderName, - }); + }).allowed; const channelUsersAllowlistConfigured = isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0; const channelCommandAuthorized = diff --git a/src/telegram/bot-access.ts b/src/telegram/bot-access.ts index 3d5189714..00ac27347 100644 --- a/src/telegram/bot-access.ts +++ b/src/telegram/bot-access.ts @@ -5,6 +5,12 @@ export type NormalizedAllowFrom = { hasEntries: boolean; }; +export type AllowFromMatch = { + allowed: boolean; + matchKey?: string; + matchSource?: "wildcard" | "id" | "username"; +}; + export const normalizeAllowFrom = (list?: Array): NormalizedAllowFrom => { const entries = (list ?? []).map((value) => String(value).trim()).filter(Boolean); const hasWildcard = entries.includes("*"); @@ -40,3 +46,27 @@ export const isSenderAllowed = (params: { if (!username) return false; return allow.entriesLower.some((entry) => entry === username || entry === `@${username}`); }; + +export const resolveSenderAllowMatch = (params: { + allow: NormalizedAllowFrom; + senderId?: string; + senderUsername?: string; +}): AllowFromMatch => { + const { allow, senderId, senderUsername } = params; + if (allow.hasWildcard) { + return { allowed: true, matchKey: "*", matchSource: "wildcard" }; + } + if (!allow.hasEntries) return { allowed: false }; + if (senderId && allow.entries.includes(senderId)) { + return { allowed: true, matchKey: senderId, matchSource: "id" }; + } + const username = senderUsername?.toLowerCase(); + if (!username) return { allowed: false }; + const entry = allow.entriesLower.find( + (candidate) => candidate === username || candidate === `@${username}`, + ); + if (entry) { + return { allowed: true, matchKey: entry, matchSource: "username" }; + } + return { allowed: false }; +}; diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 69ac146bd..53a22702e 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -34,7 +34,12 @@ import { hasBotMention, resolveTelegramForumThreadId, } from "./bot/helpers.js"; -import { firstDefined, isSenderAllowed, normalizeAllowFrom } from "./bot-access.js"; +import { + firstDefined, + isSenderAllowed, + normalizeAllowFrom, + resolveSenderAllowMatch, +} from "./bot-access.js"; import { upsertTelegramPairingRequest } from "./pairing-store.js"; import type { TelegramContext } from "./bot/types.js"; @@ -174,14 +179,16 @@ export const buildTelegramMessageContext = async ({ if (dmPolicy !== "open") { const candidate = String(chatId); const senderUsername = msg.from?.username ?? ""; + const allowMatch = resolveSenderAllowMatch({ + allow: effectiveDmAllow, + senderId: candidate, + senderUsername, + }); + const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${ + allowMatch.matchSource ?? "none" + }`; const allowed = - effectiveDmAllow.hasWildcard || - (effectiveDmAllow.hasEntries && - isSenderAllowed({ - allow: effectiveDmAllow, - senderId: candidate, - senderUsername, - })); + effectiveDmAllow.hasWildcard || (effectiveDmAllow.hasEntries && allowMatch.allowed); if (!allowed) { if (dmPolicy === "pairing") { try { @@ -207,6 +214,8 @@ export const buildTelegramMessageContext = async ({ username: from?.username, firstName: from?.first_name, lastName: from?.last_name, + matchKey: allowMatch.matchKey ?? "none", + matchSource: allowMatch.matchSource ?? "none", }, "telegram pairing request", ); @@ -228,7 +237,9 @@ export const buildTelegramMessageContext = async ({ logVerbose(`telegram pairing reply failed for chat ${chatId}: ${String(err)}`); } } else { - logVerbose(`Blocked unauthorized telegram sender ${candidate} (dmPolicy=${dmPolicy})`); + logVerbose( + `Blocked unauthorized telegram sender ${candidate} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`, + ); } return null; }