refactor(security): centralize channel allowlist auth policy

This commit is contained in:
Peter Steinberger
2026-02-26 13:06:27 +01:00
parent eac86c2081
commit 892a9c24b0
12 changed files with 137 additions and 90 deletions

View File

@@ -55,6 +55,16 @@ describe("resolveGroupAllowFromSources", () => {
}),
).toEqual(["owner", "owner2"]);
});
it("can disable fallback to DM allowlist", () => {
expect(
resolveGroupAllowFromSources({
allowFrom: ["owner", "owner2"],
groupAllowFrom: [],
fallbackToAllowFrom: false,
}),
).toEqual([]);
});
});
describe("firstDefined", () => {

View File

@@ -12,10 +12,16 @@ export function mergeDmAllowFromSources(params: {
export function resolveGroupAllowFromSources(params: {
allowFrom?: Array<string | number>;
groupAllowFrom?: Array<string | number>;
fallbackToAllowFrom?: boolean;
}): string[] {
const scoped =
params.groupAllowFrom && params.groupAllowFrom.length > 0
const explicitGroupAllowFrom =
Array.isArray(params.groupAllowFrom) && params.groupAllowFrom.length > 0
? params.groupAllowFrom
: undefined;
const scoped = explicitGroupAllowFrom
? explicitGroupAllowFrom
: params.fallbackToAllowFrom === false
? []
: (params.allowFrom ?? []);
return scoped.map((value) => String(value).trim()).filter(Boolean);
}

View File

@@ -20,6 +20,7 @@ import {
resolveChannelGroupRequireMention,
} from "../../config/group-policy.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { resolveEffectiveAllowFromLists } from "../../security/dm-policy-shared.js";
import { truncateUtf16Safe } from "../../utils.js";
import {
formatIMessageChatTarget,
@@ -138,14 +139,14 @@ export function resolveIMessageInboundDecision(params: {
}
const groupId = isGroup ? groupIdCandidate : undefined;
const storeAllowFrom = params.dmPolicy === "allowlist" ? [] : params.storeAllowFrom;
const effectiveDmAllowFrom = Array.from(new Set([...params.allowFrom, ...storeAllowFrom]))
.map((v) => String(v).trim())
.filter(Boolean);
// Keep DM pairing-store authorization scoped to DMs; group access must come from explicit group allowlist config.
const effectiveGroupAllowFrom = Array.from(new Set(params.groupAllowFrom))
.map((v) => String(v).trim())
.filter(Boolean);
const { effectiveAllowFrom: effectiveDmAllowFrom, effectiveGroupAllowFrom } =
resolveEffectiveAllowFromLists({
allowFrom: params.allowFrom,
groupAllowFrom: params.groupAllowFrom,
storeAllowFrom: params.storeAllowFrom,
dmPolicy: params.dmPolicy,
groupAllowFromFallbackToAllowFrom: false,
});
if (isGroup) {
if (params.groupPolicy === "disabled") {

View File

@@ -54,6 +54,17 @@ describe("security/dm-policy-shared", () => {
expect(lists.effectiveGroupAllowFrom).toEqual(["owner"]);
});
it("can keep group allowlist empty when fallback is disabled", () => {
const lists = resolveEffectiveAllowFromLists({
allowFrom: ["owner"],
groupAllowFrom: [],
storeAllowFrom: ["paired-user"],
groupAllowFromFallbackToAllowFrom: false,
});
expect(lists.effectiveAllowFrom).toEqual(["owner", "paired-user"]);
expect(lists.effectiveGroupAllowFrom).toEqual([]);
});
it("excludes storeAllowFrom when dmPolicy is allowlist", () => {
const lists = resolveEffectiveAllowFromLists({
allowFrom: ["+1111"],

View File

@@ -8,6 +8,7 @@ export function resolveEffectiveAllowFromLists(params: {
groupAllowFrom?: Array<string | number> | null;
storeAllowFrom?: Array<string | number> | null;
dmPolicy?: string | null;
groupAllowFromFallbackToAllowFrom?: boolean | null;
}): {
effectiveAllowFrom: string[];
effectiveGroupAllowFrom: string[];
@@ -27,6 +28,7 @@ export function resolveEffectiveAllowFromLists(params: {
resolveGroupAllowFromSources({
allowFrom,
groupAllowFrom,
fallbackToAllowFrom: params.groupAllowFromFallbackToAllowFrom ?? undefined,
}),
);
return { effectiveAllowFrom, effectiveGroupAllowFrom };
@@ -87,6 +89,7 @@ export function resolveDmGroupAccessWithLists(params: {
allowFrom?: Array<string | number> | null;
groupAllowFrom?: Array<string | number> | null;
storeAllowFrom?: Array<string | number> | null;
groupAllowFromFallbackToAllowFrom?: boolean | null;
isSenderAllowed: (allowFrom: string[]) => boolean;
}): {
decision: DmGroupAccessDecision;
@@ -99,6 +102,7 @@ export function resolveDmGroupAccessWithLists(params: {
groupAllowFrom: params.groupAllowFrom,
storeAllowFrom: params.storeAllowFrom,
dmPolicy: params.dmPolicy,
groupAllowFromFallbackToAllowFrom: params.groupAllowFromFallbackToAllowFrom,
});
const access = resolveDmGroupAccessDecision({
isGroup: params.isGroup,

View File

@@ -61,5 +61,6 @@ export async function buildTelegramMessageContextForTest(
groupConfig: { requireMention: false },
topicConfig: undefined,
})),
sendChatActionHandler: { sendChatAction: vi.fn() } as never,
});
}