Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 1a1a83a

Browse files
babolivierrichvdh
andauthored
Rework room freeze and implement unfreezing the room (#100)
Co-authored-by: Richard van der Hoff <[email protected]>
1 parent fcc10d9 commit 1a1a83a

File tree

4 files changed

+308
-221
lines changed

4 files changed

+308
-221
lines changed

synapse/handlers/message.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,11 +1414,13 @@ async def _rebuild_event_after_third_party_rules(
14141414
for k, v in original_event.internal_metadata.get_dict().items():
14151415
setattr(builder.internal_metadata, k, v)
14161416

1417-
# the event type hasn't changed, so there's no point in re-calculating the
1418-
# auth events.
1417+
# modules can send new state events, so we re-calculate the auth events just in
1418+
# case.
1419+
prev_event_ids = await self.store.get_prev_events_for_room(builder.room_id)
1420+
14191421
event = await builder.build(
1420-
prev_event_ids=original_event.prev_event_ids(),
1421-
auth_event_ids=original_event.auth_event_ids(),
1422+
prev_event_ids=prev_event_ids,
1423+
auth_event_ids=None,
14221424
)
14231425

14241426
# we rebuild the event context, to be on the safe side. If nothing else,

synapse/module_api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class ModuleApi:
4343
can register new users etc if necessary.
4444
"""
4545

46-
def __init__(self, hs, auth_handler):
46+
def __init__(self, hs: "HomeServer", auth_handler):
4747
self._hs = hs
4848

4949
self._store = hs.get_datastore()

synapse/third_party_rules/access_rules.py

Lines changed: 151 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,20 @@
1414
# limitations under the License.
1515
import email.utils
1616
import logging
17-
from typing import Dict, List, Optional, Tuple
17+
from typing import Dict, List, Optional, Tuple, Union
1818

1919
from synapse.api.constants import EventTypes, JoinRules, Membership, RoomCreationPreset
2020
from synapse.api.errors import SynapseError
2121
from synapse.config._base import ConfigError
2222
from synapse.events import EventBase
2323
from synapse.module_api import ModuleApi
2424
from synapse.types import Requester, StateMap, UserID, get_domain_from_id
25+
from synapse.util.frozenutils import unfreeze
2526

2627
logger = logging.getLogger(__name__)
2728

2829
ACCESS_RULES_TYPE = "im.vector.room.access_rules"
30+
FROZEN_STATE_TYPE = "io.element.room.frozen"
2931

3032

3133
class AccessRules:
@@ -108,7 +110,7 @@ def parse_config(config: Dict) -> Dict:
108110
ConfigError: If there was an issue with the provided module configuration.
109111
"""
110112
if "id_server" not in config:
111-
raise ConfigError("No IS for event rules TchapEventRules")
113+
raise ConfigError("No IS for event rules RoomAccessRules")
112114

113115
return config
114116

@@ -320,7 +322,7 @@ async def check_event_allowed(
320322
self,
321323
event: EventBase,
322324
state_events: StateMap[EventBase],
323-
) -> bool:
325+
) -> Union[bool, dict]:
324326
"""Implements synapse.events.ThirdPartyEventRules.check_event_allowed.
325327
326328
Checks the event's type and the current rule and calls the right function to
@@ -332,8 +334,18 @@ async def check_event_allowed(
332334
State events in the room the event originated from.
333335
334336
Returns:
335-
True if the event can be allowed, False otherwise.
337+
True if the event should be allowed, False if it should be rejected, or a dictionary if the
338+
event needs to be rebuilt (containing the event's new content).
336339
"""
340+
if event.type == FROZEN_STATE_TYPE:
341+
return await self._on_frozen_state_change(event, state_events)
342+
343+
# If the room is frozen, we allow a very small number of events to go through
344+
# (unfreezing, leaving, etc.).
345+
frozen_state = state_events.get((FROZEN_STATE_TYPE, ""))
346+
if frozen_state and frozen_state.content.get("frozen", False):
347+
return await self._on_event_when_frozen(event, state_events)
348+
337349
if event.type == ACCESS_RULES_TYPE:
338350
return await self._on_rules_change(event, state_events)
339351

@@ -394,6 +406,129 @@ async def check_visibility_can_be_modified(
394406
# published to the public rooms directory.
395407
return True
396408

409+
async def _on_event_when_frozen(
410+
self,
411+
event: EventBase,
412+
state_events: StateMap[EventBase],
413+
) -> Union[bool, dict]:
414+
"""Check if the provided event is allowed when the room is frozen.
415+
416+
The only events allowed are for a member to leave the room, and for the room to
417+
be (un)frozen. In the latter case, also attempt to unfreeze the room.
418+
419+
420+
Args:
421+
event: The event to allow or deny.
422+
state_events: A dict mapping (event type, state key) to state event.
423+
State events in the room before the event was sent.
424+
Returns:
425+
A boolean indicating whether the event is allowed, or a dict if the event is
426+
allowed but the state of the room has been modified (i.e. the room has been
427+
unfrozen). This is because returning a dict of the event forces Synapse to
428+
rebuild it, which is needed if the state of the room has changed.
429+
"""
430+
# Allow users to leave the room; don't allow kicks though.
431+
if (
432+
event.type == EventTypes.Member
433+
and event.membership == Membership.LEAVE
434+
and event.sender == event.state_key
435+
):
436+
return True
437+
438+
if event.type == EventTypes.PowerLevels:
439+
# Check if the power level event is associated with a room unfreeze (because
440+
# the power level events will be sent before the frozen state event). This
441+
# means we check that the users_default is back to 0 and the sender set
442+
# themselves as admin.
443+
current_power_levels = state_events.get((EventTypes.PowerLevels, ""))
444+
if current_power_levels:
445+
old_content = current_power_levels.content.copy()
446+
old_content["users_default"] = 0
447+
448+
new_content = unfreeze(event.content)
449+
sender_pl = new_content.get("users", {}).get(event.sender, 0)
450+
451+
# We don't care about the users section as long as the new event gives
452+
# full power to the sender.
453+
del old_content["users"]
454+
del new_content["users"]
455+
456+
if new_content == old_content and sender_pl == 100:
457+
return True
458+
459+
return False
460+
461+
async def _on_frozen_state_change(
462+
self,
463+
event: EventBase,
464+
state_events: StateMap[EventBase],
465+
) -> Union[bool, dict]:
466+
frozen = event.content.get("frozen", None)
467+
if not isinstance(frozen, bool):
468+
# Invalid event: frozen is either missing or not a boolean.
469+
return False
470+
471+
# If the event was sent from a restricted homeserver, don't allow the state
472+
# change.
473+
if (
474+
UserID.from_string(event.sender).domain
475+
in self.domains_forbidden_when_restricted
476+
):
477+
return False
478+
479+
current_frozen_state = state_events.get(
480+
(FROZEN_STATE_TYPE, ""),
481+
) # type: EventBase
482+
483+
if (
484+
current_frozen_state is not None
485+
and current_frozen_state.content.get("frozen") == frozen
486+
):
487+
# This is a noop, accept the new event but don't do anything more.
488+
return True
489+
490+
# If the event was received over federation, we want to accept it but not to
491+
# change the power levels.
492+
if not self._is_local_user(event.sender):
493+
return True
494+
495+
current_power_levels = state_events.get(
496+
(EventTypes.PowerLevels, ""),
497+
) # type: EventBase
498+
499+
power_levels_content = unfreeze(current_power_levels.content)
500+
501+
if not frozen:
502+
# We're unfreezing the room: enforce the right value for the power levels so
503+
# the room isn't in a weird/broken state afterwards.
504+
users = power_levels_content.setdefault("users", {})
505+
users[event.sender] = 100
506+
power_levels_content["users_default"] = 0
507+
else:
508+
# Send a new power levels event with a similar content to the previous one
509+
# except users_default is 100 to allow any user to unfreeze the room.
510+
power_levels_content["users_default"] = 100
511+
512+
# Just to be safe, also delete all users that don't have a power level of
513+
# 100, in order to prevent anyone from being unable to unfreeze the room.
514+
users = {}
515+
for user, level in power_levels_content["users"].items():
516+
if level == 100:
517+
users[user] = level
518+
power_levels_content["users"] = users
519+
520+
await self.module_api.create_and_send_event_into_room(
521+
{
522+
"room_id": event.room_id,
523+
"sender": event.sender,
524+
"type": EventTypes.PowerLevels,
525+
"content": power_levels_content,
526+
"state_key": "",
527+
}
528+
)
529+
530+
return event.get_dict()
531+
397532
async def _on_rules_change(
398533
self, event: EventBase, state_events: StateMap[EventBase]
399534
):
@@ -448,7 +583,7 @@ async def _on_membership_or_invite(
448583
event: EventBase,
449584
rule: str,
450585
state_events: StateMap[EventBase],
451-
) -> bool:
586+
) -> Union[bool, dict]:
452587
"""Applies the correct rule for incoming m.room.member and
453588
m.room.third_party_invite events.
454589
@@ -459,7 +594,10 @@ async def _on_membership_or_invite(
459594
The state of the room before the event was sent.
460595
461596
Returns:
462-
True if the event can be allowed, False otherwise.
597+
A boolean indicating whether the event is allowed, or a dict if the event is
598+
allowed but the state of the room has been modified (i.e. the room has been
599+
frozen). This is because returning a dict of the event forces Synapse to
600+
rebuild it, which is needed if the state of the room has changed.
463601
"""
464602
if rule == AccessRules.RESTRICTED:
465603
ret = self._on_membership_or_invite_restricted(event)
@@ -472,7 +610,7 @@ async def _on_membership_or_invite(
472610
# might want to change that in the future.
473611
ret = self._on_membership_or_invite_restricted(event)
474612

475-
if event.type == "m.room.member":
613+
if event.type == EventTypes.Member:
476614
# If this is an admin leaving, and they are the last admin in the room,
477615
# raise the power levels of the room so that the room is 'frozen'.
478616
#
@@ -484,6 +622,9 @@ async def _on_membership_or_invite(
484622
and event.membership == Membership.LEAVE
485623
):
486624
await self._freeze_room_if_last_admin_is_leaving(event, state_events)
625+
if ret:
626+
# Return an event dict to force Synapse into rebuilding the event.
627+
return event.get_dict()
487628

488629
return ret
489630

@@ -535,88 +676,13 @@ async def _freeze_room_if_last_admin_is_leaving(
535676
# Freeze the room by raising the required power level to send events to 100
536677
logger.info("Freezing room '%s'", event.room_id)
537678

538-
# Modify the existing power levels to raise all required types to 100
539-
#
540-
# This changes a power level state event's content from something like:
541-
# {
542-
# "redact": 50,
543-
# "state_default": 50,
544-
# "ban": 50,
545-
# "notifications": {
546-
# "room": 50
547-
# },
548-
# "events": {
549-
# "m.room.avatar": 50,
550-
# "m.room.encryption": 50,
551-
# "m.room.canonical_alias": 50,
552-
# "m.room.name": 50,
553-
# "im.vector.modular.widgets": 50,
554-
# "m.room.topic": 50,
555-
# "m.room.tombstone": 50,
556-
# "m.room.history_visibility": 100,
557-
# "m.room.power_levels": 100
558-
# },
559-
# "users_default": 0,
560-
# "events_default": 0,
561-
# "users": {
562-
# "@admin:example.com": 100,
563-
# },
564-
# "kick": 50,
565-
# "invite": 0
566-
# }
567-
#
568-
# to
569-
#
570-
# {
571-
# "redact": 100,
572-
# "state_default": 100,
573-
# "ban": 100,
574-
# "notifications": {
575-
# "room": 50
576-
# },
577-
# "events": {}
578-
# "users_default": 0,
579-
# "events_default": 100,
580-
# "users": {
581-
# "@admin:example.com": 100,
582-
# },
583-
# "kick": 100,
584-
# "invite": 100
585-
# }
586-
new_content = {}
587-
for key, value in power_level_content.items():
588-
# Do not change "users_default", as that key specifies the default power
589-
# level of new users
590-
if isinstance(value, int) and key != "users_default":
591-
value = 100
592-
new_content[key] = value
593-
594-
# Set some values in case they are missing from the original
595-
# power levels event content
596-
new_content.update(
597-
{
598-
# Clear out any special-cased event keys
599-
"events": {},
600-
# Ensure state_default and events_default keys exist and are 100.
601-
# Otherwise a lower PL user could potentially send state events that
602-
# aren't explicitly mentioned elsewhere in the power level dict
603-
"state_default": 100,
604-
"events_default": 100,
605-
# Membership events default to 50 if they aren't present. Set them
606-
# to 100 here, as they would be set to 100 if they were present anyways
607-
"ban": 100,
608-
"kick": 100,
609-
"invite": 100,
610-
"redact": 100,
611-
}
612-
)
613-
679+
# Mark the room as frozen
614680
await self.module_api.create_and_send_event_into_room(
615681
{
616682
"room_id": event.room_id,
617683
"sender": user_id,
618-
"type": EventTypes.PowerLevels,
619-
"content": new_content,
684+
"type": FROZEN_STATE_TYPE,
685+
"content": {"frozen": True},
620686
"state_key": "",
621687
}
622688
)

0 commit comments

Comments
 (0)