1# Copyright 2017, 2018 New Vector Ltd 2# Copyright 2019 The Matrix.org Foundation C.I.C. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15import logging 16from typing import TYPE_CHECKING, Optional 17 18from synapse.api.errors import SynapseError 19from synapse.metrics.background_process_metrics import run_as_background_process 20from synapse.types import Requester, UserID, create_requester 21 22if TYPE_CHECKING: 23 from synapse.server import HomeServer 24 25logger = logging.getLogger(__name__) 26 27 28class DeactivateAccountHandler: 29 """Handler which deals with deactivating user accounts.""" 30 31 def __init__(self, hs: "HomeServer"): 32 self.store = hs.get_datastore() 33 self.hs = hs 34 self._auth_handler = hs.get_auth_handler() 35 self._device_handler = hs.get_device_handler() 36 self._room_member_handler = hs.get_room_member_handler() 37 self._identity_handler = hs.get_identity_handler() 38 self._profile_handler = hs.get_profile_handler() 39 self.user_directory_handler = hs.get_user_directory_handler() 40 self._server_name = hs.hostname 41 42 # Flag that indicates whether the process to part users from rooms is running 43 self._user_parter_running = False 44 45 # Start the user parter loop so it can resume parting users from rooms where 46 # it left off (if it has work left to do). 47 if hs.config.worker.run_background_tasks: 48 hs.get_reactor().callWhenRunning(self._start_user_parting) 49 50 self._account_validity_enabled = ( 51 hs.config.account_validity.account_validity_enabled 52 ) 53 54 async def deactivate_account( 55 self, 56 user_id: str, 57 erase_data: bool, 58 requester: Requester, 59 id_server: Optional[str] = None, 60 by_admin: bool = False, 61 ) -> bool: 62 """Deactivate a user's account 63 64 Args: 65 user_id: ID of user to be deactivated 66 erase_data: whether to GDPR-erase the user's data 67 requester: The user attempting to make this change. 68 id_server: Use the given identity server when unbinding 69 any threepids. If None then will attempt to unbind using the 70 identity server specified when binding (if known). 71 by_admin: Whether this change was made by an administrator. 72 73 Returns: 74 True if identity server supports removing threepids, otherwise False. 75 """ 76 # FIXME: Theoretically there is a race here wherein user resets 77 # password using threepid. 78 79 # delete threepids first. We remove these from the IS so if this fails, 80 # leave the user still active so they can try again. 81 # Ideally we would prevent password resets and then do this in the 82 # background thread. 83 84 # This will be set to false if the identity server doesn't support 85 # unbinding 86 identity_server_supports_unbinding = True 87 88 # Retrieve the 3PIDs this user has bound to an identity server 89 threepids = await self.store.user_get_bound_threepids(user_id) 90 91 for threepid in threepids: 92 try: 93 result = await self._identity_handler.try_unbind_threepid( 94 user_id, 95 { 96 "medium": threepid["medium"], 97 "address": threepid["address"], 98 "id_server": id_server, 99 }, 100 ) 101 identity_server_supports_unbinding &= result 102 except Exception: 103 # Do we want this to be a fatal error or should we carry on? 104 logger.exception("Failed to remove threepid from ID server") 105 raise SynapseError(400, "Failed to remove threepid from ID server") 106 await self.store.user_delete_threepid( 107 user_id, threepid["medium"], threepid["address"] 108 ) 109 110 # Remove all 3PIDs this user has bound to the homeserver 111 await self.store.user_delete_threepids(user_id) 112 113 # delete any devices belonging to the user, which will also 114 # delete corresponding access tokens. 115 await self._device_handler.delete_all_devices_for_user(user_id) 116 # then delete any remaining access tokens which weren't associated with 117 # a device. 118 await self._auth_handler.delete_access_tokens_for_user(user_id) 119 120 await self.store.user_set_password_hash(user_id, None) 121 122 # Most of the pushers will have been deleted when we logged out the 123 # associated devices above, but we still need to delete pushers not 124 # associated with devices, e.g. email pushers. 125 await self.store.delete_all_pushers_for_user(user_id) 126 127 # Add the user to a table of users pending deactivation (ie. 128 # removal from all the rooms they're a member of) 129 await self.store.add_user_pending_deactivation(user_id) 130 131 # delete from user directory 132 await self.user_directory_handler.handle_local_user_deactivated(user_id) 133 134 # Mark the user as erased, if they asked for that 135 if erase_data: 136 user = UserID.from_string(user_id) 137 # Remove avatar URL from this user 138 await self._profile_handler.set_avatar_url(user, requester, "", by_admin) 139 # Remove displayname from this user 140 await self._profile_handler.set_displayname(user, requester, "", by_admin) 141 142 logger.info("Marking %s as erased", user_id) 143 await self.store.mark_user_erased(user_id) 144 145 # Now start the process that goes through that list and 146 # parts users from rooms (if it isn't already running) 147 self._start_user_parting() 148 149 # Reject all pending invites for the user, so that the user doesn't show up in the 150 # "invited" section of rooms' members list. 151 await self._reject_pending_invites_for_user(user_id) 152 153 # Remove all information on the user from the account_validity table. 154 if self._account_validity_enabled: 155 await self.store.delete_account_validity_for_user(user_id) 156 157 # Mark the user as deactivated. 158 await self.store.set_user_deactivated_status(user_id, True) 159 160 return identity_server_supports_unbinding 161 162 async def _reject_pending_invites_for_user(self, user_id: str) -> None: 163 """Reject pending invites addressed to a given user ID. 164 165 Args: 166 user_id: The user ID to reject pending invites for. 167 """ 168 user = UserID.from_string(user_id) 169 pending_invites = await self.store.get_invited_rooms_for_local_user(user_id) 170 171 for room in pending_invites: 172 try: 173 await self._room_member_handler.update_membership( 174 create_requester(user, authenticated_entity=self._server_name), 175 user, 176 room.room_id, 177 "leave", 178 ratelimit=False, 179 require_consent=False, 180 ) 181 logger.info( 182 "Rejected invite for deactivated user %r in room %r", 183 user_id, 184 room.room_id, 185 ) 186 except Exception: 187 logger.exception( 188 "Failed to reject invite for user %r in room %r:" 189 " ignoring and continuing", 190 user_id, 191 room.room_id, 192 ) 193 194 def _start_user_parting(self) -> None: 195 """ 196 Start the process that goes through the table of users 197 pending deactivation, if it isn't already running. 198 """ 199 if not self._user_parter_running: 200 run_as_background_process("user_parter_loop", self._user_parter_loop) 201 202 async def _user_parter_loop(self) -> None: 203 """Loop that parts deactivated users from rooms""" 204 self._user_parter_running = True 205 logger.info("Starting user parter") 206 try: 207 while True: 208 user_id = await self.store.get_user_pending_deactivation() 209 if user_id is None: 210 break 211 logger.info("User parter parting %r", user_id) 212 await self._part_user(user_id) 213 await self.store.del_user_pending_deactivation(user_id) 214 logger.info("User parter finished parting %r", user_id) 215 logger.info("User parter finished: stopping") 216 finally: 217 self._user_parter_running = False 218 219 async def _part_user(self, user_id: str) -> None: 220 """Causes the given user_id to leave all the rooms they're joined to""" 221 user = UserID.from_string(user_id) 222 223 rooms_for_user = await self.store.get_rooms_for_user(user_id) 224 for room_id in rooms_for_user: 225 logger.info("User parter parting %r from %r", user_id, room_id) 226 try: 227 await self._room_member_handler.update_membership( 228 create_requester(user, authenticated_entity=self._server_name), 229 user, 230 room_id, 231 "leave", 232 ratelimit=False, 233 require_consent=False, 234 ) 235 except Exception: 236 logger.exception( 237 "Failed to part user %r from room %r: ignoring and continuing", 238 user_id, 239 room_id, 240 ) 241 242 async def activate_account(self, user_id: str) -> None: 243 """ 244 Activate an account that was previously deactivated. 245 246 This marks the user as active and not erased in the database, but does 247 not attempt to rejoin rooms, re-add threepids, etc. 248 249 If enabled, the user will be re-added to the user directory. 250 251 The user will also need a password hash set to actually login. 252 253 Args: 254 user_id: ID of user to be re-activated 255 """ 256 user = UserID.from_string(user_id) 257 258 # Ensure the user is not marked as erased. 259 await self.store.mark_user_not_erased(user_id) 260 261 # Mark the user as active. 262 await self.store.set_user_deactivated_status(user_id, False) 263 264 # Add the user to the directory, if necessary. Note that 265 # this must be done after the user is re-activated, because 266 # deactivated users are excluded from the user directory. 267 profile = await self.store.get_profileinfo(user.localpart) 268 await self.user_directory_handler.handle_local_profile_change(user_id, profile) 269