diff --git a/changelog.d/15695.bugfix b/changelog.d/15695.bugfix
new file mode 100644
index 0000000000..99bf1fe05e
--- /dev/null
+++ b/changelog.d/15695.bugfix
@@ -0,0 +1 @@
+Check permissions for enabling encryption earlier during room creation to avoid creating broken rooms.
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index cb957f2033..bf907b7881 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -872,6 +872,8 @@ class RoomCreationHandler:
visibility = config.get("visibility", "private")
is_public = visibility == "public"
+ self._validate_room_config(config, visibility)
+
room_id = await self._generate_and_create_room_id(
creator_id=user_id,
is_public=is_public,
@@ -1111,20 +1113,7 @@ class RoomCreationHandler:
return new_event, new_unpersisted_context
- visibility = room_config.get("visibility", "private")
- preset_config = room_config.get(
- "preset",
- RoomCreationPreset.PRIVATE_CHAT
- if visibility == "private"
- else RoomCreationPreset.PUBLIC_CHAT,
- )
-
- try:
- config = self._presets_dict[preset_config]
- except KeyError:
- raise SynapseError(
- 400, f"'{preset_config}' is not a valid preset", errcode=Codes.BAD_JSON
- )
+ preset_config, config = self._room_preset_config(room_config)
# MSC2175 removes the creator field from the create event.
if not room_version.msc2175_implicit_room_creator:
@@ -1306,6 +1295,65 @@ class RoomCreationHandler:
assert last_event.internal_metadata.stream_ordering is not None
return last_event.internal_metadata.stream_ordering, last_event.event_id, depth
+ def _validate_room_config(
+ self,
+ config: JsonDict,
+ visibility: str,
+ ) -> None:
+ """Checks configuration parameters for a /createRoom request.
+
+ If validation detects invalid parameters an exception may be raised to
+ cause room creation to be aborted and an error response to be returned
+ to the client.
+
+ Args:
+ config: A dict of configuration options. Originally from the body of
+ the /createRoom request
+ visibility: One of "public" or "private"
+ """
+
+ # Validate the requested preset, raise a 400 error if not valid
+ preset_name, preset_config = self._room_preset_config(config)
+
+ # If the user is trying to create an encrypted room and this is forbidden
+ # by the configured default_power_level_content_override, then reject the
+ # request before the room is created.
+ raw_initial_state = config.get("initial_state", [])
+ room_encryption_event = any(
+ s.get("type", "") == EventTypes.RoomEncryption for s in raw_initial_state
+ )
+
+ if preset_config["encrypted"] or room_encryption_event:
+ if self._default_power_level_content_override:
+ override = self._default_power_level_content_override.get(preset_name)
+ if override is not None:
+ event_levels = override.get("events", {})
+ room_admin_level = event_levels.get(EventTypes.PowerLevels, 100)
+ encryption_level = event_levels.get(EventTypes.RoomEncryption, 100)
+ if encryption_level > room_admin_level:
+ raise SynapseError(
+ 403,
+ f"You cannot create an encrypted room. user_level ({room_admin_level}) < send_level ({encryption_level})",
+ )
+
+ def _room_preset_config(self, room_config: JsonDict) -> Tuple[str, dict]:
+ # The spec says rooms should default to private visibility if
+ # `visibility` is not specified.
+ visibility = room_config.get("visibility", "private")
+ preset_name = room_config.get(
+ "preset",
+ RoomCreationPreset.PRIVATE_CHAT
+ if visibility == "private"
+ else RoomCreationPreset.PUBLIC_CHAT,
+ )
+ try:
+ preset_config = self._presets_dict[preset_name]
+ except KeyError:
+ raise SynapseError(
+ 400, f"'{preset_name}' is not a valid preset", errcode=Codes.BAD_JSON
+ )
+ return preset_name, preset_config
+
def _generate_room_id(self) -> str:
"""Generates a random room ID.
diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py
index 4d39c89f6f..f1b4e1ad2f 100644
--- a/tests/rest/client/test_rooms.py
+++ b/tests/rest/client/test_rooms.py
@@ -1941,6 +1941,43 @@ class RoomPowerLevelOverridesInPracticeTestCase(RoomBase):
channel.json_body["error"],
)
+ @unittest.override_config(
+ {
+ "default_power_level_content_override": {
+ "private_chat": {
+ "events": {
+ "m.room.avatar": 50,
+ "m.room.canonical_alias": 50,
+ "m.room.encryption": 999,
+ "m.room.history_visibility": 100,
+ "m.room.name": 50,
+ "m.room.power_levels": 100,
+ "m.room.server_acl": 100,
+ "m.room.tombstone": 100,
+ },
+ "events_default": 0,
+ },
+ }
+ },
+ )
+ def test_config_override_blocks_encrypted_room(self) -> None:
+ # Given the server has config for private_chats,
+
+ # When I attempt to create an encrypted private_chat room
+ channel = self.make_request(
+ "POST",
+ "/createRoom",
+ '{"creation_content": {"m.federate": false},"name": "Secret Private Room","preset": "private_chat","initial_state": [{"type": "m.room.encryption","state_key": "","content": {"algorithm": "m.megolm.v1.aes-sha2"}}]}',
+ )
+
+ # Then I am not allowed because the required power level is unattainable
+ self.assertEqual(HTTPStatus.FORBIDDEN, channel.code, msg=channel.result["body"])
+ self.assertEqual(
+ "You cannot create an encrypted room. "
+ + "user_level (100) < send_level (999)",
+ channel.json_body["error"],
+ )
+
class RoomInitialSyncTestCase(RoomBase):
"""Tests /rooms/$room_id/initialSync."""
|