From 1cea8154bb4a69ea23be10c1c5d7d0982b94a09f Mon Sep 17 00:00:00 2001 From: Shay Date: Thu, 21 Nov 2024 09:42:09 -0800 Subject: [PATCH] Don't allow mods to demote bot or members of management room in protected rooms (#555) --- src/commands/CommandHandler.ts | 2 +- src/commands/SetPowerLevelCommand.ts | 31 +++++ .../commands/powerLevelCommandTest.ts | 121 ++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 test/integration/commands/powerLevelCommandTest.ts diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 01b8ae64..3271df66 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -168,7 +168,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st "!mjolnir kick [room alias/ID] [reason] - Kicks a user or all of those matching a glob in a particular room or all protected rooms\n" + "!mjolnir deactivate - Deactivates a user ID\n" + "!mjolnir since / [rooms...] [reason] - Apply an action ('kick', 'ban', 'mute', 'unmute' or 'show') to all users who joined a room since / (up to users)\n" + - "!mjolnir powerlevel [room alias/ID] - Sets the power level of the user in the specified room (or all protected rooms)\n" + + "!mjolnir powerlevel [room alias/ID] - Sets the power level of the user in the specified room (or all protected rooms) - mjolnir will resist lowering the power level of the bot/users in the moderation room unless a --force argument is added\n" + "!mjolnir make admin [user alias/ID] - Make the specified user or the bot itself admin of the room\n" + "!mjolnir suspend - Suspend the specified user\n" + "!mjolnir unsuspend - Unsuspend the specified user\n" + diff --git a/src/commands/SetPowerLevelCommand.ts b/src/commands/SetPowerLevelCommand.ts index 7a76552f..e6234e65 100644 --- a/src/commands/SetPowerLevelCommand.ts +++ b/src/commands/SetPowerLevelCommand.ts @@ -23,12 +23,43 @@ export async function execSetPowerLevelCommand(roomId: string, event: any, mjoln const level = Math.round(Number(parts[3])); const inRoom = parts[4]; + const mjolnirId = await mjolnir.client.getUserId(); + let targetRooms = inRoom ? [await mjolnir.client.resolveRoom(inRoom)] : mjolnir.protectedRoomsTracker.getProtectedRooms(); + let force = false; + if (parts[parts.length - 1] === "--force") { + force = true; + parts.pop(); + } + for (const targetRoomId of targetRooms) { try { + const currentLevels = await mjolnir.client.getRoomStateEvent(targetRoomId, "m.room.power_levels", ""); + const currentLevel = currentLevels["users"][mjolnirId]; + if (!force) { + if (mjolnir.moderators.checkMembership(target)) { + // don't let the bot demote members of moderation room without --force arg + if (level < currentLevel) { + await mjolnir.managementRoomOutput.logMessage( + LogLevel.INFO, + "PowerLevelCommand", + `You are attempting to lower the bot/a moderator's power level: current level ${currentLevel}, requested level ${level}, aborting. This check can be overriden with a --force argument at the end of the command.`, + ); + return; + } + } + } + if (target === mjolnirId && level < currentLevel) { + await mjolnir.managementRoomOutput.logMessage( + LogLevel.INFO, + "PowerLevelCommand", + `You are attempting to lower the bot power level: current level ${currentLevel}, requested level ${level}, aborting.`, + ); + return; + } await mjolnir.client.setUserPowerLevel(target, targetRoomId, level); } catch (e) { const message = e.message || (e.body ? e.body.error : ""); diff --git a/test/integration/commands/powerLevelCommandTest.ts b/test/integration/commands/powerLevelCommandTest.ts new file mode 100644 index 00000000..71127757 --- /dev/null +++ b/test/integration/commands/powerLevelCommandTest.ts @@ -0,0 +1,121 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { strict as assert } from "assert"; +import { newTestUser } from "../clientHelper"; +import { getFirstReaction } from "./commandUtils"; + +describe("Test: power levels", function () { + it("Does not allow the bot to demote itself or members of management room in a protected room.", async function () { + this.timeout(60000); + const mod = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); + const mod2 = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator2" } }); + await mod.joinRoom(this.config.managementRoom); + await mod2.joinRoom(this.config.managementRoom); + + const targetRoom = await mod.createRoom({ preset: "public_chat" }); + await this.mjolnir.client.joinRoom(targetRoom); + await mod2.joinRoom(targetRoom); + const botId = await this.mjolnir.client.getUserId(); + await mod.setUserPowerLevel(botId, targetRoom, 100); + const mod2Id = await mod2.getUserId(); + await mod.setUserPowerLevel(mod2Id, targetRoom, 100); + + await mod.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text.", + body: `!mjolnir rooms add ${targetRoom}`, + }); + + await mod.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir powerlevel ${botId} 50 ${targetRoom}`, + }); + + mod.start(); + let reply = new Promise((resolve, reject) => { + mod.on("room.message", (roomId: string, event: any) => { + if ( + roomId === this.mjolnir.managementRoomId && + event.content?.body.includes("You are attempting to lower the bot/a moderator's power level") + ) { + resolve(event); + } + }); + }); + await reply; + + let currentLevels = await mod.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + const botLevel = currentLevels["users"][botId]; + assert.equal(botLevel, 100); + + await mod.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir powerlevel ${mod2Id} 50 ${targetRoom}`, + }); + + let reply2 = new Promise((resolve, reject) => { + mod.on("room.message", (roomId: string, event: any) => { + if ( + roomId === this.mjolnir.managementRoomId && + event.content?.body.includes("You are attempting to lower the bot/a moderator's power level") + ) { + resolve(event); + } + }); + }); + await reply2; + + currentLevels = await mod.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + const mod2Level = currentLevels["users"][mod2Id]; + assert.equal(mod2Level, 100); + + mod.stop(); + }); + + it("Does allow the bot to demote members of management room in a protected room with a --force argument.", async function () { + this.timeout(60000); + const mod = await newTestUser(this.config.homeserverUrl, { name: { contains: "force-moderator" } }); + const mod2 = await newTestUser(this.config.homeserverUrl, { name: { contains: "force-moderator2" } }); + await mod.joinRoom(this.config.managementRoom); + await mod2.joinRoom(this.config.managementRoom); + + const targetRoom = await mod.createRoom({ preset: "public_chat" }); + await this.mjolnir.client.joinRoom(targetRoom); + await mod2.joinRoom(targetRoom); + const botId = await this.mjolnir.client.getUserId(); + await mod.setUserPowerLevel(botId, targetRoom, 100); + const mod2Id = await mod2.getUserId(); + await mod.setUserPowerLevel(mod2Id, targetRoom, 75); + + await mod.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text.", + body: `!mjolnir rooms add ${targetRoom}`, + }); + + mod.start(); + await getFirstReaction(mod, this.mjolnir.managementRoomId, "✅", async () => { + return await mod.sendMessage(this.mjolnir.managementRoomId, { + msgtype: "m.text", + body: `!mjolnir powerlevel ${mod2Id} 50 ${targetRoom} --force`, + }); + }); + let currentLevels = await mod.getRoomStateEvent(targetRoom, "m.room.power_levels", ""); + const mod2Level = currentLevels["users"][mod2Id]; + assert.equal(mod2Level, 50); + + mod.stop(); + }); +});