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

Commit 6f7417c

Browse files
authored
Handle missing content keys when calculating presentable names. (#9165)
Treat the content as untrusted and do not assume it is of the proper form.
1 parent a7882f9 commit 6f7417c

File tree

4 files changed

+242
-16
lines changed

4 files changed

+242
-16
lines changed

changelog.d/9165.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug where invalid data could cause errors when calculating the presentable room name for push.

synapse/push/presentable_names.py

+11-15
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import re
1818
from typing import TYPE_CHECKING, Dict, Iterable, Optional
1919

20-
from synapse.api.constants import EventTypes
20+
from synapse.api.constants import EventTypes, Membership
2121
from synapse.events import EventBase
2222
from synapse.types import StateMap
2323

@@ -63,7 +63,7 @@ async def calculate_room_name(
6363
m_room_name = await store.get_event(
6464
room_state_ids[(EventTypes.Name, "")], allow_none=True
6565
)
66-
if m_room_name and m_room_name.content and m_room_name.content["name"]:
66+
if m_room_name and m_room_name.content and m_room_name.content.get("name"):
6767
return m_room_name.content["name"]
6868

6969
# does it have a canonical alias?
@@ -74,15 +74,11 @@ async def calculate_room_name(
7474
if (
7575
canon_alias
7676
and canon_alias.content
77-
and canon_alias.content["alias"]
77+
and canon_alias.content.get("alias")
7878
and _looks_like_an_alias(canon_alias.content["alias"])
7979
):
8080
return canon_alias.content["alias"]
8181

82-
# at this point we're going to need to search the state by all state keys
83-
# for an event type, so rearrange the data structure
84-
room_state_bytype_ids = _state_as_two_level_dict(room_state_ids)
85-
8682
if not fallback_to_members:
8783
return None
8884

@@ -94,7 +90,7 @@ async def calculate_room_name(
9490

9591
if (
9692
my_member_event is not None
97-
and my_member_event.content["membership"] == "invite"
93+
and my_member_event.content.get("membership") == Membership.INVITE
9894
):
9995
if (EventTypes.Member, my_member_event.sender) in room_state_ids:
10096
inviter_member_event = await store.get_event(
@@ -111,6 +107,10 @@ async def calculate_room_name(
111107
else:
112108
return "Room Invite"
113109

110+
# at this point we're going to need to search the state by all state keys
111+
# for an event type, so rearrange the data structure
112+
room_state_bytype_ids = _state_as_two_level_dict(room_state_ids)
113+
114114
# we're going to have to generate a name based on who's in the room,
115115
# so find out who is in the room that isn't the user.
116116
if EventTypes.Member in room_state_bytype_ids:
@@ -120,8 +120,8 @@ async def calculate_room_name(
120120
all_members = [
121121
ev
122122
for ev in member_events.values()
123-
if ev.content["membership"] == "join"
124-
or ev.content["membership"] == "invite"
123+
if ev.content.get("membership") == Membership.JOIN
124+
or ev.content.get("membership") == Membership.INVITE
125125
]
126126
# Sort the member events oldest-first so the we name people in the
127127
# order the joined (it should at least be deterministic rather than
@@ -194,11 +194,7 @@ def descriptor_from_member_events(member_events: Iterable[EventBase]) -> str:
194194

195195

196196
def name_from_member_event(member_event: EventBase) -> str:
197-
if (
198-
member_event.content
199-
and "displayname" in member_event.content
200-
and member_event.content["displayname"]
201-
):
197+
if member_event.content and member_event.content.get("displayname"):
202198
return member_event.content["displayname"]
203199
return member_event.state_key
204200

tests/push/test_presentable_names.py

+229
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# Copyright 2021 The Matrix.org Foundation C.I.C.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import Iterable, Optional, Tuple
16+
17+
from synapse.api.constants import EventTypes, Membership
18+
from synapse.api.room_versions import RoomVersions
19+
from synapse.events import FrozenEvent
20+
from synapse.push.presentable_names import calculate_room_name
21+
from synapse.types import StateKey, StateMap
22+
23+
from tests import unittest
24+
25+
26+
class MockDataStore:
27+
"""
28+
A fake data store which stores a mapping of state key to event content.
29+
(I.e. the state key is used as the event ID.)
30+
"""
31+
32+
def __init__(self, events: Iterable[Tuple[StateKey, dict]]):
33+
"""
34+
Args:
35+
events: A state map to event contents.
36+
"""
37+
self._events = {}
38+
39+
for i, (event_id, content) in enumerate(events):
40+
self._events[event_id] = FrozenEvent(
41+
{
42+
"event_id": "$event_id",
43+
"type": event_id[0],
44+
"sender": "@user:test",
45+
"state_key": event_id[1],
46+
"room_id": "#room:test",
47+
"content": content,
48+
"origin_server_ts": i,
49+
},
50+
RoomVersions.V1,
51+
)
52+
53+
async def get_event(
54+
self, event_id: StateKey, allow_none: bool = False
55+
) -> Optional[FrozenEvent]:
56+
assert allow_none, "Mock not configured for allow_none = False"
57+
58+
return self._events.get(event_id)
59+
60+
async def get_events(self, event_ids: Iterable[StateKey]):
61+
# This is cheating since it just returns all events.
62+
return self._events
63+
64+
65+
class PresentableNamesTestCase(unittest.HomeserverTestCase):
66+
USER_ID = "@test:test"
67+
OTHER_USER_ID = "@user:test"
68+
69+
def _calculate_room_name(
70+
self,
71+
events: StateMap[dict],
72+
user_id: str = "",
73+
fallback_to_members: bool = True,
74+
fallback_to_single_member: bool = True,
75+
):
76+
# This isn't 100% accurate, but works with MockDataStore.
77+
room_state_ids = {k[0]: k[0] for k in events}
78+
79+
return self.get_success(
80+
calculate_room_name(
81+
MockDataStore(events),
82+
room_state_ids,
83+
user_id or self.USER_ID,
84+
fallback_to_members,
85+
fallback_to_single_member,
86+
)
87+
)
88+
89+
def test_name(self):
90+
"""A room name event should be used."""
91+
events = [
92+
((EventTypes.Name, ""), {"name": "test-name"}),
93+
]
94+
self.assertEqual("test-name", self._calculate_room_name(events))
95+
96+
# Check if the event content has garbage.
97+
events = [((EventTypes.Name, ""), {"foo": 1})]
98+
self.assertEqual("Empty Room", self._calculate_room_name(events))
99+
100+
events = [((EventTypes.Name, ""), {"name": 1})]
101+
self.assertEqual(1, self._calculate_room_name(events))
102+
103+
def test_canonical_alias(self):
104+
"""An canonical alias should be used."""
105+
events = [
106+
((EventTypes.CanonicalAlias, ""), {"alias": "#test-name:test"}),
107+
]
108+
self.assertEqual("#test-name:test", self._calculate_room_name(events))
109+
110+
# Check if the event content has garbage.
111+
events = [((EventTypes.CanonicalAlias, ""), {"foo": 1})]
112+
self.assertEqual("Empty Room", self._calculate_room_name(events))
113+
114+
events = [((EventTypes.CanonicalAlias, ""), {"alias": "test-name"})]
115+
self.assertEqual("Empty Room", self._calculate_room_name(events))
116+
117+
def test_invite(self):
118+
"""An invite has special behaviour."""
119+
events = [
120+
((EventTypes.Member, self.USER_ID), {"membership": Membership.INVITE}),
121+
((EventTypes.Member, self.OTHER_USER_ID), {"displayname": "Other User"}),
122+
]
123+
self.assertEqual("Invite from Other User", self._calculate_room_name(events))
124+
self.assertIsNone(
125+
self._calculate_room_name(events, fallback_to_single_member=False)
126+
)
127+
# Ensure this logic is skipped if we don't fallback to members.
128+
self.assertIsNone(self._calculate_room_name(events, fallback_to_members=False))
129+
130+
# Check if the event content has garbage.
131+
events = [
132+
((EventTypes.Member, self.USER_ID), {"membership": Membership.INVITE}),
133+
((EventTypes.Member, self.OTHER_USER_ID), {"foo": 1}),
134+
]
135+
self.assertEqual("Invite from @user:test", self._calculate_room_name(events))
136+
137+
# No member event for sender.
138+
events = [
139+
((EventTypes.Member, self.USER_ID), {"membership": Membership.INVITE}),
140+
]
141+
self.assertEqual("Room Invite", self._calculate_room_name(events))
142+
143+
def test_no_members(self):
144+
"""Behaviour of an empty room."""
145+
events = []
146+
self.assertEqual("Empty Room", self._calculate_room_name(events))
147+
148+
# Note that events with invalid (or missing) membership are ignored.
149+
events = [
150+
((EventTypes.Member, self.OTHER_USER_ID), {"foo": 1}),
151+
((EventTypes.Member, "@foo:test"), {"membership": "foo"}),
152+
]
153+
self.assertEqual("Empty Room", self._calculate_room_name(events))
154+
155+
def test_no_other_members(self):
156+
"""Behaviour of a room with no other members in it."""
157+
events = [
158+
(
159+
(EventTypes.Member, self.USER_ID),
160+
{"membership": Membership.JOIN, "displayname": "Me"},
161+
),
162+
]
163+
self.assertEqual("Me", self._calculate_room_name(events))
164+
165+
# Check if the event content has no displayname.
166+
events = [
167+
((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}),
168+
]
169+
self.assertEqual("@test:test", self._calculate_room_name(events))
170+
171+
# 3pid invite, use the other user (who is set as the sender).
172+
events = [
173+
((EventTypes.Member, self.OTHER_USER_ID), {"membership": Membership.JOIN}),
174+
]
175+
self.assertEqual(
176+
"nobody", self._calculate_room_name(events, user_id=self.OTHER_USER_ID)
177+
)
178+
179+
events = [
180+
((EventTypes.Member, self.OTHER_USER_ID), {"membership": Membership.JOIN}),
181+
((EventTypes.ThirdPartyInvite, self.OTHER_USER_ID), {}),
182+
]
183+
self.assertEqual(
184+
"Inviting email address",
185+
self._calculate_room_name(events, user_id=self.OTHER_USER_ID),
186+
)
187+
188+
def test_one_other_member(self):
189+
"""Behaviour of a room with a single other member."""
190+
events = [
191+
((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}),
192+
(
193+
(EventTypes.Member, self.OTHER_USER_ID),
194+
{"membership": Membership.JOIN, "displayname": "Other User"},
195+
),
196+
]
197+
self.assertEqual("Other User", self._calculate_room_name(events))
198+
self.assertIsNone(
199+
self._calculate_room_name(events, fallback_to_single_member=False)
200+
)
201+
202+
# Check if the event content has no displayname and is an invite.
203+
events = [
204+
((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}),
205+
(
206+
(EventTypes.Member, self.OTHER_USER_ID),
207+
{"membership": Membership.INVITE},
208+
),
209+
]
210+
self.assertEqual("@user:test", self._calculate_room_name(events))
211+
212+
def test_other_members(self):
213+
"""Behaviour of a room with multiple other members."""
214+
# Two other members.
215+
events = [
216+
((EventTypes.Member, self.USER_ID), {"membership": Membership.JOIN}),
217+
(
218+
(EventTypes.Member, self.OTHER_USER_ID),
219+
{"membership": Membership.JOIN, "displayname": "Other User"},
220+
),
221+
((EventTypes.Member, "@foo:test"), {"membership": Membership.JOIN}),
222+
]
223+
self.assertEqual("Other User and @foo:test", self._calculate_room_name(events))
224+
225+
# Three or more other members.
226+
events.append(
227+
((EventTypes.Member, "@fourth:test"), {"membership": Membership.INVITE})
228+
)
229+
self.assertEqual("Other User and 2 others", self._calculate_room_name(events))

tests/push/test_push_rule_evaluator.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def _get_evaluator(self, content):
2929
"type": "m.room.history_visibility",
3030
"sender": "@user:test",
3131
"state_key": "",
32-
"room_id": "@room:test",
32+
"room_id": "#room:test",
3333
"content": content,
3434
},
3535
RoomVersions.V1,

0 commit comments

Comments
 (0)