mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-31 03:41:51 +00:00
fix: unify telegram exec approval auth
This commit is contained in:
@@ -78,9 +78,7 @@ import {
|
||||
} from "./conversation-route.js";
|
||||
import { enforceTelegramDmAccess } from "./dm-access.js";
|
||||
import {
|
||||
isTelegramExecApprovalApprover,
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
isTelegramExecApprovalTargetRecipient,
|
||||
isTelegramExecApprovalAuthorizedSender,
|
||||
shouldEnableTelegramExecApprovalButtons,
|
||||
} from "./exec-approvals.js";
|
||||
import {
|
||||
@@ -1290,13 +1288,7 @@ export const registerTelegramHandlers = ({
|
||||
|
||||
const runtimeCfg = telegramDeps.loadConfig();
|
||||
if (isApprovalCallback) {
|
||||
const isExplicitApprover =
|
||||
isTelegramExecApprovalClientEnabled({ cfg: runtimeCfg, accountId }) &&
|
||||
isTelegramExecApprovalApprover({ cfg: runtimeCfg, accountId, senderId });
|
||||
if (
|
||||
!isExplicitApprover &&
|
||||
!isTelegramExecApprovalTargetRecipient({ cfg: runtimeCfg, senderId, accountId })
|
||||
) {
|
||||
if (!isTelegramExecApprovalAuthorizedSender({ cfg: runtimeCfg, accountId, senderId })) {
|
||||
logVerbose(
|
||||
`Blocked telegram exec approval callback from ${senderId || "unknown"} (not an approver)`,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import {
|
||||
isTelegramExecApprovalAuthorizedSender,
|
||||
isTelegramExecApprovalApprover,
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
isTelegramExecApprovalTargetRecipient,
|
||||
@@ -164,5 +165,52 @@ describe("telegram exec approvals", () => {
|
||||
isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345", accountId: "any-account" }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("requires active target forwarding mode", () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
approvals: {
|
||||
exec: {
|
||||
enabled: true,
|
||||
mode: "session",
|
||||
targets: [{ channel: "telegram", to: "12345" }],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes prefixed Telegram DM targets", () => {
|
||||
const cfg = buildTargetConfig([{ channel: "telegram", to: "tg:12345" }]);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(true);
|
||||
});
|
||||
|
||||
it("normalizes accountId matching", () => {
|
||||
const cfg = buildTargetConfig([{ channel: "telegram", to: "12345", accountId: "Work Bot" }]);
|
||||
expect(
|
||||
isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345", accountId: "work-bot" }),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTelegramExecApprovalAuthorizedSender", () => {
|
||||
it("accepts explicit approvers", () => {
|
||||
const cfg = buildConfig({ enabled: true, approvers: ["123"] });
|
||||
expect(isTelegramExecApprovalAuthorizedSender({ cfg, senderId: "123" })).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts active forwarded DM targets", () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
approvals: {
|
||||
exec: {
|
||||
enabled: true,
|
||||
mode: "targets",
|
||||
targets: [{ channel: "telegram", to: "12345" }],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
expect(isTelegramExecApprovalAuthorizedSender({ cfg, senderId: "12345" })).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,10 @@ import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runti
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { TelegramExecApprovalConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
|
||||
import { resolveTelegramAccount } from "./accounts.js";
|
||||
import { resolveTelegramInlineButtonsConfigScope } from "./inline-buttons.js";
|
||||
import { isNumericTelegramChatId, resolveTelegramTargetChatType } from "./targets.js";
|
||||
import { normalizeTelegramChatId, resolveTelegramTargetChatType } from "./targets.js";
|
||||
|
||||
function normalizeApproverId(value: string | number): string {
|
||||
return String(value).trim();
|
||||
@@ -47,37 +48,55 @@ export function isTelegramExecApprovalApprover(params: {
|
||||
return approvers.includes(senderId);
|
||||
}
|
||||
|
||||
/** Check if sender is an implicit approver via exec approval forwarding targets. */
|
||||
function isTelegramExecApprovalTargetsMode(cfg: OpenClawConfig): boolean {
|
||||
const execApprovals = cfg.approvals?.exec;
|
||||
if (!execApprovals?.enabled) {
|
||||
return false;
|
||||
}
|
||||
return execApprovals.mode === "targets" || execApprovals.mode === "both";
|
||||
}
|
||||
|
||||
export function isTelegramExecApprovalTargetRecipient(params: {
|
||||
cfg: OpenClawConfig;
|
||||
senderId?: string | null;
|
||||
accountId?: string | null;
|
||||
}): boolean {
|
||||
const senderId = params.senderId?.trim();
|
||||
if (!senderId) {
|
||||
if (!senderId || !isTelegramExecApprovalTargetsMode(params.cfg)) {
|
||||
return false;
|
||||
}
|
||||
const targets = params.cfg.approvals?.exec?.targets;
|
||||
if (!targets || !Array.isArray(targets)) {
|
||||
if (!targets) {
|
||||
return false;
|
||||
}
|
||||
const accountId = params.accountId?.trim() || undefined;
|
||||
const accountId = params.accountId ? normalizeAccountId(params.accountId) : undefined;
|
||||
return targets.some((target) => {
|
||||
const channel = target.channel?.trim().toLowerCase();
|
||||
if (channel !== "telegram") {
|
||||
return false;
|
||||
}
|
||||
if (accountId && target.accountId && target.accountId !== accountId) {
|
||||
if (accountId && target.accountId && normalizeAccountId(target.accountId) !== accountId) {
|
||||
return false;
|
||||
}
|
||||
const to = target.to?.trim();
|
||||
if (!to || !isNumericTelegramChatId(to) || to.startsWith("-")) {
|
||||
const to = target.to ? normalizeTelegramChatId(target.to) : undefined;
|
||||
if (!to || to.startsWith("-")) {
|
||||
return false;
|
||||
}
|
||||
return to === senderId;
|
||||
});
|
||||
}
|
||||
|
||||
export function isTelegramExecApprovalAuthorizedSender(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
senderId?: string | null;
|
||||
}): boolean {
|
||||
return (
|
||||
(isTelegramExecApprovalClientEnabled(params) && isTelegramExecApprovalApprover(params)) ||
|
||||
isTelegramExecApprovalTargetRecipient(params)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveTelegramExecApprovalTarget(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
|
||||
@@ -897,7 +897,9 @@ export const GENERATED_PLUGIN_SDK_FACADES = [
|
||||
"inspectTelegramAccount",
|
||||
"InspectedTelegramAccount",
|
||||
"isTelegramExecApprovalApprover",
|
||||
"isTelegramExecApprovalAuthorizedSender",
|
||||
"isTelegramExecApprovalClientEnabled",
|
||||
"isTelegramExecApprovalTargetRecipient",
|
||||
"listTelegramAccountIds",
|
||||
"listTelegramDirectoryGroupsFromConfig",
|
||||
"listTelegramDirectoryPeersFromConfig",
|
||||
|
||||
@@ -6,9 +6,8 @@ import {
|
||||
isDiscordExecApprovalClientEnabled,
|
||||
} from "../../plugin-sdk/discord-surface.js";
|
||||
import {
|
||||
isTelegramExecApprovalAuthorizedSender,
|
||||
isTelegramExecApprovalApprover,
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
isTelegramExecApprovalTargetRecipient,
|
||||
} from "../../plugin-sdk/telegram-runtime.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
import { requireGatewayClientScopeForInternalChannel } from "./command-gates.js";
|
||||
@@ -141,24 +140,14 @@ export const handleApproveCommand: CommandHandler = async (params, allowTextComm
|
||||
senderId: params.command.senderId,
|
||||
};
|
||||
|
||||
const isImplicitTargetApprover = isTelegramExecApprovalTargetRecipient(telegramApproverContext);
|
||||
|
||||
if (!isPluginId) {
|
||||
const isExplicitApprover =
|
||||
isTelegramExecApprovalClientEnabled({ cfg: params.cfg, accountId: params.ctx.AccountId }) &&
|
||||
isTelegramExecApprovalApprover(telegramApproverContext);
|
||||
if (!isExplicitApprover && !isImplicitTargetApprover) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "❌ You are not authorized to approve exec requests on Telegram." },
|
||||
};
|
||||
}
|
||||
if (!isPluginId && !isTelegramExecApprovalAuthorizedSender(telegramApproverContext)) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "❌ You are not authorized to approve exec requests on Telegram." },
|
||||
};
|
||||
}
|
||||
|
||||
// Keep plugin-ID routing independent from exec approval client enablement so
|
||||
// forwarded plugin approvals remain resolvable, but still require explicit
|
||||
// Telegram approver membership for security parity.
|
||||
if (isPluginId && !isTelegramExecApprovalApprover(telegramApproverContext) && !isImplicitTargetApprover) {
|
||||
if (isPluginId && !isTelegramExecApprovalApprover(telegramApproverContext)) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "❌ You are not authorized to approve plugin requests on Telegram." },
|
||||
|
||||
@@ -361,6 +361,28 @@ describe("/approve command", () => {
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
function createTelegramTargetApproveCfg(
|
||||
targets: Array<{ channel: string; to: string; accountId?: string }> = [
|
||||
{ channel: "telegram", to: "123" },
|
||||
],
|
||||
): OpenClawConfig {
|
||||
return {
|
||||
commands: { text: true },
|
||||
channels: {
|
||||
telegram: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
approvals: {
|
||||
exec: {
|
||||
enabled: true,
|
||||
mode: "targets",
|
||||
targets,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
function createDiscordApproveCfg(
|
||||
execApprovals: {
|
||||
enabled: boolean;
|
||||
@@ -607,7 +629,7 @@ describe("/approve command", () => {
|
||||
SenderId: "123",
|
||||
},
|
||||
setup: undefined,
|
||||
expectedText: "Telegram exec approvals are not enabled",
|
||||
expectedText: "not authorized to approve",
|
||||
expectGatewayCalls: 0,
|
||||
},
|
||||
{
|
||||
@@ -635,6 +657,27 @@ describe("/approve command", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts Telegram /approve from active exec forwarding targets", async () => {
|
||||
const cfg = createTelegramTargetApproveCfg([{ channel: "telegram", to: "tg:123" }]);
|
||||
const params = buildParams("/approve abc12345 allow-once", cfg, {
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
SenderId: "123",
|
||||
});
|
||||
|
||||
callGatewayMock.mockResolvedValue({ ok: true });
|
||||
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("Approval allow-once submitted");
|
||||
expect(callGatewayMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: "exec.approval.resolve",
|
||||
params: { id: "abc12345", decision: "allow-once" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects Telegram plugin-prefixed IDs when no approver policy is configured", async () => {
|
||||
const cfg = createTelegramApproveCfg(null);
|
||||
const params = buildParams("/approve plugin:abc123 allow-once", cfg, {
|
||||
@@ -685,6 +728,20 @@ describe("/approve command", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps Telegram plugin-prefixed IDs explicit-only for exec forwarding targets", async () => {
|
||||
const cfg = createTelegramTargetApproveCfg();
|
||||
const params = buildParams("/approve plugin:abc123 allow-once", cfg, {
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
SenderId: "123",
|
||||
});
|
||||
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("not authorized to approve plugin requests");
|
||||
expect(callGatewayMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("enforces gateway approval scopes", async () => {
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
|
||||
@@ -15,6 +15,7 @@ export {
|
||||
getModelsPageSize,
|
||||
inspectTelegramAccount,
|
||||
isTelegramExecApprovalApprover,
|
||||
isTelegramExecApprovalAuthorizedSender,
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
isTelegramExecApprovalTargetRecipient,
|
||||
listTelegramAccountIds,
|
||||
|
||||
@@ -11,7 +11,9 @@ export {
|
||||
getModelsPageSize,
|
||||
inspectTelegramAccount,
|
||||
isTelegramExecApprovalApprover,
|
||||
isTelegramExecApprovalAuthorizedSender,
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
isTelegramExecApprovalTargetRecipient,
|
||||
listTelegramAccountIds,
|
||||
listTelegramDirectoryGroupsFromConfig,
|
||||
listTelegramDirectoryPeersFromConfig,
|
||||
|
||||
Reference in New Issue
Block a user