Skip to content

Commit ae242fd

Browse files
authored
Do not mutate power levels on upgrade to v12 room (#19727)
When upgrading a room to v12, we accidentally ended up mutating the content of the old power level. Since we cache events, this meant any future usage of the old power level event would see the wrong content (until it dropped from the cache). This meant that the creator of the new room would not be able to perform admin actions on the old room. Any federation requests for the event would fail the hash checks, since the content had been changed. All in all, quite a nasty bug.
1 parent 107029d commit ae242fd

3 files changed

Lines changed: 79 additions & 12 deletions

File tree

changelog.d/19727.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug where when upgrading a room to v12 the power level event in the old room got mutated to remove the user upgrading the room's power.

synapse/handlers/room.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import random
2828
import string
2929
from collections import OrderedDict
30+
from collections.abc import Mapping
3031
from http import HTTPStatus
3132
from typing import (
3233
TYPE_CHECKING,
@@ -67,7 +68,11 @@
6768
from synapse.event_auth import validate_event_for_room_version
6869
from synapse.events import EventBase, event_exists_in_state_dag
6970
from synapse.events.snapshot import UnpersistedEventContext
70-
from synapse.events.utils import FilteredEvent, copy_and_fixup_power_levels_contents
71+
from synapse.events.utils import (
72+
FilteredEvent,
73+
PowerLevelsContent,
74+
copy_and_fixup_power_levels_contents,
75+
)
7176
from synapse.handlers.relations import BundledAggregations
7277
from synapse.rest.admin._base import assert_user_is_admin
7378
from synapse.streams import EventSource
@@ -500,10 +505,12 @@ async def _update_upgraded_room_pls(
500505
except AuthError as e:
501506
logger.warning("Unable to update PLs in old room: %s", e)
502507

508+
power_levels_content: JsonMapping = old_room_pl_state.content
509+
503510
new_room_version = await self.store.get_room_version(new_room_id)
504511
if new_room_version.msc4289_creator_power_enabled:
505-
self._remove_creators_from_pl_users_map(
506-
old_room_pl_state.content.get("users", {}),
512+
power_levels_content = self._copy_and_remove_creators_from_pl_users_map(
513+
power_levels_content,
507514
requester.user.to_string(),
508515
additional_creators,
509516
)
@@ -515,9 +522,7 @@ async def _update_upgraded_room_pls(
515522
"state_key": "",
516523
"room_id": new_room_id,
517524
"sender": requester.user.to_string(),
518-
"content": copy_and_fixup_power_levels_contents(
519-
old_room_pl_state.content
520-
),
525+
"content": copy_and_fixup_power_levels_contents(power_levels_content),
521526
},
522527
ratelimit=False,
523528
)
@@ -686,11 +691,12 @@ async def clone_existing_room(
686691

687692
if new_room_version.msc4289_creator_power_enabled:
688693
# the creator(s) cannot be in the users map
689-
self._remove_creators_from_pl_users_map(
690-
user_power_levels,
694+
fixed_power_levels = self._copy_and_remove_creators_from_pl_users_map(
695+
power_levels,
691696
user_id,
692697
additional_creators,
693698
)
699+
initial_state[(EventTypes.PowerLevels, "")] = fixed_power_levels
694700

695701
# We construct a subset of what the body of a call to /createRoom would look like
696702
# for passing to the spam checker. We don't include a preset here, as we expect the
@@ -1829,19 +1835,29 @@ def _room_preset_config(self, room_config: JsonDict) -> tuple[str, dict]:
18291835
)
18301836
return preset_name, preset_config
18311837

1832-
def _remove_creators_from_pl_users_map(
1838+
def _copy_and_remove_creators_from_pl_users_map(
18331839
self,
1834-
users_map: dict[str, int],
1840+
power_levels_content: PowerLevelsContent,
18351841
creator: str,
18361842
additional_creators: list[str] | None,
1837-
) -> None:
1843+
) -> PowerLevelsContent:
1844+
users_map = power_levels_content.get("users", {})
1845+
if not users_map:
1846+
return power_levels_content
1847+
1848+
assert isinstance(users_map, Mapping)
1849+
users_map = dict(users_map)
1850+
18381851
creators = [creator]
18391852
if additional_creators:
18401853
creators.extend(additional_creators)
18411854
for creator in creators:
18421855
# the creator(s) cannot be in the users map
18431856
users_map.pop(creator, None)
18441857

1858+
power_levels_content = {**power_levels_content, "users": users_map}
1859+
return power_levels_content
1860+
18451861
def _generate_room_id(self) -> str:
18461862
"""Generates a random room ID.
18471863

tests/rest/client/test_upgrade_room.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from twisted.internet.testing import MemoryReactor
2424

2525
from synapse.api.constants import EventContentFields, EventTypes, Membership, RoomTypes
26+
from synapse.api.room_versions import RoomVersions
2627
from synapse.config.server import DEFAULT_ROOM_VERSION
2728
from synapse.rest import admin
2829
from synapse.rest.client import login, room, room_upgrade_rest_servlet
@@ -58,6 +59,7 @@ def _upgrade_room(
5859
token: str | None = None,
5960
room_id: str | None = None,
6061
expire_cache: bool = True,
62+
new_version: str = DEFAULT_ROOM_VERSION,
6163
) -> FakeChannel:
6264
if expire_cache:
6365
# We don't want a cached response.
@@ -70,7 +72,7 @@ def _upgrade_room(
7072
"POST",
7173
f"/_matrix/client/r0/rooms/{room_id}/upgrade",
7274
# This will upgrade a room to the same version, but that's fine.
73-
content={"new_version": DEFAULT_ROOM_VERSION},
75+
content={"new_version": new_version},
7476
access_token=token or self.creator_token,
7577
)
7678

@@ -431,3 +433,51 @@ def test_bans(self) -> None:
431433
tok=self.creator_token,
432434
)
433435
self.assertEqual(content[EventContentFields.MEMBERSHIP], Membership.BAN)
436+
437+
def test_creator_removed_from_powerlevels_v12(self) -> None:
438+
"""
439+
Test that the creator is removed from the power levels users map when
440+
upgrading to a room version with MSC4289.
441+
"""
442+
# Create a room on room version 11, which doesn't have MSC4289.
443+
room_id = self.helper.create_room_as(
444+
self.creator, tok=self.creator_token, room_version="11"
445+
)
446+
self.helper.join(room_id, self.other, tok=self.other_token)
447+
448+
# Retrieve the room's current power levels.
449+
old_power_level_event = self.get_success(
450+
self.hs.get_storage_controllers().state.get_current_state_event(
451+
room_id, "m.room.power_levels", ""
452+
)
453+
)
454+
assert old_power_level_event is not None
455+
456+
# The creator should be in the users map with power level 100.
457+
self.assertEqual(old_power_level_event.content["users"][self.creator], 100)
458+
459+
# Upgrade the room to version 12, which has MSC4289.
460+
channel = self._upgrade_room(
461+
room_id=room_id, new_version=RoomVersions.V12.identifier
462+
)
463+
self.assertEqual(200, channel.code, channel.result)
464+
465+
# Extract the new room ID.
466+
new_room_id = channel.json_body["replacement_room"]
467+
468+
# Fetch the new room's power level event.
469+
new_power_levels = self.helper.get_state(
470+
new_room_id,
471+
"m.room.power_levels",
472+
tok=self.creator_token,
473+
)
474+
475+
# The creator should no longer be in the users map.
476+
self.assertNotIn(self.creator, new_power_levels["users"])
477+
478+
# The creator should still be in the old power levels event with power
479+
# level 100.
480+
#
481+
# This is a regression test where previously Synapse would accidentally
482+
# mutate the old power levels event.
483+
self.assertEqual(old_power_level_event.content["users"][self.creator], 100)

0 commit comments

Comments
 (0)