Allow accounts to be re-activated from the admin APIs. (#7847)

This commit is contained in:
Patrick Cloke 2020-07-15 11:00:21 -04:00 committed by GitHub
parent f13061d515
commit 8c7d0f163d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 90 additions and 22 deletions

1
changelog.d/7847.feature Normal file
View file

@ -0,0 +1 @@
Add the ability to re-activate an account from the admin API.

View file

@ -91,10 +91,14 @@ Body parameters:
- ``admin``, optional, defaults to ``false``. - ``admin``, optional, defaults to ``false``.
- ``deactivated``, optional, defaults to ``false``. - ``deactivated``, optional. If unspecified, deactivation state will be left
unchanged on existing accounts and set to ``false`` for new accounts.
If the user already exists then optional parameters default to the current value. If the user already exists then optional parameters default to the current value.
In order to re-activate an account ``deactivated`` must be set to ``false``. If
users do not login via single-sign-on, a new ``password`` must be provided.
List Accounts List Accounts
============= =============

View file

@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import logging import logging
from typing import Optional
from synapse.api.errors import SynapseError from synapse.api.errors import SynapseError
from synapse.metrics.background_process_metrics import run_as_background_process from synapse.metrics.background_process_metrics import run_as_background_process
@ -45,19 +46,20 @@ class DeactivateAccountHandler(BaseHandler):
self._account_validity_enabled = hs.config.account_validity.enabled self._account_validity_enabled = hs.config.account_validity.enabled
async def deactivate_account(self, user_id, erase_data, id_server=None): async def deactivate_account(
self, user_id: str, erase_data: bool, id_server: Optional[str] = None
) -> bool:
"""Deactivate a user's account """Deactivate a user's account
Args: Args:
user_id (str): ID of user to be deactivated user_id: ID of user to be deactivated
erase_data (bool): whether to GDPR-erase the user's data erase_data: whether to GDPR-erase the user's data
id_server (str|None): Use the given identity server when unbinding id_server: Use the given identity server when unbinding
any threepids. If None then will attempt to unbind using the any threepids. If None then will attempt to unbind using the
identity server specified when binding (if known). identity server specified when binding (if known).
Returns: Returns:
Deferred[bool]: True if identity server supports removing True if identity server supports removing threepids, otherwise False.
threepids, otherwise False.
""" """
# FIXME: Theoretically there is a race here wherein user resets # FIXME: Theoretically there is a race here wherein user resets
# password using threepid. # password using threepid.
@ -134,11 +136,11 @@ class DeactivateAccountHandler(BaseHandler):
return identity_server_supports_unbinding return identity_server_supports_unbinding
async def _reject_pending_invites_for_user(self, user_id): async def _reject_pending_invites_for_user(self, user_id: str):
"""Reject pending invites addressed to a given user ID. """Reject pending invites addressed to a given user ID.
Args: Args:
user_id (str): The user ID to reject pending invites for. user_id: The user ID to reject pending invites for.
""" """
user = UserID.from_string(user_id) user = UserID.from_string(user_id)
pending_invites = await self.store.get_invited_rooms_for_local_user(user_id) pending_invites = await self.store.get_invited_rooms_for_local_user(user_id)
@ -166,22 +168,16 @@ class DeactivateAccountHandler(BaseHandler):
room.room_id, room.room_id,
) )
def _start_user_parting(self): def _start_user_parting(self) -> None:
""" """
Start the process that goes through the table of users Start the process that goes through the table of users
pending deactivation, if it isn't already running. pending deactivation, if it isn't already running.
Returns:
None
""" """
if not self._user_parter_running: if not self._user_parter_running:
run_as_background_process("user_parter_loop", self._user_parter_loop) run_as_background_process("user_parter_loop", self._user_parter_loop)
async def _user_parter_loop(self): async def _user_parter_loop(self) -> None:
"""Loop that parts deactivated users from rooms """Loop that parts deactivated users from rooms
Returns:
None
""" """
self._user_parter_running = True self._user_parter_running = True
logger.info("Starting user parter") logger.info("Starting user parter")
@ -198,11 +194,8 @@ class DeactivateAccountHandler(BaseHandler):
finally: finally:
self._user_parter_running = False self._user_parter_running = False
async def _part_user(self, user_id): async def _part_user(self, user_id: str) -> None:
"""Causes the given user_id to leave all the rooms they're joined to """Causes the given user_id to leave all the rooms they're joined to
Returns:
None
""" """
user = UserID.from_string(user_id) user = UserID.from_string(user_id)
@ -224,3 +217,18 @@ class DeactivateAccountHandler(BaseHandler):
user_id, user_id,
room_id, room_id,
) )
async def activate_account(self, user_id: str) -> None:
"""
Activate an account that was previously deactivated.
This simply marks the user as activate in the database and does not
attempt to rejoin rooms, re-add threepids, etc.
The user will also need a password hash set to actually login.
Args:
user_id: ID of user to be deactivated
"""
# Mark the user as activate.
await self.store.set_user_deactivated_status(user_id, False)

View file

@ -239,6 +239,15 @@ class UserRestServletV2(RestServlet):
await self.deactivate_account_handler.deactivate_account( await self.deactivate_account_handler.deactivate_account(
target_user.to_string(), False target_user.to_string(), False
) )
elif not deactivate and user["deactivated"]:
if "password" not in body:
raise SynapseError(
400, "Must provide a password to re-activate an account."
)
await self.deactivate_account_handler.activate_account(
target_user.to_string()
)
user = await self.admin_handler.get_user(target_user) user = await self.admin_handler.get_user(target_user)
return 200, user return 200, user
@ -254,7 +263,6 @@ class UserRestServletV2(RestServlet):
admin = body.get("admin", None) admin = body.get("admin", None)
user_type = body.get("user_type", None) user_type = body.get("user_type", None)
displayname = body.get("displayname", None) displayname = body.get("displayname", None)
threepids = body.get("threepids", None)
if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES: if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
raise SynapseError(400, "Invalid user type") raise SynapseError(400, "Invalid user type")

View file

@ -857,6 +857,53 @@ class UserRestTestCase(unittest.HomeserverTestCase):
self.assertEqual("@user:test", channel.json_body["name"]) self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(True, channel.json_body["deactivated"]) self.assertEqual(True, channel.json_body["deactivated"])
def test_reactivate_user(self):
"""
Test reactivating another user.
"""
# Deactivate the user.
request, channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content=json.dumps({"deactivated": True}).encode(encoding="utf_8"),
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# Attempt to reactivate the user (without a password).
request, channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content=json.dumps({"deactivated": False}).encode(encoding="utf_8"),
)
self.render(request)
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
# Reactivate the user.
request, channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content=json.dumps({"deactivated": False, "password": "foo"}).encode(
encoding="utf_8"
),
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# Get user
request, channel = self.make_request(
"GET", self.url_other_user, access_token=self.admin_user_tok,
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(False, channel.json_body["deactivated"])
def test_set_user_as_admin(self): def test_set_user_as_admin(self):
""" """
Test setting the admin flag on a user. Test setting the admin flag on a user.