Skip to content

Commit d30e6f2

Browse files
authored
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
1 parent 5324834 commit d30e6f2

File tree

2 files changed

+116
-59
lines changed

2 files changed

+116
-59
lines changed

src/createRoom.ts

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/*
2+
Copyright 2025 Element Creations Ltd.
23
Copyright 2024 New Vector Ltd.
34
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
45
Copyright 2015, 2016 OpenMarket Ltd
@@ -39,10 +40,10 @@ import { findDMForUser } from "./utils/dm/findDMForUser";
3940
import { privateShouldBeEncrypted } from "./utils/rooms";
4041
import { shouldForceDisableEncryption } from "./utils/crypto/shouldForceDisableEncryption";
4142
import { waitForMember } from "./utils/membership";
42-
import { PreferredRoomVersions } from "./utils/PreferredRoomVersions";
43+
import { doesRoomVersionSupport, PreferredRoomVersions } from "./utils/PreferredRoomVersions";
4344
import SettingsStore from "./settings/SettingsStore";
4445
import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto";
45-
import { ElementCallEventType, ElementCallMemberEventType } from "./call-types";
46+
import { ElementCallMemberEventType } from "./call-types";
4647

4748
// we define a number of interfaces which take their names from the js-sdk
4849
/* eslint-disable camelcase */
@@ -159,32 +160,19 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
159160
};
160161

161162
// Video rooms require custom power levels
162-
if (opts.roomType === RoomType.ElementVideo) {
163+
if (opts.roomType === RoomType.ElementVideo || opts.roomType === RoomType.UnstableCall) {
163164
createOpts.power_level_content_override = {
164165
events: {
165166
...DEFAULT_EVENT_POWER_LEVELS,
166167
// Allow all users to send call membership updates
167-
[JitsiCall.MEMBER_EVENT_TYPE]: 0,
168-
// Make widgets immutable, even to admins
169-
"im.vector.modular.widgets": 200,
170-
},
171-
users: {
172-
// Temporarily give ourselves the power to set up a widget
173-
[client.getSafeUserId()]: 200,
174-
},
175-
};
176-
} else if (opts.roomType === RoomType.UnstableCall) {
177-
createOpts.power_level_content_override = {
178-
events: {
179-
...DEFAULT_EVENT_POWER_LEVELS,
180-
// Allow all users to send call membership updates
181-
[ElementCallMemberEventType.name]: 0,
182-
// Make calls immutable, even to admins
183-
[ElementCallEventType.name]: 200,
184-
},
185-
users: {
186-
// Temporarily give ourselves the power to set up a call
187-
[client.getSafeUserId()]: 200,
168+
[opts.roomType === RoomType.ElementVideo
169+
? JitsiCall.MEMBER_EVENT_TYPE
170+
: ElementCallMemberEventType.name]: 0,
171+
// Ensure all but admins can't change widgets
172+
// A previous version of the code prevented even administrators
173+
// from changing this, but this is not possible now that room creators
174+
// have an immutable power level
175+
["im.vector.modular.widgets"]: 100,
188176
},
189177
};
190178
}
@@ -194,8 +182,6 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
194182
...DEFAULT_EVENT_POWER_LEVELS,
195183
// It should always (including non video rooms) be possible to join a group call.
196184
[ElementCallMemberEventType.name]: 0,
197-
// Make sure only admins can enable it (DEPRECATED)
198-
[ElementCallEventType.name]: 100,
199185
},
200186
};
201187
}
@@ -230,15 +216,22 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
230216
});
231217
}
232218

