mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 11:21:41 +00:00
refactor(channels): unify dm pairing policy flows
This commit is contained in:
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