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

Commit e09be0c

Browse files
David Robertsonclokep
andauthored
Correctly exclude users when making a room public or private (#11075)
Co-authored-by: Patrick Cloke <[email protected]>
1 parent 5573133 commit e09be0c

File tree

4 files changed

+148
-83
lines changed

4 files changed

+148
-83
lines changed

changelog.d/11075.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug where users excluded from the user directory were added into the directory if they belonged to a room which became public or private.

synapse/handlers/user_directory.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,14 +266,17 @@ async def _handle_room_publicity_change(
266266
for user_id in users_in_room:
267267
await self.store.remove_user_who_share_room(user_id, room_id)
268268

269-
# Then, re-add them to the tables.
269+
# Then, re-add all remote users and some local users to the tables.
270270
# NOTE: this is not the most efficient method, as _track_user_joined_room sets
271271
# up local_user -> other_user and other_user_whos_local -> local_user,
272272
# which when ran over an entire room, will result in the same values
273273
# being added multiple times. The batching upserts shouldn't make this
274274
# too bad, though.
275275
for user_id in users_in_room:
276-
await self._track_user_joined_room(room_id, user_id)
276+
if not self.is_mine_id(
277+
user_id
278+
) or await self.store.should_include_local_user_in_dir(user_id):
279+
await self._track_user_joined_room(room_id, user_id)
277280

278281
async def _handle_room_membership_event(
279282
self,
@@ -364,8 +367,8 @@ async def _track_user_joined_room(self, room_id: str, user_id: str) -> None:
364367
"""Someone's just joined a room. Update `users_in_public_rooms` or
365368
`users_who_share_private_rooms` as appropriate.
366369
367-
The caller is responsible for ensuring that the given user is not excluded
368-
from the user directory.
370+
The caller is responsible for ensuring that the given user should be
371+
included in the user directory.
369372
"""
370373
is_public = await self.store.is_room_world_readable_or_publicly_joinable(
371374
room_id

tests/handlers/test_user_directory.py

Lines changed: 107 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -109,18 +109,14 @@ def test_normal_user_pair(self) -> None:
109109
tok=alice_token,
110110
)
111111

112-
users = self.get_success(self.user_dir_helper.get_users_in_user_directory())
113-
in_public = self.get_success(self.user_dir_helper.get_users_in_public_rooms())
114-
in_private = self.get_success(
115-
self.user_dir_helper.get_users_who_share_private_rooms()
112+
# The user directory should reflect the room memberships above.
113+
users, in_public, in_private = self.get_success(
114+
self.user_dir_helper.get_tables()
116115
)
117-
118116
self.assertEqual(users, {alice, bob})
117+
self.assertEqual(in_public, {(alice, public), (bob, public), (alice, public2)})
119118
self.assertEqual(
120-
set(in_public), {(alice, public), (bob, public), (alice, public2)}
121-
)
122-
self.assertEqual(
123-
self.user_dir_helper._compress_shared(in_private),
119+
in_private,
124120
{(alice, bob, private), (bob, alice, private)},
125121
)
126122

@@ -209,6 +205,88 @@ def test_user_not_in_users_table(self) -> None:
209205
in_public = self.get_success(self.user_dir_helper.get_users_in_public_rooms())
210206
self.assertEqual(set(in_public), {(user1, room), (user2, room)})
211207

208+
def test_excludes_users_when_making_room_public(self) -> None:
209+
# Create a regular user and a support user.
210+
alice = self.register_user("alice", "pass")
211+
alice_token = self.login(alice, "pass")
212+
support = "@support1:test"
213+
self.get_success(
214+
self.store.register_user(
215+
user_id=support, password_hash=None, user_type=UserTypes.SUPPORT
216+
)
217+
)
218+
219+
# Make a public and private room containing Alice and the support user
220+
public, initially_private = self._create_rooms_and_inject_memberships(
221+
alice, alice_token, support
222+
)
223+
self._check_only_one_user_in_directory(alice, public)
224+
225+
# Alice makes the private room public.
226+
self.helper.send_state(
227+
initially_private,
228+
"m.room.join_rules",
229+
{"join_rule": "public"},
230+
tok=alice_token,
231+
)
232+
233+
users, in_public, in_private = self.get_success(
234+
self.user_dir_helper.get_tables()
235+
)
236+
self.assertEqual(users, {alice})
237+
self.assertEqual(in_public, {(alice, public), (alice, initially_private)})
238+
self.assertEqual(in_private, set())
239+
240+
def test_switching_from_private_to_public_to_private(self) -> None:
241+
"""Check we update the room sharing tables when switching a room
242+
from private to public, then back again to private."""
243+
# Alice and Bob share a private room.
244+
alice = self.register_user("alice", "pass")
245+
alice_token = self.login(alice, "pass")
246+
bob = self.register_user("bob", "pass")
247+
bob_token = self.login(bob, "pass")
248+
room = self.helper.create_room_as(alice, is_public=False, tok=alice_token)
249+
self.helper.invite(room, alice, bob, tok=alice_token)
250+
self.helper.join(room, bob, tok=bob_token)
251+
252+
# The user directory should reflect this.
253+
def check_user_dir_for_private_room() -> None:
254+
users, in_public, in_private = self.get_success(
255+
self.user_dir_helper.get_tables()
256+
)
257+
self.assertEqual(users, {alice, bob})
258+
self.assertEqual(in_public, set())
259+
self.assertEqual(in_private, {(alice, bob, room), (bob, alice, room)})
260+
261+
check_user_dir_for_private_room()
262+
263+
# Alice makes the room public.
264+
self.helper.send_state(
265+
room,
266+
"m.room.join_rules",
267+
{"join_rule": "public"},
268+
tok=alice_token,
269+
)
270+
271+
# The user directory should be updated accordingly
272+
users, in_public, in_private = self.get_success(
273+
self.user_dir_helper.get_tables()
274+
)
275+
self.assertEqual(users, {alice, bob})
276+
self.assertEqual(in_public, {(alice, room), (bob, room)})
277+
self.assertEqual(in_private, set())
278+
279+
# Alice makes the room private.
280+
self.helper.send_state(
281+
room,
282+
"m.room.join_rules",
283+
{"join_rule": "invite"},
284+
tok=alice_token,
285+
)
286+
287+
# The user directory should be updated accordingly
288+
check_user_dir_for_private_room()
289+
212290
def _create_rooms_and_inject_memberships(
213291
self, creator: str, token: str, joiner: str
214292
) -> Tuple[str, str]:
@@ -232,15 +310,18 @@ def _create_rooms_and_inject_memberships(
232310
return public_room, private_room
233311

234312
def _check_only_one_user_in_directory(self, user: str, public: str) -> None:
235-
users = self.get_success(self.user_dir_helper.get_users_in_user_directory())
236-
in_public = self.get_success(self.user_dir_helper.get_users_in_public_rooms())
237-
in_private = self.get_success(
238-
self.user_dir_helper.get_users_who_share_private_rooms()
239-
)
313+
"""Check that the user directory DB tables show that:
240314
315+
- only one user is in the user directory
316+
- they belong to exactly one public room
317+
- they don't share a private room with anyone.
318+
"""
319+
users, in_public, in_private = self.get_success(
320+
self.user_dir_helper.get_tables()
321+
)
241322
self.assertEqual(users, {user})
242-
self.assertEqual(set(in_public), {(user, public)})
243-
self.assertEqual(in_private, [])
323+
self.assertEqual(in_public, {(user, public)})
324+
self.assertEqual(in_private, set())
244325

245326
def test_handle_local_profile_change_with_support_user(self) -> None:
246327
support_user_id = "@support:test"
@@ -581,11 +662,8 @@ def test_private_room(self) -> None:
581662
self.user_dir_helper.get_users_in_public_rooms()
582663
)
583664

584-
self.assertEqual(
585-
self.user_dir_helper._compress_shared(shares_private),
586-
{(u1, u2, room), (u2, u1, room)},
587-
)
588-
self.assertEqual(public_users, [])
665+
self.assertEqual(shares_private, {(u1, u2, room), (u2, u1, room)})
666+
self.assertEqual(public_users, set())
589667

590668
# We get one search result when searching for user2 by user1.
591669
s = self.get_success(self.handler.search_users(u1, "user2", 10))
@@ -610,8 +688,8 @@ def test_private_room(self) -> None:
610688
self.user_dir_helper.get_users_in_public_rooms()
611689
)
612690

613-
self.assertEqual(self.user_dir_helper._compress_shared(shares_private), set())
614-
self.assertEqual(public_users, [])
691+
self.assertEqual(shares_private, set())
692+
self.assertEqual(public_users, set())
615693

616694
# User1 now gets no search results for any of the other users.
617695
s = self.get_success(self.handler.search_users(u1, "user2", 10))
@@ -645,11 +723,8 @@ def test_spam_checker(self) -> None:
645723
self.user_dir_helper.get_users_in_public_rooms()
646724
)
647725

648-
self.assertEqual(
649-
self.user_dir_helper._compress_shared(shares_private),
650-
{(u1, u2, room), (u2, u1, room)},
651-
)
652-
self.assertEqual(public_users, [])
726+
self.assertEqual(shares_private, {(u1, u2, room), (u2, u1, room)})
727+
self.assertEqual(public_users, set())
653728

654729
# We get one search result when searching for user2 by user1.
655730
s = self.get_success(self.handler.search_users(u1, "user2", 10))
@@ -704,11 +779,8 @@ def test_legacy_spam_checker(self) -> None:
704779
self.user_dir_helper.get_users_in_public_rooms()
705780
)
706781

707-
self.assertEqual(
708-
self.user_dir_helper._compress_shared(shares_private),
709-
{(u1, u2, room), (u2, u1, room)},
710-
)
711-
self.assertEqual(public_users, [])
782+
self.assertEqual(shares_private, {(u1, u2, room), (u2, u1, room)})
783+
self.assertEqual(public_users, set())
712784

713785
# Configure a spam checker.
714786
spam_checker = self.hs.get_spam_checker()
@@ -740,8 +812,8 @@ def test_initial_share_all_users(self) -> None:
740812
)
741813

