summary refs log tree commit diff
path: root/synapse/third_party_rules
diff options
context:
space:
mode:
Diffstat (limited to 'synapse/third_party_rules')
-rw-r--r--synapse/third_party_rules/access_rules.py174
1 files changed, 171 insertions, 3 deletions
diff --git a/synapse/third_party_rules/access_rules.py b/synapse/third_party_rules/access_rules.py

index a8b21048ec..2519e05ae0 100644 --- a/synapse/third_party_rules/access_rules.py +++ b/synapse/third_party_rules/access_rules.py
@@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import email.utils +import logging from typing import Dict, List, Optional, Tuple from twisted.internet import defer @@ -22,7 +23,9 @@ from synapse.api.errors import SynapseError from synapse.config._base import ConfigError from synapse.events import EventBase from synapse.module_api import ModuleApi -from synapse.types import Requester, StateMap, get_domain_from_id +from synapse.types import Requester, StateMap, UserID, get_domain_from_id + +logger = logging.getLogger(__name__) ACCESS_RULES_TYPE = "im.vector.room.access_rules" @@ -323,7 +326,7 @@ class RoomAccessRules(object): ) if event.type == EventTypes.Member or event.type == EventTypes.ThirdPartyInvite: - return self._on_membership_or_invite(event, rule, state_events) + return await self._on_membership_or_invite(event, rule, state_events) if event.type == EventTypes.JoinRules: return self._on_join_rule_change(event, rule) @@ -420,7 +423,7 @@ class RoomAccessRules(object): prev_rule == AccessRules.RESTRICTED and new_rule == AccessRules.UNRESTRICTED ) - def _on_membership_or_invite( + async def _on_membership_or_invite( self, event: EventBase, rule: str, state_events: StateMap[EventBase], ) -> bool: """Applies the correct rule for incoming m.room.member and @@ -446,8 +449,154 @@ 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 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'. + # + # We have to freeze the room by puppeting an admin user, which we can + # only do for local users + if ( + self._is_local_user(event.sender) + and event.membership == Membership.LEAVE + ): + await self._freeze_room_if_last_admin_is_leaving(event, state_events) + return ret + async def _freeze_room_if_last_admin_is_leaving( + self, event: EventBase, state_events: StateMap[EventBase] + ): + power_level_state_event = state_events.get( + (EventTypes.PowerLevels, "") + ) # type: EventBase + if not power_level_state_event: + return + power_level_content = power_level_state_event.content + + # Do some validation checks on the power level state event + if ( + not isinstance(power_level_content, dict) + or "users" not in power_level_content + or not isinstance(power_level_content["users"], dict) + ): + # We can't use this power level event to determine whether the room should be + # frozen. Bail out. + return + + user_id = event.get("sender") + if not user_id: + return + + # Get every admin user defined in the room's state + admin_users = { + user + for user, power_level in power_level_content["users"].items() + if power_level >= 100 + } + + if user_id not in admin_users: + # This user is not an admin, ignore them + return + + if any( + event_type == EventTypes.Member + and event.membership in [Membership.JOIN, Membership.INVITE] + and state_key in admin_users + and state_key != user_id + for (event_type, state_key), event in state_events.items() + ): + # There's another admin user in, or invited to, the room + return + + # 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, + } + ) + + await self.module_api.create_and_send_event_into_room( + { + "room_id": event.room_id, + "sender": user_id, + "type": EventTypes.PowerLevels, + "content": new_content, + "state_key": "", + } + ) + def _on_membership_or_invite_restricted(self, event: EventBase) -> bool: """Implements the checks and behaviour specified for the "restricted" rule. @@ -753,6 +902,25 @@ class RoomAccessRules(object): return token == threepid_invite_token + def _is_local_user(self, user_id: str) -> bool: + """Checks whether a given user ID belongs to this homeserver, or a remote + + Args: + user_id: A user ID to check. + + Returns: + True if the user belongs to this homeserver, False otherwise. + """ + user = UserID.from_string(user_id) + + # Extract the localpart and ask the module API for a user ID from the localpart + # The module API will append the local homeserver's server_name + local_user_id = self.module_api.get_qualified_user_id(user.localpart) + + # If the user ID we get based on the localpart is the same as the original user ID, + # then they were a local user + return user_id == local_user_id + def _user_is_invited_to_room( self, user_id: str, state_events: StateMap[EventBase] ) -> bool: