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

View File

@@ -1,4 +1,5 @@
/* /*
Copyright 2025 Element Creations Ltd.
Copyright 2024 New Vector Ltd. Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C. 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 { 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 { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc"; 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 { MatrixClientPeg } from "../../src/MatrixClientPeg";
import WidgetStore from "../../src/stores/WidgetStore"; import WidgetStore from "../../src/stores/WidgetStore";
import WidgetUtils from "../../src/utils/WidgetUtils"; import WidgetUtils from "../../src/utils/WidgetUtils";
import { JitsiCall, ElementCall } from "../../src/models/Call"; import { JitsiCall, ElementCall } from "../../src/models/Call";
import createRoom, { checkUserIsAllowedToChangeEncryption, canEncryptToAllUsers } from "../../src/createRoom"; import createRoom, { checkUserIsAllowedToChangeEncryption, canEncryptToAllUsers } from "../../src/createRoom";
import SettingsStore from "../../src/settings/SettingsStore"; 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 DMRoomMap from "../../src/utils/DMRoomMap";
import { PreferredRoomVersions } from "../../src/utils/PreferredRoomVersions";
describe("createRoom", () => { describe("createRoom", () => {
mockPlatformPeg(); mockPlatformPeg();
@@ -103,14 +118,12 @@ describe("createRoom", () => {
jest.spyOn(WidgetUtils, "waitForRoomWidget").mockResolvedValue(); jest.spyOn(WidgetUtils, "waitForRoomWidget").mockResolvedValue();
const createCallSpy = jest.spyOn(JitsiCall, "create"); const createCallSpy = jest.spyOn(JitsiCall, "create");
const userId = client.getUserId()!; await createRoom(client, { roomType: RoomType.ElementVideo });
const roomId = await createRoom(client, { roomType: RoomType.ElementVideo });
const [ const [
[ [
{ {
power_level_content_override: { power_level_content_override: {
users: { [userId]: userPower },
events: { events: {
"im.vector.modular.widgets": widgetPower, "im.vector.modular.widgets": widgetPower,
[JitsiCall.MEMBER_EVENT_TYPE]: callMemberPower, [JitsiCall.MEMBER_EVENT_TYPE]: callMemberPower,
@@ -120,44 +133,30 @@ describe("createRoom", () => {
], ],
] = client.createRoom.mock.calls as any; // no good type ] = 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 // and should have actually set it up
expect(createCallSpy).toHaveBeenCalled(); expect(createCallSpy).toHaveBeenCalled();
// All members should be able to update their connected devices // All members should be able to update their connected devices
expect(callMemberPower).toEqual(0); expect(callMemberPower).toEqual(0);
// widget should be immutable for admins // widget should be immutable for admins
expect(widgetPower).toBeGreaterThan(100); expect(widgetPower).toEqual(100);
// and we should have been reset back to admin
expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, userId, 100);
}); });
it("sets up Element video rooms correctly", async () => { it("sets up Element video rooms correctly", async () => {
const userId = client.getUserId()!;
const createCallSpy = jest.spyOn(ElementCall, "create"); const createCallSpy = jest.spyOn(ElementCall, "create");
const callMembershipSpy = jest.spyOn(MatrixRTCSession, "callMembershipsForRoom"); const callMembershipSpy = jest.spyOn(MatrixRTCSession, "callMembershipsForRoom");
callMembershipSpy.mockReturnValue([]); 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 = const callMemberPower =
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallMemberEventType.name]; 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 // and should have actually set it up
expect(createCallSpy).toHaveBeenCalled(); expect(createCallSpy).toHaveBeenCalled();
// All members should be able to update their connected devices // All members should be able to update their connected devices
expect(callMemberPower).toEqual(0); 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 () => { it("doesn't create calls in non-video-rooms", async () => {
@@ -177,12 +176,9 @@ describe("createRoom", () => {
await createRoom(client, {}); await createRoom(client, {});
const callPower =
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallEventType.name];
const callMemberPower = const callMemberPower =
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallMemberEventType.name]; client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallMemberEventType.name];
expect(callPower).toBe(100);
expect(callMemberPower).toBe(0); 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", () => { describe("canEncryptToAllUsers", () => {