mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
Matrix: gate verification notices on DM access (#55122)
This commit is contained in:
@@ -24,6 +24,10 @@ function createHarness(params?: {
|
||||
cryptoAvailable?: boolean;
|
||||
selfUserId?: string;
|
||||
selfUserIdError?: Error;
|
||||
allowFrom?: string[];
|
||||
dmEnabled?: boolean;
|
||||
dmPolicy?: "open" | "pairing" | "allowlist" | "disabled";
|
||||
storeAllowFrom?: string[];
|
||||
joinedMembersByRoom?: Record<string, string[]>;
|
||||
verifications?: Array<{
|
||||
id: string;
|
||||
@@ -67,6 +71,7 @@ function createHarness(params?: {
|
||||
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
||||
const formatNativeDependencyHint = vi.fn(() => "install hint");
|
||||
const logVerboseMessage = vi.fn();
|
||||
const readStoreAllowFrom = vi.fn(async () => params?.storeAllowFrom ?? []);
|
||||
const client = {
|
||||
on: vi.fn((eventName: string, listener: (...args: unknown[]) => void) => {
|
||||
listeners.set(eventName, listener);
|
||||
@@ -101,6 +106,10 @@ function createHarness(params?: {
|
||||
accountId: params?.accountId ?? "default",
|
||||
encryption: params?.authEncryption ?? true,
|
||||
} as MatrixAuth,
|
||||
allowFrom: params?.allowFrom ?? [],
|
||||
dmEnabled: params?.dmEnabled ?? true,
|
||||
dmPolicy: params?.dmPolicy ?? "open",
|
||||
readStoreAllowFrom,
|
||||
directTracker: {
|
||||
invalidateRoom,
|
||||
},
|
||||
@@ -123,6 +132,7 @@ function createHarness(params?: {
|
||||
invalidateRoom,
|
||||
roomEventListener,
|
||||
listVerifications,
|
||||
readStoreAllowFrom,
|
||||
logger,
|
||||
formatNativeDependencyHint,
|
||||
logVerboseMessage,
|
||||
@@ -255,6 +265,112 @@ describe("registerMatrixMonitorEvents verification routing", () => {
|
||||
expect(body).toContain('Open "Verify by emoji"');
|
||||
});
|
||||
|
||||
it("blocks verification request notices when dmPolicy pairing would block the sender", async () => {
|
||||
const { onRoomMessage, sendMessage, roomMessageListener, logVerboseMessage } = createHarness({
|
||||
dmPolicy: "pairing",
|
||||
});
|
||||
if (!roomMessageListener) {
|
||||
throw new Error("room.message listener was not registered");
|
||||
}
|
||||
|
||||
roomMessageListener("!room:example.org", {
|
||||
event_id: "$req-pairing-blocked",
|
||||
sender: "@alice:example.org",
|
||||
type: EventType.RoomMessage,
|
||||
origin_server_ts: Date.now(),
|
||||
content: {
|
||||
msgtype: "m.key.verification.request",
|
||||
body: "verification request",
|
||||
},
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(logVerboseMessage).toHaveBeenCalledWith(
|
||||
expect.stringContaining("blocked verification sender @alice:example.org"),
|
||||
);
|
||||
});
|
||||
expect(sendMessage).not.toHaveBeenCalled();
|
||||
expect(onRoomMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows verification notices for pairing-authorized DM senders from the allow store", async () => {
|
||||
const { sendMessage, roomMessageListener, readStoreAllowFrom } = createHarness({
|
||||
dmPolicy: "pairing",
|
||||
storeAllowFrom: ["@alice:example.org"],
|
||||
});
|
||||
if (!roomMessageListener) {
|
||||
throw new Error("room.message listener was not registered");
|
||||
}
|
||||
|
||||
roomMessageListener("!room:example.org", {
|
||||
event_id: "$req-pairing-allowed",
|
||||
sender: "@alice:example.org",
|
||||
type: EventType.RoomMessage,
|
||||
origin_server_ts: Date.now(),
|
||||
content: {
|
||||
msgtype: "m.key.verification.request",
|
||||
body: "verification request",
|
||||
},
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(readStoreAllowFrom).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not consult the allow store when dmPolicy is open", async () => {
|
||||
const { sendMessage, roomMessageListener, readStoreAllowFrom } = createHarness({
|
||||
dmPolicy: "open",
|
||||
});
|
||||
if (!roomMessageListener) {
|
||||
throw new Error("room.message listener was not registered");
|
||||
}
|
||||
|
||||
roomMessageListener("!room:example.org", {
|
||||
event_id: "$req-open-policy",
|
||||
sender: "@alice:example.org",
|
||||
type: EventType.RoomMessage,
|
||||
origin_server_ts: Date.now(),
|
||||
content: {
|
||||
msgtype: "m.key.verification.request",
|
||||
body: "verification request",
|
||||
},
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(readStoreAllowFrom).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks verification notices when Matrix DMs are disabled", async () => {
|
||||
const { sendMessage, roomMessageListener, logVerboseMessage } = createHarness({
|
||||
dmEnabled: false,
|
||||
});
|
||||
if (!roomMessageListener) {
|
||||
throw new Error("room.message listener was not registered");
|
||||
}
|
||||
|
||||
roomMessageListener("!room:example.org", {
|
||||
event_id: "$req-dm-disabled",
|
||||
sender: "@alice:example.org",
|
||||
type: EventType.RoomMessage,
|
||||
origin_server_ts: Date.now(),
|
||||
content: {
|
||||
msgtype: "m.key.verification.request",
|
||||
body: "verification request",
|
||||
},
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(logVerboseMessage).toHaveBeenCalledWith(
|
||||
expect.stringContaining("blocked verification sender @alice:example.org"),
|
||||
);
|
||||
});
|
||||
expect(sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("posts ready-stage guidance for emoji verification", async () => {
|
||||
const { sendMessage, roomEventListener } = createHarness();
|
||||
roomEventListener("!room:example.org", {
|
||||
@@ -423,6 +539,51 @@ describe("registerMatrixMonitorEvents verification routing", () => {
|
||||
expect(body).toContain("SAS decimal: 6158 1986 3513");
|
||||
});
|
||||
|
||||
it("blocks summary SAS notices when dmPolicy allowlist would block the sender", async () => {
|
||||
const { sendMessage, verificationSummaryListener, logVerboseMessage } = createHarness({
|
||||
dmPolicy: "allowlist",
|
||||
joinedMembersByRoom: {
|
||||
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
||||
},
|
||||
});
|
||||
if (!verificationSummaryListener) {
|
||||
throw new Error("verification.summary listener was not registered");
|
||||
}
|
||||
|
||||
verificationSummaryListener({
|
||||
id: "verification-blocked-summary",
|
||||
roomId: "!dm:example.org",
|
||||
otherUserId: "@alice:example.org",
|
||||
isSelfVerification: false,
|
||||
initiatedByMe: false,
|
||||
phase: 3,
|
||||
phaseName: "started",
|
||||
pending: true,
|
||||
methods: ["m.sas.v1"],
|
||||
canAccept: false,
|
||||
hasSas: true,
|
||||
sas: {
|
||||
decimal: [6158, 1986, 3513],
|
||||
emoji: [
|
||||
["🎁", "Gift"],
|
||||
["🌍", "Globe"],
|
||||
["🐴", "Horse"],
|
||||
],
|
||||
},
|
||||
hasReciprocateQr: false,
|
||||
completed: false,
|
||||
createdAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
||||
updatedAt: new Date("2026-02-25T21:42:55.000Z").toISOString(),
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(logVerboseMessage).toHaveBeenCalledWith(
|
||||
expect.stringContaining("blocked verification sender @alice:example.org"),
|
||||
);
|
||||
});
|
||||
expect(sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("posts SAS notices from summary updates using the room mapped by earlier flow events", async () => {
|
||||
const { sendMessage, roomEventListener, verificationSummaryListener } = createHarness({
|
||||
joinedMembersByRoom: {
|
||||
|
||||
@@ -34,6 +34,10 @@ export function registerMatrixMonitorEvents(params: {
|
||||
cfg: CoreConfig;
|
||||
client: MatrixClient;
|
||||
auth: MatrixAuth;
|
||||
allowFrom: string[];
|
||||
dmEnabled: boolean;
|
||||
dmPolicy: "open" | "pairing" | "allowlist" | "disabled";
|
||||
readStoreAllowFrom: () => Promise<string[]>;
|
||||
directTracker?: {
|
||||
invalidateRoom: (roomId: string) => void;
|
||||
};
|
||||
@@ -48,6 +52,10 @@ export function registerMatrixMonitorEvents(params: {
|
||||
cfg,
|
||||
client,
|
||||
auth,
|
||||
allowFrom,
|
||||
dmEnabled,
|
||||
dmPolicy,
|
||||
readStoreAllowFrom,
|
||||
directTracker,
|
||||
logVerboseMessage,
|
||||
warnedEncryptedRooms,
|
||||
@@ -58,6 +66,10 @@ export function registerMatrixMonitorEvents(params: {
|
||||
} = params;
|
||||
const { routeVerificationEvent, routeVerificationSummary } = createMatrixVerificationEventRouter({
|
||||
client,
|
||||
allowFrom,
|
||||
dmEnabled,
|
||||
dmPolicy,
|
||||
readStoreAllowFrom,
|
||||
logVerboseMessage,
|
||||
});
|
||||
|
||||
|
||||
@@ -266,6 +266,17 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
cfg,
|
||||
client,
|
||||
auth,
|
||||
allowFrom,
|
||||
dmEnabled,
|
||||
dmPolicy,
|
||||
readStoreAllowFrom: async () =>
|
||||
await core.channel.pairing
|
||||
.readAllowFromStore({
|
||||
channel: "matrix",
|
||||
env: process.env,
|
||||
accountId: account.accountId,
|
||||
})
|
||||
.catch(() => []),
|
||||
directTracker,
|
||||
logVerboseMessage,
|
||||
warnedEncryptedRooms,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { inspectMatrixDirectRooms } from "../direct-management.js";
|
||||
import { isStrictDirectRoom } from "../direct-room.js";
|
||||
import type { MatrixClient } from "../sdk.js";
|
||||
import { resolveMatrixMonitorAccessState } from "./access-state.js";
|
||||
import type { MatrixRawEvent } from "./types.js";
|
||||
import { EventType } from "./types.js";
|
||||
import {
|
||||
@@ -309,8 +310,51 @@ async function sendVerificationNotice(params: {
|
||||
}
|
||||
}
|
||||
|
||||
async function isVerificationNoticeAuthorized(params: {
|
||||
senderId: string;
|
||||
allowFrom: string[];
|
||||
dmEnabled: boolean;
|
||||
dmPolicy: "open" | "pairing" | "allowlist" | "disabled";
|
||||
readStoreAllowFrom: () => Promise<string[]>;
|
||||
logVerboseMessage: (message: string) => void;
|
||||
}): Promise<boolean> {
|
||||
// Verification notices are DM-only. If DM ingress is disabled, there is no
|
||||
// policy-compatible path for posting these notices back into the room.
|
||||
if (!params.dmEnabled || params.dmPolicy === "disabled") {
|
||||
params.logVerboseMessage(
|
||||
`matrix: blocked verification sender ${params.senderId} (dmPolicy=${params.dmPolicy}, dmEnabled=${String(params.dmEnabled)})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (params.dmPolicy === "open") {
|
||||
return true;
|
||||
}
|
||||
const storeAllowFrom = await params.readStoreAllowFrom();
|
||||
const accessState = resolveMatrixMonitorAccessState({
|
||||
allowFrom: params.allowFrom,
|
||||
storeAllowFrom,
|
||||
// Verification flows only exist in strict DMs, so room/group allowlists do
|
||||
// not participate in the authorization decision here.
|
||||
groupAllowFrom: [],
|
||||
roomUsers: [],
|
||||
senderId: params.senderId,
|
||||
isRoom: false,
|
||||
});
|
||||
if (accessState.directAllowMatch.allowed) {
|
||||
return true;
|
||||
}
|
||||
params.logVerboseMessage(
|
||||
`matrix: blocked verification sender ${params.senderId} (dmPolicy=${params.dmPolicy})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function createMatrixVerificationEventRouter(params: {
|
||||
client: MatrixClient;
|
||||
allowFrom: string[];
|
||||
dmEnabled: boolean;
|
||||
dmPolicy: "open" | "pairing" | "allowlist" | "disabled";
|
||||
readStoreAllowFrom: () => Promise<string[]>;
|
||||
logVerboseMessage: (message: string) => void;
|
||||
}) {
|
||||
const routerStartedAtMs = Date.now();
|
||||
@@ -411,6 +455,18 @@ export function createMatrixVerificationEventRouter(params: {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!(await isVerificationNoticeAuthorized({
|
||||
senderId: summary.otherUserId,
|
||||
allowFrom: params.allowFrom,
|
||||
dmEnabled: params.dmEnabled,
|
||||
dmPolicy: params.dmPolicy,
|
||||
readStoreAllowFrom: params.readStoreAllowFrom,
|
||||
logVerboseMessage: params.logVerboseMessage,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const sasNotice = formatVerificationSasNotice(summary);
|
||||
if (!sasNotice) {
|
||||
return;
|
||||
@@ -459,6 +515,18 @@ export function createMatrixVerificationEventRouter(params: {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!(await isVerificationNoticeAuthorized({
|
||||
senderId,
|
||||
allowFrom: params.allowFrom,
|
||||
dmEnabled: params.dmEnabled,
|
||||
dmPolicy: params.dmPolicy,
|
||||
readStoreAllowFrom: params.readStoreAllowFrom,
|
||||
logVerboseMessage: params.logVerboseMessage,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
rememberVerificationUserRoom(senderId, roomId);
|
||||
if (!trackBounded(routedVerificationEvents, sourceFingerprint)) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user