Check required power levels earlier in createRoom handler. (#15695)
* Check required power levels earlier in createRoom handler. - If a server was configured to reject the creation of rooms with E2EE enabled (by specifying an unattainably high power level for "m.room.encryption" in default_power_level_content_override), the 403 error was not being triggered until after the room was created and before the "m.room.power_levels" was sent. This allowed a user to access the partially-configured room and complete the setup of E2EE and power levels manually. - This change causes the power level overrides to be checked earlier and the request to be rejected before the user gains access to the room. - A new `_validate_room_config` method is added to contain checks that should be run before a room is created. - The new test case confirms that a user request is rejected by the new validation method. Signed-off-by: Grant McLean <grant@catalyst.net.nz> * Add a changelog file. * Formatting fix for black. * Remove unneeded line from test. --------- Signed-off-by: Grant McLean <grant@catalyst.net.nz>pull/15752/head
parent
8934c11935
commit
5c24d7b9eb
|
@ -0,0 +1 @@
|
||||||
|
Check permissions for enabling encryption earlier during room creation to avoid creating broken rooms.
|
|
@ -872,6 +872,8 @@ class RoomCreationHandler:
|
||||||
visibility = config.get("visibility", "private")
|
visibility = config.get("visibility", "private")
|
||||||
is_public = visibility == "public"
|
is_public = visibility == "public"
|
||||||
|
|
||||||
|
self._validate_room_config(config, visibility)
|
||||||
|
|
||||||
room_id = await self._generate_and_create_room_id(
|
room_id = await self._generate_and_create_room_id(
|
||||||
creator_id=user_id,
|
creator_id=user_id,
|
||||||
is_public=is_public,
|
is_public=is_public,
|
||||||
|
@ -1111,20 +1113,7 @@ class RoomCreationHandler:
|
||||||
|
|
||||||
return new_event, new_unpersisted_context
|
return new_event, new_unpersisted_context
|
||||||
|
|
||||||
visibility = room_config.get("visibility", "private")
|
preset_config, config = self._room_preset_config(room_config)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
# MSC2175 removes the creator field from the create event.
|
# MSC2175 removes the creator field from the create event.
|
||||||
if not room_version.msc2175_implicit_room_creator:
|
if not room_version.msc2175_implicit_room_creator:
|
||||||
|
@ -1306,6 +1295,65 @@ class RoomCreationHandler:
|
||||||
assert last_event.internal_metadata.stream_ordering is not None
|
assert last_event.internal_metadata.stream_ordering is not None
|
||||||
return last_event.internal_metadata.stream_ordering, last_event.event_id, depth
|
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:
|
def _generate_room_id(self) -> str:
|
||||||
"""Generates a random room ID.
|
"""Generates a random room ID.
|
||||||
|
|
||||||
|
|
|
@ -1941,6 +1941,43 @@ class RoomPowerLevelOverridesInPracticeTestCase(RoomBase):
|
||||||
channel.json_body["error"],
|
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):
|
class RoomInitialSyncTestCase(RoomBase):
|
||||||
"""Tests /rooms/$room_id/initialSync."""
|
"""Tests /rooms/$room_id/initialSync."""
|
||||||
|
|
Loading…
Reference in New Issue