fix: enforce strict allowlist across pairing stores (#23017)

This commit is contained in:
Peter Steinberger
2026-02-22 00:00:23 +01:00
committed by GitHub
parent 617e38cec0
commit 0bd9f0d4ac
31 changed files with 162 additions and 45 deletions

View File

@@ -332,6 +332,7 @@ export async function processMessage(
allowFrom: account.config.allowFrom, allowFrom: account.config.allowFrom,
groupAllowFrom: account.config.groupAllowFrom, groupAllowFrom: account.config.groupAllowFrom,
storeAllowFrom, storeAllowFrom,
dmPolicy,
}); });
const groupAllowEntry = formatGroupAllowlistEntry({ const groupAllowEntry = formatGroupAllowlistEntry({
chatGuid: message.chatGuid, chatGuid: message.chatGuid,
@@ -1107,6 +1108,7 @@ export async function processReaction(
allowFrom: account.config.allowFrom, allowFrom: account.config.allowFrom,
groupAllowFrom: account.config.groupAllowFrom, groupAllowFrom: account.config.groupAllowFrom,
storeAllowFrom, storeAllowFrom,
dmPolicy,
}); });
const accessDecision = resolveDmGroupAccessDecision({ const accessDecision = resolveDmGroupAccessDecision({
isGroup: reaction.isGroup, isGroup: reaction.isGroup,

View File

@@ -630,7 +630,9 @@ export async function handleFeishuMessage(params: {
cfg, cfg,
); );
const storeAllowFrom = const storeAllowFrom =
!isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized) !isGroup &&
dmPolicy !== "allowlist" &&
(dmPolicy !== "open" || shouldComputeCommandAuthorized)
? await core.channel.pairing.readAllowFromStore("feishu").catch(() => []) ? await core.channel.pairing.readAllowFromStore("feishu").catch(() => [])
: []; : [];
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom]; const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];

View File

@@ -485,7 +485,7 @@ async function processMessageWithPipeline(params: {
const configAllowFrom = (account.config.dm?.allowFrom ?? []).map((v) => String(v)); const configAllowFrom = (account.config.dm?.allowFrom ?? []).map((v) => String(v));
const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config); const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config);
const storeAllowFrom = const storeAllowFrom =
!isGroup && (dmPolicy !== "open" || shouldComputeAuth) !isGroup && dmPolicy !== "allowlist" && (dmPolicy !== "open" || shouldComputeAuth)
? await core.channel.pairing.readAllowFromStore("googlechat").catch(() => []) ? await core.channel.pairing.readAllowFromStore("googlechat").catch(() => [])
: []; : [];
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]; const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];

View File