742814
# No users share rooms
743-
self.assertEqual(public_users, [])
744-
self.assertEqual(self.user_dir_helper._compress_shared(shares_private), set())
815+
self.assertEqual(public_users, set())
816+
self.assertEqual(shares_private, set())
745817

746818
# Despite not sharing a room, search_all_users means we get a search
747819
# result.

tests/storage/test_user_directory.py

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
from typing import Any, Dict, List, Set, Tuple
14+
from typing import Any, Dict, Set, Tuple
1515
from unittest import mock
1616
from unittest.mock import Mock, patch
1717

@@ -42,18 +42,7 @@ class GetUserDirectoryTables:
4242
def __init__(self, store: DataStore):
4343
self.store = store
4444

45-
def _compress_shared(
46-
self, shared: List[Dict[str, str]]
47-
) -> Set[Tuple[str, str, str]]:
48-
"""
49-
Compress a list of users who share rooms dicts to a list of tuples.
50-
"""
51-
r = set()
52-
for i in shared:
53-
r.add((i["user_id"], i["other_user_id"], i["room_id"]))
54-
return r
55-
56-
async def get_users_in_public_rooms(self) -> List[Tuple[str, str]]:
45+
async def get_users_in_public_rooms(self) -> Set[Tuple[str, str]]:
5746
"""Fetch the entire `users_in_public_rooms` table.
5847
5948
Returns a list of tuples (user_id, room_id) where room_id is public and
@@ -63,24 +52,27 @@ async def get_users_in_public_rooms(self) -> List[Tuple[str, str]]:
6352
"users_in_public_rooms", None, ("user_id", "room_id")
6453
)
6554

66-
retval = []
55+
retval = set()
6756
for i in r:
68-
retval.append((i["user_id"], i["room_id"]))
57+
retval.add((i["user_id"], i["room_id"]))
6958
return retval
7059

71-
async def get_users_who_share_private_rooms(self) -> List[Dict[str, str]]:
60+
async def get_users_who_share_private_rooms(self) -> Set[Tuple[str, str, str]]:
7261
"""Fetch the entire `users_who_share_private_rooms` table.
7362
74-
Returns a dict containing "user_id", "other_user_id" and "room_id" keys.
75-
The dicts can be flattened to Tuples with the `_compress_shared` method.
76-
(This seems a little awkward---maybe we could clean this up.)
63+
Returns a set of tuples (user_id, other_user_id, room_id) corresponding
64+
to the rows of `users_who_share_private_rooms`.
7765
"""
7866

79-
return await self.store.db_pool.simple_select_list(
67+
rows = await self.store.db_pool.simple_select_list(
8068
"users_who_share_private_rooms",
8169
None,
8270
["user_id", "other_user_id", "room_id"],
8371
)
72+
rv = set()
73+
for row in rows:
74+
rv.add((row["user_id"], row["other_user_id"], row["room_id"]))
75+
return rv
8476

8577
async def get_users_in_user_directory(self) -> Set[str]:
8678
"""Fetch the set of users in the `user_directory` table.
@@ -113,6 +105,16 @@ async def get_profiles_in_user_directory(self) -> Dict[str, ProfileInfo]:
113105
for row in rows
114106
}
115107

