mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 11:57:27 +00:00
fix(telegram): honor commands.allowFrom in native command auth (#39310)
* telegram: honor commands.allowFrom in native auth * test(telegram): cover native commands.allowFrom precedence * changelog: note telegram native commands allowFrom fix * Update CHANGELOG.md * telegram: preserve group policy in native command auth * test(telegram): keep commands.allowFrom under group gating
This commit is contained in:
@@ -173,6 +173,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/DM draft final delivery: materialize text-only `sendMessageDraft` previews into one permanent final message and skip duplicate final payload sends, while preserving fallback behavior when materialization fails. (#34318) Thanks @Brotherinlaw-13.
|
||||
- Telegram/DM draft duplicate display: clear stale DM draft previews after materializing the real final message, including threadless fallback when DM topic lookup fails, so partial streaming no longer briefly shows duplicate replies. (#36746) Thanks @joelnishanth.
|
||||
- Telegram/draft preview boundary + silent-token reliability: stabilize answer-lane message boundaries across late-partial/message-start races, preserve/reset finalized preview state at the correct boundaries, and suppress `NO_REPLY` lead-fragment leaks without broad heartbeat-prefix false positives. (#33169) Thanks @obviyus.
|
||||
- Telegram/native commands `commands.allowFrom` precedence: make native Telegram commands honor `commands.allowFrom` as the command-specific authorization source, including group chats, instead of falling back to channel sender allowlists. (#28216) Thanks @toolsbybuddy and @vincentkoc.
|
||||
- Telegram/`groupAllowFrom` sender-ID validation: restore sender-only runtime validation so negative chat/group IDs remain invalid entries instead of appearing accepted while still being unable to authorize group access. (#37134) Thanks @qiuyuemartin-max and @vincentkoc.
|
||||
- Telegram/native group command auth: authorize native commands in groups and forum topics against `groupAllowFrom` and per-group/topic sender overrides, while keeping auth rejection replies in the originating topic thread. (#39267) Thanks @edwluo.
|
||||
- Telegram/named-account DMs: restore non-default-account DM routing when a named Telegram account falls back to the default agent by keeping groups fail-closed but deriving a per-account session key for DMs, including identity-link canonicalization and regression coverage for account isolation. (from #32426; fixes #32351) Thanks @chengzhichao-xydt.
|
||||
|
||||
@@ -24,10 +24,13 @@ vi.mock("../pairing/pairing-store.js", () => ({
|
||||
|
||||
describe("native command auth in groups", () => {
|
||||
function setup(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
telegramCfg?: TelegramAccountConfig;
|
||||
allowFrom?: string[];
|
||||
groupAllowFrom?: string[];
|
||||
useAccessGroups?: boolean;
|
||||
groupConfig?: Record<string, unknown>;
|
||||
resolveGroupPolicy?: () => ChannelGroupPolicy;
|
||||
}) {
|
||||
const handlers: Record<string, (ctx: unknown) => Promise<void>> = {};
|
||||
const sendMessage = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -43,10 +46,10 @@ describe("native command auth in groups", () => {
|
||||
|
||||
registerTelegramNativeCommands({
|
||||
bot: bot as unknown as Parameters<typeof registerTelegramNativeCommands>[0]["bot"],
|
||||
cfg: {} as OpenClawConfig,
|
||||
cfg: params.cfg ?? ({} as OpenClawConfig),
|
||||
runtime: {} as unknown as RuntimeEnv,
|
||||
accountId: "default",
|
||||
telegramCfg: {} as TelegramAccountConfig,
|
||||
telegramCfg: params.telegramCfg ?? ({} as TelegramAccountConfig),
|
||||
allowFrom: params.allowFrom ?? [],
|
||||
groupAllowFrom: params.groupAllowFrom ?? [],
|
||||
replyToMode: "off",
|
||||
@@ -55,11 +58,13 @@ describe("native command auth in groups", () => {
|
||||
nativeEnabled: true,
|
||||
nativeSkillsEnabled: false,
|
||||
nativeDisabledExplicit: false,
|
||||
resolveGroupPolicy: () =>
|
||||
({
|
||||
allowlistEnabled: false,
|
||||
allowed: true,
|
||||
}) as ChannelGroupPolicy,
|
||||
resolveGroupPolicy:
|
||||
params.resolveGroupPolicy ??
|
||||
(() =>
|
||||
({
|
||||
allowlistEnabled: false,
|
||||
allowed: true,
|
||||
}) as ChannelGroupPolicy),
|
||||
resolveTelegramGroupConfig: () => ({
|
||||
groupConfig: params.groupConfig as undefined,
|
||||
topicConfig: undefined,
|
||||
@@ -98,6 +103,149 @@ describe("native command auth in groups", () => {
|
||||
expect(notAuthCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("authorizes native commands in groups from commands.allowFrom.telegram", async () => {
|
||||
const { handlers, sendMessage } = setup({
|
||||
cfg: {
|
||||
commands: {
|
||||
allowFrom: {
|
||||
telegram: ["12345"],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
allowFrom: ["99999"],
|
||||
groupAllowFrom: ["99999"],
|
||||
useAccessGroups: true,
|
||||
});
|
||||
|
||||
const ctx = {
|
||||
message: {
|
||||
chat: { id: -100999, type: "supergroup", is_forum: true },
|
||||
from: { id: 12345, username: "testuser" },
|
||||
message_thread_id: 42,
|
||||
message_id: 1,
|
||||
date: 1700000000,
|
||||
},
|
||||
match: "",
|
||||
};
|
||||
|
||||
await handlers.status?.(ctx);
|
||||
|
||||
const notAuthCalls = sendMessage.mock.calls.filter(
|
||||
(call) => typeof call[1] === "string" && call[1].includes("not authorized"),
|
||||
);
|
||||
expect(notAuthCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("uses commands.allowFrom.telegram as the sole auth source when configured", async () => {
|
||||
const { handlers, sendMessage } = setup({
|
||||
cfg: {
|
||||
commands: {
|
||||
allowFrom: {
|
||||
telegram: ["99999"],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
groupAllowFrom: ["12345"],
|
||||
useAccessGroups: true,
|
||||
});
|
||||
|
||||
const ctx = {
|
||||
message: {
|
||||
chat: { id: -100999, type: "supergroup", is_forum: true },
|
||||
from: { id: 12345, username: "testuser" },
|
||||
message_thread_id: 42,
|
||||
message_id: 1,
|
||||
date: 1700000000,
|
||||
},
|
||||
match: "",
|
||||
};
|
||||
|
||||
await handlers.status?.(ctx);
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
-100999,
|
||||
"You are not authorized to use this command.",
|
||||
expect.objectContaining({ message_thread_id: 42 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps groupPolicy disabled enforced when commands.allowFrom is configured", async () => {
|
||||
const { handlers, sendMessage } = setup({
|
||||
cfg: {
|
||||
commands: {
|
||||
allowFrom: {
|
||||
telegram: ["12345"],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
telegramCfg: {
|
||||
groupPolicy: "disabled",
|
||||
} as TelegramAccountConfig,
|
||||
useAccessGroups: true,
|
||||
resolveGroupPolicy: () =>
|
||||
({
|
||||
allowlistEnabled: false,
|
||||
allowed: false,
|
||||
}) as ChannelGroupPolicy,
|
||||
});
|
||||
|
||||
const ctx = {
|
||||
message: {
|
||||
chat: { id: -100999, type: "supergroup", is_forum: true },
|
||||
from: { id: 12345, username: "testuser" },
|
||||
message_thread_id: 42,
|
||||
message_id: 1,
|
||||
date: 1700000000,
|
||||
},
|
||||
match: "",
|
||||
};
|
||||
|
||||
await handlers.status?.(ctx);
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
-100999,
|
||||
"Telegram group commands are disabled.",
|
||||
expect.objectContaining({ message_thread_id: 42 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps group chat allowlists enforced when commands.allowFrom is configured", async () => {
|
||||
const { handlers, sendMessage } = setup({
|
||||
cfg: {
|
||||
commands: {
|
||||
allowFrom: {
|
||||
telegram: ["12345"],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
useAccessGroups: true,
|
||||
resolveGroupPolicy: () =>
|
||||
({
|
||||
allowlistEnabled: true,
|
||||
allowed: false,
|
||||
}) as ChannelGroupPolicy,
|
||||
});
|
||||
|
||||
const ctx = {
|
||||
message: {
|
||||
chat: { id: -100999, type: "supergroup", is_forum: true },
|
||||
from: { id: 12345, username: "testuser" },
|
||||
message_thread_id: 42,
|
||||
message_id: 1,
|
||||
date: 1700000000,
|
||||
},
|
||||
match: "",
|
||||
};
|
||||
|
||||
await handlers.status?.(ctx);
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
-100999,
|
||||
"This group is not allowed.",
|
||||
expect.objectContaining({ message_thread_id: 42 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects native commands in groups when sender is in neither allowlist", async () => {
|
||||
const { handlers, sendMessage } = setup({
|
||||
allowFrom: ["99999"],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Bot, Context } from "grammy";
|
||||
import { ensureConfiguredAcpRouteReady } from "../acp/persistent-bindings.route.js";
|
||||
import { resolveChunkMode } from "../auto-reply/chunk.js";
|
||||
import { resolveCommandAuthorization } from "../auto-reply/command-auth.js";
|
||||
import type { CommandArgs } from "../auto-reply/commands-registry.js";
|
||||
import {
|
||||
buildCommandTextFromArgs,
|
||||
@@ -209,6 +210,28 @@ async function resolveTelegramCommandAuth(params: {
|
||||
const dmAllowFrom = groupAllowOverride ?? allowFrom;
|
||||
const senderId = msg.from?.id ? String(msg.from.id) : "";
|
||||
const senderUsername = msg.from?.username ?? "";
|
||||
const commandsAllowFrom = cfg.commands?.allowFrom;
|
||||
const commandsAllowFromConfigured =
|
||||
commandsAllowFrom != null &&
|
||||
typeof commandsAllowFrom === "object" &&
|
||||
(Array.isArray(commandsAllowFrom.telegram) || Array.isArray(commandsAllowFrom["*"]));
|
||||
const commandsAllowFromAccess = commandsAllowFromConfigured
|
||||
? resolveCommandAuthorization({
|
||||
ctx: {
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
OriginatingChannel: "telegram",
|
||||
AccountId: accountId,
|
||||
ChatType: isGroup ? "group" : "direct",
|
||||
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
|
||||
SenderId: senderId || undefined,
|
||||
SenderUsername: senderUsername || undefined,
|
||||
},
|
||||
cfg,
|
||||
// commands.allowFrom is the only auth source when configured.
|
||||
commandAuthorized: false,
|
||||
})
|
||||
: null;
|
||||
|
||||
const sendAuthMessage = async (text: string) => {
|
||||
const threadParams = buildTelegramThreadParams(threadSpec) ?? {};
|
||||
@@ -256,7 +279,7 @@ async function resolveTelegramCommandAuth(params: {
|
||||
resolveGroupPolicy,
|
||||
enforcePolicy: useAccessGroups,
|
||||
useTopicAndGroupOverrides: false,
|
||||
enforceAllowlistAuthorization: requireAuth,
|
||||
enforceAllowlistAuthorization: requireAuth && !commandsAllowFromConfigured,
|
||||
allowEmptyAllowlistEntries: true,
|
||||
requireSenderForAllowlistAuthorization: true,
|
||||
checkChatAllowlist: useAccessGroups,
|
||||
@@ -289,16 +312,18 @@ async function resolveTelegramCommandAuth(params: {
|
||||
const groupSenderAllowed = isGroup
|
||||
? isSenderAllowed({ allow: effectiveGroupAllow, senderId, senderUsername })
|
||||
: false;
|
||||
const commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: dmAllow.hasEntries, allowed: senderAllowed },
|
||||
...(isGroup
|
||||
? [{ configured: effectiveGroupAllow.hasEntries, allowed: groupSenderAllowed }]
|
||||
: []),
|
||||
],
|
||||
modeWhenAccessGroupsOff: "configured",
|
||||
});
|
||||
const commandAuthorized = commandsAllowFromConfigured
|
||||
? Boolean(commandsAllowFromAccess?.isAuthorizedSender)
|
||||
: resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: dmAllow.hasEntries, allowed: senderAllowed },
|
||||
...(isGroup
|
||||
? [{ configured: effectiveGroupAllow.hasEntries, allowed: groupSenderAllowed }]
|
||||
: []),
|
||||
],
|
||||
modeWhenAccessGroupsOff: "configured",
|
||||
});
|
||||
if (requireAuth && !commandAuthorized) {
|
||||
return await rejectNotAuthorized();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user