fix: enforce Nextcloud Talk allowlist by user id
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
34
extensions/nextcloud-talk/src/policy.test.ts
Normal file
34
extensions/nextcloud-talk/src/policy.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user