summary refs log tree commit diff
path: root/synapse/third_party_rules/access_rules.py
diff options
context:
space:
mode:
authorBrendan Abolivier <babolivier@matrix.org>2021-07-22 17:50:07 +0200
committerGitHub <noreply@github.com>2021-07-22 17:50:07 +0200
commit1a1a83abcb767ec067492ef982b7264351ecc350 (patch)
treef68afe618e1e088b0d4ad8981b051c991a417f08 /synapse/third_party_rules/access_rules.py
parentMerge pull request #99 from matrix-org/anoa/fix_pipeline (diff)
downloadsynapse-1a1a83abcb767ec067492ef982b7264351ecc350.tar.xz
Rework room freeze and implement unfreezing the room (#100)
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Diffstat (limited to 'synapse/third_party_rules/access_rules.py')
-rw-r--r--synapse/third_party_rules/access_rules.py236
1 files changed, 151 insertions, 85 deletions
diff --git a/synapse/third_party_rules/access_rules.py b/synapse/third_party_rules/access_rules.py

index a047699cc4..01d100c82d 100644 --- a/synapse/third_party_rules/access_rules.py +++ b/synapse/third_party_rules/access_rules.py
@@ -14,7 +14,7 @@ # limitations under the License. import email.utils import logging -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple, Union from synapse.api.constants import EventTypes, JoinRules, Membership, RoomCreationPreset from synapse.api.errors import SynapseError @@ -22,10 +22,12 @@ from synapse.config._base import ConfigError from synapse.events import EventBase from synapse.module_api import ModuleApi from synapse.types import Requester, StateMap, UserID, get_domain_from_id +from synapse.util.frozenutils import unfreeze logger = logging.getLogger(__name__) ACCESS_RULES_TYPE = "im.vector.room.access_rules" +FROZEN_STATE_TYPE = "io.element.room.frozen" class AccessRules: @@ -108,7 +110,7 @@ class RoomAccessRules(object): ConfigError: If there was an issue with the provided module configuration. """ if "id_server" not in config: - raise ConfigError("No IS for event rules TchapEventRules") + raise ConfigError("No IS for event rules RoomAccessRules") return config @@ -320,7 +322,7 @@ class RoomAccessRules(object): self, event: EventBase, state_events: StateMap[EventBase], - ) -> bool: + ) -> Union[bool, dict]: """Implements synapse.events.ThirdPartyEventRules.check_event_allowed. Checks the event's type and the current rule and calls the right function to @@ -332,8 +334,18 @@ class RoomAccessRules(object): State events in the room the event originated from. Returns: - True if the event can be allowed, False otherwise. + True if the event should be allowed, False if it should be rejected, or a dictionary if the + event needs to be rebuilt (containing the event's new content). """ + if event.type == FROZEN_STATE_TYPE: + return await self._on_frozen_state_change(event, state_events) + + # If the room is frozen, we allow a very small number of events to go through + # (unfreezing, leaving, etc.). + frozen_state = state_events.get((FROZEN_STATE_TYPE, "")) + if frozen_state and frozen_state.content.get("frozen", False): + return await self._on_event_when_frozen(event, state_events) + if event.type == ACCESS_RULES_TYPE: return await self._on_rules_change(event, state_events) @@ -394,6 +406,129 @@ class RoomAccessRules(object): # published to the public rooms directory. return True + async def _on_event_when_frozen( + self, + event: EventBase, + state_events: StateMap[EventBase], + ) -> Union[bool, dict]: + """Check if the provided event is allowed when the room is frozen. + + The only events allowed are for a member to leave the room, and for the room to + be (un)frozen. In the latter case, also attempt to unfreeze the room. + + + Args: + event: The event to allow or deny. + state_events: A dict mapping (event type, state key) to state event. + State events in the room before the event was sent. + Returns: + A boolean indicating whether the event is allowed, or a dict if the event is + allowed but the state of the room has been modified (i.e. the room has been + unfrozen). This is because returning a dict of the event forces Synapse to + rebuild it, which is needed if the state of the room has changed. + """ + # Allow users to leave the room; don't allow kicks though. + if ( + event.type == EventTypes.Member + and event.membership == Membership.LEAVE + and event.sender == event.state_key + ): + return True + + if event.type == EventTypes.PowerLevels: + # Check if the power level event is associated with a room unfreeze (because + # the power level events will be sent before the frozen state event). This + # means we check that the users_default is back to 0 and the sender set + # themselves as admin. + current_power_levels = state_events.get((EventTypes.PowerLevels, "")) + if current_power_levels: + old_content = current_power_levels.content.copy() + old_content["users_default"] = 0 + + new_content = unfreeze(event.content) + sender_pl = new_content.get("users", {}).get(event.sender, 0) + + # We don't care about the users section as long as the new event gives + # full power to the sender. + del old_content["users"] + del new_content["users"] + + if new_content == old_content and sender_pl == 100: + return True + + return False + + async def _on_frozen_state_change( + self, + event: EventBase, + state_events: StateMap[EventBase], + ) -> Union[bool, dict]: + frozen = event.content.get("frozen", None) + if not isinstance(frozen, bool): + # Invalid event: frozen is either missing or not a boolean. + return False + + # If the event was sent from a restricted homeserver, don't allow the state + # change. + if ( + UserID.from_string(event.sender).domain + in self.domains_forbidden_when_restricted + ): + return False + + current_frozen_state = state_events.get( + (FROZEN_STATE_TYPE, ""), + ) # type: EventBase + + if ( + current_frozen_state is not None + and current_frozen_state.content.get("frozen") == frozen + ): + # This is a noop, accept the new event but don't do anything more. + return True + + # If the event was received over federation, we want to accept it but not to + # change the power levels. + if not self._is_local_user(event.sender): + return True + + current_power_levels = state_events.get( + (EventTypes.PowerLevels, ""), + ) # type: EventBase + + power_levels_content = unfreeze(current_power_levels.content) + + if not frozen: + # We're unfreezing the room: enforce the right value for the power levels so + # the room isn't in a weird/broken state afterwards. + users = power_levels_content.setdefault("users", {}) + users[event.sender] = 100 + power_levels_content["users_default"] = 0 + else: + # Send a new power levels event with a similar content to the previous one + # except users_default is 100 to allow any user to unfreeze the room. + power_levels_content["users_default"] = 100 + + # Just to be safe, also delete all users that don't have a power level of + # 100, in order to prevent anyone from being unable to unfreeze the room. + users = {} + for user, level in power_levels_content["users"].items(): + if level == 100: + users[user] = level + power_levels_content["users"] = users + + await self.module_api.create_and_send_event_into_room( + { + "room_id": event.room_id, + "sender": event.sender, + "type": EventTypes.PowerLevels, + "content": power_levels_content, + "state_key": "", + } + ) + + return event.get_dict() + async def _on_rules_change( self, event: EventBase, state_events: StateMap[EventBase] ): @@ -448,7 +583,7 @@ class RoomAccessRules(object): event: EventBase, rule: str, state_events: StateMap[EventBase], - ) -> bool: + ) -> Union[bool, dict]: """Applies the correct rule for incoming m.room.member and m.room.third_party_invite events. @@ -459,7 +594,10 @@ class RoomAccessRules(object): The state of the room before the event was sent. Returns: - True if the event can be allowed, False otherwise. + A boolean indicating whether the event is allowed, or a dict if the event is + allowed but the state of the room has been modified (i.e. the room has been + frozen). This is because returning a dict of the event forces Synapse to + rebuild it, which is needed if the state of the room has changed. """ if rule == AccessRules.RESTRICTED: ret = self._on_membership_or_invite_restricted(event) @@ -472,7 +610,7 @@ class RoomAccessRules(object): # might want to change that in the future. ret = self._on_membership_or_invite_restricted(event) - if event.type == "m.room.member": + if event.type == EventTypes.Member: # If this is an admin leaving, and they are the last admin in the room, # raise the power levels of the room so that the room is 'frozen'. # @@ -484,6 +622,9 @@ class RoomAccessRules(object): and event.membership == Membership.LEAVE ): await self._freeze_room_if_last_admin_is_leaving(event, state_events) + if ret: + # Return an event dict to force Synapse into rebuilding the event. + return event.get_dict() return ret @@ -535,88 +676,13 @@ class RoomAccessRules(object): # Freeze the room by raising the required power level to send events to 100 logger.info("Freezing room '%s'", event.room_id) - # Modify the existing power levels to raise all required types to 100 - # - # This changes a power level state event's content from something like: - # { - # "redact": 50, - # "state_default": 50, - # "ban": 50, - # "notifications": { - # "room": 50 - # }, - # "events": { - # "m.room.avatar": 50, - # "m.room.encryption": 50, - # "m.room.canonical_alias": 50, - # "m.room.name": 50, - # "im.vector.modular.widgets": 50, - # "m.room.topic": 50, - # "m.room.tombstone": 50, - # "m.room.history_visibility": 100, - # "m.room.power_levels": 100 - # }, - # "users_default": 0, - # "events_default": 0, - # "users": { - # "@admin:example.com": 100, - # }, - # "kick": 50, - # "invite": 0 - # } - # - # to - # - # { - # "redact": 100, - # "state_default": 100, - # "ban": 100, - # "notifications": { - # "room": 50 - # }, - # "events": {} - # "users_default": 0, - # "events_default": 100, - # "users": { - # "@admin:example.com": 100, - # }, - # "kick": 100, - # "invite": 100 - # } - new_content = {} - for key, value in power_level_content.items(): - # Do not change "users_default", as that key specifies the default power - # level of new users - if isinstance(value, int) and key != "users_default": - value = 100 - new_content[key] = value - - # Set some values in case they are missing from the original - # power levels event content - new_content.update( - { - # Clear out any special-cased event keys - "events": {}, - # Ensure state_default and events_default keys exist and are 100. - # Otherwise a lower PL user could potentially send state events that - # aren't explicitly mentioned elsewhere in the power level dict - "state_default": 100, - "events_default": 100, - # Membership events default to 50 if they aren't present. Set them - # to 100 here, as they would be set to 100 if they were present anyways - "ban": 100, - "kick": 100, - "invite": 100, - "redact": 100, - } - ) - + # Mark the room as frozen await self.module_api.create_and_send_event_into_room( { "room_id": event.room_id, "sender": user_id, - "type": EventTypes.PowerLevels, - "content": new_content, + "type": FROZEN_STATE_TYPE, + "content": {"frozen": True}, "state_key": "", } )