mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 03:57:26 +00:00
refactor: share route-level group gating decisions
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
GROUP_POLICY_BLOCKED_LABEL,
|
GROUP_POLICY_BLOCKED_LABEL,
|
||||||
createScopedPairingAccess,
|
createScopedPairingAccess,
|
||||||
|
evaluateGroupRouteAccessForPolicy,
|
||||||
issuePairingChallenge,
|
issuePairingChallenge,
|
||||||
isDangerousNameMatchingEnabled,
|
isDangerousNameMatchingEnabled,
|
||||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||||
@@ -195,24 +196,23 @@ export async function applyGoogleChatInboundAccessPolicy(params: {
|
|||||||
let effectiveWasMentioned: boolean | undefined;
|
let effectiveWasMentioned: boolean | undefined;
|
||||||
|
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
if (groupPolicy === "disabled") {
|
|
||||||
logVerbose(`drop group message (groupPolicy=disabled, space=${spaceId})`);
|
|
||||||
return { ok: false };
|
|
||||||
}
|
|
||||||
const groupAllowlistConfigured = groupConfigResolved.allowlistConfigured;
|
const groupAllowlistConfigured = groupConfigResolved.allowlistConfigured;
|
||||||
const groupAllowed = Boolean(groupEntry) || Boolean((account.config.groups ?? {})["*"]);
|
const routeAccess = evaluateGroupRouteAccessForPolicy({
|
||||||
if (groupPolicy === "allowlist") {
|
groupPolicy,
|
||||||
if (!groupAllowlistConfigured) {
|
routeAllowlistConfigured: groupAllowlistConfigured,
|
||||||
|
routeMatched: Boolean(groupEntry),
|
||||||
|
routeEnabled: groupEntry?.enabled !== false && groupEntry?.allow !== false,
|
||||||
|
});
|
||||||
|
if (!routeAccess.allowed) {
|
||||||
|
if (routeAccess.reason === "disabled") {
|
||||||
|
logVerbose(`drop group message (groupPolicy=disabled, space=${spaceId})`);
|
||||||
|
} else if (routeAccess.reason === "empty_allowlist") {
|
||||||
logVerbose(`drop group message (groupPolicy=allowlist, no allowlist, space=${spaceId})`);
|
logVerbose(`drop group message (groupPolicy=allowlist, no allowlist, space=${spaceId})`);
|
||||||
return { ok: false };
|
} else if (routeAccess.reason === "route_not_allowlisted") {
|
||||||
}
|
|
||||||
if (!groupAllowed) {
|
|
||||||
logVerbose(`drop group message (not allowlisted, space=${spaceId})`);
|
logVerbose(`drop group message (not allowlisted, space=${spaceId})`);
|
||||||
return { ok: false };
|
} else if (routeAccess.reason === "route_disabled") {
|
||||||
|
logVerbose(`drop group message (space disabled, space=${spaceId})`);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (groupEntry?.enabled === false || groupEntry?.allow === false) {
|
|
||||||
logVerbose(`drop group message (space disabled, space=${spaceId})`);
|
|
||||||
return { ok: false };
|
return { ok: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
createReplyPrefixOptions,
|
createReplyPrefixOptions,
|
||||||
createTypingCallbacks,
|
createTypingCallbacks,
|
||||||
dispatchReplyFromConfigWithSettledDispatcher,
|
dispatchReplyFromConfigWithSettledDispatcher,
|
||||||
|
evaluateGroupRouteAccessForPolicy,
|
||||||
formatAllowlistMatchMeta,
|
formatAllowlistMatchMeta,
|
||||||
logInboundDrop,
|
logInboundDrop,
|
||||||
logTypingFailure,
|
logTypingFailure,
|
||||||
@@ -194,10 +195,6 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|||||||
});
|
});
|
||||||
const isRoom = !isDirectMessage;
|
const isRoom = !isDirectMessage;
|
||||||
|
|
||||||
if (isRoom && groupPolicy === "disabled") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomConfigInfo = isRoom
|
const roomConfigInfo = isRoom
|
||||||
? resolveMatrixRoomConfig({
|
? resolveMatrixRoomConfig({
|
||||||
rooms: roomsConfig,
|
rooms: roomsConfig,
|
||||||
@@ -213,17 +210,21 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|||||||
}`
|
}`
|
||||||
: "matchKey=none matchSource=none";
|
: "matchKey=none matchSource=none";
|
||||||
|
|
||||||
if (isRoom && roomConfig && !roomConfigInfo?.allowed) {
|
if (isRoom) {
|
||||||
logVerboseMessage(`matrix: room disabled room=${roomId} (${roomMatchMeta})`);
|
const routeAccess = evaluateGroupRouteAccessForPolicy({
|
||||||
return;
|
groupPolicy,
|
||||||
}
|
routeAllowlistConfigured: Boolean(roomConfigInfo?.allowlistConfigured),
|
||||||
if (isRoom && groupPolicy === "allowlist") {
|
routeMatched: Boolean(roomConfig),
|
||||||
if (!roomConfigInfo?.allowlistConfigured) {
|
routeEnabled: roomConfigInfo?.allowed ?? true,
|
||||||
logVerboseMessage(`matrix: drop room message (no allowlist, ${roomMatchMeta})`);
|
});
|
||||||
return;
|
if (!routeAccess.allowed) {
|
||||||
}
|
if (routeAccess.reason === "route_disabled") {
|
||||||
if (!roomConfig) {
|
logVerboseMessage(`matrix: room disabled room=${roomId} (${roomMatchMeta})`);
|
||||||
logVerboseMessage(`matrix: drop room message (not in allowlist, ${roomMatchMeta})`);
|
} else if (routeAccess.reason === "empty_allowlist") {
|
||||||
|
logVerboseMessage(`matrix: drop room message (no allowlist, ${roomMatchMeta})`);
|
||||||
|
} else if (routeAccess.reason === "route_not_allowlisted") {
|
||||||
|
logVerboseMessage(`matrix: drop room message (not in allowlist, ${roomMatchMeta})`);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
createTypingCallbacks,
|
createTypingCallbacks,
|
||||||
createScopedPairingAccess,
|
createScopedPairingAccess,
|
||||||
createReplyPrefixOptions,
|
createReplyPrefixOptions,
|
||||||
|
evaluateGroupRouteAccessForPolicy,
|
||||||
issuePairingChallenge,
|
issuePairingChallenge,
|
||||||
resolveOutboundMediaUrls,
|
resolveOutboundMediaUrls,
|
||||||
mergeAllowlist,
|
mergeAllowlist,
|
||||||
@@ -94,28 +95,6 @@ function isSenderAllowed(senderId: string | undefined, allowFrom: string[]): boo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGroupAllowed(params: {
|
|
||||||
groupId: string;
|
|
||||||
groupName?: string | null;
|
|
||||||
groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
|
|
||||||
}): boolean {
|
|
||||||
const groups = params.groups ?? {};
|
|
||||||
const keys = Object.keys(groups);
|
|
||||||
if (keys.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const entry = findZalouserGroupEntry(
|
|
||||||
groups,
|
|
||||||
buildZalouserGroupCandidates({
|
|
||||||
groupId: params.groupId,
|
|
||||||
groupName: params.groupName,
|
|
||||||
includeGroupIdAlias: true,
|
|
||||||
includeWildcard: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return isZalouserGroupEntryAllowed(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveGroupRequireMention(params: {
|
function resolveGroupRequireMention(params: {
|
||||||
groupId: string;
|
groupId: string;
|
||||||
groupName?: string | null;
|
groupName?: string | null;
|
||||||
@@ -223,16 +202,36 @@ async function processMessage(
|
|||||||
|
|
||||||
const groups = account.config.groups ?? {};
|
const groups = account.config.groups ?? {};
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
if (groupPolicy === "disabled") {
|
const groupEntry = findZalouserGroupEntry(
|
||||||
logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
|
groups,
|
||||||
return;
|
buildZalouserGroupCandidates({
|
||||||
}
|
groupId: chatId,
|
||||||
if (groupPolicy === "allowlist") {
|
groupName,
|
||||||
const allowed = isGroupAllowed({ groupId: chatId, groupName, groups });
|
includeGroupIdAlias: true,
|
||||||
if (!allowed) {
|
includeWildcard: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const routeAccess = evaluateGroupRouteAccessForPolicy({
|
||||||
|
groupPolicy,
|
||||||
|
routeAllowlistConfigured: Object.keys(groups).length > 0,
|
||||||
|
routeMatched: Boolean(groupEntry),
|
||||||
|
routeEnabled: isZalouserGroupEntryAllowed(groupEntry),
|
||||||
|
});
|
||||||
|
if (!routeAccess.allowed) {
|
||||||
|
if (routeAccess.reason === "disabled") {
|
||||||
|
logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
|
||||||
|
} else if (routeAccess.reason === "empty_allowlist") {
|
||||||
|
logVerbose(
|
||||||
|
core,
|
||||||
|
runtime,
|
||||||
|
`zalouser: drop group ${chatId} (groupPolicy=allowlist, no allowlist)`,
|
||||||
|
);
|
||||||
|
} else if (routeAccess.reason === "route_not_allowlisted") {
|
||||||
logVerbose(core, runtime, `zalouser: drop group ${chatId} (not allowlisted)`);
|
logVerbose(core, runtime, `zalouser: drop group ${chatId} (not allowlisted)`);
|
||||||
return;
|
} else if (routeAccess.reason === "route_disabled") {
|
||||||
|
logVerbose(core, runtime, `zalouser: drop group ${chatId} (group disabled)`);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ export type { WizardPrompter } from "../wizard/prompts.js";
|
|||||||
export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope.js";
|
export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope.js";
|
||||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||||
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||||
export { resolveSenderScopedGroupPolicy } from "./group-access.js";
|
export {
|
||||||
|
evaluateGroupRouteAccessForPolicy,
|
||||||
|
resolveSenderScopedGroupPolicy,
|
||||||
|
} from "./group-access.js";
|
||||||
export { extractToolSend } from "./tool-send.js";
|
export { extractToolSend } from "./tool-send.js";
|
||||||
export { resolveWebhookPath } from "./webhook-path.js";
|
export { resolveWebhookPath } from "./webhook-path.js";
|
||||||
export type { WebhookInFlightLimiter } from "./webhook-request-guards.js";
|
export type { WebhookInFlightLimiter } from "./webhook-request-guards.js";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
evaluateGroupRouteAccessForPolicy,
|
||||||
evaluateSenderGroupAccess,
|
evaluateSenderGroupAccess,
|
||||||
evaluateSenderGroupAccessForPolicy,
|
evaluateSenderGroupAccessForPolicy,
|
||||||
resolveSenderScopedGroupPolicy,
|
resolveSenderScopedGroupPolicy,
|
||||||
@@ -59,6 +60,66 @@ describe("evaluateSenderGroupAccessForPolicy", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("evaluateGroupRouteAccessForPolicy", () => {
|
||||||
|
it("blocks disabled policy", () => {
|
||||||
|
expect(
|
||||||
|
evaluateGroupRouteAccessForPolicy({
|
||||||
|
groupPolicy: "disabled",
|
||||||
|
routeAllowlistConfigured: true,
|
||||||
|
routeMatched: true,
|
||||||
|
routeEnabled: true,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: "disabled",
|
||||||
|
reason: "disabled",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks allowlist without configured routes", () => {
|
||||||
|
expect(
|
||||||
|
evaluateGroupRouteAccessForPolicy({
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
routeAllowlistConfigured: false,
|
||||||
|
routeMatched: false,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
reason: "empty_allowlist",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks unmatched allowlist route", () => {
|
||||||
|
expect(
|
||||||
|
evaluateGroupRouteAccessForPolicy({
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
routeAllowlistConfigured: true,
|
||||||
|
routeMatched: false,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
reason: "route_not_allowlisted",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks disabled matched route even when group policy is open", () => {
|
||||||
|
expect(
|
||||||
|
evaluateGroupRouteAccessForPolicy({
|
||||||
|
groupPolicy: "open",
|
||||||
|
routeAllowlistConfigured: true,
|
||||||
|
routeMatched: true,
|
||||||
|
routeEnabled: false,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: "open",
|
||||||
|
reason: "route_disabled",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("evaluateSenderGroupAccess", () => {
|
describe("evaluateSenderGroupAccess", () => {
|
||||||
it("defaults missing provider config to allowlist", () => {
|
it("defaults missing provider config to allowlist", () => {
|
||||||
const decision = evaluateSenderGroupAccess({
|
const decision = evaluateSenderGroupAccess({
|
||||||
|
|||||||
@@ -14,6 +14,19 @@ export type SenderGroupAccessDecision = {
|
|||||||
reason: SenderGroupAccessReason;
|
reason: SenderGroupAccessReason;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GroupRouteAccessReason =
|
||||||
|
| "allowed"
|
||||||
|
| "disabled"
|
||||||
|
| "empty_allowlist"
|
||||||
|
| "route_not_allowlisted"
|
||||||
|
| "route_disabled";
|
||||||
|
|
||||||
|
export type GroupRouteAccessDecision = {
|
||||||
|
allowed: boolean;
|
||||||
|
groupPolicy: GroupPolicy;
|
||||||
|
reason: GroupRouteAccessReason;
|
||||||
|
};
|
||||||
|
|
||||||
export function resolveSenderScopedGroupPolicy(params: {
|
export function resolveSenderScopedGroupPolicy(params: {
|
||||||
groupPolicy: GroupPolicy;
|
groupPolicy: GroupPolicy;
|
||||||
groupAllowFrom: string[];
|
groupAllowFrom: string[];
|
||||||
@@ -24,6 +37,52 @@ export function resolveSenderScopedGroupPolicy(params: {
|
|||||||
return params.groupAllowFrom.length > 0 ? "allowlist" : "open";
|
return params.groupAllowFrom.length > 0 ? "allowlist" : "open";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function evaluateGroupRouteAccessForPolicy(params: {
|
||||||
|
groupPolicy: GroupPolicy;
|
||||||
|
routeAllowlistConfigured: boolean;
|
||||||
|
routeMatched: boolean;
|
||||||
|
routeEnabled?: boolean;
|
||||||
|
}): GroupRouteAccessDecision {
|
||||||
|
if (params.groupPolicy === "disabled") {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: params.groupPolicy,
|
||||||
|
reason: "disabled",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.routeMatched && params.routeEnabled === false) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: params.groupPolicy,
|
||||||
|
reason: "route_disabled",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.groupPolicy === "allowlist") {
|
||||||
|
if (!params.routeAllowlistConfigured) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: params.groupPolicy,
|
||||||
|
reason: "empty_allowlist",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!params.routeMatched) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: params.groupPolicy,
|
||||||
|
reason: "route_not_allowlisted",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
groupPolicy: params.groupPolicy,
|
||||||
|
reason: "allowed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function evaluateSenderGroupAccessForPolicy(params: {
|
export function evaluateSenderGroupAccessForPolicy(params: {
|
||||||
groupPolicy: GroupPolicy;
|
groupPolicy: GroupPolicy;
|
||||||
providerMissingFallbackApplied?: boolean;
|
providerMissingFallbackApplied?: boolean;
|
||||||
|
|||||||
@@ -278,9 +278,12 @@ export {
|
|||||||
isNormalizedSenderAllowed,
|
isNormalizedSenderAllowed,
|
||||||
} from "./allow-from.js";
|
} from "./allow-from.js";
|
||||||
export {
|
export {
|
||||||
|
evaluateGroupRouteAccessForPolicy,
|
||||||
evaluateSenderGroupAccess,
|
evaluateSenderGroupAccess,
|
||||||
evaluateSenderGroupAccessForPolicy,
|
evaluateSenderGroupAccessForPolicy,
|
||||||
resolveSenderScopedGroupPolicy,
|
resolveSenderScopedGroupPolicy,
|
||||||
|
type GroupRouteAccessDecision,
|
||||||
|
type GroupRouteAccessReason,
|
||||||
type SenderGroupAccessDecision,
|
type SenderGroupAccessDecision,
|
||||||
type SenderGroupAccessReason,
|
type SenderGroupAccessReason,
|
||||||
} from "./group-access.js";
|
} from "./group-access.js";
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ export {
|
|||||||
} from "../security/dm-policy-shared.js";
|
} from "../security/dm-policy-shared.js";
|
||||||
export { formatDocsLink } from "../terminal/links.js";
|
export { formatDocsLink } from "../terminal/links.js";
|
||||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
export { resolveSenderScopedGroupPolicy } from "./group-access.js";
|
export {
|
||||||
|
evaluateGroupRouteAccessForPolicy,
|
||||||
|
resolveSenderScopedGroupPolicy,
|
||||||
|
} from "./group-access.js";
|
||||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||||
export { formatResolvedUnresolvedNote } from "./resolution-notes.js";
|
export { formatResolvedUnresolvedNote } from "./resolution-notes.js";
|
||||||
export { runPluginCommandWithTimeout } from "./run-command.js";
|
export { runPluginCommandWithTimeout } from "./run-command.js";
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export type { WizardPrompter } from "../wizard/prompts.js";
|
|||||||
export { formatAllowFromLowercase } from "./allow-from.js";
|
export { formatAllowFromLowercase } from "./allow-from.js";
|
||||||
export { resolveSenderCommandAuthorization } from "./command-auth.js";
|
export { resolveSenderCommandAuthorization } from "./command-auth.js";
|
||||||
export { resolveChannelAccountConfigBasePath } from "./config-paths.js";
|
export { resolveChannelAccountConfigBasePath } from "./config-paths.js";
|
||||||
|
export { evaluateGroupRouteAccessForPolicy } from "./group-access.js";
|
||||||
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
|
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
|
||||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||||
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user