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();
}
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;
}

View File

@@ -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: {

View File

@@ -9,6 +9,12 @@ export type DiscordAllowList = {
names: Set<string>;
};
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<string | number>;
userId: string;

View File

@@ -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;
}

View File

@@ -14,22 +14,48 @@ export function normalizeAllowListLower(list?: Array<string | number>) {
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: {

View File

@@ -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 =

View File

@@ -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<string | number>): 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 };
};

View File

@@ -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;
}