mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 09:58:38 +00:00
refactor: share missing-sender matched allowlist evaluation
This commit is contained in:
@@ -265,6 +265,40 @@ describe("handleLineWebhookEvents", () => {
|
|||||||
expect(readAllowFromStoreMock).toHaveBeenCalledWith("line", undefined, "default");
|
expect(readAllowFromStoreMock).toHaveBeenCalledWith("line", undefined, "default");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("blocks group messages without sender id when groupPolicy is allowlist", async () => {
|
||||||
|
const processMessage = vi.fn();
|
||||||
|
const event = {
|
||||||
|
type: "message",
|
||||||
|
message: { id: "m5a", type: "text", text: "hi" },
|
||||||
|
replyToken: "reply-token",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
source: { type: "group", groupId: "group-1" },
|
||||||
|
mode: "active",
|
||||||
|
webhookEventId: "evt-5a",
|
||||||
|
deliveryContext: { isRedelivery: false },
|
||||||
|
} as MessageEvent;
|
||||||
|
|
||||||
|
await handleLineWebhookEvents([event], {
|
||||||
|
cfg: {
|
||||||
|
channels: { line: { groupPolicy: "allowlist", groupAllowFrom: ["user-5"] } },
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
accountId: "default",
|
||||||
|
enabled: true,
|
||||||
|
channelAccessToken: "token",
|
||||||
|
channelSecret: "secret",
|
||||||
|
tokenSource: "config",
|
||||||
|
config: { groupPolicy: "allowlist", groupAllowFrom: ["user-5"] },
|
||||||
|
},
|
||||||
|
runtime: createRuntime(),
|
||||||
|
mediaMaxBytes: 1,
|
||||||
|
processMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(processMessage).not.toHaveBeenCalled();
|
||||||
|
expect(buildLineMessageContextMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("does not authorize group messages from DM pairing-store entries when group allowlist is empty", async () => {
|
it("does not authorize group messages from DM pairing-store entries when group allowlist is empty", async () => {
|
||||||
readAllowFromStoreMock.mockResolvedValueOnce(["user-5"]);
|
readAllowFromStoreMock.mockResolvedValueOnce(["user-5"]);
|
||||||
const processMessage = vi.fn();
|
const processMessage = vi.fn();
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
readChannelAllowFromStore,
|
readChannelAllowFromStore,
|
||||||
upsertChannelPairingRequest,
|
upsertChannelPairingRequest,
|
||||||
} from "../pairing/pairing-store.js";
|
} from "../pairing/pairing-store.js";
|
||||||
import { evaluateSenderGroupAccessForPolicy } from "../plugin-sdk/group-access.js";
|
import { evaluateMatchedGroupAccessForPolicy } from "../plugin-sdk/group-access.js";
|
||||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import {
|
import {
|
||||||
@@ -345,29 +345,31 @@ async function shouldProcessLineEvent(
|
|||||||
return denied;
|
return denied;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (groupPolicy === "allowlist" && !senderId) {
|
const senderGroupAccess = evaluateMatchedGroupAccessForPolicy({
|
||||||
logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
|
|
||||||
return denied;
|
|
||||||
}
|
|
||||||
const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
|
|
||||||
groupPolicy,
|
groupPolicy,
|
||||||
groupAllowFrom: effectiveGroupAllow.entries,
|
requireMatchInput: true,
|
||||||
senderId,
|
hasMatchInput: Boolean(senderId),
|
||||||
isSenderAllowed: (candidateSenderId, allowFrom) =>
|
allowlistConfigured: effectiveGroupAllow.entries.length > 0,
|
||||||
|
allowlistMatched:
|
||||||
|
Boolean(senderId) &&
|
||||||
isSenderAllowed({
|
isSenderAllowed({
|
||||||
allow: normalizeAllowFrom(allowFrom),
|
allow: effectiveGroupAllow,
|
||||||
senderId: candidateSenderId,
|
senderId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") {
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") {
|
||||||
logVerbose("Blocked line group message (groupPolicy: disabled)");
|
logVerbose("Blocked line group message (groupPolicy: disabled)");
|
||||||
return denied;
|
return denied;
|
||||||
}
|
}
|
||||||
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "missing_match_input") {
|
||||||
|
logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
|
||||||
|
return denied;
|
||||||
|
}
|
||||||
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
|
||||||
logVerbose("Blocked line group message (groupPolicy: allowlist, no groupAllowFrom)");
|
logVerbose("Blocked line group message (groupPolicy: allowlist, no groupAllowFrom)");
|
||||||
return denied;
|
return denied;
|
||||||
}
|
}
|
||||||
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "sender_not_allowlisted") {
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "not_allowlisted") {
|
||||||
logVerbose(`Blocked line group message from ${senderId} (groupPolicy: allowlist)`);
|
logVerbose(`Blocked line group message from ${senderId} (groupPolicy: allowlist)`);
|
||||||
return denied;
|
return denied;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,22 @@ describe("evaluateMatchedGroupAccessForPolicy", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("blocks allowlist when required match input is missing", () => {
|
||||||
|
expect(
|
||||||
|
evaluateMatchedGroupAccessForPolicy({
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
requireMatchInput: true,
|
||||||
|
hasMatchInput: false,
|
||||||
|
allowlistConfigured: true,
|
||||||
|
allowlistMatched: false,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
reason: "missing_match_input",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("blocks unmatched allowlist sender", () => {
|
it("blocks unmatched allowlist sender", () => {
|
||||||
expect(
|
expect(
|
||||||
evaluateMatchedGroupAccessForPolicy({
|
evaluateMatchedGroupAccessForPolicy({
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type GroupRouteAccessDecision = {
|
|||||||
export type MatchedGroupAccessReason =
|
export type MatchedGroupAccessReason =
|
||||||
| "allowed"
|
| "allowed"
|
||||||
| "disabled"
|
| "disabled"
|
||||||
|
| "missing_match_input"
|
||||||
| "empty_allowlist"
|
| "empty_allowlist"
|
||||||
| "not_allowlisted";
|
| "not_allowlisted";
|
||||||
|
|
||||||
@@ -99,6 +100,8 @@ export function evaluateMatchedGroupAccessForPolicy(params: {
|
|||||||
groupPolicy: GroupPolicy;
|
groupPolicy: GroupPolicy;
|
||||||
allowlistConfigured: boolean;
|
allowlistConfigured: boolean;
|
||||||
allowlistMatched: boolean;
|
allowlistMatched: boolean;
|
||||||
|
requireMatchInput?: boolean;
|
||||||
|
hasMatchInput?: boolean;
|
||||||
}): MatchedGroupAccessDecision {
|
}): MatchedGroupAccessDecision {
|
||||||
if (params.groupPolicy === "disabled") {
|
if (params.groupPolicy === "disabled") {
|
||||||
return {
|
return {
|
||||||
@@ -109,6 +112,13 @@ export function evaluateMatchedGroupAccessForPolicy(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.groupPolicy === "allowlist") {
|
if (params.groupPolicy === "allowlist") {
|
||||||
|
if (params.requireMatchInput && !params.hasMatchInput) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: params.groupPolicy,
|
||||||
|
reason: "missing_match_input",
|
||||||
|
};
|
||||||
|
}
|
||||||
if (!params.allowlistConfigured) {
|
if (!params.allowlistConfigured) {
|
||||||
return {
|
return {
|
||||||
allowed: false,
|
allowed: false,
|
||||||
|
|||||||
@@ -180,6 +180,25 @@ describe("evaluateTelegramGroupPolicyAccess – chat allowlist vs sender allowli
|
|||||||
expect(result).toEqual({ allowed: true, groupPolicy: "allowlist" });
|
expect(result).toEqual({ allowed: true, groupPolicy: "allowlist" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("blocks allowlist groups without sender identity before sender matching", () => {
|
||||||
|
const result = runAccess({
|
||||||
|
senderId: undefined,
|
||||||
|
senderUsername: undefined,
|
||||||
|
effectiveGroupAllow: senderAllow,
|
||||||
|
resolveGroupPolicy: () => ({
|
||||||
|
allowlistEnabled: true,
|
||||||
|
allowed: true,
|
||||||
|
groupConfig: { requireMention: false },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
allowed: false,
|
||||||
|
reason: "group-policy-allowlist-no-sender",
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("allows authorized sender in wildcard-matched group with sender entries", () => {
|
it("allows authorized sender in wildcard-matched group with sender entries", () => {
|
||||||
const result = runAccess({
|
const result = runAccess({
|
||||||
effectiveGroupAllow: senderAllow, // entries: ["111"]
|
effectiveGroupAllow: senderAllow, // entries: ["111"]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
TelegramGroupConfig,
|
TelegramGroupConfig,
|
||||||
TelegramTopicConfig,
|
TelegramTopicConfig,
|
||||||
} from "../config/types.js";
|
} from "../config/types.js";
|
||||||
|
import { evaluateMatchedGroupAccessForPolicy } from "../plugin-sdk/group-access.js";
|
||||||
import { isSenderAllowed, type NormalizedAllowFrom } from "./bot-access.js";
|
import { isSenderAllowed, type NormalizedAllowFrom } from "./bot-access.js";
|
||||||
import { firstDefined } from "./bot-access.js";
|
import { firstDefined } from "./bot-access.js";
|
||||||
|
|
||||||
@@ -174,31 +175,29 @@ export const evaluateTelegramGroupPolicyAccess = (params: {
|
|||||||
}
|
}
|
||||||
if (groupPolicy === "allowlist" && params.enforceAllowlistAuthorization) {
|
if (groupPolicy === "allowlist" && params.enforceAllowlistAuthorization) {
|
||||||
const senderId = params.senderId ?? "";
|
const senderId = params.senderId ?? "";
|
||||||
if (params.requireSenderForAllowlistAuthorization && !senderId) {
|
const senderAuthorization = evaluateMatchedGroupAccessForPolicy({
|
||||||
|
groupPolicy,
|
||||||
|
requireMatchInput: params.requireSenderForAllowlistAuthorization,
|
||||||
|
hasMatchInput: Boolean(senderId),
|
||||||
|
allowlistConfigured:
|
||||||
|
chatExplicitlyAllowed ||
|
||||||
|
params.allowEmptyAllowlistEntries ||
|
||||||
|
params.effectiveGroupAllow.hasEntries,
|
||||||
|
allowlistMatched:
|
||||||
|
(chatExplicitlyAllowed && !params.effectiveGroupAllow.hasEntries) ||
|
||||||
|
isSenderAllowed({
|
||||||
|
allow: params.effectiveGroupAllow,
|
||||||
|
senderId,
|
||||||
|
senderUsername: params.senderUsername ?? "",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!senderAuthorization.allowed && senderAuthorization.reason === "missing_match_input") {
|
||||||
return { allowed: false, reason: "group-policy-allowlist-no-sender", groupPolicy };
|
return { allowed: false, reason: "group-policy-allowlist-no-sender", groupPolicy };
|
||||||
}
|
}
|
||||||
// Skip the "empty allowlist" guard when the chat itself is explicitly
|
if (!senderAuthorization.allowed && senderAuthorization.reason === "empty_allowlist") {
|
||||||
// listed in the groups config — the group ID acts as the allowlist entry.
|
|
||||||
if (
|
|
||||||
!chatExplicitlyAllowed &&
|
|
||||||
!params.allowEmptyAllowlistEntries &&
|
|
||||||
!params.effectiveGroupAllow.hasEntries
|
|
||||||
) {
|
|
||||||
return { allowed: false, reason: "group-policy-allowlist-empty", groupPolicy };
|
return { allowed: false, reason: "group-policy-allowlist-empty", groupPolicy };
|
||||||
}
|
}
|
||||||
// When the chat is explicitly allowed and there are no sender-level entries,
|
if (!senderAuthorization.allowed && senderAuthorization.reason === "not_allowlisted") {
|
||||||
// skip the sender check — the group ID itself is the authorization.
|
|
||||||
if (chatExplicitlyAllowed && !params.effectiveGroupAllow.hasEntries) {
|
|
||||||
return { allowed: true, groupPolicy };
|
|
||||||
}
|
|
||||||
const senderUsername = params.senderUsername ?? "";
|
|
||||||
if (
|
|
||||||
!isSenderAllowed({
|
|
||||||
allow: params.effectiveGroupAllow,
|
|
||||||
senderId,
|
|
||||||
senderUsername,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return { allowed: false, reason: "group-policy-allowlist-unauthorized", groupPolicy };
|
return { allowed: false, reason: "group-policy-allowlist-unauthorized", groupPolicy };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user