mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(discord): gate guild metadata reads [AI] (#98966)
* fix(discord): gate guild metadata reads * fix(discord): require guild-wide active thread reads
This commit is contained in:
committed by
GitHub
parent
22d686d13e
commit
5bcd25f0fb
@@ -37,6 +37,7 @@ import {
|
||||
uploadStickerDiscord,
|
||||
resolveEventCoverImage,
|
||||
} from "../send.js";
|
||||
import { createDiscordMessagingActionContext } from "./runtime.messaging.shared.js";
|
||||
import {
|
||||
createDiscordActionOptions,
|
||||
readDiscordChannelCreateParams,
|
||||
@@ -364,8 +365,22 @@ export async function handleDiscordGuildAction(
|
||||
}
|
||||
assertGuildAdminActionEnabled(action, isActionEnabled);
|
||||
await verifySenderGuildAdminPermission({ action, values: params, accountId, cfg });
|
||||
const readTargetGate = createDiscordMessagingActionContext({
|
||||
action,
|
||||
input: params,
|
||||
isActionEnabled,
|
||||
cfg,
|
||||
options,
|
||||
});
|
||||
const withOpts = (extra?: Record<string, unknown>) =>
|
||||
createDiscordActionOptions({ cfg, accountId, extra });
|
||||
const assertGuildMetadataReadAllowed = async (guildId: string) => {
|
||||
await readTargetGate.assertGuildReadTargetAllowed({
|
||||
guildId,
|
||||
channelTargetRequiredMessage:
|
||||
"Discord guild metadata reads require a wildcard channel allowlist for this guild.",
|
||||
});
|
||||
};
|
||||
switch (action) {
|
||||
case "memberInfo": {
|
||||
if (!isActionEnabled("memberInfo")) {
|
||||
@@ -374,6 +389,7 @@ export async function handleDiscordGuildAction(
|
||||
const guildId = readStringParam(params, "guildId", {
|
||||
required: true,
|
||||
});
|
||||
await assertGuildMetadataReadAllowed(guildId);
|
||||
const userId = readStringParam(params, "userId", {
|
||||
required: true,
|
||||
});
|
||||
@@ -395,6 +411,7 @@ export async function handleDiscordGuildAction(
|
||||
const guildId = readStringParam(params, "guildId", {
|
||||
required: true,
|
||||
});
|
||||
await assertGuildMetadataReadAllowed(guildId);
|
||||
const roles = await discordGuildActionRuntime.fetchRoleInfoDiscord(guildId, withOpts());
|
||||
return jsonResult({ ok: true, roles });
|
||||
}
|
||||
@@ -405,6 +422,7 @@ export async function handleDiscordGuildAction(
|
||||
const guildId = readStringParam(params, "guildId", {
|
||||
required: true,
|
||||
});
|
||||
await assertGuildMetadataReadAllowed(guildId);
|
||||
const emojis = await discordGuildActionRuntime.listGuildEmojisDiscord(guildId, withOpts());
|
||||
return jsonResult({ ok: true, emojis });
|
||||
}
|
||||
@@ -489,6 +507,7 @@ export async function handleDiscordGuildAction(
|
||||
const channelId = readStringParam(params, "channelId", {
|
||||
required: true,
|
||||
});
|
||||
await readTargetGate.assertReadTargetAllowed({ channelId });
|
||||
const channel = await discordGuildActionRuntime.fetchChannelInfoDiscord(
|
||||
channelId,
|
||||
withOpts(),
|
||||
@@ -502,6 +521,7 @@ export async function handleDiscordGuildAction(
|
||||
const guildId = readStringParam(params, "guildId", {
|
||||
required: true,
|
||||
});
|
||||
await assertGuildMetadataReadAllowed(guildId);
|
||||
const channels = await discordGuildActionRuntime.listGuildChannelsDiscord(
|
||||
guildId,
|
||||
withOpts(),
|
||||
@@ -515,6 +535,7 @@ export async function handleDiscordGuildAction(
|
||||
const guildId = readStringParam(params, "guildId", {
|
||||
required: true,
|
||||
});
|
||||
await assertGuildMetadataReadAllowed(guildId);
|
||||
const userId = readStringParam(params, "userId", {
|
||||
required: true,
|
||||
});
|
||||
@@ -532,6 +553,7 @@ export async function handleDiscordGuildAction(
|
||||
const guildId = readStringParam(params, "guildId", {
|
||||
required: true,
|
||||
});
|
||||
await assertGuildMetadataReadAllowed(guildId);
|
||||
const events = await discordGuildActionRuntime.listScheduledEventsDiscord(
|
||||
guildId,
|
||||
withOpts(),
|
||||
|
||||
@@ -365,6 +365,15 @@ export async function handleDiscordMessageSendAction(ctx: DiscordMessagingAction
|
||||
const includeArchived = readBooleanParam(ctx.params, "includeArchived");
|
||||
const before = readStringParam(ctx.params, "before");
|
||||
const limit = readPositiveIntegerParam(ctx.params, "limit");
|
||||
if (channelId && includeArchived === true) {
|
||||
await ctx.assertReadTargetAllowed({ guildId, channelId });
|
||||
} else {
|
||||
await ctx.assertGuildReadTargetAllowed({
|
||||
guildId,
|
||||
channelTargetRequiredMessage:
|
||||
"Discord active thread lists require a wildcard channel allowlist so each read target can be authorized.",
|
||||
});
|
||||
}
|
||||
const threads = await discordMessagingActionRuntime.listThreadsDiscord(
|
||||
{
|
||||
guildId,
|
||||
|
||||
@@ -38,7 +38,10 @@ export type DiscordMessagingActionContext = {
|
||||
accountId?: string;
|
||||
resolveChannelId: () => string;
|
||||
assertReadTargetAllowed: (params: { guildId?: string; channelId: string }) => Promise<void>;
|
||||
assertGuildReadTargetAllowed: (params: { guildId: string }) => Promise<void>;
|
||||
assertGuildReadTargetAllowed: (params: {
|
||||
guildId: string;
|
||||
channelTargetRequiredMessage?: string;
|
||||
}) => Promise<void>;
|
||||
resolveReactionChannelId: () => Promise<string>;
|
||||
withOpts: (extra?: Record<string, unknown>) => { cfg: OpenClawConfig; accountId?: string };
|
||||
withReactionRuntimeOptions: <T extends Record<string, unknown> = Record<string, never>>(
|
||||
@@ -357,7 +360,7 @@ export function createDiscordMessagingActionContext(params: {
|
||||
throw new Error("Discord read target channel is not allowed.");
|
||||
}
|
||||
},
|
||||
assertGuildReadTargetAllowed: async ({ guildId }) => {
|
||||
assertGuildReadTargetAllowed: async ({ guildId, channelTargetRequiredMessage }) => {
|
||||
const guildInfo = await resolveReadGuildEntry(guildId);
|
||||
if (
|
||||
!isDiscordGroupAllowedByPolicy({
|
||||
@@ -374,7 +377,8 @@ export function createDiscordMessagingActionContext(params: {
|
||||
!allowsAllDiscordGuildChannels(guildInfo.channels)
|
||||
) {
|
||||
throw new Error(
|
||||
"Discord message search requires channelId or channelIds so each read target can be authorized.",
|
||||
channelTargetRequiredMessage ??
|
||||
"Discord message search requires channelId or channelIds so each read target can be authorized.",
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -55,13 +55,18 @@ const discordSendMocks = {
|
||||
id: guildId,
|
||||
name: "Guild",
|
||||
})),
|
||||
fetchMemberInfoDiscord: vi.fn(async () => ({ user: { id: "U1" } })),
|
||||
hasAnyChannelPermissionDiscord: vi.fn(async () => true),
|
||||
hasAnyGuildPermissionDiscord: vi.fn(async () => true),
|
||||
fetchMessageDiscord: vi.fn(async () => ({})),
|
||||
fetchReactionsDiscord: vi.fn(async () => ({})),
|
||||
fetchRoleInfoDiscord: vi.fn(async () => []),
|
||||
fetchVoiceStatusDiscord: vi.fn(async () => ({})),
|
||||
kickMemberDiscord: vi.fn(async () => ({})),
|
||||
listGuildChannelsDiscord: vi.fn(async () => []),
|
||||
listGuildEmojisDiscord: vi.fn(async () => []),
|
||||
listPinsDiscord: vi.fn(async () => ({})),
|
||||
listScheduledEventsDiscord: vi.fn(async () => []),
|
||||
listThreadsDiscord: vi.fn(async () => ({})),
|
||||
moveChannelDiscord: vi.fn(async () => ({ ok: true })),
|
||||
pinMessageDiscord: vi.fn(async () => ({})),
|
||||
@@ -94,13 +99,18 @@ const {
|
||||
fetchChannelInfoDiscord,
|
||||
fetchChannelPermissionsDiscord,
|
||||
fetchGuildInfoDiscord,
|
||||
fetchMemberInfoDiscord,
|
||||
fetchReactionsDiscord,
|
||||
fetchMessageDiscord,
|
||||
fetchRoleInfoDiscord,
|
||||
fetchVoiceStatusDiscord,
|
||||
hasAnyChannelPermissionDiscord,
|
||||
hasAnyGuildPermissionDiscord,
|
||||
kickMemberDiscord,
|
||||
listGuildChannelsDiscord,
|
||||
listGuildEmojisDiscord,
|
||||
listPinsDiscord,
|
||||
listScheduledEventsDiscord,
|
||||
listThreadsDiscord,
|
||||
moveChannelDiscord,
|
||||
reactMessageDiscord,
|
||||
@@ -127,6 +137,18 @@ const DISCORD_TEST_CFG = {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
function discordAllowlistCfg(guilds: Record<string, unknown>): OpenClawConfig {
|
||||
return {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "token",
|
||||
groupPolicy: "allowlist",
|
||||
guilds,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
type MockCallSource = { mock: { calls: Array<Array<unknown>> } };
|
||||
|
||||
function mockCall(source: MockCallSource, label: string, callIndex = 0): Array<unknown> {
|
||||
@@ -404,6 +426,71 @@ describe("handleDiscordMessagingAction", () => {
|
||||
expect(result.details).not.toHaveProperty("nextBefore");
|
||||
});
|
||||
|
||||
it("rejects archived Discord thread lists for non-allowlisted target channels", async () => {
|
||||
const cfg = discordAllowlistCfg({
|
||||
"111": {
|
||||
channels: {
|
||||
"222": { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
handleMessagingAction(
|
||||
"threadList",
|
||||
{ guildId: "111", channelId: "333", includeArchived: true },
|
||||
enableAllActions,
|
||||
cfg,
|
||||
),
|
||||
).rejects.toThrow("Discord read target channel is not allowed.");
|
||||
expect(listThreadsDiscord).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires guild-wide authorization for active Discord thread lists", async () => {
|
||||
const cfg = discordAllowlistCfg({
|
||||
"111": {
|
||||
channels: {
|
||||
"222": { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
handleMessagingAction(
|
||||
"threadList",
|
||||
{ guildId: "111", channelId: "222" },
|
||||
enableAllActions,
|
||||
cfg,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
"Discord active thread lists require a wildcard channel allowlist so each read target can be authorized.",
|
||||
);
|
||||
expect(listThreadsDiscord).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows guild-wide Discord thread lists when the guild has a wildcard channel allowlist", async () => {
|
||||
const cfg = discordAllowlistCfg({
|
||||
"111": {
|
||||
channels: {
|
||||
"*": { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await handleMessagingAction("threadList", { guildId: "111" }, enableAllActions, cfg);
|
||||
|
||||
expect(listThreadsDiscord).toHaveBeenCalledWith(
|
||||
{
|
||||
guildId: "111",
|
||||
channelId: undefined,
|
||||
includeArchived: undefined,
|
||||
before: undefined,
|
||||
limit: undefined,
|
||||
},
|
||||
{ cfg },
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves Discord DM targets for reaction adds", async () => {
|
||||
const resolveReactionTarget = vi.fn(async () => "DM1");
|
||||
discordMessagingActionRuntime.resolveDiscordReactionTargetChannelId = resolveReactionTarget;
|
||||
@@ -1614,6 +1701,115 @@ describe("handleDiscordGuildAction", () => {
|
||||
expect(details.status).toBe("online");
|
||||
expect(details.activities).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
action: "memberInfo",
|
||||
params: { guildId: "333", userId: "U1" },
|
||||
runtimeCall: fetchMemberInfoDiscord,
|
||||
},
|
||||
{ action: "roleInfo", params: { guildId: "333" }, runtimeCall: fetchRoleInfoDiscord },
|
||||
{ action: "emojiList", params: { guildId: "333" }, runtimeCall: listGuildEmojisDiscord },
|
||||
{ action: "channelList", params: { guildId: "333" }, runtimeCall: listGuildChannelsDiscord },
|
||||
{
|
||||
action: "voiceStatus",
|
||||
params: { guildId: "333", userId: "U1" },
|
||||
runtimeCall: fetchVoiceStatusDiscord,
|
||||
},
|
||||
{ action: "eventList", params: { guildId: "333" }, runtimeCall: listScheduledEventsDiscord },
|
||||
])(
|
||||
"rejects Discord guild metadata action $action for non-allowlisted guilds",
|
||||
async ({ action, params, runtimeCall }) => {
|
||||
const cfg = discordAllowlistCfg({
|
||||
"111": {
|
||||
channels: {
|
||||
"*": { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(handleGuildAction(action, params, enableAllActions, cfg)).rejects.toThrow(
|
||||
"Discord read target channel is not allowed.",
|
||||
);
|
||||
expect(runtimeCall).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("requires a guild-wide allowlist for Discord guild metadata reads", async () => {
|
||||
const cfg = discordAllowlistCfg({
|
||||
"111": {
|
||||
channels: {
|
||||
"222": { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
handleGuildAction("memberInfo", { guildId: "111", userId: "U1" }, enableAllActions, cfg),
|
||||
).rejects.toThrow(
|
||||
"Discord guild metadata reads require a wildcard channel allowlist for this guild.",
|
||||
);
|
||||
expect(fetchMemberInfoDiscord).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows Discord guild metadata reads when the guild has a wildcard channel allowlist", async () => {
|
||||
const cfg = discordAllowlistCfg({
|
||||
"111": {
|
||||
channels: {
|
||||
"*": { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await handleGuildAction("roleInfo", { guildId: "111" }, enableAllActions, cfg);
|
||||
|
||||
expect(fetchRoleInfoDiscord).toHaveBeenCalledWith("111", { cfg });
|
||||
});
|
||||
|
||||
it("rejects Discord channel info reads for non-allowlisted target channels", async () => {
|
||||
fetchChannelInfoDiscord.mockResolvedValue({
|
||||
id: "333",
|
||||
guild_id: "111",
|
||||
name: "private",
|
||||
type: 0,
|
||||
});
|
||||
const cfg = discordAllowlistCfg({
|
||||
"111": {
|
||||
channels: {
|
||||
"222": { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
handleGuildAction("channelInfo", { channelId: "333" }, channelInfoEnabled, cfg),
|
||||
).rejects.toThrow("Discord read target channel is not allowed.");
|
||||
expect(fetchChannelInfoDiscord).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("allows Discord channel info reads for allowlisted target channels", async () => {
|
||||
fetchChannelInfoDiscord.mockResolvedValue({
|
||||
id: "222",
|
||||
guild_id: "111",
|
||||
name: "allowed",
|
||||
type: 0,
|
||||
});
|
||||
const cfg = discordAllowlistCfg({
|
||||
"111": {
|
||||
channels: {
|
||||
"222": { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await handleGuildAction("channelInfo", { channelId: "222" }, channelInfoEnabled, cfg);
|
||||
|
||||
expect(fetchChannelInfoDiscord).toHaveBeenCalledTimes(2);
|
||||
expect(mockCall(fetchChannelInfoDiscord, "fetchChannelInfoDiscord", 1)).toEqual([
|
||||
"222",
|
||||
{ cfg },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const channelsEnabled = (key: keyof DiscordActionConfig) => key === "channels";
|
||||
|
||||
Reference in New Issue
Block a user