feat: add dm allowlist match metadata logs
Co-authored-by: thewilloftheshadow <thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user