feat: add dm allowlist match metadata logs

Co-authored-by: thewilloftheshadow <thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-18 00:14:41 +00:00
parent 1bf3861ca4
commit a5aa48beea
8 changed files with 211 additions and 59 deletions

View File

@@ -10,23 +10,49 @@ function normalizeMatrixUser(raw?: string | null): string {
return (raw ?? "").trim().toLowerCase(); 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: { export function resolveMatrixAllowListMatches(params: {
allowList: string[]; allowList: string[];
userId?: string; userId?: string;
userName?: string; userName?: string;
}) { }) {
const allowList = params.allowList; return resolveMatrixAllowListMatch(params).allowed;
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));
} }

View File

@@ -41,7 +41,11 @@ import {
parsePollStartContent, parsePollStartContent,
} from "../poll-types.js"; } from "../poll-types.js";
import { reactMatrixMessage, sendMessageMatrix, sendTypingMatrix } from "../send.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 { registerMatrixAutoJoin } from "./auto-join.js";
import { createDirectRoomTracker } from "./direct.js"; import { createDirectRoomTracker } from "./direct.js";
import { downloadMatrixMedia } from "./media.js"; import { downloadMatrixMedia } from "./media.js";
@@ -210,14 +214,15 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
if (isDirectMessage) { if (isDirectMessage) {
if (!dmEnabled || dmPolicy === "disabled") return; if (!dmEnabled || dmPolicy === "disabled") return;
if (dmPolicy !== "open") { if (dmPolicy !== "open") {
const permitted = const allowMatch = resolveMatrixAllowListMatch({
effectiveAllowFrom.length > 0 && allowList: effectiveAllowFrom,
resolveMatrixAllowListMatches({ userId: senderId,
allowList: effectiveAllowFrom, userName: senderName,
userId: senderId, });
userName: senderName, const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${
}); allowMatch.matchSource ?? "none"
if (!permitted) { }`;
if (!allowMatch.allowed) {
if (dmPolicy === "pairing") { if (dmPolicy === "pairing") {
const { code, created } = await upsertChannelPairingRequest({ const { code, created } = await upsertChannelPairingRequest({
channel: "matrix", channel: "matrix",
@@ -225,6 +230,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
meta: { name: senderName }, meta: { name: senderName },
}); });
if (created) { if (created) {
logVerbose(
`matrix pairing request sender=${senderId} name=${senderName ?? "unknown"} (${allowMatchMeta})`,
);
try { try {
await sendMessageMatrix( await sendMessageMatrix(
`room:${roomId}`, `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; return;
} }
} }
@@ -261,6 +274,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
return; return;
} }
} }
if (isRoom) {
logVerbose(`matrix: allow room ${roomId} (${roomMatchMeta})`);
}
const rawBody = content.body.trim(); const rawBody = content.body.trim();
let media: { let media: {

View File

@@ -9,6 +9,12 @@ export type DiscordAllowList = {
names: Set<string>; names: Set<string>;
}; };
export type DiscordAllowListMatch = {
allowed: boolean;
matchKey?: string;
matchSource?: "wildcard" | "id" | "name" | "tag";
};
export type DiscordGuildEntryResolved = { export type DiscordGuildEntryResolved = {
id?: string; id?: string;
slug?: string; slug?: string;
@@ -92,6 +98,28 @@ export function allowListMatches(
return false; 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: { export function resolveDiscordUserAllowed(params: {
allowList?: Array<string | number>; allowList?: Array<string | number>;
userId: string; userId: string;

View File

@@ -22,6 +22,7 @@ import {
isDiscordGroupAllowedByPolicy, isDiscordGroupAllowedByPolicy,
normalizeDiscordAllowList, normalizeDiscordAllowList,
normalizeDiscordSlug, normalizeDiscordSlug,
resolveDiscordAllowListMatch,
resolveDiscordChannelConfigWithFallback, resolveDiscordChannelConfigWithFallback,
resolveDiscordGuildEntry, resolveDiscordGuildEntry,
resolveDiscordShouldRequireMention, resolveDiscordShouldRequireMention,
@@ -89,13 +90,20 @@ export async function preflightDiscordMessage(
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []); const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
const effectiveAllowFrom = [...(params.allowFrom ?? []), ...storeAllowFrom]; const effectiveAllowFrom = [...(params.allowFrom ?? []), ...storeAllowFrom];
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:"]); const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:"]);
const permitted = allowList const allowMatch = allowList
? allowListMatches(allowList, { ? resolveDiscordAllowListMatch({
id: author.id, allowList,
name: author.username, candidate: {
tag: formatDiscordUserTag(author), 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) { if (!permitted) {
commandAuthorized = false; commandAuthorized = false;
if (dmPolicy === "pairing") { if (dmPolicy === "pairing") {
@@ -109,7 +117,7 @@ export async function preflightDiscordMessage(
}); });
if (created) { if (created) {
logVerbose( logVerbose(
`discord pairing request sender=${author.id} tag=${formatDiscordUserTag(author)}`, `discord pairing request sender=${author.id} tag=${formatDiscordUserTag(author)} (${allowMatchMeta})`,
); );
try { try {
await sendMessageDiscord( await sendMessageDiscord(
@@ -130,7 +138,9 @@ export async function preflightDiscordMessage(
} }
} }
} else { } else {
logVerbose(`Blocked unauthorized discord sender ${author.id} (dmPolicy=${dmPolicy})`); logVerbose(
`Blocked unauthorized discord sender ${author.id} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`,
);
} }
return null; return null;
} }

View File

@@ -14,22 +14,48 @@ export function normalizeAllowListLower(list?: Array<string | number>) {
return normalizeAllowList(list).map((entry) => entry.toLowerCase()); 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; const allowList = params.allowList;
if (allowList.length === 0) return false; if (allowList.length === 0) return { allowed: false };
if (allowList.includes("*")) return true; if (allowList.includes("*")) {
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
}
const id = params.id?.toLowerCase(); const id = params.id?.toLowerCase();
const name = params.name?.toLowerCase(); const name = params.name?.toLowerCase();
const slug = normalizeSlackSlug(name); const slug = normalizeSlackSlug(name);
const candidates = [ const candidates: Array<{ value?: string; source: SlackAllowListMatch["matchSource"] }> = [
id, { value: id, source: "id" },
id ? `slack:${id}` : undefined, { value: id ? `slack:${id}` : undefined, source: "prefixed-id" },
id ? `user:${id}` : undefined, { value: id ? `user:${id}` : undefined, source: "prefixed-user" },
name, { value: name, source: "name" },
name ? `slack:${name}` : undefined, { value: name ? `slack:${name}` : undefined, source: "prefixed-name" },
slug, { value: slug, source: "slug" },
].filter(Boolean) as string[]; ];
return candidates.some((value) => allowList.includes(value)); 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: { export function resolveSlackUserAllowed(params: {

View File

@@ -27,7 +27,7 @@ import { reactSlackMessage } from "../../actions.js";
import { sendMessageSlack } from "../../send.js"; import { sendMessageSlack } from "../../send.js";
import type { SlackMessageEvent } from "../../types.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 { resolveSlackEffectiveAllowFrom } from "../auth.js";
import { resolveSlackChannelConfig } from "../channel-config.js"; import { resolveSlackChannelConfig } from "../channel-config.js";
import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js"; import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js";
@@ -121,11 +121,14 @@ export async function prepareSlackMessage(params: {
return null; return null;
} }
if (ctx.dmPolicy !== "open") { if (ctx.dmPolicy !== "open") {
const permitted = allowListMatches({ const allowMatch = resolveSlackAllowListMatch({
allowList: allowFromLower, allowList: allowFromLower,
id: directUserId, id: directUserId,
}); });
if (!permitted) { const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${
allowMatch.matchSource ?? "none"
}`;
if (!allowMatch.allowed) {
if (ctx.dmPolicy === "pairing") { if (ctx.dmPolicy === "pairing") {
const sender = await ctx.resolveUserName(directUserId); const sender = await ctx.resolveUserName(directUserId);
const senderName = sender?.name ?? undefined; const senderName = sender?.name ?? undefined;
@@ -136,7 +139,9 @@ export async function prepareSlackMessage(params: {
}); });
if (created) { if (created) {
logVerbose( logVerbose(
`slack pairing request sender=${directUserId} name=${senderName ?? "unknown"}`, `slack pairing request sender=${directUserId} name=${
senderName ?? "unknown"
} (${allowMatchMeta})`,
); );
try { try {
await sendMessageSlack( await sendMessageSlack(
@@ -158,7 +163,7 @@ export async function prepareSlackMessage(params: {
} }
} else { } else {
logVerbose( logVerbose(
`Blocked unauthorized slack sender ${message.user} (dmPolicy=${ctx.dmPolicy})`, `Blocked unauthorized slack sender ${message.user} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`,
); );
} }
return null; return null;
@@ -225,11 +230,11 @@ export async function prepareSlackMessage(params: {
surface: "slack", surface: "slack",
}); });
const ownerAuthorized = allowListMatches({ const ownerAuthorized = resolveSlackAllowListMatch({
allowList: allowFromLower, allowList: allowFromLower,
id: senderId, id: senderId,
name: senderName, name: senderName,
}); }).allowed;
const channelUsersAllowlistConfigured = const channelUsersAllowlistConfigured =
isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0; isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0;
const channelCommandAuthorized = const channelCommandAuthorized =

View File

@@ -5,6 +5,12 @@ export type NormalizedAllowFrom = {
hasEntries: boolean; hasEntries: boolean;
}; };
export type AllowFromMatch = {
allowed: boolean;
matchKey?: string;
matchSource?: "wildcard" | "id" | "username";
};
export const normalizeAllowFrom = (list?: Array<string | number>): NormalizedAllowFrom => { export const normalizeAllowFrom = (list?: Array<string | number>): NormalizedAllowFrom => {
const entries = (list ?? []).map((value) => String(value).trim()).filter(Boolean); const entries = (list ?? []).map((value) => String(value).trim()).filter(Boolean);
const hasWildcard = entries.includes("*"); const hasWildcard = entries.includes("*");
@@ -40,3 +46,27 @@ export const isSenderAllowed = (params: {
if (!username) return false; if (!username) return false;
return allow.entriesLower.some((entry) => entry === username || entry === `@${username}`); 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 };
};

View File

@@ -34,7 +34,12 @@ import {
hasBotMention, hasBotMention,
resolveTelegramForumThreadId, resolveTelegramForumThreadId,
} from "./bot/helpers.js"; } 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 { upsertTelegramPairingRequest } from "./pairing-store.js";
import type { TelegramContext } from "./bot/types.js"; import type { TelegramContext } from "./bot/types.js";
@@ -174,14 +179,16 @@ export const buildTelegramMessageContext = async ({
if (dmPolicy !== "open") { if (dmPolicy !== "open") {
const candidate = String(chatId); const candidate = String(chatId);
const senderUsername = msg.from?.username ?? ""; 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 = const allowed =
effectiveDmAllow.hasWildcard || effectiveDmAllow.hasWildcard || (effectiveDmAllow.hasEntries && allowMatch.allowed);
(effectiveDmAllow.hasEntries &&
isSenderAllowed({
allow: effectiveDmAllow,
senderId: candidate,
senderUsername,
}));
if (!allowed) { if (!allowed) {
if (dmPolicy === "pairing") { if (dmPolicy === "pairing") {
try { try {
@@ -207,6 +214,8 @@ export const buildTelegramMessageContext = async ({
username: from?.username, username: from?.username,
firstName: from?.first_name, firstName: from?.first_name,
lastName: from?.last_name, lastName: from?.last_name,
matchKey: allowMatch.matchKey ?? "none",
matchSource: allowMatch.matchSource ?? "none",
}, },
"telegram pairing request", "telegram pairing request",
); );
@@ -228,7 +237,9 @@ export const buildTelegramMessageContext = async ({
logVerbose(`telegram pairing reply failed for chat ${chatId}: ${String(err)}`); logVerbose(`telegram pairing reply failed for chat ${chatId}: ${String(err)}`);
} }
} else { } else {
logVerbose(`Blocked unauthorized telegram sender ${candidate} (dmPolicy=${dmPolicy})`); logVerbose(
`Blocked unauthorized telegram sender ${candidate} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`,
);
} }
return null; return null;
} }