From 528715db9231c2a9b168ba1dbb009fd788827fd5 Mon Sep 17 00:00:00 2001 From: m4us1ne <33946590+m4us1ne@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:40:06 +0200 Subject: [PATCH] Recreate user profile on account reactivation Erasing an account deletes its row from the profiles table. Reactivating the account did not recreate it, leaving the user without a profile row, which broke display name changes and invites. Recreate a blank profile row on reactivation if one is missing, and make create_profile idempotent so reactivating a non-erased user is a no-op. --- changelog.d/19902.bugfix | 2 + synapse/handlers/deactivate_account.py | 3 + synapse/storage/databases/main/profile.py | 8 ++- tests/handlers/test_deactivate_account.py | 67 +++++++++++++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 changelog.d/19902.bugfix diff --git a/changelog.d/19902.bugfix b/changelog.d/19902.bugfix new file mode 100644 index 00000000000..fb5f75c2956 --- /dev/null +++ b/changelog.d/19902.bugfix @@ -0,0 +1,2 @@ +Fix a bug introduced in Synapse v1.150.0 where reactivating a deactivated and erased user did not restore their profile, breaking login, name changes, and invitations. +Contributed by @m4us1ne. diff --git a/synapse/handlers/deactivate_account.py b/synapse/handlers/deactivate_account.py index 538bdaaaf8e..9ec00d55ad3 100644 --- a/synapse/handlers/deactivate_account.py +++ b/synapse/handlers/deactivate_account.py @@ -349,6 +349,9 @@ async def activate_account(self, user_id: str) -> None: # Ensure the user is not marked as erased. await self.store.mark_user_not_erased(user_id) + # The profile row is deleted on erasure, so recreate it if missing. + await self.store.create_profile(user) + # Mark the user as active. await self.store.set_user_deactivated_status(user_id, False) diff --git a/synapse/storage/databases/main/profile.py b/synapse/storage/databases/main/profile.py index 9b787e19a3d..380dc6c0984 100644 --- a/synapse/storage/databases/main/profile.py +++ b/synapse/storage/databases/main/profile.py @@ -293,15 +293,17 @@ async def get_profile_fields(self, user_id: UserID) -> dict[str, str]: async def create_profile(self, user_id: UserID) -> None: """ - Create a blank profile for a user. + Create a blank profile for a user, if one does not already exist. Args: user_id: The user to create the profile for. """ user_localpart = user_id.localpart - await self.db_pool.simple_insert( + await self.db_pool.simple_upsert( table="profiles", - values={"user_id": user_localpart, "full_user_id": user_id.to_string()}, + keyvalues={"user_id": user_localpart}, + values={}, + insertion_values={"full_user_id": user_id.to_string()}, desc="create_profile", ) diff --git a/tests/handlers/test_deactivate_account.py b/tests/handlers/test_deactivate_account.py index 1b749cee1f8..f8b4098c714 100644 --- a/tests/handlers/test_deactivate_account.py +++ b/tests/handlers/test_deactivate_account.py @@ -483,3 +483,70 @@ def test_rooms_forgotten_upon_deactivation(self) -> None: # Validate that the created room is forgotten self.assertTrue(room_id in forgotten_rooms) + + def _get_profile_row(self) -> str | None: + """Return the `user_id` of `self.user`'s `profiles` row, or None if absent.""" + return self.get_success( + self._store.db_pool.simple_select_one_onecol( + table="profiles", + keyvalues={"full_user_id": self.user}, + retcol="user_id", + allow_none=True, + desc="_get_profile_row", + ) + ) + + def test_reactivation_recreates_profile(self) -> None: + """ + Tests that reactivating an erased user recreates their profile row, so + that subsequent profile operations work. + """ + self.assertIsNotNone(self._get_profile_row()) + + # Erasure deletes the whole profiles row. + self._deactivate_my_account() + self.assertIsNone(self._get_profile_row()) + + # Reactivating recreates a blank profile row. + deactivate_handler = self.hs.get_deactivate_account_handler() + self.get_success(deactivate_handler.activate_account(self.user)) + self.assertIsNotNone(self._get_profile_row()) + + # Setting a display name now works again. + user = UserID.from_string(self.user) + self.get_success( + self.hs.get_profile_handler().set_displayname( + user, create_requester(user), "Reactivated", by_admin=True + ) + ) + self.assertEqual( + self.get_success(self._store.get_profile_displayname(user)), + "Reactivated", + ) + + def test_reactivation_without_erasure_keeps_profile(self) -> None: + """ + Reactivating a user whose profile row still exists leaves it untouched. + """ + user = UserID.from_string(self.user) + self.get_success( + self.hs.get_profile_handler().set_displayname( + user, create_requester(user), "Original", by_admin=True + ) + ) + + # Deactivate without erasure, so the profile row is left intact. + deactivate_handler = self.hs.get_deactivate_account_handler() + self.get_success( + deactivate_handler.deactivate_account( + self.user, erase_data=False, requester=create_requester(user) + ) + ) + self.assertIsNotNone(self._get_profile_row()) + + # Reactivating must not raise despite the existing profile row. + self.get_success(deactivate_handler.activate_account(self.user)) + self.assertEqual( + self.get_success(self._store.get_profile_displayname(user)), + "Original", + )