108+
async def get_tables(
109+
self,
110+
) -> Tuple[Set[str], Set[Tuple[str, str]], Set[Tuple[str, str, str]]]:
111+
"""Multiple tests want to inspect these tables, so expose them together."""
112+
return (
113+
await self.get_users_in_user_directory(),
114+
await self.get_users_in_public_rooms(),
115+
await self.get_users_who_share_private_rooms(),
116+
)
117+
116118

117119
class UserDirectoryInitialPopulationTestcase(HomeserverTestCase):
118120
"""Ensure that rebuilding the directory writes the correct data to the DB.
@@ -166,8 +168,8 @@ def _purge_and_rebuild_user_dir(self) -> None:
166168
)
167169

168170
# Nothing updated yet
169-
self.assertEqual(shares_private, [])
170-
self.assertEqual(public_users, [])
171+
self.assertEqual(shares_private, set())
172+
self.assertEqual(public_users, set())
171173

172174
# Ugh, have to reset this flag
173175
self.store.db_pool.updates._all_done = False
@@ -236,24 +238,15 @@ def test_initial(self) -> None:
236238
# Do the initial population of the user directory via the background update
237239
self._purge_and_rebuild_user_dir()
238240

239-
shares_private = self.get_success(
240-
self.user_dir_helper.get_users_who_share_private_rooms()
241-
)
242-
public_users = self.get_success(
243-
self.user_dir_helper.get_users_in_public_rooms()
241+
users, in_public, in_private = self.get_success(
242+
self.user_dir_helper.get_tables()
244243
)
245244

246245
# User 1 and User 2 are in the same public room
247-
self.assertEqual(set(public_users), {(u1, room), (u2, room)})
248-
246+
self.assertEqual(in_public, {(u1, room), (u2, room)})
249247
# User 1 and User 3 share private rooms
250-
self.assertEqual(
251-
self.user_dir_helper._compress_shared(shares_private),
252-
{(u1, u3, private_room), (u3, u1, private_room)},
253-
)
254-
248+
self.assertEqual(in_private, {(u1, u3, private_room), (u3, u1, private_room)})
255249
# All three should have entries in the directory
256-
users = self.get_success(self.user_dir_helper.get_users_in_user_directory())
257250
self.assertEqual(users, {u1, u2, u3})
258251

259252
# The next four tests (test_population_excludes_*) all set up
@@ -289,16 +282,12 @@ def _check_room_sharing_tables(
289282
self, normal_user: str, public_room: str, private_room: str
290283
) -> None:
291284
# After rebuilding the directory, we should only see the normal user.
292-
users = self.get_success(self.user_dir_helper.get_users_in_user_directory())
293-
self.assertEqual(users, {normal_user})
294-
in_public_rooms = self.get_success(
295-
self.user_dir_helper.get_users_in_public_rooms()
285+
users, in_public, in_private = self.get_success(
286+
self.user_dir_helper.get_tables()
296287
)
297-
self.assertEqual(set(in_public_rooms), {(normal_user, public_room)})
298-
in_private_rooms = self.get_success(
299-
self.user_dir_helper.get_users_who_share_private_rooms()
300-
)
301-
self.assertEqual(in_private_rooms, [])
288+
self.assertEqual(users, {normal_user})
289+
self.assertEqual(in_public, {(normal_user, public_room)})
290+
self.assertEqual(in_private, set())
302291

303292
def test_population_excludes_support_user(self) -> None:
304293
# Create a normal and support user.

0 commit comments

Comments
 (0)