@@ -89,7 +89,10 @@ export async function handleIrcInbound(params: {
const configAllowFrom = normalizeIrcAllowlist(account.config.allowFrom); const configAllowFrom = normalizeIrcAllowlist(account.config.allowFrom);
const configGroupAllowFrom = normalizeIrcAllowlist(account.config.groupAllowFrom); const configGroupAllowFrom = normalizeIrcAllowlist(account.config.groupAllowFrom);
const storeAllowFrom = await core.channel.pairing.readAllowFromStore(CHANNEL_ID).catch(() => []); const storeAllowFrom =
dmPolicy === "allowlist"
? []
: await core.channel.pairing.readAllowFromStore(CHANNEL_ID).catch(() => []);
const storeAllowList = normalizeIrcAllowlist(storeAllowFrom); const storeAllowList = normalizeIrcAllowlist(storeAllowFrom);
const groupMatch = resolveIrcGroupMatch({ const groupMatch = resolveIrcGroupMatch({

View File

@@ -218,9 +218,10 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
} }
const senderName = await getMemberDisplayName(roomId, senderId); const senderName = await getMemberDisplayName(roomId, senderId);
const storeAllowFrom = await core.channel.pairing const storeAllowFrom =
.readAllowFromStore("matrix") dmPolicy === "allowlist"
.catch(() => []); ? []
: await core.channel.pairing.readAllowFromStore("matrix").catch(() => []);
const effectiveAllowFrom = normalizeMatrixAllowList([...allowFrom, ...storeAllowFrom]); const effectiveAllowFrom = normalizeMatrixAllowList([...allowFrom, ...storeAllowFrom]);
const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? []; const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
const effectiveGroupAllowFrom = normalizeMatrixAllowList(groupAllowFrom); const effectiveGroupAllowFrom = normalizeMatrixAllowList(groupAllowFrom);

View File

@@ -380,7 +380,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
const configAllowFrom = normalizeAllowList(account.config.allowFrom ?? []); const configAllowFrom = normalizeAllowList(account.config.allowFrom ?? []);
const configGroupAllowFrom = normalizeAllowList(account.config.groupAllowFrom ?? []); const configGroupAllowFrom = normalizeAllowList(account.config.groupAllowFrom ?? []);
const storeAllowFrom = normalizeAllowList( const storeAllowFrom = normalizeAllowList(
await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []), dmPolicy === "allowlist"
? []
: await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []),
); );
const effectiveAllowFrom = Array.from(new Set([...configAllowFrom, ...storeAllowFrom])); const effectiveAllowFrom = Array.from(new Set([...configAllowFrom, ...storeAllowFrom]));
const effectiveGroupAllowFrom = Array.from( const effectiveGroupAllowFrom = Array.from(
@@ -867,7 +869,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
if (dmPolicy !== "open") { if (dmPolicy !== "open") {
const configAllowFrom = normalizeAllowList(account.config.allowFrom ?? []); const configAllowFrom = normalizeAllowList(account.config.allowFrom ?? []);
const storeAllowFrom = normalizeAllowList( const storeAllowFrom = normalizeAllowList(
await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []), dmPolicy === "allowlist"
? []
: await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []),
); );
const effectiveAllowFrom = Array.from(new Set([...configAllowFrom, ...storeAllowFrom])); const effectiveAllowFrom = Array.from(new Set([...configAllowFrom, ...storeAllowFrom]));
const allowed = isSenderAllowed({ const allowed = isSenderAllowed({
@@ -890,10 +894,13 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
return; return;
} }
if (groupPolicy === "allowlist") { if (groupPolicy === "allowlist") {
const dmPolicyForStore = account.config.dmPolicy ?? "pairing";
const configAllowFrom = normalizeAllowList(account.config.allowFrom ?? []); const configAllowFrom = normalizeAllowList(account.config.allowFrom ?? []);
const configGroupAllowFrom = normalizeAllowList(account.config.groupAllowFrom ?? []); const configGroupAllowFrom = normalizeAllowList(account.config.groupAllowFrom ?? []);
const storeAllowFrom = normalizeAllowList( const storeAllowFrom = normalizeAllowList(
await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []), dmPolicyForStore === "allowlist"
? []
: await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []),
); );
const effectiveGroupAllowFrom = Array.from( const effectiveGroupAllowFrom = Array.from(
new Set([ new Set([

View File

@@ -124,16 +124,17 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
const senderName = from.name ?? from.id; const senderName = from.name ?? from.id;
const senderId = from.aadObjectId ?? from.id; const senderId = from.aadObjectId ?? from.id;
const storedAllowFrom = await core.channel.pairing const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
.readAllowFromStore("msteams") const storedAllowFrom =
.catch(() => []); dmPolicy === "allowlist"
? []
: await core.channel.pairing.readAllowFromStore("msteams").catch(() => []);
const useAccessGroups = cfg.commands?.useAccessGroups !== false; const useAccessGroups = cfg.commands?.useAccessGroups !== false;
// Check DM policy for direct messages. // Check DM policy for direct messages.
const dmAllowFrom = msteamsCfg?.allowFrom ?? []; const dmAllowFrom = msteamsCfg?.allowFrom ?? [];
const effectiveDmAllowFrom = [...dmAllowFrom.map((v) => String(v)), ...storedAllowFrom]; const effectiveDmAllowFrom = [...dmAllowFrom.map((v) => String(v)), ...storedAllowFrom];
if (isDirectMessage && msteamsCfg) { if (isDirectMessage && msteamsCfg) {
const dmPolicy = msteamsCfg.dmPolicy ?? "pairing";
const allowFrom = dmAllowFrom; const allowFrom = dmAllowFrom;
if (dmPolicy === "disabled") { if (dmPolicy === "disabled") {

View File

@@ -93,7 +93,10 @@ export async function handleNextcloudTalkInbound(params: {
const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom); const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom);
const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom); const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom);
const storeAllowFrom = await core.channel.pairing.readAllowFromStore(CHANNEL_ID).catch(() => []); const storeAllowFrom =
dmPolicy === "allowlist"
? []
: await core.channel.pairing.readAllowFromStore(CHANNEL_ID).catch(() => []);
const storeAllowList = normalizeNextcloudTalkAllowlist(storeAllowFrom); const storeAllowList = normalizeNextcloudTalkAllowlist(storeAllowFrom);
const roomMatch = resolveNextcloudTalkRoomMatch({ const roomMatch = resolveNextcloudTalkRoomMatch({

View File

@@ -10,6 +10,26 @@ describe("mergeAllowFromSources", () => {
}), }),
).toEqual(["line:user:abc", "123", "telegram:456"]); ).toEqual(["line:user:abc", "123", "telegram:456"]);
}); });
it("excludes pairing-store entries when dmPolicy is allowlist", () => {
expect(
mergeAllowFromSources({
allowFrom: ["+1111"],
storeAllowFrom: ["+2222", "+3333"],
dmPolicy: "allowlist",
}),
).toEqual(["+1111"]);
});
it("keeps pairing-store entries for non-allowlist policies", () => {
expect(
mergeAllowFromSources({
allowFrom: ["+1111"],
storeAllowFrom: ["+2222"],
dmPolicy: "pairing",
}),
).toEqual(["+1111", "+2222"]);
});
}); });
describe("firstDefined", () => { describe("firstDefined", () => {

View File

@@ -1,8 +1,10 @@
export function mergeAllowFromSources(params: { export function mergeAllowFromSources(params: {
allowFrom?: Array<string | number>; allowFrom?: Array<string | number>;
storeAllowFrom?: string[]; storeAllowFrom?: string[];
dmPolicy?: string;
}): string[] { }): string[] {
return [...(params.allowFrom ?? []), ...(params.storeAllowFrom ?? [])] const storeEntries = params.dmPolicy === "allowlist" ? [] : (params.storeAllowFrom ?? []);
return [...(params.allowFrom ?? []), ...storeEntries]
.map((value) => String(value).trim()) .map((value) => String(value).trim())
.filter(Boolean); .filter(Boolean);
} }

View File

@@ -464,7 +464,8 @@ async function ensureDmComponentAuthorized(params: {
return true; return true;
} }
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []); const storeAllowFrom =
dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("discord").catch(() => []);
const effectiveAllowFrom = [...(ctx.allowFrom ?? []), ...storeAllowFrom]; const effectiveAllowFrom = [...(ctx.allowFrom ?? []), ...storeAllowFrom];
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]); const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]);
const allowMatch = allowList const allowMatch = allowList

View File

@@ -178,7 +178,8 @@ export async function preflightDiscordMessage(
return null; return null;
} }
if (dmPolicy !== "open") { if (dmPolicy !== "open") {
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []); const storeAllowFrom =
dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("discord").catch(() => []);
const effectiveAllowFrom = [...(params.allowFrom ?? []), ...storeAllowFrom]; const effectiveAllowFrom = [...(params.allowFrom ?? []), ...storeAllowFrom];
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]); const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]);
const allowMatch = allowList const allowMatch = allowList

View File

@@ -140,7 +140,7 @@ describe("agent components", () => {
expect(enqueueSystemEventMock).not.toHaveBeenCalled(); expect(enqueueSystemEventMock).not.toHaveBeenCalled();
}); });
it("allows DM interactions when pairing store allowlist matches", async () => { it("blocks DM interactions when only pairing store entries match in allowlist mode", async () => {
readAllowFromStoreMock.mockResolvedValue(["123456789"]); readAllowFromStoreMock.mockResolvedValue(["123456789"]);
const button = createAgentComponentButton({ const button = createAgentComponentButton({
cfg: createCfg(), cfg: createCfg(),
@@ -152,8 +152,9 @@ describe("agent components", () => {
await button.run(interaction, { componentId: "hello" } as ComponentData); await button.run(interaction, { componentId: "hello" } as ComponentData);
expect(defer).toHaveBeenCalledWith({ ephemeral: true }); expect(defer).toHaveBeenCalledWith({ ephemeral: true });
expect(reply).toHaveBeenCalledWith({ content: "" }); expect(reply).toHaveBeenCalledWith({ content: "You are not authorized to use this button." });
expect(enqueueSystemEventMock).toHaveBeenCalled(); expect(enqueueSystemEventMock).not.toHaveBeenCalled();
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
}); });
it("matches tag-based allowlist entries for DM select menus", async () => { it("matches tag-based allowlist entries for DM select menus", async () => {

View File

@@ -1349,7 +1349,8 @@ async function dispatchDiscordCommandInteraction(params: {
return; return;
} }
if (dmPolicy !== "open") { if (dmPolicy !== "open") {
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []); const storeAllowFrom =
dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("discord").catch(() => []);
const effectiveAllowFrom = [ const effectiveAllowFrom = [
...(discordConfig?.allowFrom ?? discordConfig?.dm?.allowFrom ?? []), ...(discordConfig?.allowFrom ?? discordConfig?.dm?.allowFrom ?? []),
...storeAllowFrom, ...storeAllowFrom,

View File

@@ -138,7 +138,8 @@ export function resolveIMessageInboundDecision(params: {
} }
const groupId = isGroup ? groupIdCandidate : undefined; const groupId = isGroup ? groupIdCandidate : undefined;
const effectiveDmAllowFrom = Array.from(new Set([...params.allowFrom, ...params.storeAllowFrom])) const storeAllowFrom = params.dmPolicy === "allowlist" ? [] : params.storeAllowFrom;
const effectiveDmAllowFrom = Array.from(new Set([...params.allowFrom, ...storeAllowFrom]))
.map((v) => String(v).trim()) .map((v) => String(v).trim())
.filter(Boolean); .filter(Boolean);
// Keep DM pairing-store authorization scoped to DMs; group access must come from explicit group allowlist config. // Keep DM pairing-store authorization scoped to DMs; group access must come from explicit group allowlist config.

View File

@@ -30,6 +30,7 @@ export const normalizeAllowFrom = (list?: Array<string | number>): NormalizedAll
export const normalizeAllowFromWithStore = (params: { export const normalizeAllowFromWithStore = (params: {
allowFrom?: Array<string | number>; allowFrom?: Array<string | number>;
storeAllowFrom?: string[]; storeAllowFrom?: string[];
dmPolicy?: string;
}): NormalizedAllowFrom => normalizeAllowFrom(mergeAllowFromSources(params)); }): NormalizedAllowFrom => normalizeAllowFrom(mergeAllowFromSources(params));
export const isSenderAllowed = (params: { export const isSenderAllowed = (params: {

View File

@@ -109,11 +109,13 @@ async function shouldProcessLineEvent(
const { cfg, account } = context; const { cfg, account } = context;
const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source); const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source);
const senderId = userId ?? ""; const senderId = userId ?? "";
const dmPolicy = account.config.dmPolicy ?? "pairing";
const storeAllowFrom = await readChannelAllowFromStore("line").catch(() => []); const storeAllowFrom = await readChannelAllowFromStore("line").catch(() => []);
const effectiveDmAllow = normalizeAllowFromWithStore({ const effectiveDmAllow = normalizeAllowFromWithStore({
allowFrom: account.config.allowFrom, allowFrom: account.config.allowFrom,
storeAllowFrom, storeAllowFrom,
dmPolicy,
}); });
const groupConfig = resolveLineGroupConfig({ config: account.config, groupId, roomId }); const groupConfig = resolveLineGroupConfig({ config: account.config, groupId, roomId });
const groupAllowOverride = groupConfig?.allowFrom; const groupAllowOverride = groupConfig?.allowFrom;
@@ -128,8 +130,8 @@ async function shouldProcessLineEvent(
const effectiveGroupAllow = normalizeAllowFromWithStore({ const effectiveGroupAllow = normalizeAllowFromWithStore({
allowFrom: groupAllowFrom, allowFrom: groupAllowFrom,
storeAllowFrom, storeAllowFrom,
dmPolicy,
}); });
const dmPolicy = account.config.dmPolicy ?? "pairing";
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";

View File

@@ -26,7 +26,9 @@ export async function resolveSenderCommandAuthorization(
}> { }> {
const shouldComputeAuth = params.shouldComputeCommandAuthorized(params.rawBody, params.cfg); const shouldComputeAuth = params.shouldComputeCommandAuthorized(params.rawBody, params.cfg);
const storeAllowFrom = const storeAllowFrom =
!params.isGroup && (params.dmPolicy !== "open" || shouldComputeAuth) !params.isGroup &&
params.dmPolicy !== "allowlist" &&
(params.dmPolicy !== "open" || shouldComputeAuth)
? await params.readAllowFromStore().catch(() => []) ? await params.readAllowFromStore().catch(() => [])
: []; : [];
const effectiveAllowFrom = [...params.configuredAllowFrom, ...storeAllowFrom]; const effectiveAllowFrom = [...params.configuredAllowFrom, ...storeAllowFrom];

View File

@@ -53,6 +53,28 @@ describe("security/dm-policy-shared", () => {
expect(lists.effectiveGroupAllowFrom).toEqual(["owner", "owner2"]); expect(lists.effectiveGroupAllowFrom).toEqual(["owner", "owner2"]);
}); });
it("excludes storeAllowFrom when dmPolicy is allowlist", () => {
const lists = resolveEffectiveAllowFromLists({
allowFrom: ["+1111"],
groupAllowFrom: ["group:abc"],
storeAllowFrom: ["+2222", "+3333"],
dmPolicy: "allowlist",
});
expect(lists.effectiveAllowFrom).toEqual(["+1111"]);
expect(lists.effectiveGroupAllowFrom).toEqual(["group:abc"]);
});
it("includes storeAllowFrom when dmPolicy is pairing", () => {
const lists = resolveEffectiveAllowFromLists({
allowFrom: ["+1111"],
groupAllowFrom: [],
storeAllowFrom: ["+2222"],
dmPolicy: "pairing",
});
expect(lists.effectiveAllowFrom).toEqual(["+1111", "+2222"]);
expect(lists.effectiveGroupAllowFrom).toEqual(["+1111", "+2222"]);
});
const channels = [ const channels = [
"bluebubbles", "bluebubbles",
"imessage", "imessage",

View File

@@ -6,6 +6,7 @@ export function resolveEffectiveAllowFromLists(params: {
allowFrom?: Array<string | number> | null; allowFrom?: Array<string | number> | null;
groupAllowFrom?: Array<string | number> | null; groupAllowFrom?: Array<string | number> | null;
storeAllowFrom?: Array<string | number> | null; storeAllowFrom?: Array<string | number> | null;
dmPolicy?: string | null;
}): { }): {
effectiveAllowFrom: string[]; effectiveAllowFrom: string[];
effectiveGroupAllowFrom: string[]; effectiveGroupAllowFrom: string[];
@@ -16,9 +17,12 @@ export function resolveEffectiveAllowFromLists(params: {
const configGroupAllowFrom = normalizeStringEntries( const configGroupAllowFrom = normalizeStringEntries(
Array.isArray(params.groupAllowFrom) ? params.groupAllowFrom : undefined, Array.isArray(params.groupAllowFrom) ? params.groupAllowFrom : undefined,
); );
const storeAllowFrom = normalizeStringEntries( const storeAllowFrom =
Array.isArray(params.storeAllowFrom) ? params.storeAllowFrom : undefined, params.dmPolicy === "allowlist"
); ? []
: normalizeStringEntries(
Array.isArray(params.storeAllowFrom) ? params.storeAllowFrom : undefined,
);
const effectiveAllowFrom = normalizeStringEntries([...configAllowFrom, ...storeAllowFrom]); const effectiveAllowFrom = normalizeStringEntries([...configAllowFrom, ...storeAllowFrom]);
const groupBase = configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom; const groupBase = configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom;
const effectiveGroupAllowFrom = normalizeStringEntries([...groupBase, ...storeAllowFrom]); const effectiveGroupAllowFrom = normalizeStringEntries([...groupBase, ...storeAllowFrom]);

View File

@@ -441,7 +441,10 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
const groupId = dataMessage.groupInfo?.groupId ?? undefined; const groupId = dataMessage.groupInfo?.groupId ?? undefined;
const groupName = dataMessage.groupInfo?.groupName ?? undefined; const groupName = dataMessage.groupInfo?.groupName ?? undefined;
const isGroup = Boolean(groupId); const isGroup = Boolean(groupId);
const storeAllowFrom = await readChannelAllowFromStore("signal").catch(() => []); const storeAllowFrom =
deps.dmPolicy === "allowlist"
? []
: await readChannelAllowFromStore("signal").catch(() => []);
const effectiveDmAllow = [...deps.allowFrom, ...storeAllowFrom]; const effectiveDmAllow = [...deps.allowFrom, ...storeAllowFrom];
const effectiveGroupAllow = [...deps.groupAllowFrom, ...storeAllowFrom]; const effectiveGroupAllow = [...deps.groupAllowFrom, ...storeAllowFrom];
const dmAllowed = const dmAllowed =

View File

@@ -3,7 +3,8 @@ import { allowListMatches, normalizeAllowList, normalizeAllowListLower } from ".
import type { SlackMonitorContext } from "./context.js"; import type { SlackMonitorContext } from "./context.js";
export async function resolveSlackEffectiveAllowFrom(ctx: SlackMonitorContext) { export async function resolveSlackEffectiveAllowFrom(ctx: SlackMonitorContext) {
const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []); const storeAllowFrom =
ctx.dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("slack").catch(() => []);
const allowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]); const allowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]);
const allowFromLower = normalizeAllowListLower(allowFrom); const allowFromLower = normalizeAllowListLower(allowFrom);
return { allowFrom, allowFromLower }; return { allowFrom, allowFromLower };

View File

@@ -350,7 +350,10 @@ export async function registerSlackMonitorSlashCommands(params: {
return; return;
} }
const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []); const storeAllowFrom =
ctx.dmPolicy === "allowlist"
? []
: await readChannelAllowFromStore("slack").catch(() => []);
const effectiveAllowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]); const effectiveAllowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]);
const effectiveAllowFromLower = normalizeAllowListLower(effectiveAllowFrom); const effectiveAllowFromLower = normalizeAllowListLower(effectiveAllowFrom);

View File

@@ -56,6 +56,7 @@ export const normalizeAllowFrom = (list?: Array<string | number>): NormalizedAll
export const normalizeAllowFromWithStore = (params: { export const normalizeAllowFromWithStore = (params: {
allowFrom?: Array<string | number>; allowFrom?: Array<string | number>;
storeAllowFrom?: string[]; storeAllowFrom?: string[];
dmPolicy?: string;
}): NormalizedAllowFrom => normalizeAllowFrom(mergeAllowFromSources(params)); }): NormalizedAllowFrom => normalizeAllowFrom(mergeAllowFromSources(params));
export const isSenderAllowed = (params: { export const isSenderAllowed = (params: {

View File

@@ -794,6 +794,7 @@ export const registerTelegramHandlers = ({
const groupAllowContext = await resolveTelegramGroupAllowFromContext({ const groupAllowContext = await resolveTelegramGroupAllowFromContext({
chatId, chatId,
accountId, accountId,
dmPolicy: telegramCfg.dmPolicy ?? "pairing",
isForum, isForum,
messageThreadId, messageThreadId,
groupAllowFrom, groupAllowFrom,
@@ -807,11 +808,12 @@ export const registerTelegramHandlers = ({
effectiveGroupAllow, effectiveGroupAllow,
hasGroupAllowOverride, hasGroupAllowOverride,
} = groupAllowContext; } = groupAllowContext;
const dmPolicy = telegramCfg.dmPolicy ?? "pairing";
const effectiveDmAllow = normalizeAllowFromWithStore({ const effectiveDmAllow = normalizeAllowFromWithStore({
allowFrom: telegramCfg.allowFrom, allowFrom: telegramCfg.allowFrom,
storeAllowFrom, storeAllowFrom,
dmPolicy,
}); });
const dmPolicy = telegramCfg.dmPolicy ?? "pairing";
const senderId = callback.from?.id ? String(callback.from.id) : ""; const senderId = callback.from?.id ? String(callback.from.id) : "";
const senderUsername = callback.from?.username ?? ""; const senderUsername = callback.from?.username ?? "";
if ( if (
@@ -1089,6 +1091,7 @@ export const registerTelegramHandlers = ({
const groupAllowContext = await resolveTelegramGroupAllowFromContext({ const groupAllowContext = await resolveTelegramGroupAllowFromContext({
chatId: event.chatId, chatId: event.chatId,
accountId, accountId,
dmPolicy: telegramCfg.dmPolicy ?? "pairing",
isForum: event.isForum, isForum: event.isForum,
messageThreadId: event.messageThreadId, messageThreadId: event.messageThreadId,
groupAllowFrom, groupAllowFrom,

View File

@@ -197,11 +197,12 @@ export const buildTelegramMessageContext = async ({
: null; : null;
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey; const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
const mentionRegexes = buildMentionRegexes(cfg, route.agentId); const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
const effectiveDmAllow = normalizeAllowFromWithStore({ allowFrom, storeAllowFrom }); const effectiveDmAllow = normalizeAllowFromWithStore({ allowFrom, storeAllowFrom, dmPolicy });
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
const effectiveGroupAllow = normalizeAllowFromWithStore({ const effectiveGroupAllow = normalizeAllowFromWithStore({
allowFrom: groupAllowOverride ?? groupAllowFrom, allowFrom: groupAllowOverride ?? groupAllowFrom,
storeAllowFrom, storeAllowFrom,
dmPolicy,
}); });
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
const senderId = msg.from?.id ? String(msg.from.id) : ""; const senderId = msg.from?.id ? String(msg.from.id) : "";

View File

@@ -167,6 +167,7 @@ async function resolveTelegramCommandAuth(params: {
const groupAllowContext = await resolveTelegramGroupAllowFromContext({ const groupAllowContext = await resolveTelegramGroupAllowFromContext({
chatId, chatId,
accountId, accountId,
dmPolicy: telegramCfg.dmPolicy ?? "pairing",
isForum, isForum,
messageThreadId, messageThreadId,
groupAllowFrom, groupAllowFrom,
@@ -251,6 +252,7 @@ async function resolveTelegramCommandAuth(params: {
const dmAllow = normalizeAllowFromWithStore({ const dmAllow = normalizeAllowFromWithStore({
allowFrom: allowFrom, allowFrom: allowFrom,
storeAllowFrom, storeAllowFrom,
dmPolicy: telegramCfg.dmPolicy ?? "pairing",
}); });
const senderAllowed = isSenderAllowed({ const senderAllowed = isSenderAllowed({
allow: dmAllow, allow: dmAllow,

View File

@@ -20,6 +20,7 @@ export type TelegramThreadSpec = {
export async function resolveTelegramGroupAllowFromContext(params: { export async function resolveTelegramGroupAllowFromContext(params: {
chatId: string | number; chatId: string | number;
accountId?: string; accountId?: string;
dmPolicy?: string;
isForum?: boolean; isForum?: boolean;
messageThreadId?: number | null; messageThreadId?: number | null;
groupAllowFrom?: Array<string | number>; groupAllowFrom?: Array<string | number>;
@@ -53,6 +54,7 @@ export async function resolveTelegramGroupAllowFromContext(params: {
const effectiveGroupAllow = normalizeAllowFromWithStore({ const effectiveGroupAllow = normalizeAllowFromWithStore({
allowFrom: groupAllowOverride ?? params.groupAllowFrom, allowFrom: groupAllowOverride ?? params.groupAllowFrom,
storeAllowFrom, storeAllowFrom,
dmPolicy: params.dmPolicy,
}); });
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
return { return {

View File

@@ -28,6 +28,7 @@ import { getAgentScopedMediaLocalRoots } from "../../../media/local-roots.js";
import { readChannelAllowFromStore } from "../../../pairing/pairing-store.js"; import { readChannelAllowFromStore } from "../../../pairing/pairing-store.js";
import type { resolveAgentRoute } from "../../../routing/resolve-route.js"; import type { resolveAgentRoute } from "../../../routing/resolve-route.js";
import { jidToE164, normalizeE164 } from "../../../utils.js"; import { jidToE164, normalizeE164 } from "../../../utils.js";
import { resolveWhatsAppAccount } from "../../accounts.js";
import { newConnectionId } from "../../reconnect.js"; import { newConnectionId } from "../../reconnect.js";
import { formatError } from "../../session.js"; import { formatError } from "../../session.js";
import { deliverWebReply } from "../deliver-reply.js"; import { deliverWebReply } from "../deliver-reply.js";
@@ -73,10 +74,11 @@ async function resolveWhatsAppCommandAuthorized(params: {
return false; return false;
} }
const configuredAllowFrom = params.cfg.channels?.whatsapp?.allowFrom ?? []; const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.msg.accountId });
const dmPolicy = account.dmPolicy ?? "pairing";
const configuredAllowFrom = account.allowFrom ?? [];
const configuredGroupAllowFrom = const configuredGroupAllowFrom =
params.cfg.channels?.whatsapp?.groupAllowFrom ?? account.groupAllowFrom ?? (configuredAllowFrom.length > 0 ? configuredAllowFrom : undefined);
(configuredAllowFrom.length > 0 ? configuredAllowFrom : undefined);
if (isGroup) { if (isGroup) {
if (!configuredGroupAllowFrom || configuredGroupAllowFrom.length === 0) { if (!configuredGroupAllowFrom || configuredGroupAllowFrom.length === 0) {
@@ -88,11 +90,12 @@ async function resolveWhatsAppCommandAuthorized(params: {
return normalizeAllowFromE164(configuredGroupAllowFrom).includes(senderE164); return normalizeAllowFromE164(configuredGroupAllowFrom).includes(senderE164);
} }
const storeAllowFrom = await readChannelAllowFromStore( const storeAllowFrom =
"whatsapp", dmPolicy === "allowlist"
process.env, ? []
params.msg.accountId, : await readChannelAllowFromStore("whatsapp", process.env, params.msg.accountId).catch(
).catch(() => []); () => [],
);
const combinedAllowFrom = Array.from( const combinedAllowFrom = Array.from(
new Set([...(configuredAllowFrom ?? []), ...storeAllowFrom]), new Set([...(configuredAllowFrom ?? []), ...storeAllowFrom]),
); );

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
readAllowFromStoreMock,
sendMessageMock, sendMessageMock,
setAccessControlTestConfig, setAccessControlTestConfig,
setupAccessControlTestHarness, setupAccessControlTestHarness,
@@ -108,4 +109,25 @@ describe("WhatsApp dmPolicy precedence", () => {
const result = await checkUnauthorizedWorkDmSender(); const result = await checkUnauthorizedWorkDmSender();
expectSilentlyBlocked(result); expectSilentlyBlocked(result);
}); });
it("does not merge persisted pairing approvals in allowlist mode", async () => {
setAccessControlTestConfig({
channels: {
whatsapp: {
dmPolicy: "allowlist",
accounts: {
work: {
allowFrom: ["+15559999999"],
},
},
},
},
});
readAllowFromStoreMock.mockResolvedValue(["+15550001111"]);
const result = await checkUnauthorizedWorkDmSender();
expectSilentlyBlocked(result);
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
});
}); });

View File

@@ -40,11 +40,10 @@ export async function checkInboundAccessControl(params: {
}); });
const dmPolicy = account.dmPolicy ?? "pairing"; const dmPolicy = account.dmPolicy ?? "pairing";
const configuredAllowFrom = account.allowFrom; const configuredAllowFrom = account.allowFrom;
const storeAllowFrom = await readChannelAllowFromStore( const storeAllowFrom =
"whatsapp", dmPolicy === "allowlist"
process.env, ? []
account.accountId, : await readChannelAllowFromStore("whatsapp", process.env, account.accountId).catch(() => []);
).catch(() => []);
// Without user config, default to self-only DM access so the owner can talk to themselves. // Without user config, default to self-only DM access so the owner can talk to themselves.
const combinedAllowFrom = Array.from( const combinedAllowFrom = Array.from(
new Set([...(configuredAllowFrom ?? []), ...storeAllowFrom]), new Set([...(configuredAllowFrom ?? []), ...storeAllowFrom]),