Ensure correct room version is used and permissions are appropriately sert when creating rooms (#31464)

* Check default PL when setting a new PL in createRoom

* Drop custom PL setting for video rooms

* lint files

* Add room version test

* Cleanup test

* fix import
This commit is contained in:
Will Hunt
2025-12-09 14:05:49 +02:00
committed by GitHub
parent 5324834b47
commit d30e6f25d3
2 changed files with 116 additions and 59 deletions

View File

@@ -1,4 +1,5 @@
/*
Copyright 2025 Element Creations Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2015, 2016 OpenMarket Ltd
@@ -39,10 +40,10 @@ import { findDMForUser } from "./utils/dm/findDMForUser";
import { privateShouldBeEncrypted } from "./utils/rooms";
import { shouldForceDisableEncryption } from "./utils/crypto/shouldForceDisableEncryption";
import { waitForMember } from "./utils/membership";
import { PreferredRoomVersions } from "./utils/PreferredRoomVersions";
import { doesRoomVersionSupport, PreferredRoomVersions } from "./utils/PreferredRoomVersions";
import SettingsStore from "./settings/SettingsStore";
import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto";
import { ElementCallEventType, ElementCallMemberEventType } from "./call-types";
import { ElementCallMemberEventType } from "./call-types";
// we define a number of interfaces which take their names from the js-sdk
/* eslint-disable camelcase */
@@ -159,32 +160,19 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
};
// Video rooms require custom power levels
if (opts.roomType === RoomType.ElementVideo) {
if (opts.roomType === RoomType.ElementVideo || opts.roomType === RoomType.UnstableCall) {
createOpts.power_level_content_override = {
events: {
...DEFAULT_EVENT_POWER_LEVELS,
// Allow all users to send call membership updates
[JitsiCall.MEMBER_EVENT_TYPE]: 0,
// Make widgets immutable, even to admins
"im.vector.modular.widgets": 200,
},
users: {
// Temporarily give ourselves the power to set up a widget
[client.getSafeUserId()]: 200,
},
};
} else if (opts.roomType === RoomType.UnstableCall) {
createOpts.power_level_content_override = {
events: {
...DEFAULT_EVENT_POWER_LEVELS,
// Allow all users to send call membership updates
[ElementCallMemberEventType.name]: 0,
// Make calls immutable, even to admins
[ElementCallEventType.name]: 200,
},
users: {
// Temporarily give ourselves the power to set up a call
[client.getSafeUserId()]: 200,
[opts.roomType === RoomType.ElementVideo
? JitsiCall.MEMBER_EVENT_TYPE
: ElementCallMemberEventType.name]: 0,
// Ensure all but admins can't change widgets
// A previous version of the code prevented even administrators
// from changing this, but this is not possible now that room creators
// have an immutable power level
["im.vector.modular.widgets"]: 100,
},
};
}
@@ -194,8 +182,6 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
...DEFAULT_EVENT_POWER_LEVELS,
// It should always (including non video rooms) be possible to join a group call.
[ElementCallMemberEventType.name]: 0,
// Make sure only admins can enable it (DEPRECATED)
[ElementCallEventType.name]: 100,
},
};
}
@@ -230,7 +216,12 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
});
}
if (opts.joinRule === JoinRule.Knock) {
const defaultRoomVersion = (await client.getCapabilities())["m.room_versions"]?.default ?? "1";
if (
opts.joinRule === JoinRule.Knock &&
!doesRoomVersionSupport(defaultRoomVersion, PreferredRoomVersions.KnockRooms)
) {
createOpts.room_version = PreferredRoomVersions.KnockRooms;
}
@@ -238,7 +229,9 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
if (opts.joinRule === JoinRule.Restricted) {
createOpts.room_version = PreferredRoomVersions.RestrictedRooms;
if (!doesRoomVersionSupport(defaultRoomVersion, PreferredRoomVersions.KnockRooms)) {
createOpts.room_version = PreferredRoomVersions.RestrictedRooms;
}
createOpts.initial_state.push({
type: EventType.RoomJoinRules,
@@ -354,15 +347,9 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
if (opts.roomType === RoomType.ElementVideo) {
// Set up this video room with a Jitsi call
await JitsiCall.create(await room);
// Reset our power level back to admin so that the widget becomes immutable
await client.setPowerLevel(roomId, client.getUserId()!, 100);
} else if (opts.roomType === RoomType.UnstableCall) {
// Set up this video room with an Element call
ElementCall.create(await room);
// Reset our power level back to admin so that the call becomes immutable
await client.setPowerLevel(roomId, client.getUserId()!, 100);
}
})
.then(

View File

@@ -1,4 +1,5 @@
/*
Copyright 2025 Element Creations Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
@@ -7,19 +8,33 @@ Please see LICENSE files in the repository root for full details.
*/
import { mocked, type Mocked } from "jest-mock";
import { type MatrixClient, type Device, Preset, RoomType } from "matrix-js-sdk/src/matrix";
import {
type MatrixClient,
type Device,
Preset,
RoomType,
JoinRule,
RoomVersionStability,
} from "matrix-js-sdk/src/matrix";
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
import { stubClient, setupAsyncStoreWithClient, mockPlatformPeg, getMockClientWithEventEmitter } from "../test-utils";
import {
stubClient,
setupAsyncStoreWithClient,
mockPlatformPeg,
getMockClientWithEventEmitter,
mkRoom,
} from "../test-utils";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import WidgetStore from "../../src/stores/WidgetStore";
import WidgetUtils from "../../src/utils/WidgetUtils";
import { JitsiCall, ElementCall } from "../../src/models/Call";
import createRoom, { checkUserIsAllowedToChangeEncryption, canEncryptToAllUsers } from "../../src/createRoom";
import SettingsStore from "../../src/settings/SettingsStore";
import { ElementCallEventType, ElementCallMemberEventType } from "../../src/call-types";
import { ElementCallMemberEventType } from "../../src/call-types";
import DMRoomMap from "../../src/utils/DMRoomMap";
import { PreferredRoomVersions } from "../../src/utils/PreferredRoomVersions";
describe("createRoom", () => {
mockPlatformPeg();
@@ -103,14 +118,12 @@ describe("createRoom", () => {
jest.spyOn(WidgetUtils, "waitForRoomWidget").mockResolvedValue();
const createCallSpy = jest.spyOn(JitsiCall, "create");
const userId = client.getUserId()!;
const roomId = await createRoom(client, { roomType: RoomType.ElementVideo });
await createRoom(client, { roomType: RoomType.ElementVideo });
const [
[
{
power_level_content_override: {
users: { [userId]: userPower },
events: {
"im.vector.modular.widgets": widgetPower,
[JitsiCall.MEMBER_EVENT_TYPE]: callMemberPower,
@@ -120,44 +133,30 @@ describe("createRoom", () => {
],
] = client.createRoom.mock.calls as any; // no good type
// We should have had enough power to be able to set up the widget
expect(userPower).toBeGreaterThanOrEqual(widgetPower);
// and should have actually set it up
expect(createCallSpy).toHaveBeenCalled();
// All members should be able to update their connected devices
expect(callMemberPower).toEqual(0);
// 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);
expect(widgetPower).toEqual(100);
});
it("sets up Element video rooms correctly", async () => {
const userId = client.getUserId()!;
const createCallSpy = jest.spyOn(ElementCall, "create");
const callMembershipSpy = jest.spyOn(MatrixRTCSession, "callMembershipsForRoom");
callMembershipSpy.mockReturnValue([]);
const roomId = await createRoom(client, { roomType: RoomType.UnstableCall });
await createRoom(client, { roomType: RoomType.UnstableCall });
const userPower = client.createRoom.mock.calls[0][0].power_level_content_override?.users?.[userId];
const callPower =
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallEventType.name];
const callMemberPower =
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallMemberEventType.name];
// We should have had enough power to be able to set up the call
expect(userPower).toBeGreaterThanOrEqual(callPower!);
// and should have actually set it up
expect(createCallSpy).toHaveBeenCalled();
// All members should be able to update their connected devices
expect(callMemberPower).toEqual(0);
// call should be immutable for admins
expect(callPower).toBeGreaterThan(100);
// and we should have been reset back to admin
expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, userId, 100);
});
it("doesn't create calls in non-video-rooms", async () => {
@@ -177,12 +176,9 @@ describe("createRoom", () => {
await createRoom(client, {});
const callPower =
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallEventType.name];
const callMemberPower =
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallMemberEventType.name];
expect(callPower).toBe(100);
expect(callMemberPower).toBe(0);
});
@@ -212,6 +208,80 @@ describe("createRoom", () => {
}),
);
});
describe("room versions", () => {
afterEach(() => {
jest.clearAllMocks();
});
it("should use the correct room version for knocking when default does not support it", async () => {
client.getCapabilities.mockResolvedValue({
"m.room_versions": {
default: "1",
available: {
[PreferredRoomVersions.KnockRooms]: RoomVersionStability.Stable,
"1": RoomVersionStability.Stable,
},
},
});
await createRoom(client, { joinRule: JoinRule.Knock });
expect(client.createRoom).toHaveBeenCalledWith(
expect.objectContaining({
room_version: PreferredRoomVersions.KnockRooms,
}),
);
});
it("should use the default room version for knocking when default supports it", async () => {
client.getCapabilities.mockResolvedValue({
"m.room_versions": {
default: "12",
available: {
[PreferredRoomVersions.KnockRooms]: RoomVersionStability.Stable,
"12": RoomVersionStability.Stable,
},
},
});
await createRoom(client, { joinRule: JoinRule.Knock });
expect(client.createRoom).toHaveBeenCalledWith(
expect.not.objectContaining({
room_version: expect.anything(),
}),
);
});
it("should use the correct room version for restricted join rules when default does not support it", async () => {
client.getCapabilities.mockResolvedValue({
"m.room_versions": {
default: "1",
available: {
[PreferredRoomVersions.RestrictedRooms]: RoomVersionStability.Stable,
"1": RoomVersionStability.Stable,
},
},
});
await createRoom(client, { parentSpace: mkRoom(client, "!parent"), joinRule: JoinRule.Restricted });
expect(client.createRoom).toHaveBeenCalledWith(
expect.objectContaining({
room_version: PreferredRoomVersions.RestrictedRooms,
}),
);
});
it("should use the default room version for restricted join rules when default supports it", async () => {
client.getCapabilities.mockResolvedValue({
"m.room_versions": {
default: "12",
available: {
[PreferredRoomVersions.RestrictedRooms]: RoomVersionStability.Stable,
"12": RoomVersionStability.Stable,
},
},
});
await createRoom(client, { parentSpace: mkRoom(client, "!parent"), joinRule: JoinRule.Restricted });
expect(client.createRoom).toHaveBeenCalledWith(
expect.not.objectContaining({
room_version: expect.anything(),
}),
);
});
});
});
describe("canEncryptToAllUsers", () => {