Skip to content

Commit 121d4cf

Browse files
authored
Mjolnir would apply stale ACL to rooms during batching (#331)
* banListTest would applyACL before rules appeared in `/state`. Mjolnir will call applyServerAcls several times while a policy list is being updated, sometimes concurrently. This means a request to set a server ACL in a room which has old data can finish after a more recent recent request with the correct ACL. This means that the old ACL gets applied to the rooms (for a moment). This is a follow up from 5510658 * Only allow one invocation of applyServerAcls at a time as to not conflict with each other by using a promise chain. We don't use the throttle queue because we don't want to be blocked by other background tasks. We don't make another throttle queue because we don't want throttling and we don't want to delay the ACL application, which can happen even with throttle time of 0.
1 parent 829e1bd commit 121d4cf

File tree

3 files changed

+26
-4
lines changed

3 files changed

+26
-4
lines changed

src/Mjolnir.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ export class Mjolnir {
104104
private webapis: WebAPIs;
105105
private protectedRoomActivityTracker: ProtectedRoomActivityTracker;
106106
public taskQueue: ThrottlingQueue;
107+
/**
108+
* Used to provide mutual exclusion when synchronizing rooms with the state of a policy list.
109+
* This is because requests operating with rules from an older version of the list that are slow
110+
* could race & give the room an inconsistent state. An example is if we add multiple m.policy.rule.server rules,
111+
* which would cause several requests to a room to send a new m.room.server_acl event.
112+
* These requests could finish in any order, which has left rooms with an inconsistent server_acl event
113+
* until Mjolnir synchronises the room with its policy lists again, which can be in the region of hours.
114+
*/
115+
public aclChain: Promise<void> = Promise.resolve();
107116
/*
108117
* Config-enabled polling of reports in Synapse, so Mjolnir can react to reports
109118
*/

src/actions/ApplyAcl.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
3131
* @param {Mjolnir} mjolnir The Mjolnir client to apply the ACLs with.
3232
*/
3333
export async function applyServerAcls(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
34+
// we need to provide mutual exclusion so that we do not have requests updating the m.room.server_acl event
35+
// finish out of order and therefore leave the room out of sync with the policy lists.
36+
return new Promise((resolve, reject) => {
37+
mjolnir.aclChain = mjolnir.aclChain
38+
.then(() => _applyServerAcls(lists, roomIds, mjolnir))
39+
.then(resolve, reject);
40+
});
41+
}
42+
43+
async function _applyServerAcls(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise<RoomUpdateError[]> {
3444
const serverName: string = new UserID(await mjolnir.client.getUserId()).domain;
3545

3646
// Construct a server ACL first

test/integration/banListTest.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,20 +261,23 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun
261261
mjolnir.joinRoom(banListId);
262262
this.mjolnir!.watchList(Permalinks.forRoom(banListId));
263263
const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*");
264-
for (let i = 0; i < 200; i++) {
264+
const evilServerCount = 200;
265+
for (let i = 0; i < evilServerCount; i++) {
265266
const badServer = `${i}.evil.com`;
266267
acl.denyServer(badServer);
267268
await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule #${i}`);
268269
// Give them a bit of a spread over time.
269270
await new Promise(resolve => setTimeout(resolve, 5));
270271
}
271-
// give the events a chance to appear in the response to `/state`, since this is a problem.
272-
await new Promise(resolve => setTimeout(resolve, 2000));
273-
274272
// We do this because it should force us to wait until all the ACL events have been applied.
275273
// Even if that does mean the last few events will not go through batching...
276274
await this.mjolnir!.syncLists();
277275

276+
// At this point we check that the state within Mjolnir is internally consistent, this is just because debugging the following
277+
// is a pita.
278+
const list: PolicyList = this.mjolnir.policyLists[0]!;
279+
assert.equal(list.serverRules.length, evilServerCount, `There should be ${evilServerCount} rules in here`);
280+
278281
// Check each of the protected rooms for ACL events and make sure they were batched and are correct.
279282
await Promise.all(protectedRooms.map(async room => {
280283
const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", "");

0 commit comments

Comments
 (0)