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

Prevent local user profiles leaking when a "per-room profile" is changed #10695

Closed
wants to merge 48 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
2b42abb
Fix passing `None` to DB helpers
Aug 25, 2021
ef0a9c5
Misc refactors
Aug 25, 2021
038a9c5
changelog stub
Aug 25, 2021
9366436
Docstrings, method reorder and rename
Aug 31, 2021
00f5809
Always load local users into user_directory
Aug 31, 2021
1819999
Remove unnecessary `set()` construction
Aug 31, 2021
bf97497
Only update remote users dir. entries from events
Aug 31, 2021
b348f0b
Refactor to remove a level of indentation
Aug 31, 2021
13d6b6e
test_profile: register a user, not just a profile
Aug 31, 2021
0d0f033
Use test-level "register_user", not the store
Aug 31, 2021
cca4d10
Missed an await GRR
Aug 31, 2021
137ecc8
Register user in RoomJoinRatelimitTestCase
Sep 1, 2021
dc25098
Need the admin servlet to register_user
Sep 1, 2021
90dcd65
Fix test: handler assumes lowercase username
Sep 1, 2021
d99eedb
Grrrrrr lint
Sep 1, 2021
ee72d78
Rename to clarify local versus remote name changes
Sep 2, 2021
61d3e88
Don't leak local users' per-room names on join
Sep 2, 2021
d643d14
Update synapse/handlers/user_directory.py
Sep 2, 2021
e77bbb2
Changelog
Sep 2, 2021
e0b5bde
Method rename again
Sep 2, 2021
3e547cb
Pull out the loop from _handle_deltas
Sep 2, 2021
6f25133
Exclude support users early
Sep 2, 2021
0933f33
Exclude events from app_services users early
Sep 2, 2021
b5b87ae
Handle `joined is MatchChange.now_false` in 1 spot
Sep 2, 2021
c9a099a
Test case for making a room public
Sep 2, 2021
4bde6b1
Remove redundant comment
Sep 2, 2021
8974d33
Only remove remote users from dir
Sep 2, 2021
9272ed9
Split apart _handle_add_user
Sep 2, 2021
a1bafa3
Lint
Sep 2, 2021
a75a3a1
Linttttttt
Sep 2, 2021
ea3037b
Remove unused arg from rem. profile change handler
Sep 3, 2021
f560472
Inline remote user joining room
Sep 3, 2021
359de94
Comment where we still have work TODO
Sep 3, 2021
474c212
Typing in storage module for user_directory
Sep 3, 2021
38bbde8
Comments about leakages
Sep 3, 2021
a3c023d
db user dir: pull out processing a room to method
Sep 3, 2021
90794cb
Exit early if process_single_room if not in room
Sep 3, 2021
e90524c
Describe data model in docs
Sep 3, 2021
bc9780f
Additional comments on the bg process
Sep 3, 2021
bbacf47
Linttttttt
Sep 3, 2021
6cca7c6
Update changelog
Sep 3, 2021
d50b492
Rationale for synapse test, not complement
Sep 3, 2021
0a217b9
Oliver's suggestions from code review.
Sep 3, 2021
9c4b7ef
Fixup docs as per review
Sep 3, 2021
8662e5f
Fix coment and extra check, as per review
Sep 3, 2021
8269250
Improve docstring for _handle_remove_user
Sep 3, 2021
401c1e8
Fix reactivated users not being added to directory (#10762)
Sep 8, 2021
2b0d714
Revert "Fix reactivated users not being added to directory (#10762)"
Sep 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/10695.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix leaking per-room nicknames and avatars to the user directory for local users when they update their per-room nickname or avatar.
29 changes: 29 additions & 0 deletions docs/user_directory.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,32 @@ DB corruption) get stale or out of sync. If this happens, for now the
solution to fix it is to execute the SQL [here](https://github.com/matrix-org/synapse/blob/master/synapse/storage/schema/main/delta/53/user_dir_populate.sql)
and then restart synapse. This should then start a background task to
flush the current tables and regenerate the directory.

Data model
----------

There are five relevant tables:

* `user_directory`. This contains the user_id, display name and avatar we'll
return when you search the directory.
- Because there's only one directory entry per user, it's important that we only
ever put publicly visible names here. Otherwise we might leak a private
nickname or avatar used in a private room.
- Indexed on rooms. Indexed on users.

* `user_directory_search`. To be joined to `user_directory`. It contains an extra
column that enables full text search based on user ids and display names.
Different schemas for SQLite and Postgres with different code paths to match.
- Indexed on the full text search data. Indexed on users.

* `user_directory_stream_pos`. When the initial background update to populate
the directory is complete, we record a stream position here. This indicates
that synapse should now listen for room changes and incrementally update
the directory where necessary.

* `users_in_public_rooms`. Contains associations between users and the public rooms they're in.
Used to determine which users are in public rooms and should be publicly visible in the directory.

* `users_who_share_private_rooms`. Rows are triples `(L, M, room id)` where `L`
is a local user and `M` is a local or remote user. `L` and `M` should be
different, but this isn't enforced by a constraint.
11 changes: 4 additions & 7 deletions synapse/handlers/deactivate_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ async def deactivate_account(
await self.store.add_user_pending_deactivation(user_id)

# delete from user directory
await self.user_directory_handler.handle_user_deactivated(user_id)
await self.user_directory_handler.handle_local_user_deactivated(user_id)

# Mark the user as erased, if they asked for that
if erase_data:
Expand Down Expand Up @@ -248,7 +248,7 @@ async def activate_account(self, user_id: str) -> None:
This marks the user as active and not erased in the database, but does
not attempt to rejoin rooms, re-add threepids, etc.

If enabled, the user will be re-added to the user directory.
The user will be re-added to the user directory.

The user will also need a password hash set to actually login.

Expand All @@ -257,11 +257,8 @@ async def activate_account(self, user_id: str) -> None:
"""
# Add the user to the directory, if necessary.
user = UserID.from_string(user_id)
if self.hs.config.user_directory_search_all_users:
profile = await self.store.get_profileinfo(user.localpart)
await self.user_directory_handler.handle_local_profile_change(
user_id, profile
)
profile = await self.store.get_profileinfo(user.localpart)
await self.user_directory_handler.handle_local_profile_change(user_id, profile)

# Ensure the user is not marked as erased.
await self.store.mark_user_not_erased(user_id)
Expand Down
18 changes: 8 additions & 10 deletions synapse/handlers/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,10 @@ async def set_displayname(
target_user.localpart, displayname_to_set
)

if self.hs.config.user_directory_search_all_users:
profile = await self.store.get_profileinfo(target_user.localpart)
await self.user_directory_handler.handle_local_profile_change(
target_user.to_string(), profile
)
profile = await self.store.get_profileinfo(target_user.localpart)
await self.user_directory_handler.handle_local_profile_change(
target_user.to_string(), profile
)

await self._update_join_states(requester, target_user)

Expand Down Expand Up @@ -300,11 +299,10 @@ async def set_avatar_url(
target_user.localpart, avatar_url_to_set
)

if self.hs.config.user_directory_search_all_users:
profile = await self.store.get_profileinfo(target_user.localpart)
await self.user_directory_handler.handle_local_profile_change(
target_user.to_string(), profile
)
profile = await self.store.get_profileinfo(target_user.localpart)
await self.user_directory_handler.handle_local_profile_change(
target_user.to_string(), profile
)

await self._update_join_states(requester, target_user)

Expand Down
9 changes: 4 additions & 5 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,10 @@ async def register_user(
shadow_banned=shadow_banned,
)

if self.hs.config.user_directory_search_all_users:
profile = await self.store.get_profileinfo(localpart)
await self.user_directory_handler.handle_local_profile_change(
user_id, profile
)
profile = await self.store.get_profileinfo(localpart)
await self.user_directory_handler.handle_local_profile_change(
user_id, profile
)

else:
# autogen a sequential user ID
Expand Down
23 changes: 12 additions & 11 deletions synapse/handlers/state_deltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import logging
from enum import Enum, auto
from typing import TYPE_CHECKING, Optional

if TYPE_CHECKING:
Expand All @@ -21,6 +22,12 @@
logger = logging.getLogger(__name__)


class MatchChange(Enum):
no_change = auto()
now_true = auto()
now_false = auto()


class StateDeltasHandler:
def __init__(self, hs: "HomeServer"):
self.store = hs.get_datastore()
Expand All @@ -31,18 +38,12 @@ async def _get_key_change(
event_id: Optional[str],
key_name: str,
public_value: str,
) -> Optional[bool]:
) -> MatchChange:
"""Given two events check if the `key_name` field in content changed
from not matching `public_value` to doing so.

For example, check if `history_visibility` (`key_name`) changed from
`shared` to `world_readable` (`public_value`).

Returns:
None if the field in the events either both match `public_value`
or if neither do, i.e. there has been no change.
True if it didn't match `public_value` but now does
False if it did match `public_value` but now doesn't
"""
prev_event = None
event = None
Expand All @@ -54,7 +55,7 @@ async def _get_key_change(

if not event and not prev_event:
logger.debug("Neither event exists: %r %r", prev_event_id, event_id)
return None
return MatchChange.no_change

prev_value = None
value = None
Expand All @@ -68,8 +69,8 @@ async def _get_key_change(
logger.debug("prev_value: %r -> value: %r", prev_value, value)

if value == public_value and prev_value != public_value:
return True
return MatchChange.now_true
elif value != public_value and prev_value == public_value:
return False
return MatchChange.now_false
else:
return None
return MatchChange.no_change
Loading