mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 14:21:26 +00:00
refactor(zalo): split monitor access and webhook logic
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isAllowedParsedChatSender } from "./allow-from.js";
|
||||
import { isAllowedParsedChatSender, isNormalizedSenderAllowed } from "./allow-from.js";
|
||||
|
||||
function parseAllowTarget(
|
||||
entry: string,
|
||||
@@ -71,3 +71,34 @@ describe("isAllowedParsedChatSender", () => {
|
||||
expect(allowed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isNormalizedSenderAllowed", () => {
|
||||
it("allows wildcard", () => {
|
||||
expect(
|
||||
isNormalizedSenderAllowed({
|
||||
senderId: "attacker",
|
||||
allowFrom: ["*"],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("normalizes case and strips prefixes", () => {
|
||||
expect(
|
||||
isNormalizedSenderAllowed({
|
||||
senderId: "12345",
|
||||
allowFrom: ["ZALO:12345", "zl:777"],
|
||||
stripPrefixRe: /^(zalo|zl):/i,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects when sender is missing", () => {
|
||||
expect(
|
||||
isNormalizedSenderAllowed({
|
||||
senderId: "999",
|
||||
allowFrom: ["zl:12345"],
|
||||
stripPrefixRe: /^(zalo|zl):/i,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,25 @@ export function formatAllowFromLowercase(params: {
|
||||
.map((entry) => entry.toLowerCase());
|
||||
}
|
||||
|
||||
export function isNormalizedSenderAllowed(params: {
|
||||
senderId: string | number;
|
||||
allowFrom: Array<string | number>;
|
||||
stripPrefixRe?: RegExp;
|
||||
}): boolean {
|
||||
const normalizedAllow = formatAllowFromLowercase({
|
||||
allowFrom: params.allowFrom,
|
||||
stripPrefixRe: params.stripPrefixRe,
|
||||
});
|
||||
if (normalizedAllow.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (normalizedAllow.includes("*")) {
|
||||
return true;
|
||||
}
|
||||
const sender = String(params.senderId).trim().toLowerCase();
|
||||
return normalizedAllow.includes(sender);
|
||||
}
|
||||
|
||||
type ParsedChatAllowTarget =
|
||||
| { kind: "chat_id"; chatId: number }
|
||||
| { kind: "chat_guid"; chatGuid: string }
|
||||
|
||||
69
src/plugin-sdk/group-access.test.ts
Normal file
69
src/plugin-sdk/group-access.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { evaluateSenderGroupAccess } from "./group-access.js";
|
||||
|
||||
describe("evaluateSenderGroupAccess", () => {
|
||||
it("defaults missing provider config to allowlist", () => {
|
||||
const decision = evaluateSenderGroupAccess({
|
||||
providerConfigPresent: false,
|
||||
configuredGroupPolicy: undefined,
|
||||
defaultGroupPolicy: "open",
|
||||
groupAllowFrom: ["123"],
|
||||
senderId: "123",
|
||||
isSenderAllowed: () => true,
|
||||
});
|
||||
|
||||
expect(decision).toEqual({
|
||||
allowed: true,
|
||||
groupPolicy: "allowlist",
|
||||
providerMissingFallbackApplied: true,
|
||||
reason: "allowed",
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks disabled policy", () => {
|
||||
const decision = evaluateSenderGroupAccess({
|
||||
providerConfigPresent: true,
|
||||
configuredGroupPolicy: "disabled",
|
||||
defaultGroupPolicy: "open",
|
||||
groupAllowFrom: ["123"],
|
||||
senderId: "123",
|
||||
isSenderAllowed: () => true,
|
||||
});
|
||||
|
||||
expect(decision).toMatchObject({ allowed: false, reason: "disabled", groupPolicy: "disabled" });
|
||||
});
|
||||
|
||||
it("blocks allowlist with empty list", () => {
|
||||
const decision = evaluateSenderGroupAccess({
|
||||
providerConfigPresent: true,
|
||||
configuredGroupPolicy: "allowlist",
|
||||
defaultGroupPolicy: "open",
|
||||
groupAllowFrom: [],
|
||||
senderId: "123",
|
||||
isSenderAllowed: () => true,
|
||||
});
|
||||
|
||||
expect(decision).toMatchObject({
|
||||
allowed: false,
|
||||
reason: "empty_allowlist",
|
||||
groupPolicy: "allowlist",
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks sender not allowlisted", () => {
|
||||
const decision = evaluateSenderGroupAccess({
|
||||
providerConfigPresent: true,
|
||||
configuredGroupPolicy: "allowlist",
|
||||
defaultGroupPolicy: "open",
|
||||
groupAllowFrom: ["123"],
|
||||
senderId: "999",
|
||||
isSenderAllowed: () => false,
|
||||
});
|
||||
|
||||
expect(decision).toMatchObject({
|
||||
allowed: false,
|
||||
reason: "sender_not_allowlisted",
|
||||
groupPolicy: "allowlist",
|
||||
});
|
||||
});
|
||||
});
|
||||
64
src/plugin-sdk/group-access.ts
Normal file
64
src/plugin-sdk/group-access.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "../config/runtime-group-policy.js";
|
||||
import type { GroupPolicy } from "../config/types.base.js";
|
||||
|
||||
export type SenderGroupAccessReason =
|
||||
| "allowed"
|
||||
| "disabled"
|
||||
| "empty_allowlist"
|
||||
| "sender_not_allowlisted";
|
||||
|
||||
export type SenderGroupAccessDecision = {
|
||||
allowed: boolean;
|
||||
groupPolicy: GroupPolicy;
|
||||
providerMissingFallbackApplied: boolean;
|
||||
reason: SenderGroupAccessReason;
|
||||
};
|
||||
|
||||
export function evaluateSenderGroupAccess(params: {
|
||||
providerConfigPresent: boolean;
|
||||
configuredGroupPolicy?: GroupPolicy;
|
||||
defaultGroupPolicy?: GroupPolicy;
|
||||
groupAllowFrom: string[];
|
||||
senderId: string;
|
||||
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
|
||||
}): SenderGroupAccessDecision {
|
||||
const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
|
||||
providerConfigPresent: params.providerConfigPresent,
|
||||
groupPolicy: params.configuredGroupPolicy,
|
||||
defaultGroupPolicy: params.defaultGroupPolicy,
|
||||
});
|
||||
|
||||
if (groupPolicy === "disabled") {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy,
|
||||
providerMissingFallbackApplied,
|
||||
reason: "disabled",
|
||||
};
|
||||
}
|
||||
if (groupPolicy === "allowlist") {
|
||||
if (params.groupAllowFrom.length === 0) {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy,
|
||||
providerMissingFallbackApplied,
|
||||
reason: "empty_allowlist",
|
||||
};
|
||||
}
|
||||
if (!params.isSenderAllowed(params.senderId, params.groupAllowFrom)) {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy,
|
||||
providerMissingFallbackApplied,
|
||||
reason: "sender_not_allowlisted",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
groupPolicy,
|
||||
providerMissingFallbackApplied,
|
||||
reason: "allowed",
|
||||
};
|
||||
}
|
||||
@@ -181,7 +181,16 @@ export {
|
||||
normalizeAccountId,
|
||||
resolveThreadSessionKeys,
|
||||
} from "../routing/session-key.js";
|
||||
export { formatAllowFromLowercase, isAllowedParsedChatSender } from "./allow-from.js";
|
||||
export {
|
||||
formatAllowFromLowercase,
|
||||
isAllowedParsedChatSender,
|
||||
isNormalizedSenderAllowed,
|
||||
} from "./allow-from.js";
|
||||
export {
|
||||
evaluateSenderGroupAccess,
|
||||
type SenderGroupAccessDecision,
|
||||
type SenderGroupAccessReason,
|
||||
} from "./group-access.js";
|
||||
export { resolveSenderCommandAuthorization } from "./command-auth.js";
|
||||
export { handleSlackMessageAction } from "./slack-message-actions.js";
|
||||
export { extractToolSend } from "./tool-send.js";
|
||||
|
||||
Reference in New Issue
Block a user