fix: enforce explicit group auth boundaries across channels

This commit is contained in:
Peter Steinberger
2026-02-26 18:15:57 +01:00
parent d0d83a2020
commit 64de4b6d6a
20 changed files with 614 additions and 331 deletions

View File

@@ -5,7 +5,7 @@ import {
formatTextWithAttachmentLinks,
logInboundDrop,
readStoreAllowFromForDmPolicy,
resolveControlCommandGate,
resolveDmGroupAccessWithCommandGate,
resolveOutboundMediaUrls,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
@@ -120,11 +120,6 @@ export async function handleNextcloudTalkInbound(params: {
}
const roomAllowFrom = normalizeNextcloudTalkAllowlist(roomConfig?.allowFrom);
const baseGroupAllowFrom =
configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom;
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowList].filter(Boolean);
const effectiveGroupAllowFrom = [...baseGroupAllowFrom].filter(Boolean);
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
cfg: config as OpenClawConfig,
@@ -132,25 +127,33 @@ export async function handleNextcloudTalkInbound(params: {
});
const useAccessGroups =
(config.commands as Record<string, unknown> | undefined)?.useAccessGroups !== false;
const senderAllowedForCommands = resolveNextcloudTalkAllowlistMatch({
allowFrom: isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom,
senderId,
}).allowed;
const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config as OpenClawConfig);
const commandGate = resolveControlCommandGate({
useAccessGroups,
authorizers: [
{
configured: (isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom).length > 0,
allowed: senderAllowedForCommands,
},
],
allowTextCommands,
hasControlCommand,
const access = resolveDmGroupAccessWithCommandGate({
isGroup,
dmPolicy,
groupPolicy,
allowFrom: configAllowFrom,
groupAllowFrom: configGroupAllowFrom,
storeAllowFrom: storeAllowList,
isSenderAllowed: (allowFrom) =>
resolveNextcloudTalkAllowlistMatch({
allowFrom,
senderId,
}).allowed,
command: {
useAccessGroups,
allowTextCommands,
hasControlCommand,
},
});
const commandAuthorized = commandGate.commandAuthorized;
const commandAuthorized = access.commandAuthorized;
const effectiveGroupAllowFrom = access.effectiveGroupAllowFrom;
if (isGroup) {
if (access.decision !== "allow") {
runtime.log?.(`nextcloud-talk: drop group sender ${senderId} (reason=${access.reason})`);
return;
}
const groupAllow = resolveNextcloudTalkGroupAllow({
groupPolicy,
outerAllowFrom: effectiveGroupAllowFrom,
@@ -162,48 +165,36 @@ export async function handleNextcloudTalkInbound(params: {
return;
}
} else {
if (dmPolicy === "disabled") {
runtime.log?.(`nextcloud-talk: drop DM sender=${senderId} (dmPolicy=disabled)`);
return;
}
if (dmPolicy !== "open") {
const dmAllowed = resolveNextcloudTalkAllowlistMatch({
allowFrom: effectiveAllowFrom,
senderId,
}).allowed;
if (!dmAllowed) {
if (dmPolicy === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: CHANNEL_ID,
id: senderId,
meta: { name: senderName || undefined },
});
if (created) {
try {
await sendMessageNextcloudTalk(
roomToken,
core.channel.pairing.buildPairingReply({
channel: CHANNEL_ID,
idLine: `Your Nextcloud user id: ${senderId}`,
code,
}),
{ accountId: account.accountId },
);
statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) {
runtime.error?.(
`nextcloud-talk: pairing reply failed for ${senderId}: ${String(err)}`,
);
}
if (access.decision !== "allow") {
if (access.decision === "pairing") {
const { code, created } = await core.channel.pairing.upsertPairingRequest({
channel: CHANNEL_ID,
id: senderId,
meta: { name: senderName || undefined },
});
if (created) {
try {
await sendMessageNextcloudTalk(
roomToken,
core.channel.pairing.buildPairingReply({
channel: CHANNEL_ID,
idLine: `Your Nextcloud user id: ${senderId}`,
code,
}),
{ accountId: account.accountId },
);
statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) {
runtime.error?.(`nextcloud-talk: pairing reply failed for ${senderId}: ${String(err)}`);
}
}
runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (dmPolicy=${dmPolicy})`);
return;
}
runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (reason=${access.reason})`);
return;
}
}
if (isGroup && commandGate.shouldBlock) {
if (access.shouldBlockControlCommand) {
logInboundDrop({
log: (message) => runtime.log?.(message),
channel: CHANNEL_ID,