From 5fbb25c7078c2ebb0a852e2aed616d20c43a256d Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 7 Apr 2022 07:34:07 -0400 Subject: [PATCH] Make Jitsi widgets in video rooms immutable (#8244) * Make Jitsi widgets in video rooms immutable * Test video room creation --- src/createRoom.ts | 20 +++++-- test/createRoom-test.js | 29 ---------- test/createRoom-test.ts | 100 ++++++++++++++++++++++++++++++++++ test/test-utils/test-utils.ts | 3 + 4 files changed, 119 insertions(+), 33 deletions(-) delete mode 100644 test/createRoom-test.js create mode 100644 test/createRoom-test.ts diff --git a/src/createRoom.ts b/src/createRoom.ts index e141808941..66177d812b 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -126,11 +126,14 @@ export default async function createRoom(opts: IOpts): Promise { [RoomCreateTypeField]: opts.roomType, }; - // In video rooms, allow all users to send video member updates + // Video rooms require custom power levels if (opts.roomType === RoomType.ElementVideo) { createOpts.power_level_content_override = { events: { + // Allow all users to send video member updates [VIDEO_CHANNEL_MEMBER]: 0, + // Make widgets immutable, even to admins + "im.vector.modular.widgets": 200, // Annoyingly, we have to reiterate all the defaults here [EventType.RoomName]: 50, [EventType.RoomAvatar]: 50, @@ -141,6 +144,10 @@ export default async function createRoom(opts: IOpts): Promise { [EventType.RoomServerAcl]: 100, [EventType.RoomEncryption]: 100, }, + users: { + // Temporarily give ourselves the power to set up a widget + [client.getUserId()]: 200, + }, }; } } @@ -259,10 +266,15 @@ export default async function createRoom(opts: IOpts): Promise { if (opts.parentSpace) { return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], opts.suggested); } - }).then(() => { - // Set up video rooms with a Jitsi widget + }).then(async () => { if (opts.roomType === RoomType.ElementVideo) { - return addVideoChannel(roomId, createOpts.name); + // Set up video rooms with a Jitsi widget + await addVideoChannel(roomId, createOpts.name); + + // Reset our power level back to admin so that the widget becomes immutable + const room = client.getRoom(roomId); + const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, ""); + await client.setPowerLevel(roomId, client.getUserId(), 100, plEvent); } }).then(function() { // NB createRoom doesn't block on the client seeing the echo that the diff --git a/test/createRoom-test.js b/test/createRoom-test.js deleted file mode 100644 index 05cc2ebb1a..0000000000 --- a/test/createRoom-test.js +++ /dev/null @@ -1,29 +0,0 @@ -import { canEncryptToAllUsers } from '../src/createRoom'; - -describe("canEncryptToAllUsers", () => { - const trueUser = { - "@goodUser:localhost": { - "DEV1": {}, - "DEV2": {}, - }, - }; - const falseUser = { - "@badUser:localhost": {}, - }; - - it("returns true if all devices have crypto", async () => { - const client = { - downloadKeys: async function(userIds) { return trueUser; }, - }; - const response = await canEncryptToAllUsers(client, ["@goodUser:localhost"]); - expect(response).toBe(true); - }); - - it("returns false if not all users have crypto", async () => { - const client = { - downloadKeys: async function(userIds) { return { ...trueUser, ...falseUser }; }, - }; - const response = await canEncryptToAllUsers(client, ["@goodUser:localhost", "@badUser:localhost"]); - expect(response).toBe(false); - }); -}); diff --git a/test/createRoom-test.ts b/test/createRoom-test.ts new file mode 100644 index 0000000000..5846823cfd --- /dev/null +++ b/test/createRoom-test.ts @@ -0,0 +1,100 @@ +/* +Copyright 2022 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 { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { IDevice } from "matrix-js-sdk/src/crypto/deviceinfo"; +import { RoomType } from "matrix-js-sdk/src/@types/event"; + +import { stubClient, setupAsyncStoreWithClient } from "./test-utils"; +import { MatrixClientPeg } from "../src/MatrixClientPeg"; +import WidgetStore from "../src/stores/WidgetStore"; +import WidgetUtils from "../src/utils/WidgetUtils"; +import { VIDEO_CHANNEL, VIDEO_CHANNEL_MEMBER } from "../src/utils/VideoChannelUtils"; +import createRoom, { canEncryptToAllUsers } from '../src/createRoom'; + +describe("createRoom", () => { + let client: MatrixClient; + beforeEach(() => { + stubClient(); + client = MatrixClientPeg.get(); + }); + + it("sets up video rooms correctly", async () => { + setupAsyncStoreWithClient(WidgetStore.instance, client); + jest.spyOn(WidgetUtils, "waitForRoomWidget").mockResolvedValue(); + + const userId = client.getUserId(); + const roomId = await createRoom({ roomType: RoomType.ElementVideo }); + + const [[{ + power_level_content_override: { + users: { + [userId]: userPower, + }, + events: { + "im.vector.modular.widgets": widgetPower, + [VIDEO_CHANNEL_MEMBER]: videoMemberPower, + }, + }, + }]] = mocked(client.createRoom).mock.calls as any; + const [[widgetRoomId, widgetStateKey, , widgetId]] = mocked(client.sendStateEvent).mock.calls; + + // We should have had enough power to be able to set up the Jitsi widget + expect(userPower).toBeGreaterThanOrEqual(widgetPower); + // and should have actually set it up + expect(widgetRoomId).toEqual(roomId); + expect(widgetStateKey).toEqual("im.vector.modular.widgets"); + expect(widgetId).toEqual(VIDEO_CHANNEL); + + // All members should be able to update their connected devices + expect(videoMemberPower).toEqual(0); + // Jitsi widget should be immutable for admins + expect(widgetPower).toBeGreaterThan(100); + // and we should have been reset back to admin + expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, userId, 100, undefined); + }); +}); + +describe("canEncryptToAllUsers", () => { + const trueUser = { + "@goodUser:localhost": { + "DEV1": {} as unknown as IDevice, + "DEV2": {} as unknown as IDevice, + }, + }; + const falseUser = { + "@badUser:localhost": {}, + }; + + let client: MatrixClient; + beforeEach(() => { + stubClient(); + client = MatrixClientPeg.get(); + }); + + it("returns true if all devices have crypto", async () => { + mocked(client.downloadKeys).mockResolvedValue(trueUser); + const response = await canEncryptToAllUsers(client, ["@goodUser:localhost"]); + expect(response).toBe(true); + }); + + it("returns false if not all users have crypto", async () => { + mocked(client.downloadKeys).mockResolvedValue({ ...trueUser, ...falseUser }); + const response = await canEncryptToAllUsers(client, ["@goodUser:localhost", "@badUser:localhost"]); + expect(response).toBe(false); + }); +}); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index ca2737d3e2..14bd85bc5c 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -108,6 +108,8 @@ export function createTestClient(): MatrixClient { getRoomHierarchy: jest.fn().mockReturnValue({ rooms: [], }), + createRoom: jest.fn().mockResolvedValue({ room_id: "!1:example.org" }), + setPowerLevel: jest.fn().mockResolvedValue(undefined), // Used by various internal bits we aren't concerned with (yet) sessionStore: { @@ -135,6 +137,7 @@ export function createTestClient(): MatrixClient { setPushRuleActions: jest.fn().mockResolvedValue(undefined), relations: jest.fn().mockRejectedValue(undefined), isCryptoEnabled: jest.fn().mockReturnValue(false), + downloadKeys: jest.fn(), fetchRoomEvent: jest.fn(), } as unknown as MatrixClient; }