From 6b4b6049b47c3329a7014509594647826669892d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 3 Feb 2026 17:35:47 -0800 Subject: [PATCH] fix: enforce Nextcloud Talk allowlist by user id --- docs/channels/nextcloud-talk.md | 1 + extensions/nextcloud-talk/src/inbound.ts | 3 -- extensions/nextcloud-talk/src/monitor.ts | 2 +- extensions/nextcloud-talk/src/policy.test.ts | 34 ++++++++++++++++++++ extensions/nextcloud-talk/src/policy.ts | 10 +----- 5 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 extensions/nextcloud-talk/src/policy.test.ts diff --git a/docs/channels/nextcloud-talk.md b/docs/channels/nextcloud-talk.md index 2c8698513..edca54bc4 100644 --- a/docs/channels/nextcloud-talk.md +++ b/docs/channels/nextcloud-talk.md @@ -72,6 +72,7 @@ Minimal config: - `openclaw pairing list nextcloud-talk` - `openclaw pairing approve nextcloud-talk ` - Public DMs: `channels.nextcloud-talk.dmPolicy="open"` plus `channels.nextcloud-talk.allowFrom=["*"]`. +- `allowFrom` matches Nextcloud user IDs only; display names are ignored. ## Rooms (groups) diff --git a/extensions/nextcloud-talk/src/inbound.ts b/extensions/nextcloud-talk/src/inbound.ts index a7fe45b9f..1964d1a8a 100644 --- a/extensions/nextcloud-talk/src/inbound.ts +++ b/extensions/nextcloud-talk/src/inbound.ts @@ -121,7 +121,6 @@ export async function handleNextcloudTalkInbound(params: { const senderAllowedForCommands = resolveNextcloudTalkAllowlistMatch({ allowFrom: isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom, senderId, - senderName, }).allowed; const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config as OpenClawConfig); const commandGate = resolveControlCommandGate({ @@ -143,7 +142,6 @@ export async function handleNextcloudTalkInbound(params: { outerAllowFrom: effectiveGroupAllowFrom, innerAllowFrom: roomAllowFrom, senderId, - senderName, }); if (!groupAllow.allowed) { runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (policy=${groupPolicy})`); @@ -158,7 +156,6 @@ export async function handleNextcloudTalkInbound(params: { const dmAllowed = resolveNextcloudTalkAllowlistMatch({ allowFrom: effectiveAllowFrom, senderId, - senderName, }).allowed; if (!dmAllowed) { if (dmPolicy === "pairing") { diff --git a/extensions/nextcloud-talk/src/monitor.ts b/extensions/nextcloud-talk/src/monitor.ts index 0981fa4cf..877313fa1 100644 --- a/extensions/nextcloud-talk/src/monitor.ts +++ b/extensions/nextcloud-talk/src/monitor.ts @@ -54,7 +54,7 @@ function payloadToInboundMessage( roomToken: payload.target.id, roomName: payload.target.name, senderId: payload.actor.id, - senderName: payload.actor.name, + senderName: payload.actor.name ?? "", text: payload.object.content || payload.object.name || "", mediaType: payload.object.mediaType || "text/plain", timestamp: Date.now(), diff --git a/extensions/nextcloud-talk/src/policy.test.ts b/extensions/nextcloud-talk/src/policy.test.ts new file mode 100644 index 000000000..47029248b --- /dev/null +++ b/extensions/nextcloud-talk/src/policy.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; + +import { resolveNextcloudTalkAllowlistMatch } from "./policy.js"; + +describe("nextcloud-talk policy", () => { + describe("resolveNextcloudTalkAllowlistMatch", () => { + it("allows wildcard", () => { + expect( + resolveNextcloudTalkAllowlistMatch({ + allowFrom: ["*"], + senderId: "user-id", + }).allowed, + ).toBe(true); + }); + + it("allows sender id match with normalization", () => { + expect( + resolveNextcloudTalkAllowlistMatch({ + allowFrom: ["nc:User-Id"], + senderId: "user-id", + }), + ).toEqual({ allowed: true, matchKey: "user-id", matchSource: "id" }); + }); + + it("blocks when sender id does not match", () => { + expect( + resolveNextcloudTalkAllowlistMatch({ + allowFrom: ["allowed"], + senderId: "other", + }).allowed, + ).toBe(false); + }); + }); +}); diff --git a/extensions/nextcloud-talk/src/policy.ts b/extensions/nextcloud-talk/src/policy.ts index 5d9b8cffd..f68d7e698 100644 --- a/extensions/nextcloud-talk/src/policy.ts +++ b/extensions/nextcloud-talk/src/policy.ts @@ -29,8 +29,7 @@ export function normalizeNextcloudTalkAllowlist( export function resolveNextcloudTalkAllowlistMatch(params: { allowFrom: Array | undefined; senderId: string; - senderName?: string | null; -}): AllowlistMatch<"wildcard" | "id" | "name"> { +}): AllowlistMatch<"wildcard" | "id"> { const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom); if (allowFrom.length === 0) { return { allowed: false }; @@ -42,10 +41,6 @@ export function resolveNextcloudTalkAllowlistMatch(params: { if (allowFrom.includes(senderId)) { return { allowed: true, matchKey: senderId, matchSource: "id" }; } - const senderName = params.senderName ? normalizeAllowEntry(params.senderName) : ""; - if (senderName && allowFrom.includes(senderName)) { - return { allowed: true, matchKey: senderName, matchSource: "name" }; - } return { allowed: false }; } @@ -132,7 +127,6 @@ export function resolveNextcloudTalkGroupAllow(params: { outerAllowFrom: Array | undefined; innerAllowFrom: Array | undefined; senderId: string; - senderName?: string | null; }): { allowed: boolean; outerMatch: AllowlistMatch; innerMatch: AllowlistMatch } { if (params.groupPolicy === "disabled") { return { allowed: false, outerMatch: { allowed: false }, innerMatch: { allowed: false } }; @@ -150,12 +144,10 @@ export function resolveNextcloudTalkGroupAllow(params: { const outerMatch = resolveNextcloudTalkAllowlistMatch({ allowFrom: params.outerAllowFrom, senderId: params.senderId, - senderName: params.senderName, }); const innerMatch = resolveNextcloudTalkAllowlistMatch({ allowFrom: params.innerAllowFrom, senderId: params.senderId, - senderName: params.senderName, }); const allowed = resolveNestedAllowlistDecision({ outerConfigured: outerAllow.length > 0 || innerAllow.length > 0,