mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(telegram): require admin for target writeback [AI] (#88973)
* fix: require admin for Telegram target writeback * fix(telegram): preserve internal target writeback * fix: scope Telegram target writeback authority * fix: infer internal writeback for durable sends * fix: preserve scoped Telegram writeback boundaries * fix: preserve direct Telegram writeback * test: make Telegram writeback scope intent explicit * fix(telegram): keep target writeback authority local --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
co-authored by
Vincent Koc
parent
70a989a97a
commit
5483ff705f
@@ -1080,6 +1080,32 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves internal target writeback when gateway scopes are absent", async () => {
|
||||
const sendMessage = vi.fn().mockResolvedValue({
|
||||
message_id: 1,
|
||||
chat: { id: "-100123" },
|
||||
});
|
||||
const getChat = vi.fn().mockResolvedValue({ id: -100123 });
|
||||
const api = { sendMessage, getChat } as unknown as {
|
||||
sendMessage: typeof sendMessage;
|
||||
getChat: typeof getChat;
|
||||
};
|
||||
|
||||
await sendMessageTelegram("https://t.me/mychannel", "hi", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
});
|
||||
|
||||
expect(getChat).toHaveBeenCalledWith("@mychannel");
|
||||
expectPersistedTarget({
|
||||
rawTarget: "https://t.me/mychannel",
|
||||
resolvedChatId: "-100123",
|
||||
gatewayClientScopes: undefined,
|
||||
trustedInternalWriteback: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("fails clearly when a legacy target cannot be resolved", async () => {
|
||||
const getChat = vi.fn().mockRejectedValue(new Error("400: Bad Request: chat not found"));
|
||||
const api = { getChat } as unknown as {
|
||||
|
||||
@@ -399,6 +399,7 @@ async function resolveAndPersistChatId(params: {
|
||||
resolvedChatId: chatId,
|
||||
verbose: params.verbose,
|
||||
gatewayClientScopes: params.gatewayClientScopes,
|
||||
...(params.gatewayClientScopes === undefined ? { trustedInternalWriteback: true } : {}),
|
||||
});
|
||||
return chatId;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ export function installMaybePersistResolvedTelegramTargetTests(params?: {
|
||||
cfg: {} as OpenClawConfig,
|
||||
rawTarget: "-100123",
|
||||
resolvedChatId: "-100123",
|
||||
gatewayClientScopes: ["operator.admin"],
|
||||
});
|
||||
|
||||
expect(readConfigFileSnapshotForWrite).not.toHaveBeenCalled();
|
||||
@@ -118,6 +119,23 @@ export function installMaybePersistResolvedTelegramTargetTests(params?: {
|
||||
expect(saveCronStore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not let internal writeback override non-admin gateway scopes", async () => {
|
||||
await maybePersistResolvedTelegramTarget({
|
||||
cfg: {
|
||||
cron: { store: "/tmp/cron/jobs.json" },
|
||||
} as OpenClawConfig,
|
||||
rawTarget: "t.me/mychannel",
|
||||
resolvedChatId: "-100123",
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
trustedInternalWriteback: true,
|
||||
});
|
||||
|
||||
expect(readConfigFileSnapshotForWrite).not.toHaveBeenCalled();
|
||||
expect(writeConfigFile).not.toHaveBeenCalled();
|
||||
expect(loadCronStore).not.toHaveBeenCalled();
|
||||
expect(saveCronStore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips config and cron writeback for gateway callers with an empty scope set", async () => {
|
||||
await maybePersistResolvedTelegramTarget({
|
||||
cfg: {
|
||||
@@ -133,6 +151,53 @@ export function installMaybePersistResolvedTelegramTargetTests(params?: {
|
||||
expect(loadCronStore).not.toHaveBeenCalled();
|
||||
expect(saveCronStore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips config and cron writeback when gateway scopes are missing", async () => {
|
||||
await maybePersistResolvedTelegramTarget({
|
||||
cfg: {
|
||||
cron: { store: "/tmp/cron/jobs.json" },
|
||||
} as OpenClawConfig,
|
||||
rawTarget: "t.me/mychannel",
|
||||
resolvedChatId: "-100123",
|
||||
gatewayClientScopes: undefined,
|
||||
});
|
||||
|
||||
expect(readConfigFileSnapshotForWrite).not.toHaveBeenCalled();
|
||||
expect(writeConfigFile).not.toHaveBeenCalled();
|
||||
expect(loadCronStore).not.toHaveBeenCalled();
|
||||
expect(saveCronStore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("writes back for gateway callers with operator.admin", async () => {
|
||||
readConfigFileSnapshotForWrite.mockResolvedValue({
|
||||
snapshot: {
|
||||
config: {
|
||||
channels: {
|
||||
telegram: {
|
||||
defaultTo: "t.me/mychannel",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
writeOptions: {},
|
||||
});
|
||||
loadCronStore.mockResolvedValue({
|
||||
version: 1,
|
||||
jobs: [{ id: "a", delivery: { channel: "telegram", to: "t.me/mychannel" } }],
|
||||
});
|
||||
|
||||
await maybePersistResolvedTelegramTarget({
|
||||
cfg: {
|
||||
cron: { store: "/tmp/cron/jobs.json" },
|
||||
} as OpenClawConfig,
|
||||
rawTarget: "t.me/mychannel",
|
||||
resolvedChatId: "-100123",
|
||||
gatewayClientScopes: ["operator.admin"],
|
||||
});
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
expect(saveCronStore).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
}
|
||||
|
||||
it("writes back matching config and cron targets", async () => {
|
||||
@@ -167,6 +232,8 @@ export function installMaybePersistResolvedTelegramTargetTests(params?: {
|
||||
} as OpenClawConfig,
|
||||
rawTarget: "t.me/mychannel",
|
||||
resolvedChatId: "-100123",
|
||||
gatewayClientScopes: undefined,
|
||||
trustedInternalWriteback: true,
|
||||
});
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
@@ -202,6 +269,8 @@ export function installMaybePersistResolvedTelegramTargetTests(params?: {
|
||||
cfg: {} as OpenClawConfig,
|
||||
rawTarget: "t.me/mychannel:topic:9",
|
||||
resolvedChatId: "-100123",
|
||||
gatewayClientScopes: undefined,
|
||||
trustedInternalWriteback: true,
|
||||
});
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
@@ -232,6 +301,8 @@ export function installMaybePersistResolvedTelegramTargetTests(params?: {
|
||||
cfg: {} as OpenClawConfig,
|
||||
rawTarget: "@MyChannel",
|
||||
resolvedChatId: "-100123",
|
||||
gatewayClientScopes: undefined,
|
||||
trustedInternalWriteback: true,
|
||||
});
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -147,6 +147,7 @@ export async function maybePersistResolvedTelegramTarget(params: {
|
||||
resolvedChatId: string;
|
||||
verbose?: boolean;
|
||||
gatewayClientScopes?: readonly string[];
|
||||
trustedInternalWriteback?: boolean;
|
||||
}): Promise<void> {
|
||||
const raw = params.rawTarget.trim();
|
||||
if (!raw) {
|
||||
@@ -160,10 +161,10 @@ export async function maybePersistResolvedTelegramTarget(params: {
|
||||
return;
|
||||
}
|
||||
const { matchKey, resolvedTarget } = rewrite;
|
||||
if (
|
||||
Array.isArray(params.gatewayClientScopes) &&
|
||||
!params.gatewayClientScopes.includes(TELEGRAM_ADMIN_SCOPE)
|
||||
) {
|
||||
const hasGatewayAdminScope = params.gatewayClientScopes?.includes(TELEGRAM_ADMIN_SCOPE) === true;
|
||||
const trustedInternalWriteback =
|
||||
params.gatewayClientScopes === undefined && params.trustedInternalWriteback === true;
|
||||
if (!hasGatewayAdminScope && !trustedInternalWriteback) {
|
||||
writebackLogger.warn(
|
||||
`skipping Telegram target writeback for ${raw} because gateway caller is missing ${TELEGRAM_ADMIN_SCOPE}`,
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@ type SendDurableMessageBatchRequest = {
|
||||
to?: string;
|
||||
threadId?: string | number | null;
|
||||
durability?: string;
|
||||
gatewayClientScopes?: readonly string[];
|
||||
};
|
||||
|
||||
type DeliverySupportRequest = {
|
||||
@@ -141,6 +142,7 @@ describe("durable inbound reply delivery", () => {
|
||||
expect(request.to).toBe("chat-1");
|
||||
expect(request.threadId).toBeNull();
|
||||
expect(request.durability).toBe("best_effort");
|
||||
expect(request.gatewayClientScopes).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not require unknown-send reconciliation for the default best-effort final path", async () => {
|
||||
|
||||
@@ -207,7 +207,7 @@ export async function deliverInboundReplyWithMessageSendContext(
|
||||
silent: params.silent,
|
||||
durability,
|
||||
session,
|
||||
gatewayClientScopes: params.ctxPayload.GatewayClientScopes,
|
||||
gatewayClientScopes: params.ctxPayload.GatewayClientScopes ?? [],
|
||||
});
|
||||
if (send.status === "failed") {
|
||||
return { status: "failed" as const, error: send.error };
|
||||
|
||||
Reference in New Issue
Block a user