233-
if (opts.joinRule === JoinRule.Knock) {
219+
const defaultRoomVersion = (await client.getCapabilities())["m.room_versions"]?.default ?? "1";
220+
221+
if (
222+
opts.joinRule === JoinRule.Knock &&
223+
!doesRoomVersionSupport(defaultRoomVersion, PreferredRoomVersions.KnockRooms)
224+
) {
234225
createOpts.room_version = PreferredRoomVersions.KnockRooms;
235226
}
236227

237228
if (opts.parentSpace) {
238229
createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
239230

240231
if (opts.joinRule === JoinRule.Restricted) {
241-
createOpts.room_version = PreferredRoomVersions.RestrictedRooms;
232+
if (!doesRoomVersionSupport(defaultRoomVersion, PreferredRoomVersions.KnockRooms)) {
233+
createOpts.room_version = PreferredRoomVersions.RestrictedRooms;
234+
}
242235

243236
createOpts.initial_state.push({
244237
type: EventType.RoomJoinRules,
@@ -354,15 +347,9 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
354347
if (opts.roomType === RoomType.ElementVideo) {
355348
// Set up this video room with a Jitsi call
356349
await JitsiCall.create(await room);
357-
358-
// Reset our power level back to admin so that the widget becomes immutable
359-
await client.setPowerLevel(roomId, client.getUserId()!, 100);
360350
} else if (opts.roomType === RoomType.UnstableCall) {
361351
// Set up this video room with an Element call
362352
ElementCall.create(await room);
363-
364-
// Reset our power level back to admin so that the call becomes immutable
365-
await client.setPowerLevel(roomId, client.getUserId()!, 100);
366353
}
367354
})
368355
.then(

test/unit-tests/createRoom-test.ts

Lines changed: 95 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/*
2+
Copyright 2025 Element Creations Ltd.
23
Copyright 2024 New Vector Ltd.
34
Copyright 2022 The Matrix.org Foundation C.I.C.
45
@@ -7,19 +8,33 @@ Please see LICENSE files in the repository root for full details.
78
*/
89

910
import { mocked, type Mocked } from "jest-mock";
10-
import { type MatrixClient, type Device, Preset, RoomType } from "matrix-js-sdk/src/matrix";
11+
import {
12+
type MatrixClient,
13+
type Device,
14+
Preset,
15+
RoomType,
16+
JoinRule,
17+
RoomVersionStability,
18+
} from "matrix-js-sdk/src/matrix";
1119
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
1220
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
1321

14-
import { stubClient, setupAsyncStoreWithClient, mockPlatformPeg, getMockClientWithEventEmitter } from "../test-utils";
22+
import {
23+
stubClient,
24+
setupAsyncStoreWithClient,
25+
mockPlatformPeg,
26+
getMockClientWithEventEmitter,
27+
mkRoom,
28+
} from "../test-utils";
1529
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
1630
import WidgetStore from "../../src/stores/WidgetStore";
1731
import WidgetUtils from "../../src/utils/WidgetUtils";
1832
import { JitsiCall, ElementCall } from "../../src/models/Call";
1933
import createRoom, { checkUserIsAllowedToChangeEncryption, canEncryptToAllUsers } from "../../src/createRoom";
2034
import SettingsStore from "../../src/settings/SettingsStore";
21-
import { ElementCallEventType, ElementCallMemberEventType } from "../../src/call-types";
35+
import { ElementCallMemberEventType } from "../../src/call-types";
2236
import DMRoomMap from "../../src/utils/DMRoomMap";
37+
import { PreferredRoomVersions } from "../../src/utils/PreferredRoomVersions";
2338

2439
describe("createRoom", () => {
2540
mockPlatformPeg();
@@ -103,14 +118,12 @@ describe("createRoom", () => {
103118
jest.spyOn(WidgetUtils, "waitForRoomWidget").mockResolvedValue();
104119
const createCallSpy = jest.spyOn(JitsiCall, "create");
105120

106-
const userId = client.getUserId()!;
107-
const roomId = await createRoom(client, { roomType: RoomType.ElementVideo });
121+
await createRoom(client, { roomType: RoomType.ElementVideo });
108122

109123
const [
110124
[
111125
{
112126
power_level_content_override: {
113-
users: { [userId]: userPower },
114127
events: {
115128
"im.vector.modular.widgets": widgetPower,
116129
[JitsiCall.MEMBER_EVENT_TYPE]: callMemberPower,
@@ -120,44 +133,30 @@ describe("createRoom", () => {
120133
],
121134
] = client.createRoom.mock.calls as any; // no good type
122135

123-
// We should have had enough power to be able to set up the widget
124-
expect(userPower).toBeGreaterThanOrEqual(widgetPower);
125136
// and should have actually set it up
126137
expect(createCallSpy).toHaveBeenCalled();
127138

128139
// All members should be able to update their connected devices
129140
expect(callMemberPower).toEqual(0);
130141
// widget should be immutable for admins
131-
expect(widgetPower).toBeGreaterThan(100);
132-
// and we should have been reset back to admin
133-
expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, userId, 100);
142+
expect(widgetPower).toEqual(100);
134143
});
135144

136145
it("sets up Element video rooms correctly", async () => {
137-
const userId = client.getUserId()!;
138146
const createCallSpy = jest.spyOn(ElementCall, "create");
139147
const callMembershipSpy = jest.spyOn(MatrixRTCSession, "callMembershipsForRoom");
140148
callMembershipSpy.mockReturnValue([]);
141149

142-
const roomId = await createRoom(client, { roomType: RoomType.UnstableCall });
150+
await createRoom(client, { roomType: RoomType.UnstableCall });
143151

144-
const userPower = client.createRoom.mock.calls[0][0].power_level_content_override?.users?.[userId];
145-
const callPower =
146-
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallEventType.name];
147152
const callMemberPower =
148153
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallMemberEventType.name];
149154

150-
// We should have had enough power to be able to set up the call
151-
expect(userPower).toBeGreaterThanOrEqual(callPower!);
152155
// and should have actually set it up
153156
expect(createCallSpy).toHaveBeenCalled();
154157

155158
// All members should be able to update their connected devices
156159
expect(callMemberPower).toEqual(0);
157-
// call should be immutable for admins
158-
expect(callPower).toBeGreaterThan(100);
159-
// and we should have been reset back to admin
160-
expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, userId, 100);
161160
});
162161

163162
it("doesn't create calls in non-video-rooms", async () => {
@@ -177,12 +176,9 @@ describe("createRoom", () => {
177176

178177
await createRoom(client, {});
179178

180-
const callPower =
181-
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallEventType.name];
182179
const callMemberPower =
183180
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallMemberEventType.name];
184181

185-
expect(callPower).toBe(100);
186182
expect(callMemberPower).toBe(0);
187183
});
188184

@@ -212,6 +208,80 @@ describe("createRoom", () => {
212208
}),
213209
);
214210
});
211+
212+
describe("room versions", () => {
213+
afterEach(() => {
214+
jest.clearAllMocks();
215+
});
216+
it("should use the correct room version for knocking when default does not support it", async () => {
217+
client.getCapabilities.mockResolvedValue({
218+
"m.room_versions": {
219+
default: "1",
220+
available: {
221+
[PreferredRoomVersions.KnockRooms]: RoomVersionStability.Stable,
222+
"1": RoomVersionStability.Stable,
223+
},
224+
},
225+
});
226+
await createRoom(client, { joinRule: JoinRule.Knock });
227+
expect(client.createRoom).toHaveBeenCalledWith(
228+
expect.objectContaining({
229+
room_version: PreferredRoomVersions.KnockRooms,
230+
}),
231+
);
232+
});
233+
it("should use the default room version for knocking when default supports it", async () => {
234+
client.getCapabilities.mockResolvedValue({
235+
"m.room_versions": {
236+
default: "12",
237+
available: {
238+
[PreferredRoomVersions.KnockRooms]: RoomVersionStability.Stable,
239+
"12": RoomVersionStability.Stable,
240+
},
241+
},
242+
});
243+
await createRoom(client, { joinRule: JoinRule.Knock });
244+
expect(client.createRoom).toHaveBeenCalledWith(
245+
expect.not.objectContaining({
246+
room_version: expect.anything(),
247+
}),
248+
);
249+
});
250+
it("should use the correct room version for restricted join rules when default does not support it", async () => {
251+
client.getCapabilities.mockResolvedValue({
252+
"m.room_versions": {
253+
default: "1",
254+
available: {
255+
[PreferredRoomVersions.RestrictedRooms]: RoomVersionStability.Stable,
256+
"1": RoomVersionStability.Stable,
257+
},
258+
},
259+
});
260+
await createRoom(client, { parentSpace: mkRoom(client, "!parent"), joinRule: JoinRule.Restricted });
261+
expect(client.createRoom).toHaveBeenCalledWith(
262+
expect.objectContaining({
263+
room_version: PreferredRoomVersions.RestrictedRooms,
264+
}),
265+
);
266+
});
267+
it("should use the default room version for restricted join rules when default supports it", async () => {
268+
client.getCapabilities.mockResolvedValue({
269+
"m.room_versions": {
270+
default: "12",
271+
available: {
272+
[PreferredRoomVersions.RestrictedRooms]: RoomVersionStability.Stable,
273+
"12": RoomVersionStability.Stable,
274+
},
275+
},
276+
});
277+
await createRoom(client, { parentSpace: mkRoom(client, "!parent"), joinRule: JoinRule.Restricted });
278+
expect(client.createRoom).toHaveBeenCalledWith(
279+
expect.not.objectContaining({
280+
room_version: expect.anything(),
281+
}),
282+
);
283+
});
284+
});
215285
});
216286

217287
describe("canEncryptToAllUsers", () => {

0 commit comments

Comments
 (0)