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:
Vincent Koc
2026-03-07 20:28:47 -05:00
committed by GitHub
parent 8cc477b873
commit c22a4450ee
3 changed files with 192 additions and 18 deletions

View File

@@ -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.

View File

@@ -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"],

View File

@@ -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();
}