mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 20:04:32 +00:00
fix: enforce strict allowlist across pairing stores (#23017)
This commit is contained in:
committed by
GitHub
parent
617e38cec0
commit
0bd9f0d4ac
@@ -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,
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) : "";
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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]),
|
||||||
|
|||||||
Reference in New Issue
Block a user