mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
refactor(channels): unify dm pairing policy flows
This commit is contained in:
127
extensions/matrix/src/matrix/monitor/access-policy.ts
Normal file
127
extensions/matrix/src/matrix/monitor/access-policy.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
formatAllowlistMatchMeta,
|
||||
issuePairingChallenge,
|
||||
readStoreAllowFromForDmPolicy,
|
||||
resolveDmGroupAccessWithLists,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import {
|
||||
normalizeMatrixAllowList,
|
||||
resolveMatrixAllowListMatch,
|
||||
resolveMatrixAllowListMatches,
|
||||
} from "./allowlist.js";
|
||||
|
||||
type MatrixDmPolicy = "open" | "pairing" | "allowlist" | "disabled";
|
||||
type MatrixGroupPolicy = "open" | "allowlist" | "disabled";
|
||||
|
||||
export async function resolveMatrixAccessState(params: {
|
||||
isDirectMessage: boolean;
|
||||
resolvedAccountId: string;
|
||||
dmPolicy: MatrixDmPolicy;
|
||||
groupPolicy: MatrixGroupPolicy;
|
||||
allowFrom: string[];
|
||||
groupAllowFrom: Array<string | number>;
|
||||
senderId: string;
|
||||
readStoreForDmPolicy: (provider: string, accountId: string) => Promise<string[]>;
|
||||
}) {
|
||||
const storeAllowFrom = params.isDirectMessage
|
||||
? await readStoreAllowFromForDmPolicy({
|
||||
provider: "matrix",
|
||||
accountId: params.resolvedAccountId,
|
||||
dmPolicy: params.dmPolicy,
|
||||
readStore: params.readStoreForDmPolicy,
|
||||
})
|
||||
: [];
|
||||
const normalizedGroupAllowFrom = normalizeMatrixAllowList(params.groupAllowFrom);
|
||||
const senderGroupPolicy =
|
||||
params.groupPolicy === "disabled"
|
||||
? "disabled"
|
||||
: normalizedGroupAllowFrom.length > 0
|
||||
? "allowlist"
|
||||
: "open";
|
||||
const access = resolveDmGroupAccessWithLists({
|
||||
isGroup: !params.isDirectMessage,
|
||||
dmPolicy: params.dmPolicy,
|
||||
groupPolicy: senderGroupPolicy,
|
||||
allowFrom: params.allowFrom,
|
||||
groupAllowFrom: normalizedGroupAllowFrom,
|
||||
storeAllowFrom,
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
isSenderAllowed: (allowFrom) =>
|
||||
resolveMatrixAllowListMatches({
|
||||
allowList: normalizeMatrixAllowList(allowFrom),
|
||||
userId: params.senderId,
|
||||
}),
|
||||
});
|
||||
const effectiveAllowFrom = normalizeMatrixAllowList(access.effectiveAllowFrom);
|
||||
const effectiveGroupAllowFrom = normalizeMatrixAllowList(access.effectiveGroupAllowFrom);
|
||||
return {
|
||||
access,
|
||||
effectiveAllowFrom,
|
||||
effectiveGroupAllowFrom,
|
||||
groupAllowConfigured: effectiveGroupAllowFrom.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function enforceMatrixDirectMessageAccess(params: {
|
||||
dmEnabled: boolean;
|
||||
dmPolicy: MatrixDmPolicy;
|
||||
accessDecision: "allow" | "block" | "pairing";
|
||||
senderId: string;
|
||||
senderName: string;
|
||||
effectiveAllowFrom: string[];
|
||||
upsertPairingRequest: (input: {
|
||||
id: string;
|
||||
meta?: Record<string, string | undefined>;
|
||||
}) => Promise<{
|
||||
code: string;
|
||||
created: boolean;
|
||||
}>;
|
||||
sendPairingReply: (text: string) => Promise<void>;
|
||||
logVerboseMessage: (message: string) => void;
|
||||
}): Promise<boolean> {
|
||||
if (!params.dmEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (params.accessDecision === "allow") {
|
||||
return true;
|
||||
}
|
||||
const allowMatch = resolveMatrixAllowListMatch({
|
||||
allowList: params.effectiveAllowFrom,
|
||||
userId: params.senderId,
|
||||
});
|
||||
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
||||
if (params.accessDecision === "pairing") {
|
||||
await issuePairingChallenge({
|
||||
channel: "matrix",
|
||||
senderId: params.senderId,
|
||||
senderIdLine: `Matrix user id: ${params.senderId}`,
|
||||
meta: { name: params.senderName },
|
||||
upsertPairingRequest: params.upsertPairingRequest,
|
||||
buildReplyText: ({ code }) =>
|
||||
[
|
||||
"OpenClaw: access not configured.",
|
||||
"",
|
||||
`Pairing code: ${code}`,
|
||||
"",
|
||||
"Ask the bot owner to approve with:",
|
||||
"openclaw pairing approve matrix <code>",
|
||||
].join("\n"),
|
||||
sendPairingReply: params.sendPairingReply,
|
||||
onCreated: () => {
|
||||
params.logVerboseMessage(
|
||||
`matrix pairing request sender=${params.senderId} name=${params.senderName ?? "unknown"} (${allowMatchMeta})`,
|
||||
);
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
params.logVerboseMessage(
|
||||
`matrix pairing reply failed for ${params.senderId}: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
params.logVerboseMessage(
|
||||
`matrix: blocked dm sender ${params.senderId} (dmPolicy=${params.dmPolicy}, ${allowMatchMeta})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -7,9 +7,7 @@ import {
|
||||
formatAllowlistMatchMeta,
|
||||
logInboundDrop,
|
||||
logTypingFailure,
|
||||
readStoreAllowFromForDmPolicy,
|
||||
resolveControlCommandGate,
|
||||
resolveDmGroupAccessWithLists,
|
||||
type PluginRuntime,
|
||||
type RuntimeEnv,
|
||||
type RuntimeLogger,
|
||||
@@ -23,6 +21,7 @@ import {
|
||||
type PollStartContent,
|
||||
} from "../poll-types.js";
|
||||
import { reactMatrixMessage, sendMessageMatrix, sendTypingMatrix } from "../send.js";
|
||||
import { enforceMatrixDirectMessageAccess, resolveMatrixAccessState } from "./access-policy.js";
|
||||
import {
|
||||
normalizeMatrixAllowList,
|
||||
resolveMatrixAllowListMatch,
|
||||
@@ -234,81 +233,34 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
senderId,
|
||||
senderUsername,
|
||||
});
|
||||
const storeAllowFrom = isDirectMessage
|
||||
? await readStoreAllowFromForDmPolicy({
|
||||
provider: "matrix",
|
||||
accountId: resolvedAccountId,
|
||||
dmPolicy,
|
||||
readStore: pairing.readStoreForDmPolicy,
|
||||
})
|
||||
: [];
|
||||
const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
|
||||
const normalizedGroupAllowFrom = normalizeMatrixAllowList(groupAllowFrom);
|
||||
const senderGroupPolicy =
|
||||
groupPolicy === "disabled"
|
||||
? "disabled"
|
||||
: normalizedGroupAllowFrom.length > 0
|
||||
? "allowlist"
|
||||
: "open";
|
||||
const access = resolveDmGroupAccessWithLists({
|
||||
isGroup: isRoom,
|
||||
dmPolicy,
|
||||
groupPolicy: senderGroupPolicy,
|
||||
allowFrom,
|
||||
groupAllowFrom: normalizedGroupAllowFrom,
|
||||
storeAllowFrom,
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
isSenderAllowed: (allowFrom) =>
|
||||
resolveMatrixAllowListMatches({
|
||||
allowList: normalizeMatrixAllowList(allowFrom),
|
||||
userId: senderId,
|
||||
}),
|
||||
});
|
||||
const effectiveAllowFrom = normalizeMatrixAllowList(access.effectiveAllowFrom);
|
||||
const effectiveGroupAllowFrom = normalizeMatrixAllowList(access.effectiveGroupAllowFrom);
|
||||
const groupAllowConfigured = effectiveGroupAllowFrom.length > 0;
|
||||
const { access, effectiveAllowFrom, effectiveGroupAllowFrom, groupAllowConfigured } =
|
||||
await resolveMatrixAccessState({
|
||||
isDirectMessage,
|
||||
resolvedAccountId,
|
||||
dmPolicy,
|
||||
groupPolicy,
|
||||
allowFrom,
|
||||
groupAllowFrom,
|
||||
senderId,
|
||||
readStoreForDmPolicy: pairing.readStoreForDmPolicy,
|
||||
});
|
||||
|
||||
if (isDirectMessage) {
|
||||
if (!dmEnabled) {
|
||||
return;
|
||||
}
|
||||
if (access.decision !== "allow") {
|
||||
const allowMatch = resolveMatrixAllowListMatch({
|
||||
allowList: effectiveAllowFrom,
|
||||
userId: senderId,
|
||||
});
|
||||
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
||||
if (access.decision === "pairing") {
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
meta: { name: senderName },
|
||||
});
|
||||
if (created) {
|
||||
logVerboseMessage(
|
||||
`matrix pairing request sender=${senderId} name=${senderName ?? "unknown"} (${allowMatchMeta})`,
|
||||
);
|
||||
try {
|
||||
await sendMessageMatrix(
|
||||
`room:${roomId}`,
|
||||
[
|
||||
"OpenClaw: access not configured.",
|
||||
"",
|
||||
`Pairing code: ${code}`,
|
||||
"",
|
||||
"Ask the bot owner to approve with:",
|
||||
"openclaw pairing approve matrix <code>",
|
||||
].join("\n"),
|
||||
{ client },
|
||||
);
|
||||
} catch (err) {
|
||||
logVerboseMessage(`matrix pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logVerboseMessage(
|
||||
`matrix: blocked dm sender ${senderId} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`,
|
||||
);
|
||||
}
|
||||
const allowedDirectMessage = await enforceMatrixDirectMessageAccess({
|
||||
dmEnabled,
|
||||
dmPolicy,
|
||||
accessDecision: access.decision,
|
||||
senderId,
|
||||
senderName,
|
||||
effectiveAllowFrom,
|
||||
upsertPairingRequest: pairing.upsertPairingRequest,
|
||||
sendPairingReply: async (text) => {
|
||||
await sendMessageMatrix(`room:${roomId}`, text, { client });
|
||||
},
|
||||
logVerboseMessage,
|
||||
});
|
||||
if (!allowedDirectMessage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
48
src/pairing/pairing-challenge.ts
Normal file
48
src/pairing/pairing-challenge.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { buildPairingReply } from "./pairing-messages.js";
|
||||
|
||||
type PairingMeta = Record<string, string | undefined>;
|
||||
|
||||
export type PairingChallengeParams = {
|
||||
channel: string;
|
||||
senderId: string;
|
||||
senderIdLine: string;
|
||||
meta?: PairingMeta;
|
||||
upsertPairingRequest: (params: {
|
||||
id: string;
|
||||
meta?: PairingMeta;
|
||||
}) => Promise<{ code: string; created: boolean }>;
|
||||
sendPairingReply: (text: string) => Promise<void>;
|
||||
buildReplyText?: (params: { code: string; senderIdLine: string }) => string;
|
||||
onCreated?: (params: { code: string }) => void;
|
||||
onReplyError?: (err: unknown) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared pairing challenge issuance for DM pairing policy pathways.
|
||||
* Ensures every channel follows the same create-if-missing + reply flow.
|
||||
*/
|
||||
export async function issuePairingChallenge(
|
||||
params: PairingChallengeParams,
|
||||
): Promise<{ created: boolean; code?: string }> {
|
||||
const { code, created } = await params.upsertPairingRequest({
|
||||
id: params.senderId,
|
||||
meta: params.meta,
|
||||
});
|
||||
if (!created) {
|
||||
return { created: false };
|
||||
}
|
||||
params.onCreated?.({ code });
|
||||
const replyText =
|
||||
params.buildReplyText?.({ code, senderIdLine: params.senderIdLine }) ??
|
||||
buildPairingReply({
|
||||
channel: params.channel,
|
||||
idLine: params.senderIdLine,
|
||||
code,
|
||||
});
|
||||
try {
|
||||
await params.sendPairingReply(replyText);
|
||||
} catch (err) {
|
||||
params.onReplyError?.(err);
|
||||
}
|
||||
return { created: true, code };
|
||||
}
|
||||
@@ -217,6 +217,7 @@ export {
|
||||
} from "./group-access.js";
|
||||
export { resolveSenderCommandAuthorization } from "./command-auth.js";
|
||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
export { handleSlackMessageAction } from "./slack-message-actions.js";
|
||||
export { extractToolSend } from "./tool-send.js";
|
||||
export {
|
||||
|
||||
87
src/signal/monitor/access-policy.ts
Normal file
87
src/signal/monitor/access-policy.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { issuePairingChallenge } from "../../pairing/pairing-challenge.js";
|
||||
import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js";
|
||||
import {
|
||||
readStoreAllowFromForDmPolicy,
|
||||
resolveDmGroupAccessWithLists,
|
||||
} from "../../security/dm-policy-shared.js";
|
||||
import { isSignalSenderAllowed, type SignalSender } from "../identity.js";
|
||||
|
||||
type SignalDmPolicy = "open" | "pairing" | "allowlist" | "disabled";
|
||||
type SignalGroupPolicy = "open" | "allowlist" | "disabled";
|
||||
|
||||
export async function resolveSignalAccessState(params: {
|
||||
accountId: string;
|
||||
dmPolicy: SignalDmPolicy;
|
||||
groupPolicy: SignalGroupPolicy;
|
||||
allowFrom: string[];
|
||||
groupAllowFrom: string[];
|
||||
sender: SignalSender;
|
||||
}) {
|
||||
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
|
||||
provider: "signal",
|
||||
accountId: params.accountId,
|
||||
dmPolicy: params.dmPolicy,
|
||||
});
|
||||
const resolveAccessDecision = (isGroup: boolean) =>
|
||||
resolveDmGroupAccessWithLists({
|
||||
isGroup,
|
||||
dmPolicy: params.dmPolicy,
|
||||
groupPolicy: params.groupPolicy,
|
||||
allowFrom: params.allowFrom,
|
||||
groupAllowFrom: params.groupAllowFrom,
|
||||
storeAllowFrom,
|
||||
isSenderAllowed: (allowEntries) => isSignalSenderAllowed(params.sender, allowEntries),
|
||||
});
|
||||
const dmAccess = resolveAccessDecision(false);
|
||||
return {
|
||||
resolveAccessDecision,
|
||||
dmAccess,
|
||||
effectiveDmAllow: dmAccess.effectiveAllowFrom,
|
||||
effectiveGroupAllow: dmAccess.effectiveGroupAllowFrom,
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleSignalDirectMessageAccess(params: {
|
||||
dmPolicy: SignalDmPolicy;
|
||||
dmAccessDecision: "allow" | "block" | "pairing";
|
||||
senderId: string;
|
||||
senderIdLine: string;
|
||||
senderDisplay: string;
|
||||
senderName?: string;
|
||||
accountId: string;
|
||||
sendPairingReply: (text: string) => Promise<void>;
|
||||
log: (message: string) => void;
|
||||
}): Promise<boolean> {
|
||||
if (params.dmAccessDecision === "allow") {
|
||||
return true;
|
||||
}
|
||||
if (params.dmAccessDecision === "block") {
|
||||
if (params.dmPolicy !== "disabled") {
|
||||
params.log(`Blocked signal sender ${params.senderDisplay} (dmPolicy=${params.dmPolicy})`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (params.dmPolicy === "pairing") {
|
||||
await issuePairingChallenge({
|
||||
channel: "signal",
|
||||
senderId: params.senderId,
|
||||
senderIdLine: params.senderIdLine,
|
||||
meta: { name: params.senderName },
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "signal",
|
||||
id,
|
||||
accountId: params.accountId,
|
||||
meta,
|
||||
}),
|
||||
sendPairingReply: params.sendPairingReply,
|
||||
onCreated: () => {
|
||||
params.log(`signal pairing request sender=${params.senderId}`);
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
params.log(`signal pairing reply failed for ${params.senderId}: ${String(err)}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -30,14 +30,8 @@ import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js
|
||||
import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { mediaKindFromMime } from "../../media/constants.js";
|
||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||
import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
import {
|
||||
DM_GROUP_ACCESS_REASON,
|
||||
readStoreAllowFromForDmPolicy,
|
||||
resolveDmGroupAccessWithLists,
|
||||
} from "../../security/dm-policy-shared.js";
|
||||
import { DM_GROUP_ACCESS_REASON } from "../../security/dm-policy-shared.js";
|
||||
import { normalizeE164 } from "../../utils.js";
|
||||
import {
|
||||
formatSignalPairingIdLine,
|
||||
@@ -50,6 +44,7 @@ import {
|
||||
type SignalSender,
|
||||
} from "../identity.js";
|
||||
import { sendMessageSignal, sendReadReceiptSignal, sendTypingSignal } from "../send.js";
|
||||
import { handleSignalDirectMessageAccess, resolveSignalAccessState } from "./access-policy.js";
|
||||
import type {
|
||||
SignalEnvelope,
|
||||
SignalEventHandlerDeps,
|
||||
@@ -454,24 +449,15 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
const hasBodyContent =
|
||||
Boolean(messageText || quoteText) || Boolean(!reaction && dataMessage?.attachments?.length);
|
||||
const senderDisplay = formatSignalSenderDisplay(sender);
|
||||
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
|
||||
provider: "signal",
|
||||
accountId: deps.accountId,
|
||||
dmPolicy: deps.dmPolicy,
|
||||
});
|
||||
const resolveAccessDecision = (isGroup: boolean) =>
|
||||
resolveDmGroupAccessWithLists({
|
||||
isGroup,
|
||||
const { resolveAccessDecision, dmAccess, effectiveDmAllow, effectiveGroupAllow } =
|
||||
await resolveSignalAccessState({
|
||||
accountId: deps.accountId,
|
||||
dmPolicy: deps.dmPolicy,
|
||||
groupPolicy: deps.groupPolicy,
|
||||
allowFrom: deps.allowFrom,
|
||||
groupAllowFrom: deps.groupAllowFrom,
|
||||
storeAllowFrom,
|
||||
isSenderAllowed: (allowEntries) => isSignalSenderAllowed(sender, allowEntries),
|
||||
sender,
|
||||
});
|
||||
const dmAccess = resolveAccessDecision(false);
|
||||
const effectiveDmAllow = dmAccess.effectiveAllowFrom;
|
||||
const effectiveGroupAllow = dmAccess.effectiveGroupAllowFrom;
|
||||
|
||||
if (
|
||||
reaction &&
|
||||
@@ -502,43 +488,25 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
const isGroup = Boolean(groupId);
|
||||
|
||||
if (!isGroup) {
|
||||
if (dmAccess.decision === "block") {
|
||||
if (deps.dmPolicy !== "disabled") {
|
||||
logVerbose(`Blocked signal sender ${senderDisplay} (dmPolicy=${deps.dmPolicy})`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (dmAccess.decision === "pairing") {
|
||||
if (deps.dmPolicy === "pairing") {
|
||||
const senderId = senderAllowId;
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
channel: "signal",
|
||||
id: senderId,
|
||||
const allowedDirectMessage = await handleSignalDirectMessageAccess({
|
||||
dmPolicy: deps.dmPolicy,
|
||||
dmAccessDecision: dmAccess.decision,
|
||||
senderId: senderAllowId,
|
||||
senderIdLine,
|
||||
senderDisplay,
|
||||
senderName: envelope.sourceName ?? undefined,
|
||||
accountId: deps.accountId,
|
||||
sendPairingReply: async (text) => {
|
||||
await sendMessageSignal(`signal:${senderRecipient}`, text, {
|
||||
baseUrl: deps.baseUrl,
|
||||
account: deps.account,
|
||||
maxBytes: deps.mediaMaxBytes,
|
||||
accountId: deps.accountId,
|
||||
meta: { name: envelope.sourceName ?? undefined },
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(`signal pairing request sender=${senderId}`);
|
||||
try {
|
||||
await sendMessageSignal(
|
||||
`signal:${senderRecipient}`,
|
||||
buildPairingReply({
|
||||
channel: "signal",
|
||||
idLine: senderIdLine,
|
||||
code,
|
||||
}),
|
||||
{
|
||||
baseUrl: deps.baseUrl,
|
||||
account: deps.account,
|
||||
maxBytes: deps.mediaMaxBytes,
|
||||
accountId: deps.accountId,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
logVerbose(`signal pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
log: logVerbose,
|
||||
});
|
||||
if (!allowedDirectMessage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
67
src/slack/monitor/dm-auth.ts
Normal file
67
src/slack/monitor/dm-auth.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js";
|
||||
import { issuePairingChallenge } from "../../pairing/pairing-challenge.js";
|
||||
import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js";
|
||||
import { resolveSlackAllowListMatch } from "./allow-list.js";
|
||||
import type { SlackMonitorContext } from "./context.js";
|
||||
|
||||
export async function authorizeSlackDirectMessage(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
accountId: string;
|
||||
senderId: string;
|
||||
allowFromLower: string[];
|
||||
resolveSenderName: (senderId: string) => Promise<{ name?: string }>;
|
||||
sendPairingReply: (text: string) => Promise<void>;
|
||||
onDisabled: () => Promise<void> | void;
|
||||
onUnauthorized: (params: { allowMatchMeta: string; senderName?: string }) => Promise<void> | void;
|
||||
log: (message: string) => void;
|
||||
}): Promise<boolean> {
|
||||
if (!params.ctx.dmEnabled || params.ctx.dmPolicy === "disabled") {
|
||||
await params.onDisabled();
|
||||
return false;
|
||||
}
|
||||
if (params.ctx.dmPolicy === "open") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const sender = await params.resolveSenderName(params.senderId);
|
||||
const senderName = sender?.name ?? undefined;
|
||||
const allowMatch = resolveSlackAllowListMatch({
|
||||
allowList: params.allowFromLower,
|
||||
id: params.senderId,
|
||||
name: senderName,
|
||||
allowNameMatching: params.ctx.allowNameMatching,
|
||||
});
|
||||
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
||||
if (allowMatch.allowed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (params.ctx.dmPolicy === "pairing") {
|
||||
await issuePairingChallenge({
|
||||
channel: "slack",
|
||||
senderId: params.senderId,
|
||||
senderIdLine: `Your Slack user id: ${params.senderId}`,
|
||||
meta: { name: senderName },
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "slack",
|
||||
id,
|
||||
accountId: params.accountId,
|
||||
meta,
|
||||
}),
|
||||
sendPairingReply: params.sendPairingReply,
|
||||
onCreated: () => {
|
||||
params.log(
|
||||
`slack pairing request sender=${params.senderId} name=${senderName ?? "unknown"} (${allowMatchMeta})`,
|
||||
);
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
params.log(`slack pairing reply failed for ${params.senderId}: ${String(err)}`);
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
await params.onUnauthorized({ allowMatchMeta, senderName });
|
||||
return false;
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
shouldAckReaction as shouldAckReactionGate,
|
||||
type AckReactionScope,
|
||||
} from "../../../channels/ack-reactions.js";
|
||||
import { formatAllowlistMatchMeta } from "../../../channels/allowlist-match.js";
|
||||
import { resolveControlCommandGate } from "../../../channels/command-gating.js";
|
||||
import { resolveConversationLabel } from "../../../channels/conversation-label.js";
|
||||
import { logInboundDrop } from "../../../channels/logging.js";
|
||||
@@ -28,8 +27,6 @@ import { recordInboundSession } from "../../../channels/session.js";
|
||||
import { readSessionUpdatedAt, resolveStorePath } from "../../../config/sessions.js";
|
||||
import { logVerbose, shouldLogVerbose } from "../../../globals.js";
|
||||
import { enqueueSystemEvent } from "../../../infra/system-events.js";
|
||||
import { buildPairingReply } from "../../../pairing/pairing-messages.js";
|
||||
import { upsertChannelPairingRequest } from "../../../pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../../../routing/resolve-route.js";
|
||||
import { resolveThreadSessionKeys } from "../../../routing/session-key.js";
|
||||
import type { ResolvedSlackAccount } from "../../accounts.js";
|
||||
@@ -42,6 +39,7 @@ import { resolveSlackEffectiveAllowFrom } from "../auth.js";
|
||||
import { resolveSlackChannelConfig } from "../channel-config.js";
|
||||
import { stripSlackMentionsForCommandDetection } from "../commands.js";
|
||||
import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js";
|
||||
import { authorizeSlackDirectMessage } from "../dm-auth.js";
|
||||
import {
|
||||
resolveSlackAttachmentContent,
|
||||
MAX_SLACK_MEDIA_FILES,
|
||||
@@ -137,59 +135,32 @@ export async function prepareSlackMessage(params: {
|
||||
logVerbose("slack: drop dm message (missing user id)");
|
||||
return null;
|
||||
}
|
||||
if (!ctx.dmEnabled || ctx.dmPolicy === "disabled") {
|
||||
logVerbose("slack: drop dm (dms disabled)");
|
||||
const allowed = await authorizeSlackDirectMessage({
|
||||
ctx,
|
||||
accountId: account.accountId,
|
||||
senderId: directUserId,
|
||||
allowFromLower,
|
||||
resolveSenderName: ctx.resolveUserName,
|
||||
sendPairingReply: async (text) => {
|
||||
await sendMessageSlack(message.channel, text, {
|
||||
token: ctx.botToken,
|
||||
client: ctx.app.client,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
},
|
||||
onDisabled: () => {
|
||||
logVerbose("slack: drop dm (dms disabled)");
|
||||
},
|
||||
onUnauthorized: ({ allowMatchMeta }) => {
|
||||
logVerbose(
|
||||
`Blocked unauthorized slack sender ${message.user} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`,
|
||||
);
|
||||
},
|
||||
log: logVerbose,
|
||||
});
|
||||
if (!allowed) {
|
||||
return null;
|
||||
}
|
||||
if (ctx.dmPolicy !== "open") {
|
||||
const allowMatch = resolveSlackAllowListMatch({
|
||||
allowList: allowFromLower,
|
||||
id: directUserId,
|
||||
allowNameMatching: ctx.allowNameMatching,
|
||||
});
|
||||
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
||||
if (!allowMatch.allowed) {
|
||||
if (ctx.dmPolicy === "pairing") {
|
||||
const sender = await ctx.resolveUserName(directUserId);
|
||||
const senderName = sender?.name ?? undefined;
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
channel: "slack",
|
||||
id: directUserId,
|
||||
accountId: account.accountId,
|
||||
meta: { name: senderName },
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(
|
||||
`slack pairing request sender=${directUserId} name=${
|
||||
senderName ?? "unknown"
|
||||
} (${allowMatchMeta})`,
|
||||
);
|
||||
try {
|
||||
await sendMessageSlack(
|
||||
message.channel,
|
||||
buildPairingReply({
|
||||
channel: "slack",
|
||||
idLine: `Your Slack user id: ${directUserId}`,
|
||||
code,
|
||||
}),
|
||||
{
|
||||
token: ctx.botToken,
|
||||
client: ctx.app.client,
|
||||
accountId: account.accountId,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
logVerbose(`slack pairing reply failed for ${message.user}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logVerbose(
|
||||
`Blocked unauthorized slack sender ${message.user} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const route = resolveAgentRoute({
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs } from "@slack/bolt";
|
||||
import type { ChatCommandDefinition, CommandArgs } from "../../auto-reply/commands-registry.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js";
|
||||
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
||||
import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../../config/commands.js";
|
||||
import { danger, logVerbose } from "../../globals.js";
|
||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||
import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js";
|
||||
import { readStoreAllowFromForDmPolicy } from "../../security/dm-policy-shared.js";
|
||||
import { chunkItems } from "../../utils/chunk-items.js";
|
||||
import type { ResolvedSlackAccount } from "../accounts.js";
|
||||
import {
|
||||
normalizeAllowList,
|
||||
normalizeAllowListLower,
|
||||
resolveSlackAllowListMatch,
|
||||
resolveSlackUserAllowed,
|
||||
} from "./allow-list.js";
|
||||
import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "./allow-list.js";
|
||||
import { resolveSlackEffectiveAllowFrom } from "./auth.js";
|
||||
import { resolveSlackChannelConfig, type SlackChannelConfigResolved } from "./channel-config.js";
|
||||
import { buildSlackSlashCommandMatcher, resolveSlackSlashCommandConfig } from "./commands.js";
|
||||
import type { SlackMonitorContext } from "./context.js";
|
||||
import { normalizeSlackChannelType } from "./context.js";
|
||||
import { authorizeSlackDirectMessage } from "./dm-auth.js";
|
||||
import {
|
||||
createSlackExternalArgMenuStore,
|
||||
SLACK_EXTERNAL_ARG_MENU_PREFIX,
|
||||
@@ -333,73 +326,50 @@ export async function registerSlackMonitorSlashCommands(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const storeAllowFrom = isDirectMessage
|
||||
? await readStoreAllowFromForDmPolicy({
|
||||
provider: "slack",
|
||||
accountId: ctx.accountId,
|
||||
dmPolicy: ctx.dmPolicy,
|
||||
})
|
||||
: [];
|
||||
const effectiveAllowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]);
|
||||
const effectiveAllowFromLower = normalizeAllowListLower(effectiveAllowFrom);
|
||||
const { allowFromLower: effectiveAllowFromLower } = await resolveSlackEffectiveAllowFrom(
|
||||
ctx,
|
||||
{
|
||||
includePairingStore: isDirectMessage,
|
||||
},
|
||||
);
|
||||
|
||||
// Privileged command surface: compute CommandAuthorized, don't assume true.
|
||||
// Keep this aligned with the Slack message path (message-handler/prepare.ts).
|
||||
let commandAuthorized = false;
|
||||
let channelConfig: SlackChannelConfigResolved | null = null;
|
||||
if (isDirectMessage) {
|
||||
if (!ctx.dmEnabled || ctx.dmPolicy === "disabled") {
|
||||
await respond({
|
||||
text: "Slack DMs are disabled.",
|
||||
response_type: "ephemeral",
|
||||
});
|
||||
const allowed = await authorizeSlackDirectMessage({
|
||||
ctx,
|
||||
accountId: ctx.accountId,
|
||||
senderId: command.user_id,
|
||||
allowFromLower: effectiveAllowFromLower,
|
||||
resolveSenderName: ctx.resolveUserName,
|
||||
sendPairingReply: async (text) => {
|
||||
await respond({
|
||||
text,
|
||||
response_type: "ephemeral",
|
||||
});
|
||||
},
|
||||
onDisabled: async () => {
|
||||
await respond({
|
||||
text: "Slack DMs are disabled.",
|
||||
response_type: "ephemeral",
|
||||
});
|
||||
},
|
||||
onUnauthorized: async ({ allowMatchMeta }) => {
|
||||
logVerbose(
|
||||
`slack: blocked slash sender ${command.user_id} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`,
|
||||
);
|
||||
await respond({
|
||||
text: "You are not authorized to use this command.",
|
||||
response_type: "ephemeral",
|
||||
});
|
||||
},
|
||||
log: logVerbose,
|
||||
});
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
if (ctx.dmPolicy !== "open") {
|
||||
const sender = await ctx.resolveUserName(command.user_id);
|
||||
const senderName = sender?.name ?? undefined;
|
||||
const allowMatch = resolveSlackAllowListMatch({
|
||||
allowList: effectiveAllowFromLower,
|
||||
id: command.user_id,
|
||||
name: senderName,
|
||||
allowNameMatching: ctx.allowNameMatching,
|
||||
});
|
||||
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
||||
if (!allowMatch.allowed) {
|
||||
if (ctx.dmPolicy === "pairing") {
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
channel: "slack",
|
||||
id: command.user_id,
|
||||
accountId: ctx.accountId,
|
||||
meta: { name: senderName },
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(
|
||||
`slack pairing request sender=${command.user_id} name=${
|
||||
senderName ?? "unknown"
|
||||
} (${allowMatchMeta})`,
|
||||
);
|
||||
await respond({
|
||||
text: buildPairingReply({
|
||||
channel: "slack",
|
||||
idLine: `Your Slack user id: ${command.user_id}`,
|
||||
code,
|
||||
}),
|
||||
response_type: "ephemeral",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
logVerbose(
|
||||
`slack: blocked slash sender ${command.user_id} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`,
|
||||
);
|
||||
await respond({
|
||||
text: "You are not authorized to use this command.",
|
||||
response_type: "ephemeral",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isRoom) {
|
||||
|
||||
Reference in New Issue
Block a user