refactor(channels): unify dm pairing policy flows

This commit is contained in:
Peter Steinberger
2026-02-26 22:36:05 +01:00
parent 7e0b3f16e3
commit 564be6b402
9 changed files with 443 additions and 252 deletions

View 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;
}

View File

@@ -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({

View File

@@ -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) {