fix(security): centralize dm/group allowlist auth composition

This commit is contained in:
Peter Steinberger
2026-02-26 16:35:18 +01:00
parent 7f863e22b0
commit 051fdcc428
8 changed files with 428 additions and 108 deletions

View File

@@ -44,7 +44,7 @@ export function isMattermostSenderAllowed(params: {
allowFrom: string[];
allowNameMatching?: boolean;
}): boolean {
const allowFrom = params.allowFrom;
const allowFrom = normalizeMattermostAllowList(params.allowFrom);
if (allowFrom.length === 0) {
return false;
}

View File

@@ -37,11 +37,7 @@ import {
type MattermostPost,
type MattermostUser,
} from "./client.js";
import {
isMattermostSenderAllowed,
normalizeMattermostAllowList,
resolveMattermostEffectiveAllowFromLists,
} from "./monitor-auth.js";
import { isMattermostSenderAllowed, normalizeMattermostAllowList } from "./monitor-auth.js";
import {
createDedupeCache,
formatInboundFromLabel,
@@ -360,18 +356,32 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
senderId;
const rawText = post.message?.trim() || "";
const dmPolicy = account.config.dmPolicy ?? "pairing";
const normalizedAllowFrom = normalizeMattermostAllowList(account.config.allowFrom ?? []);
const normalizedGroupAllowFrom = normalizeMattermostAllowList(
account.config.groupAllowFrom ?? [],
);
const storeAllowFrom = normalizeMattermostAllowList(
dmPolicy === "allowlist"
? []
: await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []),
);
const { effectiveAllowFrom, effectiveGroupAllowFrom } =
resolveMattermostEffectiveAllowFromLists({
dmPolicy,
allowFrom: account.config.allowFrom,
groupAllowFrom: account.config.groupAllowFrom,
storeAllowFrom,
});
const accessDecision = resolveDmGroupAccessWithLists({
isGroup: kind !== "direct",
dmPolicy,
groupPolicy,
allowFrom: normalizedAllowFrom,
groupAllowFrom: normalizedGroupAllowFrom,
storeAllowFrom,
isSenderAllowed: (allowFrom) =>
isMattermostSenderAllowed({
senderId,
senderName,
allowFrom,
allowNameMatching,
}),
});
const effectiveAllowFrom = accessDecision.effectiveAllowFrom;
const effectiveGroupAllowFrom = accessDecision.effectiveGroupAllowFrom;
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
cfg,
surface: "mattermost",
@@ -404,17 +414,15 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
hasControlCommand,
});
const commandAuthorized =
kind === "direct"
? dmPolicy === "open" || senderAllowedForCommands
: commandGate.commandAuthorized;
kind === "direct" ? accessDecision.decision === "allow" : commandGate.commandAuthorized;
if (kind === "direct") {
if (dmPolicy === "disabled") {
logVerboseMessage(`mattermost: drop dm (dmPolicy=disabled sender=${senderId})`);
return;
}
if (dmPolicy !== "open" && !senderAllowedForCommands) {
if (dmPolicy === "pairing") {
if (accessDecision.decision !== "allow") {
if (kind === "direct") {
if (accessDecision.reason === "dmPolicy=disabled") {
logVerboseMessage(`mattermost: drop dm (dmPolicy=disabled sender=${senderId})`);
return;
}
if (accessDecision.decision === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: "mattermost",
id: senderId,
@@ -437,26 +445,27 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
logVerboseMessage(`mattermost: pairing reply failed for ${senderId}: ${String(err)}`);
}
}
} else {
logVerboseMessage(`mattermost: drop dm sender=${senderId} (dmPolicy=${dmPolicy})`);
return;
}
logVerboseMessage(`mattermost: drop dm sender=${senderId} (dmPolicy=${dmPolicy})`);
return;
}
} else {
if (groupPolicy === "disabled") {
if (accessDecision.reason === "groupPolicy=disabled") {
logVerboseMessage("mattermost: drop group message (groupPolicy=disabled)");
return;
}
if (groupPolicy === "allowlist") {
if (effectiveGroupAllowFrom.length === 0) {
logVerboseMessage("mattermost: drop group message (no group allowlist)");
return;
}
if (!groupAllowedForCommands) {
logVerboseMessage(`mattermost: drop group sender=${senderId} (not in groupAllowFrom)`);
return;
}
if (accessDecision.reason === "groupPolicy=allowlist (empty allowlist)") {
logVerboseMessage("mattermost: drop group message (no group allowlist)");
return;
}
if (accessDecision.reason === "groupPolicy=allowlist (not allowlisted)") {
logVerboseMessage(`mattermost: drop group sender=${senderId} (not in groupAllowFrom)`);
return;
}
logVerboseMessage(
`mattermost: drop group message (groupPolicy=${groupPolicy} reason=${accessDecision.reason})`,
);
return;
}
if (kind !== "direct" && commandGate.shouldBlock) {
@@ -852,14 +861,14 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
isGroup: kind !== "direct",
dmPolicy,
groupPolicy,
allowFrom: account.config.allowFrom,
groupAllowFrom: account.config.groupAllowFrom,
allowFrom: normalizeMattermostAllowList(account.config.allowFrom ?? []),
groupAllowFrom: normalizeMattermostAllowList(account.config.groupAllowFrom ?? []),
storeAllowFrom,
isSenderAllowed: (allowFrom) =>
isMattermostSenderAllowed({
senderId: userId,
senderName,
allowFrom: normalizeMattermostAllowList(allowFrom),
allowFrom,
allowNameMatching,
}),
});

View File

@@ -146,18 +146,15 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
});
const effectiveDmAllowFrom = resolvedAllowFromLists.effectiveAllowFrom;
if (isDirectMessage && msteamsCfg) {
const allowFrom = dmAllowFrom;
if (dmPolicy === "disabled") {
log.debug?.("dropping dm (dms disabled)");
return;
}
if (dmPolicy !== "open") {
const effectiveAllowFrom = [...allowFrom.map((v) => String(v)), ...storedAllowFrom];
const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
const allowMatch = resolveMSTeamsAllowlistMatch({
allowFrom: effectiveAllowFrom,
allowFrom: effectiveDmAllowFrom,
senderId,
senderName,
allowNameMatching,