Skip to content

Commit 107be4e

Browse files
feat(feishu): add global groupSenderAllowFrom for sender-level group access control (#29174) thanks @1MoreBuild
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: 1MoreBuild <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent aef5355 commit 107be4e

File tree

4 files changed

+129
-4
lines changed

4 files changed

+129
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
2626
- Feishu/Opus media send type: send `.opus` attachments with `msg_type: "audio"` (instead of `"media"`) so Feishu voice messages deliver correctly while `.mp4` remains `msg_type: "media"` and documents remain `msg_type: "file"`. (#28269) Thanks @Glucksberg.
2727
- Feishu/Inbound rich-text parsing: preserve `share_chat` payload summaries when available and add explicit parsing for rich-text `code`/`code_block`/`pre` tags so forwarded and code-heavy messages keep useful context in agent input. (#28591) Thanks @kevinWangSheng.
2828
- Feishu/Local media sends: propagate `mediaLocalRoots` through Feishu outbound media sending into `loadWebMedia` so local path attachments work with post-CVE local-root enforcement. (#27884) Thanks @joelnishanth.
29+
- Feishu/Group sender allowlist fallback: add global `channels.feishu.groupSenderAllowFrom` sender authorization for group chats, with per-group `groups.<id>.allowFrom` precedence and regression coverage for allow/block/precedence behavior. (#29174) Thanks @1MoreBuild.
2930
- Security/Feishu webhook ingress: bound unauthenticated webhook rate-limit state with stale-window pruning and a hard key cap to prevent unbounded pre-auth memory growth from rotating source keys. (#26050) Thanks @bmendonca3.
3031
- Security/Compaction audit: remove the post-compaction audit injection message. (#28507) Thanks @fuller-stack-dev and @vincentkoc.
3132
- Telegram/Reply media context: include replied media files in inbound context when replying to media, defer reply-media downloads to debounce flush, gate reply-media fetch behind DM authorization, and preserve replied media when non-vision sticker fallback runs (including cached-sticker paths). (#28488) Thanks @obviyus.

extensions/feishu/src/bot.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,126 @@ describe("handleFeishuMessage command authorization", () => {
403403
);
404404
});
405405

406+
it("allows group sender when global groupSenderAllowFrom includes sender", async () => {
407+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
408+
409+
const cfg: ClawdbotConfig = {
410+
channels: {
411+
feishu: {
412+
groupPolicy: "open",
413+
groupSenderAllowFrom: ["ou-allowed"],
414+
groups: {
415+
"oc-group": {
416+
requireMention: false,
417+
},
418+
},
419+
},
420+
},
421+
} as ClawdbotConfig;
422+
423+
const event: FeishuMessageEvent = {
424+
sender: {
425+
sender_id: {
426+
open_id: "ou-allowed",
427+
},
428+
},
429+
message: {
430+
message_id: "msg-global-group-sender-allow",
431+
chat_id: "oc-group",
432+
chat_type: "group",
433+
message_type: "text",
434+
content: JSON.stringify({ text: "hello" }),
435+
},
436+
};
437+
438+
await dispatchMessage({ cfg, event });
439+
440+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
441+
expect.objectContaining({
442+
ChatType: "group",
443+
SenderId: "ou-allowed",
444+
}),
445+
);
446+
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
447+
});
448+
449+
it("blocks group sender when global groupSenderAllowFrom excludes sender", async () => {
450+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
451+
452+
const cfg: ClawdbotConfig = {
453+
channels: {
454+
feishu: {
455+
groupPolicy: "open",
456+
groupSenderAllowFrom: ["ou-allowed"],
457+
groups: {
458+
"oc-group": {
459+
requireMention: false,
460+
},
461+
},
462+
},
463+
},
464+
} as ClawdbotConfig;
465+
466+
const event: FeishuMessageEvent = {
467+
sender: {
468+
sender_id: {
469+
open_id: "ou-blocked",
470+
},
471+
},
472+
message: {
473+
message_id: "msg-global-group-sender-block",
474+
chat_id: "oc-group",
475+
chat_type: "group",
476+
message_type: "text",
477+
content: JSON.stringify({ text: "hello" }),
478+
},
479+
};
480+
481+
await dispatchMessage({ cfg, event });
482+
483+
expect(mockFinalizeInboundContext).not.toHaveBeenCalled();
484+
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
485+
});
486+
487+
it("prefers per-group allowFrom over global groupSenderAllowFrom", async () => {
488+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
489+
490+
const cfg: ClawdbotConfig = {
491+
channels: {
492+
feishu: {
493+
groupPolicy: "open",
494+
groupSenderAllowFrom: ["ou-global"],
495+
groups: {
496+
"oc-group": {
497+
allowFrom: ["ou-group-only"],
498+
requireMention: false,
499+
},
500+
},
501+
},
502+
},
503+
} as ClawdbotConfig;
504+
505+
const event: FeishuMessageEvent = {
506+
sender: {
507+
sender_id: {
508+
open_id: "ou-global",
509+
},
510+
},
511+
message: {
512+
message_id: "msg-per-group-precedence",
513+
chat_id: "oc-group",
514+
chat_type: "group",
515+
message_type: "text",
516+
content: JSON.stringify({ text: "hello" }),
517+
},
518+
};
519+
520+
await dispatchMessage({ cfg, event });
521+
522+
expect(mockFinalizeInboundContext).not.toHaveBeenCalled();
523+
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
524+
});
525+
406526
it("uses video file_key (not thumbnail image_key) for inbound video download", async () => {
407527
mockShouldComputeCommandAuthorized.mockReturnValue(false);
408528

extensions/feishu/src/bot.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -812,12 +812,15 @@ export async function handleFeishuMessage(params: {
812812
return;
813813
}
814814

815-
// Additional sender-level allowlist check if group has specific allowFrom config
816-
const senderAllowFrom = groupConfig?.allowFrom ?? [];
817-
if (senderAllowFrom.length > 0) {
815+
// Sender-level allowlist: per-group allowFrom takes precedence, then global groupSenderAllowFrom
816+
const perGroupSenderAllowFrom = groupConfig?.allowFrom ?? [];
817+
const globalSenderAllowFrom = feishuCfg?.groupSenderAllowFrom ?? [];
818+
const effectiveSenderAllowFrom =
819+
perGroupSenderAllowFrom.length > 0 ? perGroupSenderAllowFrom : globalSenderAllowFrom;
820+
if (effectiveSenderAllowFrom.length > 0) {
818821
const senderAllowed = isFeishuGroupAllowed({
819822
groupPolicy: "allowlist",
820-
allowFrom: senderAllowFrom,
823+
allowFrom: effectiveSenderAllowFrom,
821824
senderId: ctx.senderOpenId,
822825
senderIds: [senderUserId],
823826
senderName: ctx.senderName,

extensions/feishu/src/config-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ const FeishuSharedConfigShape = {
146146
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
147147
groupPolicy: GroupPolicySchema.optional(),
148148
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
149+
groupSenderAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
149150
requireMention: z.boolean().optional(),
150151
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
151152
historyLimit: z.number().int().min(0).optional(),

0 commit comments

Comments
 (0)