mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-28 08:52:45 +00:00
refactor: normalize runtime group sender gating decisions
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost";
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost";
|
||||||
import {
|
import {
|
||||||
|
evaluateSenderGroupAccessForPolicy,
|
||||||
isDangerousNameMatchingEnabled,
|
isDangerousNameMatchingEnabled,
|
||||||
resolveAllowlistMatchSimple,
|
resolveAllowlistMatchSimple,
|
||||||
resolveControlCommandGate,
|
resolveControlCommandGate,
|
||||||
@@ -231,7 +232,20 @@ export function authorizeMattermostCommandInvocation(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (groupPolicy === "disabled") {
|
const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
|
||||||
|
groupPolicy,
|
||||||
|
groupAllowFrom: effectiveGroupAllowFrom,
|
||||||
|
senderId,
|
||||||
|
isSenderAllowed: (_senderId, allowFrom) =>
|
||||||
|
isMattermostSenderAllowed({
|
||||||
|
senderId,
|
||||||
|
senderName,
|
||||||
|
allowFrom,
|
||||||
|
allowNameMatching,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
denyReason: "channels-disabled",
|
denyReason: "channels-disabled",
|
||||||
@@ -245,33 +259,32 @@ export function authorizeMattermostCommandInvocation(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupPolicy === "allowlist") {
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
|
||||||
if (effectiveGroupAllowFrom.length === 0) {
|
return {
|
||||||
return {
|
ok: false,
|
||||||
ok: false,
|
denyReason: "channel-no-allowlist",
|
||||||
denyReason: "channel-no-allowlist",
|
commandAuthorized: false,
|
||||||
commandAuthorized: false,
|
channelInfo,
|
||||||
channelInfo,
|
kind,
|
||||||
kind,
|
chatType,
|
||||||
chatType,
|
channelName,
|
||||||
channelName,
|
channelDisplay,
|
||||||
channelDisplay,
|
roomLabel,
|
||||||
roomLabel,
|
};
|
||||||
};
|
}
|
||||||
}
|
|
||||||
if (!groupAllowedForCommands) {
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "sender_not_allowlisted") {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
denyReason: "unauthorized",
|
denyReason: "unauthorized",
|
||||||
commandAuthorized: false,
|
commandAuthorized: false,
|
||||||
channelInfo,
|
channelInfo,
|
||||||
kind,
|
kind,
|
||||||
chatType,
|
chatType,
|
||||||
channelName,
|
channelName,
|
||||||
channelDisplay,
|
channelDisplay,
|
||||||
roomLabel,
|
roomLabel,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandGate.shouldBlock) {
|
if (commandGate.shouldBlock) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||||
createScopedPairingAccess,
|
createScopedPairingAccess,
|
||||||
logInboundDrop,
|
logInboundDrop,
|
||||||
|
evaluateSenderGroupAccessForPolicy,
|
||||||
recordPendingHistoryEntryIfEnabled,
|
recordPendingHistoryEntryIfEnabled,
|
||||||
resolveControlCommandGate,
|
resolveControlCommandGate,
|
||||||
resolveDefaultGroupPolicy,
|
resolveDefaultGroupPolicy,
|
||||||
@@ -230,46 +231,57 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isDirectMessage && msteamsCfg) {
|
if (!isDirectMessage && msteamsCfg) {
|
||||||
if (groupPolicy === "disabled") {
|
if (channelGate.allowlistConfigured && !channelGate.allowed) {
|
||||||
|
log.debug?.("dropping group message (not in team/channel allowlist)", {
|
||||||
|
conversationId,
|
||||||
|
teamKey: channelGate.teamKey ?? "none",
|
||||||
|
channelKey: channelGate.channelKey ?? "none",
|
||||||
|
channelMatchKey: channelGate.channelMatchKey ?? "none",
|
||||||
|
channelMatchSource: channelGate.channelMatchSource ?? "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
|
||||||
|
groupPolicy,
|
||||||
|
groupAllowFrom:
|
||||||
|
effectiveGroupAllowFrom.length > 0 || !channelGate.allowlistConfigured
|
||||||
|
? effectiveGroupAllowFrom
|
||||||
|
: ["*"],
|
||||||
|
senderId,
|
||||||
|
isSenderAllowed: (_senderId, allowFrom) =>
|
||||||
|
resolveMSTeamsAllowlistMatch({
|
||||||
|
allowFrom,
|
||||||
|
senderId,
|
||||||
|
senderName,
|
||||||
|
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
|
||||||
|
}).allowed,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") {
|
||||||
log.debug?.("dropping group message (groupPolicy: disabled)", {
|
log.debug?.("dropping group message (groupPolicy: disabled)", {
|
||||||
conversationId,
|
conversationId,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
|
||||||
if (groupPolicy === "allowlist") {
|
log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", {
|
||||||
if (channelGate.allowlistConfigured && !channelGate.allowed) {
|
conversationId,
|
||||||
log.debug?.("dropping group message (not in team/channel allowlist)", {
|
});
|
||||||
conversationId,
|
return;
|
||||||
teamKey: channelGate.teamKey ?? "none",
|
}
|
||||||
channelKey: channelGate.channelKey ?? "none",
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "sender_not_allowlisted") {
|
||||||
channelMatchKey: channelGate.channelMatchKey ?? "none",
|
const allowMatch = resolveMSTeamsAllowlistMatch({
|
||||||
channelMatchSource: channelGate.channelMatchSource ?? "none",
|
allowFrom: effectiveGroupAllowFrom,
|
||||||
});
|
senderId,
|
||||||
return;
|
senderName,
|
||||||
}
|
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
|
||||||
if (effectiveGroupAllowFrom.length === 0 && !channelGate.allowlistConfigured) {
|
});
|
||||||
log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", {
|
log.debug?.("dropping group message (not in groupAllowFrom)", {
|
||||||
conversationId,
|
sender: senderId,
|
||||||
});
|
label: senderName,
|
||||||
return;
|
allowlistMatch: formatAllowlistMatchMeta(allowMatch),
|
||||||
}
|
});
|
||||||
if (effectiveGroupAllowFrom.length > 0 && access.decision !== "allow") {
|
return;
|
||||||
const allowMatch = resolveMSTeamsAllowlistMatch({
|
|
||||||
allowFrom: effectiveGroupAllowFrom,
|
|
||||||
senderId,
|
|
||||||
senderName,
|
|
||||||
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
|
|
||||||
});
|
|
||||||
if (!allowMatch.allowed) {
|
|
||||||
log.debug?.("dropping group message (not in groupAllowFrom)", {
|
|
||||||
sender: senderId,
|
|
||||||
label: senderName,
|
|
||||||
allowlistMatch: formatAllowlistMatchMeta(allowMatch),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +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 { 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 {
|
||||||
@@ -344,23 +345,31 @@ async function shouldProcessLineEvent(
|
|||||||
return denied;
|
return denied;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (groupPolicy === "disabled") {
|
if (groupPolicy === "allowlist" && !senderId) {
|
||||||
|
logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
|
||||||
|
return denied;
|
||||||
|
}
|
||||||
|
const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
|
||||||
|
groupPolicy,
|
||||||
|
groupAllowFrom: effectiveGroupAllow.entries,
|
||||||
|
senderId,
|
||||||
|
isSenderAllowed: (candidateSenderId, allowFrom) =>
|
||||||
|
isSenderAllowed({
|
||||||
|
allow: normalizeAllowFrom(allowFrom),
|
||||||
|
senderId: candidateSenderId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
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 (groupPolicy === "allowlist") {
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
|
||||||
if (!senderId) {
|
logVerbose("Blocked line group message (groupPolicy: allowlist, no groupAllowFrom)");
|
||||||
logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
|
return denied;
|
||||||
return denied;
|
}
|
||||||
}
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "sender_not_allowlisted") {
|
||||||
if (!effectiveGroupAllow.hasEntries) {
|
logVerbose(`Blocked line group message from ${senderId} (groupPolicy: allowlist)`);
|
||||||
logVerbose("Blocked line group message (groupPolicy: allowlist, no groupAllowFrom)");
|
return denied;
|
||||||
return denied;
|
|
||||||
}
|
|
||||||
if (!isSenderAllowed({ allow: effectiveGroupAllow, senderId })) {
|
|
||||||
logVerbose(`Blocked line group message from ${senderId} (groupPolicy: allowlist)`);
|
|
||||||
return denied;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
allowed: true,
|
allowed: true,
|
||||||
|
|||||||
@@ -1,5 +1,33 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { evaluateSenderGroupAccess } from "./group-access.js";
|
import { evaluateSenderGroupAccess, evaluateSenderGroupAccessForPolicy } from "./group-access.js";
|
||||||
|
|
||||||
|
describe("evaluateSenderGroupAccessForPolicy", () => {
|
||||||
|
it("blocks disabled policy", () => {
|
||||||
|
const decision = evaluateSenderGroupAccessForPolicy({
|
||||||
|
groupPolicy: "disabled",
|
||||||
|
groupAllowFrom: ["123"],
|
||||||
|
senderId: "123",
|
||||||
|
isSenderAllowed: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision).toMatchObject({ allowed: false, reason: "disabled", groupPolicy: "disabled" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks allowlist with empty list", () => {
|
||||||
|
const decision = evaluateSenderGroupAccessForPolicy({
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
groupAllowFrom: [],
|
||||||
|
senderId: "123",
|
||||||
|
isSenderAllowed: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision).toMatchObject({
|
||||||
|
allowed: false,
|
||||||
|
reason: "empty_allowlist",
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("evaluateSenderGroupAccess", () => {
|
describe("evaluateSenderGroupAccess", () => {
|
||||||
it("defaults missing provider config to allowlist", () => {
|
it("defaults missing provider config to allowlist", () => {
|
||||||
|
|||||||
@@ -14,6 +14,48 @@ export type SenderGroupAccessDecision = {
|
|||||||
reason: SenderGroupAccessReason;
|
reason: SenderGroupAccessReason;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function evaluateSenderGroupAccessForPolicy(params: {
|
||||||
|
groupPolicy: GroupPolicy;
|
||||||
|
providerMissingFallbackApplied?: boolean;
|
||||||
|
groupAllowFrom: string[];
|
||||||
|
senderId: string;
|
||||||
|
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
|
||||||
|
}): SenderGroupAccessDecision {
|
||||||
|
if (params.groupPolicy === "disabled") {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: params.groupPolicy,
|
||||||
|
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
||||||
|
reason: "disabled",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (params.groupPolicy === "allowlist") {
|
||||||
|
if (params.groupAllowFrom.length === 0) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: params.groupPolicy,
|
||||||
|
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
||||||
|
reason: "empty_allowlist",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!params.isSenderAllowed(params.senderId, params.groupAllowFrom)) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
groupPolicy: params.groupPolicy,
|
||||||
|
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
||||||
|
reason: "sender_not_allowlisted",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
groupPolicy: params.groupPolicy,
|
||||||
|
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
||||||
|
reason: "allowed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function evaluateSenderGroupAccess(params: {
|
export function evaluateSenderGroupAccess(params: {
|
||||||
providerConfigPresent: boolean;
|
providerConfigPresent: boolean;
|
||||||
configuredGroupPolicy?: GroupPolicy;
|
configuredGroupPolicy?: GroupPolicy;
|
||||||
@@ -28,37 +70,11 @@ export function evaluateSenderGroupAccess(params: {
|
|||||||
defaultGroupPolicy: params.defaultGroupPolicy,
|
defaultGroupPolicy: params.defaultGroupPolicy,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (groupPolicy === "disabled") {
|
return evaluateSenderGroupAccessForPolicy({
|
||||||
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,
|
groupPolicy,
|
||||||
providerMissingFallbackApplied,
|
providerMissingFallbackApplied,
|
||||||
reason: "allowed",
|
groupAllowFrom: params.groupAllowFrom,
|
||||||
};
|
senderId: params.senderId,
|
||||||
|
isSenderAllowed: params.isSenderAllowed,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ export {
|
|||||||
} from "./allow-from.js";
|
} from "./allow-from.js";
|
||||||
export {
|
export {
|
||||||
evaluateSenderGroupAccess,
|
evaluateSenderGroupAccess,
|
||||||
|
evaluateSenderGroupAccessForPolicy,
|
||||||
type SenderGroupAccessDecision,
|
type SenderGroupAccessDecision,
|
||||||
type SenderGroupAccessReason,
|
type SenderGroupAccessReason,
|
||||||
} from "./group-access.js";
|
} from "./group-access.js";
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export {
|
|||||||
resolveDmGroupAccessWithLists,
|
resolveDmGroupAccessWithLists,
|
||||||
resolveEffectiveAllowFromLists,
|
resolveEffectiveAllowFromLists,
|
||||||
} from "../security/dm-policy-shared.js";
|
} from "../security/dm-policy-shared.js";
|
||||||
|
export { evaluateSenderGroupAccessForPolicy } from "./group-access.js";
|
||||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
export { buildAgentMediaPayload } from "./agent-media-payload.js";
|
export { buildAgentMediaPayload } from "./agent-media-payload.js";
|
||||||
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
|
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export {
|
|||||||
resolveDmGroupAccessWithLists,
|
resolveDmGroupAccessWithLists,
|
||||||
resolveEffectiveAllowFromLists,
|
resolveEffectiveAllowFromLists,
|
||||||
} from "../security/dm-policy-shared.js";
|
} from "../security/dm-policy-shared.js";
|
||||||
|
export { evaluateSenderGroupAccessForPolicy } from "./group-access.js";
|
||||||
export { formatDocsLink } from "../terminal/links.js";
|
export { formatDocsLink } from "../terminal/links.js";
|
||||||
export { sleep } from "../utils.js";
|
export { sleep } from "../utils.js";
|
||||||
export { loadWebMedia } from "../web/media.js";
|
export { loadWebMedia } from "../web/media.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user