refactor: share allowlist match metadata

Co-authored-by: thewilloftheshadow <thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-18 01:49:13 +00:00
parent ccb30665f7
commit 62354dff9c
10 changed files with 94 additions and 55 deletions

View File

@@ -1,3 +1,5 @@
import type { AllowlistMatch } from "../../../../../src/channels/plugins/allowlist-match.js";
function normalizeAllowList(list?: Array<string | number>) { function normalizeAllowList(list?: Array<string | number>) {
return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean); return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean);
} }
@@ -10,11 +12,9 @@ function normalizeMatrixUser(raw?: string | null): string {
return (raw ?? "").trim().toLowerCase(); return (raw ?? "").trim().toLowerCase();
} }
export type MatrixAllowListMatch = { export type MatrixAllowListMatch = AllowlistMatch<
allowed: boolean; "wildcard" | "id" | "prefixed-id" | "prefixed-user" | "name" | "localpart"
matchKey?: string; >;
matchSource?: "wildcard" | "id" | "prefixed-id" | "prefixed-user" | "name" | "localpart";
};
export function resolveMatrixAllowListMatch(params: { export function resolveMatrixAllowListMatch(params: {
allowList: string[]; allowList: string[];

View File

@@ -16,6 +16,7 @@ import {
import { createReplyDispatcherWithTyping } from "../../../../../src/auto-reply/reply/reply-dispatcher.js"; import { createReplyDispatcherWithTyping } from "../../../../../src/auto-reply/reply/reply-dispatcher.js";
import type { ReplyPayload } from "../../../../../src/auto-reply/types.js"; import type { ReplyPayload } from "../../../../../src/auto-reply/types.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../../../../src/channels/command-gating.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 { loadConfig } from "../../../../../src/config/config.js";
import { resolveStorePath, updateLastRoute } from "../../../../../src/config/sessions.js"; import { resolveStorePath, updateLastRoute } from "../../../../../src/config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../../../../../src/globals.js"; import { danger, logVerbose, shouldLogVerbose } from "../../../../../src/globals.js";
@@ -326,9 +327,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
userId: senderId, userId: senderId,
userName: senderName, userName: senderName,
}); });
const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${ const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
allowMatch.matchSource ?? "none"
}`;
if (!allowMatch.allowed) { if (!allowMatch.allowed) {
if (dmPolicy === "pairing") { if (dmPolicy === "pairing") {
const { code, created } = await upsertChannelPairingRequest({ const { code, created } = await upsertChannelPairingRequest({
@@ -369,14 +368,16 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
} }
if (isRoom && roomConfigInfo.config?.users?.length) { if (isRoom && roomConfigInfo.config?.users?.length) {
const userAllowed = resolveMatrixAllowListMatches({ const userMatch = resolveMatrixAllowListMatch({
allowList: normalizeAllowListLower(roomConfigInfo.config.users), allowList: normalizeAllowListLower(roomConfigInfo.config.users),
userId: senderId, userId: senderId,
userName: senderName, userName: senderName,
}); });
if (!userAllowed) { if (!userMatch.allowed) {
logVerbose( logVerbose(
`matrix: blocked sender ${senderId} (room users allowlist, ${roomMatchMeta})`, `matrix: blocked sender ${senderId} (room users allowlist, ${roomMatchMeta}, ${formatAllowlistMatchMeta(
userMatch,
)})`,
); );
return; return;
} }

View File

@@ -15,6 +15,7 @@ import {
} from "../../../../src/auto-reply/reply/history.js"; } from "../../../../src/auto-reply/reply/history.js";
import { resolveMentionGating } from "../../../../src/channels/mention-gating.js"; import { resolveMentionGating } from "../../../../src/channels/mention-gating.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../../../src/channels/command-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 { danger, logVerbose, shouldLogVerbose } from "../../../../src/globals.js";
import { enqueueSystemEvent } from "../../../../src/infra/system-events.js"; import { enqueueSystemEvent } from "../../../../src/infra/system-events.js";
import { import {
@@ -41,6 +42,7 @@ import {
import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js"; import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js";
import { import {
isMSTeamsGroupAllowed, isMSTeamsGroupAllowed,
resolveMSTeamsAllowlistMatch,
resolveMSTeamsReplyPolicy, resolveMSTeamsReplyPolicy,
resolveMSTeamsRouteConfig, resolveMSTeamsRouteConfig,
} from "../policy.js"; } from "../policy.js";
@@ -141,19 +143,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
} }
if (dmPolicy !== "open") { if (dmPolicy !== "open") {
const effectiveAllowFrom = [ const effectiveAllowFrom = [...allowFrom.map((v) => String(v)), ...storedAllowFrom];
...allowFrom.map((v) => String(v).toLowerCase()), const allowMatch = resolveMSTeamsAllowlistMatch({
...storedAllowFrom, allowFrom: effectiveAllowFrom,
]; senderId,
senderName,
});
const senderLower = senderId.toLowerCase(); if (!allowMatch.allowed) {
const senderNameLower = senderName.toLowerCase();
const allowed =
effectiveAllowFrom.includes("*") ||
effectiveAllowFrom.includes(senderLower) ||
effectiveAllowFrom.includes(senderNameLower);
if (!allowed) {
if (dmPolicy === "pairing") { if (dmPolicy === "pairing") {
const request = await upsertChannelPairingRequest({ const request = await upsertChannelPairingRequest({
channel: "msteams", channel: "msteams",
@@ -170,6 +167,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
log.debug("dropping dm (not allowlisted)", { log.debug("dropping dm (not allowlisted)", {
sender: senderId, sender: senderId,
label: senderName, label: senderName,
allowlistMatch: formatAllowlistMatchMeta(allowMatch),
}); });
return; return;
} }
@@ -213,6 +211,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
if (channelGate.allowlistConfigured && !channelGate.allowed) { if (channelGate.allowlistConfigured && !channelGate.allowed) {
log.debug("dropping group message (not in team/channel allowlist)", { log.debug("dropping group message (not in team/channel allowlist)", {
conversationId, conversationId,
teamKey: channelGate.teamKey ?? "none",
channelKey: channelGate.channelKey ?? "none",
channelMatchKey: channelGate.channelMatchKey ?? "none",
channelMatchSource: channelGate.channelMatchSource ?? "none",
}); });
return; return;
} }
@@ -223,16 +225,17 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
return; return;
} }
if (effectiveGroupAllowFrom.length > 0) { if (effectiveGroupAllowFrom.length > 0) {
const allowed = isMSTeamsGroupAllowed({ const allowMatch = resolveMSTeamsAllowlistMatch({
groupPolicy, groupPolicy,
allowFrom: effectiveGroupAllowFrom, allowFrom: effectiveGroupAllowFrom,
senderId, senderId,
senderName, senderName,
}); });
if (!allowed) { if (!allowMatch.allowed) {
log.debug("dropping group message (not in groupAllowFrom)", { log.debug("dropping group message (not in groupAllowFrom)", {
sender: senderId, sender: senderId,
label: senderName, label: senderName,
allowlistMatch: formatAllowlistMatchMeta(allowMatch),
}); });
return; return;
} }

View File

@@ -11,6 +11,7 @@ import {
resolveChannelEntryMatchWithFallback, resolveChannelEntryMatchWithFallback,
resolveNestedAllowlistDecision, resolveNestedAllowlistDecision,
} from "../../../src/channels/plugins/channel-config.js"; } from "../../../src/channels/plugins/channel-config.js";
import type { AllowlistMatch } from "../../../src/channels/plugins/allowlist-match.js";
export type MSTeamsResolvedRouteConfig = { export type MSTeamsResolvedRouteConfig = {
teamConfig?: MSTeamsTeamConfig; teamConfig?: MSTeamsTeamConfig;
@@ -90,6 +91,31 @@ export type MSTeamsReplyPolicy = {
replyStyle: MSTeamsReplyStyle; replyStyle: MSTeamsReplyStyle;
}; };
export type MSTeamsAllowlistMatch = AllowlistMatch<"wildcard" | "id" | "name">;
export function resolveMSTeamsAllowlistMatch(params: {
allowFrom: Array<string | number>;
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: { export function resolveMSTeamsReplyPolicy(params: {
isDirectMessage: boolean; isDirectMessage: boolean;
globalConfig?: MSTeamsConfig; globalConfig?: MSTeamsConfig;
@@ -126,12 +152,5 @@ export function isMSTeamsGroupAllowed(params: {
const { groupPolicy } = params; const { groupPolicy } = params;
if (groupPolicy === "disabled") return false; if (groupPolicy === "disabled") return false;
if (groupPolicy === "open") return true; if (groupPolicy === "open") return true;
const allowFrom = params.allowFrom return resolveMSTeamsAllowlistMatch(params).allowed;
.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);
} }

View File

@@ -0,0 +1,21 @@
export type AllowlistMatchSource =
| "wildcard"
| "id"
| "name"
| "tag"
| "username"
| "prefixed-id"
| "prefixed-user"
| "prefixed-name"
| "slug"
| "localpart";
export type AllowlistMatch<TSource extends string = AllowlistMatchSource> = {
allowed: boolean;
matchKey?: string;
matchSource?: TSource;
};
export function formatAllowlistMatchMeta(match?: AllowlistMatch | null): string {
return `matchKey=${match?.matchKey ?? "none"} matchSource=${match?.matchSource ?? "none"}`;
}

View File

@@ -0,0 +1,2 @@
export type { AllowlistMatch, AllowlistMatchSource } from "../allowlist-match.js";
export { formatAllowlistMatchMeta } from "../allowlist-match.js";

View File

@@ -93,4 +93,9 @@ export {
type ChannelEntryMatch, type ChannelEntryMatch,
type ChannelMatchSource, type ChannelMatchSource,
} from "./channel-config.js"; } from "./channel-config.js";
export {
formatAllowlistMatchMeta,
type AllowlistMatch,
type AllowlistMatchSource,
} from "./allowlist-match.js";
export type { ChannelId, ChannelPlugin } from "./types.js"; export type { ChannelId, ChannelPlugin } from "./types.js";

View File

@@ -4,6 +4,7 @@ import {
buildChannelKeyCandidates, buildChannelKeyCandidates,
resolveChannelEntryMatchWithFallback, resolveChannelEntryMatchWithFallback,
} from "../../channels/channel-config.js"; } from "../../channels/channel-config.js";
import type { AllowlistMatch } from "../../channels/allowlist-match.js";
import { formatDiscordUserTag } from "./format.js"; import { formatDiscordUserTag } from "./format.js";
export type DiscordAllowList = { export type DiscordAllowList = {
@@ -12,11 +13,7 @@ export type DiscordAllowList = {
names: Set<string>; names: Set<string>;
}; };
export type DiscordAllowListMatch = { export type DiscordAllowListMatch = AllowlistMatch<"wildcard" | "id" | "name" | "tag">;
allowed: boolean;
matchKey?: string;
matchSource?: "wildcard" | "id" | "name" | "tag";
};
export type DiscordGuildEntryResolved = { export type DiscordGuildEntryResolved = {
id?: string; id?: string;

View File

@@ -1,3 +1,5 @@
import type { AllowlistMatch } from "../../channels/allowlist-match.js";
export function normalizeSlackSlug(raw?: string) { export function normalizeSlackSlug(raw?: string) {
const trimmed = raw?.trim().toLowerCase() ?? ""; const trimmed = raw?.trim().toLowerCase() ?? "";
if (!trimmed) return ""; if (!trimmed) return "";
@@ -14,18 +16,9 @@ export function normalizeAllowListLower(list?: Array<string | number>) {
return normalizeAllowList(list).map((entry) => entry.toLowerCase()); return normalizeAllowList(list).map((entry) => entry.toLowerCase());
} }
export type SlackAllowListMatch = { export type SlackAllowListMatch = AllowlistMatch<
allowed: boolean; "wildcard" | "id" | "prefixed-id" | "prefixed-user" | "name" | "prefixed-name" | "slug"
matchKey?: string; >;
matchSource?:
| "wildcard"
| "id"
| "prefixed-id"
| "prefixed-user"
| "name"
| "prefixed-name"
| "slug";
};
export function resolveSlackAllowListMatch(params: { export function resolveSlackAllowListMatch(params: {
allowList: string[]; allowList: string[];

View File

@@ -1,3 +1,5 @@
import type { AllowlistMatch } from "../channels/allowlist-match.js";
export type NormalizedAllowFrom = { export type NormalizedAllowFrom = {
entries: string[]; entries: string[];
entriesLower: string[]; entriesLower: string[];
@@ -5,11 +7,7 @@ export type NormalizedAllowFrom = {
hasEntries: boolean; hasEntries: boolean;
}; };
export type AllowFromMatch = { export type AllowFromMatch = AllowlistMatch<"wildcard" | "id" | "username">;
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);