From 4537cf0d8abd6f4b759316f94463da2dadc9fe9f Mon Sep 17 00:00:00 2001 From: gnuxie Date: Tue, 9 Nov 2021 11:20:43 +0000 Subject: [PATCH] Add test for ruleserver policy consumption. Ensures that the consumer of the ruleserver rules is enforcing them. --- test/integration/commands/commandUtils.ts | 64 ++++++++++ test/integration/policyConsumptionTest.ts | 143 ++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 test/integration/policyConsumptionTest.ts diff --git a/test/integration/commands/commandUtils.ts b/test/integration/commands/commandUtils.ts index af911d6..4c3a89d 100644 --- a/test/integration/commands/commandUtils.ts +++ b/test/integration/commands/commandUtils.ts @@ -1,4 +1,52 @@ import { MatrixClient } from "matrix-bot-sdk"; +import { strict as assert } from "assert"; +import * as crypto from "crypto"; + +/** + * Returns a promise that resolves to the first event replying to the event produced by targetEventThunk. + * @param client A MatrixClient that is already in the targetRoom. We will use it to listen for the event produced by targetEventThunk. + * This function assumes that the start() has already been called on the client. + * @param targetRoom The room to listen for the reply in. + * @param targetEventThunk A function that produces an event ID when called. This event ID is then used to listen for a reply. + * @returns The replying event. + */ + export async function getFirstReply(client: MatrixClient, targetRoom: string, targetEventThunk: () => Promise): Promise { + let reactionEvents = []; + const addEvent = function (roomId, event) { + if (roomId !== targetRoom) return; + if (event.type !== 'm.room.message') return; + reactionEvents.push(event); + }; + let targetCb; + try { + client.on('room.event', addEvent) + const targetEventId = await targetEventThunk(); + for (let event of reactionEvents) { + const in_reply_to = event.content['m.relates_to']?.['m.in_reply_to']; + if (in_reply_to.event_id === targetEventId) { + return event; + } + } + return await new Promise(resolve => { + targetCb = function(roomId, event) { + if (roomId !== targetRoom) return; + if (event.type !== 'm.room.message') return; + const in_reply_to = event.content['m.relates_to']?.['m.in_reply_to']; + if (in_reply_to?.event_id === targetEventId) { + resolve(event) + } + } + client.on('room.event', targetCb); + }); + } finally { + client.removeListener('room.event', addEvent); + if (targetCb) { + client.removeListener('room.event', targetCb); + } + } +} + + /** * Returns a promise that resolves to an event that is reacting to the event produced by targetEventThunk. @@ -44,3 +92,19 @@ export async function getFirstReaction(client: MatrixClient, targetRoom: string, } } } + +/** + * Create a new banlist for mjolnir to watch and return the shortcode that can be used to refer to the list in future commands. + * @param managementRoom The room to send the create command to. + * @param mjolnir A syncing matrix client. + * @param client A client that isn't mjolnir to send the message with, as you will be invited to the room. + * @returns The shortcode for the list that can be used to refer to the list in future commands. + */ +export async function createBanList(managementRoom: string, mjolnir: MatrixClient, client: MatrixClient): Promise { + const listName = crypto.randomUUID(); + const listCreationResponse = await getFirstReply(mjolnir, managementRoom, async () => { + return await client.sendMessage(managementRoom, { msgtype: 'm.text', body: `!mjolnir list create ${listName} ${listName}`}); + }); + assert.equal(listCreationResponse.content.body.includes('This list is now being watched.'), true, 'could not create a list to test with.'); + return listName; +} diff --git a/test/integration/policyConsumptionTest.ts b/test/integration/policyConsumptionTest.ts new file mode 100644 index 0000000..d432c87 --- /dev/null +++ b/test/integration/policyConsumptionTest.ts @@ -0,0 +1,143 @@ +import { strict as assert } from "assert"; + +import { newTestUser } from "./clientHelper"; +import { getMessagesByUserIn } from "../../src/utils"; +import config from "../../src/config"; +import axios from "axios"; +import { LogService } from "matrix-bot-sdk"; +import { createBanList, getFirstReaction } from "./commands/commandUtils"; + +/** + * Get a copy of the rules from the ruleserver. + */ +async function currentRules() { + return await (await axios.get(`http://${config.web.address}:${config.web.port}/api/1/ruleserver/updates/`)).data +} + +/** + * Wait for the rules to change as a result of the thunk. The returned promise will resolve when the rules being served have changed. + * @param thunk Should cause the rules the RuleServer is serving to change some way. + */ +async function waitForRuleChange(thunk): Promise { + const initialRules = await currentRules(); + let rules = initialRules; + // We use JSON.stringify like this so that it is pretty printed in the log and human readable. + LogService.debug('policyConsumptionTest', `Rules before we wait for them to change: ${JSON.stringify(rules, null, 2)}`); + await thunk(); + while (rules.since === initialRules.since) { + await new Promise(resolve => { + setTimeout(resolve, 500); + }) + rules = await currentRules(); + }; + // The problem is, we have no idea how long a consumer will take to process the changed rules. + // We know the pull peroid is 1 second though. + await new Promise(resolve => { + setTimeout(resolve, 1500); + }) + LogService.debug('policyConsumptionTest', `Rules after they have changed: ${JSON.stringify(rules, null, 2)}`); +} + +describe("Test: that policy lists are consumed by the associated synapse module", function () { + this.afterEach(async function () { + this.timeout(5000) + LogService.debug('policyConsumptionTest', `Rules at end of test ${JSON.stringify(await currentRules(), null, 2)}`); + const mjolnir = config.RUNTIME.client!; + // Clear any state associated with the account. + await mjolnir.setAccountData('org.matrix.mjolnir.watched_lists', { + references: [], + }); + }) + this.beforeEach(async function () { + this.timeout(1000); + const mjolnir = config.RUNTIME.client!; + }) + it('blocks users in antispam when they are banned from sending messages and invites serverwide.', async function() { + this.timeout(20000); + // Create a few users and a room. + let badUser = await newTestUser(false, "spammer"); + let badUserId = await badUser.getUserId(); + const mjolnir = config.RUNTIME.client! + let mjolnirUserId = await mjolnir.getUserId(); + let moderator = await newTestUser(false, "moderator"); + this.moderator = moderator; + await moderator.joinRoom(this.mjolnir.managementRoomId); + let unprotectedRoom = await badUser.createRoom({ invite: [await moderator.getUserId()]}); + // We do this so the moderator can send invites, no other reason. + await badUser.setUserPowerLevel(await moderator.getUserId(), unprotectedRoom, 100); + await moderator.joinRoom(unprotectedRoom); + const banList = await createBanList(this.mjolnir.managementRoomId, mjolnir, moderator); + await badUser.sendMessage(unprotectedRoom, {msgtype: 'm.text', body: 'Something bad and mean'}); + + await waitForRuleChange(async () => { + await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { + return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir ban ${banList} ${badUserId}` }); + }); + }); + await assert.rejects(badUser.sendMessage(unprotectedRoom, { msgtype: 'm.text', body: 'test'}), 'The bad user should be banned and unable to send messages.'); + await assert.rejects(badUser.inviteUser(mjolnirUserId, unprotectedRoom), 'They should also be unable to send invitations.'); + assert.ok(await moderator.inviteUser('@test:localhost:9999', unprotectedRoom), 'The moderator is not banned though so should still be able to invite'); + assert.ok(await moderator.sendMessage(unprotectedRoom, { msgtype: 'm.text', body: 'test'}), 'They should be able to send messages still too.'); + + // Test we can remove the rules. + await waitForRuleChange(async () => { + await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { + return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir unban ${banList} ${badUserId}` }); + }); + }); + assert.ok(await badUser.sendMessage(unprotectedRoom, { msgtype: 'm.text', body: 'test'})); + assert.ok(await badUser.inviteUser(mjolnirUserId, unprotectedRoom)); + }) + it('Test: Cannot send message to a room that is listed in a policy list and cannot invite a user to the room either', async function () { + this.timeout(20000); + let badUser = await newTestUser(false, "spammer"); + const mjolnir = config.RUNTIME.client! + let moderator = await newTestUser(false, "moderator"); + await moderator.joinRoom(this.mjolnir.managementRoomId); + const banList = await createBanList(this.mjolnir.managementRoomId, mjolnir, moderator); + let badRoom = await badUser.createRoom(); + let unrelatedRoom = await badUser.createRoom(); + await badUser.sendMessage(badRoom, {msgtype: 'm.text', body: "Very Bad Stuff in this room"}); + await waitForRuleChange(async () => { + await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { + return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir ban ${banList} ${badRoom}` }); + }); + }); + await assert.rejects(badUser.sendMessage(badRoom, { msgtype: 'm.text', body: 'test'}), 'should not be able to send messagea to a room which is listed.'); + await assert.rejects(badUser.inviteUser(await moderator.getUserId(), badRoom), 'should not be able to invite people to a listed room.'); + assert.ok(await badUser.sendMessage(unrelatedRoom, { msgtype: 'm.text.', body: 'hey'}), 'should be able to send messages to unrelated room'); + assert.ok(await badUser.inviteUser(await moderator.getUserId(), unrelatedRoom), 'They should still be able to invite to other rooms though'); + // Test we can remove these rules. + await waitForRuleChange(async () => { + await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { + return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir unban ${banList} ${badRoom}` }); + }); + }); + + assert.ok(await badUser.sendMessage(badRoom, { msgtype: 'm.text', body: 'test'}), 'should now be able to send messages to the room.'); + assert.ok(await badUser.inviteUser(await moderator.getUserId(), badRoom), 'should now be able to send messages to the room.'); + }) + it('Test: When a list becomes unwatched, the associated policies are stopped.', async function () { + this.timeout(20000); + const mjolnir = config.RUNTIME.client! + let moderator = await newTestUser(false, "moderator"); + await moderator.joinRoom(this.mjolnir.managementRoomId); + const banList = await createBanList(this.mjolnir.managementRoomId, mjolnir, moderator); + let targetRoom = await moderator.createRoom(); + await moderator.sendMessage(targetRoom, {msgtype: 'm.text', body: "Fluffy Foxes."}); + await waitForRuleChange(async () => { + await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { + return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir ban ${banList} ${targetRoom}` }); + }); + }); + await assert.rejects(moderator.sendMessage(targetRoom, { msgtype: 'm.text', body: 'test'}), 'should not be able to send messages to a room which is listed.'); + + await waitForRuleChange(async () => { + await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { + return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir unwatch #${banList}:localhost:9999` }); + }); + }); + + assert.ok(await moderator.sendMessage(targetRoom, { msgtype: 'm.text', body: 'test'}), 'should now be able to send messages to the room.'); + }) +});