mjolnir/test/integration/banListTest.ts

293 lines
18 KiB
TypeScript
Raw Normal View History

2021-11-19 16:44:48 +00:00
import { strict as assert } from "assert";
import config from "../../src/config";
import { newTestUser, noticeListener } from "./clientHelper";
import { LogService, MatrixClient, Permalinks, UserID } from "matrix-bot-sdk";
import BanList, { ALL_RULE_TYPES, ChangeType, ListRuleChange, RULE_SERVER, RULE_USER } from "../../src/models/BanList";
import { ServerAcl, ServerAclContent } from "../../src/models/ServerAcl";
import { createBanList } from "./commands/commandUtils";
import { getMessagesByUserIn } from "../../src/utils";
2021-11-19 16:44:48 +00:00
/**
* Create a policy rule in a policy room.
* @param client A matrix client that is logged in
* @param policyRoomId The room id to add the policy to.
* @param policyType The type of policy to add e.g. m.policy.rule.user. (Use RULE_USER though).
* @param entity The entity to ban e.g. @foo:example.org
* @param reason A reason for the rule e.g. 'Wouldn't stop posting spam links'
* @param template The template to use for the policy rule event.
2021-11-19 16:44:48 +00:00
* @returns The event id of the newly created policy rule.
*/
async function createPolicyRule(client: MatrixClient, policyRoomId: string, policyType: string, entity: string, reason: string, template = {recommendation: 'm.ban'}) {
2021-11-19 16:44:48 +00:00
return await client.sendStateEvent(policyRoomId, policyType, `rule:${entity}`, {
entity,
reason,
...template,
2021-11-19 16:44:48 +00:00
});
}
describe("Test: Updating the BanList", function () {
it("Calculates what has changed correctly.", async function () {
this.timeout(10000);
const mjolnir = config.RUNTIME.client!
const moderator = await newTestUser({ name: { contains: "moderator" }});
2021-11-19 16:44:48 +00:00
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
const banList = new BanList(banListId, banListId, mjolnir);
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
assert.equal(banList.allRules.length, 0);
2021-11-19 16:44:48 +00:00
// Test adding a new rule
await createPolicyRule(mjolnir, banListId, RULE_USER, '@added:localhost:9999', '');
let changes: ListRuleChange[] = await banList.updateList();
assert.equal(changes.length, 1, 'There should only be one change');
assert.equal(changes[0].changeType, ChangeType.Added);
assert.equal(changes[0].sender, await mjolnir.getUserId());
assert.equal(banList.userRules.length, 1);
assert.equal(banList.allRules.length, 1);
2021-11-19 16:44:48 +00:00
// Test modifiying a rule
let originalEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', '');
await banList.updateList();
let modifyingEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', 'modified reason');
2021-11-19 16:44:48 +00:00
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified);
assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule');
assert.equal(changes[0].event['event_id'], modifyingEventId);
let modifyingAgainEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', 'modified again');
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified);
assert.equal(changes[0].previousState['event_id'], modifyingEventId, 'There should be a previous state event for a modified rule');
assert.equal(changes[0].event['event_id'], modifyingAgainEventId);
assert.equal(banList.userRules.length, 2, 'There should be two rules, one for @modified:localhost:9999 and one for @added:localhost:9999');
2021-11-19 16:44:48 +00:00
// Test redacting a rule
const redactThis = await createPolicyRule(mjolnir, banListId, RULE_USER, '@redacted:localhost:9999', '');
await banList.updateList();
assert.equal(banList.userRules.filter(r => r.entity === '@redacted:localhost:9999').length, 1);
2021-11-19 16:44:48 +00:00
await mjolnir.redactEvent(banListId, redactThis);
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed);
assert.equal(changes[0].event['event_id'], redactThis, 'Should show the new version of the event with redacted content');
assert.equal(Object.keys(changes[0].event['content']).length, 0, 'Should show the new version of the event with redacted content');
assert.notEqual(Object.keys(changes[0].previousState['content']), 0, 'Should have a copy of the unredacted state');
assert.notEqual(changes[0].rule, undefined, 'The previous rule should be present');
assert.equal(banList.userRules.filter(r => r.entity === '@redacted:localhost:9999').length, 0, 'The rule should be removed.');
2021-11-19 16:44:48 +00:00
// Test soft redaction of a rule
const softRedactedEntity = '@softredacted:localhost:9999'
await createPolicyRule(mjolnir, banListId, RULE_USER, softRedactedEntity, '');
await banList.updateList();
assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 1);
await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {});
2021-11-19 16:44:48 +00:00
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed);
assert.equal(Object.keys(changes[0].event['content']).length, 0, 'Should show the new version of the event with redacted content');
assert.notEqual(Object.keys(changes[0].previousState['content']), 0, 'Should have a copy of the unredacted state');
assert.notEqual(changes[0].rule, undefined, 'The previous rule should be present');
assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 0, 'The rule should have been removed');
2021-11-19 16:44:48 +00:00
// Now test a double soft redaction just to make sure stuff doesn't explode
await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {});
2021-11-19 16:44:48 +00:00
changes = await banList.updateList();
assert.equal(changes.length, 0, "It shouldn't detect a double soft redaction as a change, it should be seen as adding an invalid rule.");
assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 0, 'The rule should have been removed');
// Test that different (old) rule types will be modelled as the latest event type.
originalEventId = await createPolicyRule(mjolnir, banListId, 'org.matrix.mjolnir.rule.user', '@old:localhost:9999', '');
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Added);
assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1);
modifyingEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', '@old:localhost:9999', 'modified reason');
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified);
assert.equal(changes[0].event['event_id'], modifyingEventId);
assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule');
assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1);
modifyingAgainEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@old:localhost:9999', 'changes again');
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified);
assert.equal(changes[0].event['event_id'], modifyingAgainEventId);
assert.equal(changes[0].previousState['event_id'], modifyingEventId, 'There should be a previous state event for a modified rule');
assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1);
})
it("Will remove rules with old types when they are 'soft redacted' with a different but more recent event type.", async function () {
this.timeout(3000);
const mjolnir = config.RUNTIME.client!
const moderator = await newTestUser({ name: { contains: "moderator" }});
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
const banList = new BanList(banListId, banListId, mjolnir);
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
const entity = '@old:localhost:9999';
let originalEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', entity, '');
let changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Added);
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'There should be a rule stored that we just added...')
let softRedactingEventId = await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {});
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed);
assert.equal(changes[0].event['event_id'], softRedactingEventId);
assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule');
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 0, 'The rule should no longer be stored.');
})
it("A rule of the most recent type won't be deleted when an old rule is deleted for the same entity.", async function () {
this.timeout(3000);
const mjolnir = config.RUNTIME.client!
const moderator = await newTestUser({ name: { contains: "moderator" }});
const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]});
const banList = new BanList(banListId, banListId, mjolnir);
mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100);
const entity = '@old:localhost:9999';
let originalEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', entity, '');
let changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Added);
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'There should be a rule stored that we just added...')
let updatedEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, entity, '');
changes = await banList.updateList();
// If in the future you change this and it fails, it's really subjective whether this constitutes a modification, since the only thing that has changed
// is the rule type. The actual content is identical.
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Modified);
assert.equal(changes[0].event['event_id'], updatedEventId);
assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule');
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'Only the latest version of the rule gets returned.');
// Now we delete the old version of the rule without consequence.
await mjolnir.sendStateEvent(banListId, 'm.room.rule.user', `rule:${entity}`, {});
changes = await banList.updateList();
assert.equal(changes.length, 0);
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'The rule should still be active.');
// And we can still delete the new version of the rule.
let softRedactingEventId = await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {});
changes = await banList.updateList();
assert.equal(changes.length, 1);
assert.equal(changes[0].changeType, ChangeType.Removed);
assert.equal(changes[0].event['event_id'], softRedactingEventId);
assert.equal(changes[0].previousState['event_id'], updatedEventId, 'There should be a previous state event for a modified rule');
assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 0, 'The rule should no longer be stored.');
2021-11-19 16:44:48 +00:00
})
it('Test: BanList Supports all entity types.', async function () {
const mjolnir = config.RUNTIME.client!
const banListId = await mjolnir.createRoom();
const banList = new BanList(banListId, banListId, mjolnir);
for (let i = 0; i < ALL_RULE_TYPES.length; i++) {
await createPolicyRule(mjolnir, banListId, ALL_RULE_TYPES[i], `*${i}*`, '');
}
let changes: ListRuleChange[] = await banList.updateList();
assert.equal(changes.length, ALL_RULE_TYPES.length);
assert.equal(banList.allRules.length, ALL_RULE_TYPES.length);
})
2021-11-19 16:44:48 +00:00
});
describe('Test: We do not respond to recommendations other than m.ban in the banlist', function () {
it('Will not respond to a rule that has a different recommendation to m.ban (or the unstable equivalent).', async function () {
const mjolnir = config.RUNTIME.client!
const banListId = await mjolnir.createRoom();
const banList = new BanList(banListId, banListId, mjolnir);
await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'exmaple.org', '', {recommendation: 'something that is not m.ban'});
let changes: ListRuleChange[] = await banList.updateList();
assert.equal(changes.length, 1, 'There should only be one change');
assert.equal(changes[0].changeType, ChangeType.Added);
assert.equal(changes[0].sender, await mjolnir.getUserId());
// We really don't want things that aren't m.ban to end up being accessible in these APIs.
assert.equal(banList.serverRules.length, 0);
assert.equal(banList.allRules.length, 0);
})
})
describe('Test: We will not be able to ban ourselves via ACL.', function () {
it('We do not ban ourselves when we put ourselves into the policy list.', async function () {
const mjolnir = config.RUNTIME.client!
const serverName = new UserID(await mjolnir.getUserId()).domain;
const banListId = await mjolnir.createRoom();
const banList = new BanList(banListId, banListId, mjolnir);
await createPolicyRule(mjolnir, banListId, RULE_SERVER, serverName, '');
await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'evil.com', '');
await createPolicyRule(mjolnir, banListId, RULE_SERVER, '*', '');
// We should still intern the matching rules rule.
let changes: ListRuleChange[] = await banList.updateList();
assert.equal(banList.serverRules.length, 3);
// But when we construct an ACL, we should be safe.
const acl = new ServerAcl(serverName)
changes.forEach(change => acl.denyServer(change.rule.entity));
assert.equal(acl.safeAclContent().deny.length, 1);
assert.equal(acl.literalAclContent().deny.length, 3);
})
})
describe('Test: ACL updates will batch when rules are added in succession.', function () {
it('Will batch ACL updates if we spam rules into a BanList', async function () {
this.timeout(180000)
const mjolnir = config.RUNTIME.client!
const serverName: string = new UserID(await mjolnir.getUserId()).domain
const moderator = await newTestUser({ name: { contains: "moderator" }});
moderator.joinRoom(this.mjolnir.managementRoomId);
const mjolnirId = await mjolnir.getUserId();
// Setup some protected rooms so we can check their ACL state later.
const protectedRooms: string[] = [];
for (let i = 0; i < 10; i++) {
const room = await moderator.createRoom({ invite: [mjolnirId]});
await mjolnir.joinRoom(room);
await moderator.setUserPowerLevel(mjolnirId, room, 100);
await this.mjolnir!.addProtectedRoom(room);
protectedRooms.push(room);
}
// If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point.
await this.mjolnir!.syncLists();
await Promise.all(protectedRooms.map(async room => {
// We're going to need timeline pagination I'm afraid.
const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", "");
assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.');
}));
// Flood the watched list with banned servers, which should prompt Mjolnir to update server ACL in protected rooms.
const banListId = await moderator.createRoom({ invite: [mjolnirId] });
mjolnir.joinRoom(banListId);
this.mjolnir!.watchList(Permalinks.forRoom(banListId));
const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*");
for (let i = 0; i < 200; i++) {
const badServer = `${i}.evil.com`;
acl.denyServer(badServer);
await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule #${i}`);
// Give them a bit of a spread over time.
await new Promise(resolve => setTimeout(resolve, 5));
}
// We do this because it should force us to wait until all the ACL events have been applied.
// Even if that does mean the last few events will not go through batching...
await this.mjolnir!.syncLists();
// Check each of the protected rooms for ACL events and make sure they were batched and are correct.
await Promise.all(protectedRooms.map(async room => {
const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", "");
if (!acl.matches(roomAcl)) {
assert.fail(`Room ${room} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`)
}
let aclEventCount = 0;
await getMessagesByUserIn(mjolnir, mjolnirId, room, 100, events => {
events.forEach(event => event.type === 'm.room.server_acl' ? aclEventCount += 1 : null);
});
LogService.debug('BanListTest', `aclEventCount: ${aclEventCount}`);
// If there's less than two then it means the ACL was updated by this test calling `this.mjolnir!.syncLists()`
// and not the listener that detects changes to ban lists (that we want to test!).
assert.equal(aclEventCount < 10 && aclEventCount > 2, true, 'We should have sent less than 10 ACL events to each room because they should be batched')
}));
})
})