mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 07:21:23 +00:00
refactor(bluebubbles): share dm/group access policy checks
This commit is contained in:
@@ -310,6 +310,11 @@ export {
|
||||
readStringParam,
|
||||
} from "../agents/tools/common.js";
|
||||
export { formatDocsLink } from "../terminal/links.js";
|
||||
export {
|
||||
resolveDmAllowState,
|
||||
resolveDmGroupAccessDecision,
|
||||
resolveEffectiveAllowFromLists,
|
||||
} from "../security/dm-policy-shared.js";
|
||||
export type { HookEntry } from "../hooks/types.js";
|
||||
export { clamp, escapeRegExp, normalizeE164, safeParseJson, sleep } from "../utils.js";
|
||||
export { stripAnsi } from "../terminal/ansi.js";
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveDmAllowState } from "./dm-policy-shared.js";
|
||||
import {
|
||||
resolveDmAllowState,
|
||||
resolveDmGroupAccessDecision,
|
||||
resolveEffectiveAllowFromLists,
|
||||
} from "./dm-policy-shared.js";
|
||||
|
||||
describe("security/dm-policy-shared", () => {
|
||||
it("normalizes config + store allow entries and counts distinct senders", async () => {
|
||||
@@ -28,4 +32,94 @@ describe("security/dm-policy-shared", () => {
|
||||
expect(state.allowCount).toBe(0);
|
||||
expect(state.isMultiUserDm).toBe(false);
|
||||
});
|
||||
|
||||
it("builds effective DM/group allowlists from config + pairing store", () => {
|
||||
const lists = resolveEffectiveAllowFromLists({
|
||||
allowFrom: [" owner ", "", "owner2"],
|
||||
groupAllowFrom: ["group:abc"],
|
||||
storeAllowFrom: [" owner3 ", ""],
|
||||
});
|
||||
expect(lists.effectiveAllowFrom).toEqual(["owner", "owner2", "owner3"]);
|
||||
expect(lists.effectiveGroupAllowFrom).toEqual(["group:abc", "owner3"]);
|
||||
});
|
||||
|
||||
it("falls back to DM allowlist for groups when groupAllowFrom is empty", () => {
|
||||
const lists = resolveEffectiveAllowFromLists({
|
||||
allowFrom: [" owner "],
|
||||
groupAllowFrom: [],
|
||||
storeAllowFrom: [" owner2 "],
|
||||
});
|
||||
expect(lists.effectiveAllowFrom).toEqual(["owner", "owner2"]);
|
||||
expect(lists.effectiveGroupAllowFrom).toEqual(["owner", "owner2"]);
|
||||
});
|
||||
|
||||
const channels = [
|
||||
"bluebubbles",
|
||||
"imessage",
|
||||
"signal",
|
||||
"telegram",
|
||||
"whatsapp",
|
||||
"msteams",
|
||||
"matrix",
|
||||
"zalo",
|
||||
] as const;
|
||||
|
||||
for (const channel of channels) {
|
||||
it(`[${channel}] blocks DM allowlist mode when allowlist is empty`, () => {
|
||||
const decision = resolveDmGroupAccessDecision({
|
||||
isGroup: false,
|
||||
dmPolicy: "allowlist",
|
||||
groupPolicy: "allowlist",
|
||||
effectiveAllowFrom: [],
|
||||
effectiveGroupAllowFrom: [],
|
||||
isSenderAllowed: () => false,
|
||||
});
|
||||
expect(decision).toEqual({
|
||||
decision: "block",
|
||||
reason: "dmPolicy=allowlist (not allowlisted)",
|
||||
});
|
||||
});
|
||||
|
||||
it(`[${channel}] uses pairing flow when DM sender is not allowlisted`, () => {
|
||||
const decision = resolveDmGroupAccessDecision({
|
||||
isGroup: false,
|
||||
dmPolicy: "pairing",
|
||||
groupPolicy: "allowlist",
|
||||
effectiveAllowFrom: [],
|
||||
effectiveGroupAllowFrom: [],
|
||||
isSenderAllowed: () => false,
|
||||
});
|
||||
expect(decision).toEqual({
|
||||
decision: "pairing",
|
||||
reason: "dmPolicy=pairing (not allowlisted)",
|
||||
});
|
||||
});
|
||||
|
||||
it(`[${channel}] allows DM sender when allowlisted`, () => {
|
||||
const decision = resolveDmGroupAccessDecision({
|
||||
isGroup: false,
|
||||
dmPolicy: "allowlist",
|
||||
groupPolicy: "allowlist",
|
||||
effectiveAllowFrom: ["owner"],
|
||||
effectiveGroupAllowFrom: [],
|
||||
isSenderAllowed: () => true,
|
||||
});
|
||||
expect(decision.decision).toBe("allow");
|
||||
});
|
||||
|
||||
it(`[${channel}] blocks group allowlist mode when sender/group is not allowlisted`, () => {
|
||||
const decision = resolveDmGroupAccessDecision({
|
||||
isGroup: true,
|
||||
dmPolicy: "pairing",
|
||||
groupPolicy: "allowlist",
|
||||
effectiveAllowFrom: ["owner"],
|
||||
effectiveGroupAllowFrom: ["group:abc"],
|
||||
isSenderAllowed: () => false,
|
||||
});
|
||||
expect(decision).toEqual({
|
||||
decision: "block",
|
||||
reason: "groupPolicy=allowlist (not allowlisted)",
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,77 @@ import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
|
||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
|
||||
export function resolveEffectiveAllowFromLists(params: {
|
||||
allowFrom?: Array<string | number> | null;
|
||||
groupAllowFrom?: Array<string | number> | null;
|
||||
storeAllowFrom?: Array<string | number> | null;
|
||||
}): {
|
||||
effectiveAllowFrom: string[];
|
||||
effectiveGroupAllowFrom: string[];
|
||||
} {
|
||||
const configAllowFrom = normalizeStringEntries(
|
||||
Array.isArray(params.allowFrom) ? params.allowFrom : undefined,
|
||||
);
|
||||
const configGroupAllowFrom = normalizeStringEntries(
|
||||
Array.isArray(params.groupAllowFrom) ? params.groupAllowFrom : undefined,
|
||||
);
|
||||
const storeAllowFrom = normalizeStringEntries(
|
||||
Array.isArray(params.storeAllowFrom) ? params.storeAllowFrom : undefined,
|
||||
);
|
||||
const effectiveAllowFrom = normalizeStringEntries([...configAllowFrom, ...storeAllowFrom]);
|
||||
const groupBase = configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom;
|
||||
const effectiveGroupAllowFrom = normalizeStringEntries([...groupBase, ...storeAllowFrom]);
|
||||
return { effectiveAllowFrom, effectiveGroupAllowFrom };
|
||||
}
|
||||
|
||||
export type DmGroupAccessDecision = "allow" | "block" | "pairing";
|
||||
|
||||
export function resolveDmGroupAccessDecision(params: {
|
||||
isGroup: boolean;
|
||||
dmPolicy?: string | null;
|
||||
groupPolicy?: string | null;
|
||||
effectiveAllowFrom: Array<string | number>;
|
||||
effectiveGroupAllowFrom: Array<string | number>;
|
||||
isSenderAllowed: (allowFrom: string[]) => boolean;
|
||||
}): {
|
||||
decision: DmGroupAccessDecision;
|
||||
reason: string;
|
||||
} {
|
||||
const dmPolicy = params.dmPolicy ?? "pairing";
|
||||
const groupPolicy = params.groupPolicy ?? "allowlist";
|
||||
const effectiveAllowFrom = normalizeStringEntries(params.effectiveAllowFrom);
|
||||
const effectiveGroupAllowFrom = normalizeStringEntries(params.effectiveGroupAllowFrom);
|
||||
|
||||
if (params.isGroup) {
|
||||
if (groupPolicy === "disabled") {
|
||||
return { decision: "block", reason: "groupPolicy=disabled" };
|
||||
}
|
||||
if (groupPolicy === "allowlist") {
|
||||
if (effectiveGroupAllowFrom.length === 0) {
|
||||
return { decision: "block", reason: "groupPolicy=allowlist (empty allowlist)" };
|
||||
}
|
||||
if (!params.isSenderAllowed(effectiveGroupAllowFrom)) {
|
||||
return { decision: "block", reason: "groupPolicy=allowlist (not allowlisted)" };
|
||||
}
|
||||
}
|
||||
return { decision: "allow", reason: `groupPolicy=${groupPolicy}` };
|
||||
}
|
||||
|
||||
if (dmPolicy === "disabled") {
|
||||
return { decision: "block", reason: "dmPolicy=disabled" };
|
||||
}
|
||||
if (dmPolicy === "open") {
|
||||
return { decision: "allow", reason: "dmPolicy=open" };
|
||||
}
|
||||
if (params.isSenderAllowed(effectiveAllowFrom)) {
|
||||
return { decision: "allow", reason: `dmPolicy=${dmPolicy} (allowlisted)` };
|
||||
}
|
||||
if (dmPolicy === "pairing") {
|
||||
return { decision: "pairing", reason: "dmPolicy=pairing (not allowlisted)" };
|
||||
}
|
||||
return { decision: "block", reason: `dmPolicy=${dmPolicy} (not allowlisted)` };
|
||||
}
|
||||
|
||||
export async function resolveDmAllowState(params: {
|
||||
provider: ChannelId;
|
||||
allowFrom?: Array<string | number> | null;
|
||||
|
||||
Reference in New Issue
Block a user