fix: enforce Nextcloud Talk allowlist by user id

This commit is contained in:
Peter Steinberger
2026-02-03 17:35:47 -08:00
parent bbe9cb3022
commit 6b4b6049b4
5 changed files with 37 additions and 13 deletions

View File

@@ -72,6 +72,7 @@ Minimal config:
- `openclaw pairing list nextcloud-talk` - `openclaw pairing list nextcloud-talk`
- `openclaw pairing approve nextcloud-talk <CODE>` - `openclaw pairing approve nextcloud-talk <CODE>`
- Public DMs: `channels.nextcloud-talk.dmPolicy="open"` plus `channels.nextcloud-talk.allowFrom=["*"]`. - Public DMs: `channels.nextcloud-talk.dmPolicy="open"` plus `channels.nextcloud-talk.allowFrom=["*"]`.
- `allowFrom` matches Nextcloud user IDs only; display names are ignored.
## Rooms (groups) ## Rooms (groups)

View File

@@ -121,7 +121,6 @@ export async function handleNextcloudTalkInbound(params: {
const senderAllowedForCommands = resolveNextcloudTalkAllowlistMatch({ const senderAllowedForCommands = resolveNextcloudTalkAllowlistMatch({
allowFrom: isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom, allowFrom: isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom,
senderId, senderId,
senderName,
}).allowed; }).allowed;
const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config as OpenClawConfig); const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config as OpenClawConfig);
const commandGate = resolveControlCommandGate({ const commandGate = resolveControlCommandGate({
@@ -143,7 +142,6 @@ export async function handleNextcloudTalkInbound(params: {
outerAllowFrom: effectiveGroupAllowFrom, outerAllowFrom: effectiveGroupAllowFrom,
innerAllowFrom: roomAllowFrom, innerAllowFrom: roomAllowFrom,
senderId, senderId,
senderName,
}); });
if (!groupAllow.allowed) { if (!groupAllow.allowed) {
runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (policy=${groupPolicy})`); runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (policy=${groupPolicy})`);
@@ -158,7 +156,6 @@ export async function handleNextcloudTalkInbound(params: {
const dmAllowed = resolveNextcloudTalkAllowlistMatch({ const dmAllowed = resolveNextcloudTalkAllowlistMatch({
allowFrom: effectiveAllowFrom, allowFrom: effectiveAllowFrom,
senderId, senderId,
senderName,
}).allowed; }).allowed;
if (!dmAllowed) { if (!dmAllowed) {
if (dmPolicy === "pairing") { if (dmPolicy === "pairing") {

View File

@@ -54,7 +54,7 @@ function payloadToInboundMessage(
roomToken: payload.target.id, roomToken: payload.target.id,
roomName: payload.target.name, roomName: payload.target.name,
senderId: payload.actor.id, senderId: payload.actor.id,
senderName: payload.actor.name, senderName: payload.actor.name ?? "",
text: payload.object.content || payload.object.name || "", text: payload.object.content || payload.object.name || "",
mediaType: payload.object.mediaType || "text/plain", mediaType: payload.object.mediaType || "text/plain",
timestamp: Date.now(), timestamp: Date.now(),

View File

@@ -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);
});
});
});

View File

@@ -29,8 +29,7 @@ export function normalizeNextcloudTalkAllowlist(
export function resolveNextcloudTalkAllowlistMatch(params: { export function resolveNextcloudTalkAllowlistMatch(params: {
allowFrom: Array<string | number> | undefined; allowFrom: Array<string | number> | undefined;
senderId: string; senderId: string;
senderName?: string | null; }): AllowlistMatch<"wildcard" | "id"> {
}): AllowlistMatch<"wildcard" | "id" | "name"> {
const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom); const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
if (allowFrom.length === 0) { if (allowFrom.length === 0) {
return { allowed: false }; return { allowed: false };
@@ -42,10 +41,6 @@ export function resolveNextcloudTalkAllowlistMatch(params: {
if (allowFrom.includes(senderId)) { if (allowFrom.includes(senderId)) {
return { allowed: true, matchKey: senderId, matchSource: "id" }; 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 }; return { allowed: false };
} }
@@ -132,7 +127,6 @@ export function resolveNextcloudTalkGroupAllow(params: {
outerAllowFrom: Array<string | number> | undefined; outerAllowFrom: Array<string | number> | undefined;
innerAllowFrom: Array<string | number> | undefined; innerAllowFrom: Array<string | number> | undefined;
senderId: string; senderId: string;
senderName?: string | null;
}): { allowed: boolean; outerMatch: AllowlistMatch; innerMatch: AllowlistMatch } { }): { allowed: boolean; outerMatch: AllowlistMatch; innerMatch: AllowlistMatch } {
if (params.groupPolicy === "disabled") { if (params.groupPolicy === "disabled") {
return { allowed: false, outerMatch: { allowed: false }, innerMatch: { allowed: false } }; return { allowed: false, outerMatch: { allowed: false }, innerMatch: { allowed: false } };
@@ -150,12 +144,10 @@ export function resolveNextcloudTalkGroupAllow(params: {
const outerMatch = resolveNextcloudTalkAllowlistMatch({ const outerMatch = resolveNextcloudTalkAllowlistMatch({
allowFrom: params.outerAllowFrom, allowFrom: params.outerAllowFrom,
senderId: params.senderId, senderId: params.senderId,
senderName: params.senderName,
}); });
const innerMatch = resolveNextcloudTalkAllowlistMatch({ const innerMatch = resolveNextcloudTalkAllowlistMatch({
allowFrom: params.innerAllowFrom, allowFrom: params.innerAllowFrom,
senderId: params.senderId, senderId: params.senderId,
senderName: params.senderName,
}); });
const allowed = resolveNestedAllowlistDecision({ const allowed = resolveNestedAllowlistDecision({
outerConfigured: outerAllow.length > 0 || innerAllow.length > 0, outerConfigured: outerAllow.length > 0 || innerAllow.length > 0,