refactor: share route-level group gating decisions

This commit is contained in:
Peter Steinberger
2026-03-07 22:57:39 +00:00
parent 5bbca5be91
commit c91bfa830a
9 changed files with 191 additions and 61 deletions

View File

@@ -1,6 +1,7 @@
import {
GROUP_POLICY_BLOCKED_LABEL,
createScopedPairingAccess,
evaluateGroupRouteAccessForPolicy,
issuePairingChallenge,
isDangerousNameMatchingEnabled,
resolveAllowlistProviderRuntimeGroupPolicy,
@@ -195,24 +196,23 @@ export async function applyGoogleChatInboundAccessPolicy(params: {
let effectiveWasMentioned: boolean | undefined;
if (isGroup) {
if (groupPolicy === "disabled") {
logVerbose(`drop group message (groupPolicy=disabled, space=${spaceId})`);
return { ok: false };
}
const groupAllowlistConfigured = groupConfigResolved.allowlistConfigured;
const groupAllowed = Boolean(groupEntry) || Boolean((account.config.groups ?? {})["*"]);
if (groupPolicy === "allowlist") {
if (!groupAllowlistConfigured) {
const routeAccess = evaluateGroupRouteAccessForPolicy({
groupPolicy,
routeAllowlistConfigured: groupAllowlistConfigured,
routeMatched: Boolean(groupEntry),
routeEnabled: groupEntry?.enabled !== false && groupEntry?.allow !== false,
});
if (!routeAccess.allowed) {
if (routeAccess.reason === "disabled") {
logVerbose(`drop group message (groupPolicy=disabled, space=${spaceId})`);
} else if (routeAccess.reason === "empty_allowlist") {
logVerbose(`drop group message (groupPolicy=allowlist, no allowlist, space=${spaceId})`);
return { ok: false };
}
if (!groupAllowed) {
} else if (routeAccess.reason === "route_not_allowlisted") {
logVerbose(`drop group message (not allowlisted, space=${spaceId})`);
return { ok: false };
} else if (routeAccess.reason === "route_disabled") {
logVerbose(`drop group message (space disabled, space=${spaceId})`);
}
}
if (groupEntry?.enabled === false || groupEntry?.allow === false) {
logVerbose(`drop group message (space disabled, space=${spaceId})`);
return { ok: false };
}

View File

@@ -5,6 +5,7 @@ import {
createReplyPrefixOptions,
createTypingCallbacks,
dispatchReplyFromConfigWithSettledDispatcher,
evaluateGroupRouteAccessForPolicy,
formatAllowlistMatchMeta,
logInboundDrop,
logTypingFailure,
@@ -194,10 +195,6 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
});
const isRoom = !isDirectMessage;
if (isRoom && groupPolicy === "disabled") {
return;
}
const roomConfigInfo = isRoom
? resolveMatrixRoomConfig({
rooms: roomsConfig,
@@ -213,17 +210,21 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
}`
: "matchKey=none matchSource=none";
if (isRoom && roomConfig && !roomConfigInfo?.allowed) {
logVerboseMessage(`matrix: room disabled room=${roomId} (${roomMatchMeta})`);
return;
}
if (isRoom && groupPolicy === "allowlist") {
if (!roomConfigInfo?.allowlistConfigured) {
logVerboseMessage(`matrix: drop room message (no allowlist, ${roomMatchMeta})`);
return;
}
if (!roomConfig) {
logVerboseMessage(`matrix: drop room message (not in allowlist, ${roomMatchMeta})`);
if (isRoom) {
const routeAccess = evaluateGroupRouteAccessForPolicy({
groupPolicy,
routeAllowlistConfigured: Boolean(roomConfigInfo?.allowlistConfigured),
routeMatched: Boolean(roomConfig),
routeEnabled: roomConfigInfo?.allowed ?? true,
});
if (!routeAccess.allowed) {
if (routeAccess.reason === "route_disabled") {
logVerboseMessage(`matrix: room disabled room=${roomId} (${roomMatchMeta})`);
} else if (routeAccess.reason === "empty_allowlist") {
logVerboseMessage(`matrix: drop room message (no allowlist, ${roomMatchMeta})`);
} else if (routeAccess.reason === "route_not_allowlisted") {
logVerboseMessage(`matrix: drop room message (not in allowlist, ${roomMatchMeta})`);
}
return;
}
}

View File

@@ -8,6 +8,7 @@ import {
createTypingCallbacks,
createScopedPairingAccess,
createReplyPrefixOptions,
evaluateGroupRouteAccessForPolicy,
issuePairingChallenge,
resolveOutboundMediaUrls,
mergeAllowlist,
@@ -94,28 +95,6 @@ function isSenderAllowed(senderId: string | undefined, allowFrom: string[]): boo
});
}
function isGroupAllowed(params: {
groupId: string;
groupName?: string | null;
groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
}): boolean {
const groups = params.groups ?? {};
const keys = Object.keys(groups);
if (keys.length === 0) {
return false;
}
const entry = findZalouserGroupEntry(
groups,
buildZalouserGroupCandidates({
groupId: params.groupId,
groupName: params.groupName,
includeGroupIdAlias: true,
includeWildcard: true,
}),
);
return isZalouserGroupEntryAllowed(entry);
}
function resolveGroupRequireMention(params: {
groupId: string;
groupName?: string | null;
@@ -223,16 +202,36 @@ async function processMessage(
const groups = account.config.groups ?? {};
if (isGroup) {
if (groupPolicy === "disabled") {
logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
return;
}
if (groupPolicy === "allowlist") {
const allowed = isGroupAllowed({ groupId: chatId, groupName, groups });
if (!allowed) {
const groupEntry = findZalouserGroupEntry(
groups,
buildZalouserGroupCandidates({
groupId: chatId,
groupName,
includeGroupIdAlias: true,
includeWildcard: true,
}),
);
const routeAccess = evaluateGroupRouteAccessForPolicy({
groupPolicy,
routeAllowlistConfigured: Object.keys(groups).length > 0,
routeMatched: Boolean(groupEntry),
routeEnabled: isZalouserGroupEntryAllowed(groupEntry),
});
if (!routeAccess.allowed) {
if (routeAccess.reason === "disabled") {
logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
} else if (routeAccess.reason === "empty_allowlist") {
logVerbose(
core,
runtime,
`zalouser: drop group ${chatId} (groupPolicy=allowlist, no allowlist)`,
);
} else if (routeAccess.reason === "route_not_allowlisted") {
logVerbose(core, runtime, `zalouser: drop group ${chatId} (not allowlisted)`);
return;
} else if (routeAccess.reason === "route_disabled") {
logVerbose(core, runtime, `zalouser: drop group ${chatId} (group disabled)`);
}
return;
}
}

View File

@@ -73,7 +73,10 @@ export type { WizardPrompter } from "../wizard/prompts.js";
export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope.js";
export { createScopedPairingAccess } from "./pairing-access.js";
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
export { resolveSenderScopedGroupPolicy } from "./group-access.js";
export {
evaluateGroupRouteAccessForPolicy,
resolveSenderScopedGroupPolicy,
} from "./group-access.js";
export { extractToolSend } from "./tool-send.js";
export { resolveWebhookPath } from "./webhook-path.js";
export type { WebhookInFlightLimiter } from "./webhook-request-guards.js";

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import {
evaluateGroupRouteAccessForPolicy,
evaluateSenderGroupAccess,
evaluateSenderGroupAccessForPolicy,
resolveSenderScopedGroupPolicy,
@@ -59,6 +60,66 @@ describe("evaluateSenderGroupAccessForPolicy", () => {
});
});
describe("evaluateGroupRouteAccessForPolicy", () => {
it("blocks disabled policy", () => {
expect(
evaluateGroupRouteAccessForPolicy({
groupPolicy: "disabled",
routeAllowlistConfigured: true,
routeMatched: true,
routeEnabled: true,
}),
).toEqual({
allowed: false,
groupPolicy: "disabled",
reason: "disabled",
});
});
it("blocks allowlist without configured routes", () => {
expect(
evaluateGroupRouteAccessForPolicy({
groupPolicy: "allowlist",
routeAllowlistConfigured: false,
routeMatched: false,
}),
).toEqual({
allowed: false,
groupPolicy: "allowlist",
reason: "empty_allowlist",
});
});
it("blocks unmatched allowlist route", () => {
expect(
evaluateGroupRouteAccessForPolicy({
groupPolicy: "allowlist",
routeAllowlistConfigured: true,
routeMatched: false,
}),
).toEqual({
allowed: false,
groupPolicy: "allowlist",
reason: "route_not_allowlisted",
});
});
it("blocks disabled matched route even when group policy is open", () => {
expect(
evaluateGroupRouteAccessForPolicy({
groupPolicy: "open",
routeAllowlistConfigured: true,
routeMatched: true,
routeEnabled: false,
}),
).toEqual({
allowed: false,
groupPolicy: "open",
reason: "route_disabled",
});
});
});
describe("evaluateSenderGroupAccess", () => {
it("defaults missing provider config to allowlist", () => {
const decision = evaluateSenderGroupAccess({

View File

@@ -14,6 +14,19 @@ export type SenderGroupAccessDecision = {
reason: SenderGroupAccessReason;
};
export type GroupRouteAccessReason =
| "allowed"
| "disabled"
| "empty_allowlist"
| "route_not_allowlisted"
| "route_disabled";
export type GroupRouteAccessDecision = {
allowed: boolean;
groupPolicy: GroupPolicy;
reason: GroupRouteAccessReason;
};
export function resolveSenderScopedGroupPolicy(params: {
groupPolicy: GroupPolicy;
groupAllowFrom: string[];
@@ -24,6 +37,52 @@ export function resolveSenderScopedGroupPolicy(params: {
return params.groupAllowFrom.length > 0 ? "allowlist" : "open";
}
export function evaluateGroupRouteAccessForPolicy(params: {
groupPolicy: GroupPolicy;
routeAllowlistConfigured: boolean;
routeMatched: boolean;
routeEnabled?: boolean;
}): GroupRouteAccessDecision {
if (params.groupPolicy === "disabled") {
return {
allowed: false,
groupPolicy: params.groupPolicy,
reason: "disabled",
};
}
if (params.routeMatched && params.routeEnabled === false) {
return {
allowed: false,
groupPolicy: params.groupPolicy,
reason: "route_disabled",
};
}
if (params.groupPolicy === "allowlist") {
if (!params.routeAllowlistConfigured) {
return {
allowed: false,
groupPolicy: params.groupPolicy,
reason: "empty_allowlist",
};
}
if (!params.routeMatched) {
return {
allowed: false,
groupPolicy: params.groupPolicy,
reason: "route_not_allowlisted",
};
}
}
return {
allowed: true,
groupPolicy: params.groupPolicy,
reason: "allowed",
};
}
export function evaluateSenderGroupAccessForPolicy(params: {
groupPolicy: GroupPolicy;
providerMissingFallbackApplied?: boolean;

View File

@@ -278,9 +278,12 @@ export {
isNormalizedSenderAllowed,
} from "./allow-from.js";
export {
evaluateGroupRouteAccessForPolicy,
evaluateSenderGroupAccess,
evaluateSenderGroupAccessForPolicy,
resolveSenderScopedGroupPolicy,
type GroupRouteAccessDecision,
type GroupRouteAccessReason,
type SenderGroupAccessDecision,
type SenderGroupAccessReason,
} from "./group-access.js";

View File

@@ -93,7 +93,10 @@ export {
} from "../security/dm-policy-shared.js";
export { formatDocsLink } from "../terminal/links.js";
export type { WizardPrompter } from "../wizard/prompts.js";
export { resolveSenderScopedGroupPolicy } from "./group-access.js";
export {
evaluateGroupRouteAccessForPolicy,
resolveSenderScopedGroupPolicy,
} from "./group-access.js";
export { createScopedPairingAccess } from "./pairing-access.js";
export { formatResolvedUnresolvedNote } from "./resolution-notes.js";
export { runPluginCommandWithTimeout } from "./run-command.js";

View File

@@ -59,6 +59,7 @@ export type { WizardPrompter } from "../wizard/prompts.js";
export { formatAllowFromLowercase } from "./allow-from.js";
export { resolveSenderCommandAuthorization } from "./command-auth.js";
export { resolveChannelAccountConfigBasePath } from "./config-paths.js";
export { evaluateGroupRouteAccessForPolicy } from "./group-access.js";
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
export { createScopedPairingAccess } from "./pairing-access.js";
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";