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