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

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

Merged
merged 7 commits into from
Jul 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/7847.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add the ability to re-activate an account from the admin API.
6 changes: 5 additions & 1 deletion docs/admin_api/user_admin_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,14 @@ Body parameters:

- ``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.

clokep marked this conversation as resolved.
Show resolved Hide resolved
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
=============

Expand Down
48 changes: 28 additions & 20 deletions synapse/handlers/deactivate_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from typing import Optional

from synapse.api.errors import SynapseError
from synapse.metrics.background_process_metrics import run_as_background_process
Expand Down Expand Up @@ -45,19 +46,20 @@ def __init__(self, hs):

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

Args:
user_id (str): ID of user to be deactivated
erase_data (bool): whether to GDPR-erase the user's data
id_server (str|None): Use the given identity server when unbinding
user_id: ID of user to be deactivated
erase_data: whether to GDPR-erase the user's data
id_server: Use the given identity server when unbinding
any threepids. If None then will attempt to unbind using the
identity server specified when binding (if known).

Returns:
Deferred[bool]: True if identity server supports removing
threepids, otherwise False.
True if identity server supports removing threepids, otherwise False.
"""
# FIXME: Theoretically there is a race here wherein user resets
# password using threepid.
Expand Down Expand Up @@ -134,11 +136,11 @@ async def deactivate_account(self, user_id, erase_data, id_server=None):

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.

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)
pending_invites = await self.store.get_invited_rooms_for_local_user(user_id)
Expand Down Expand Up @@ -166,22 +168,16 @@ async def _reject_pending_invites_for_user(self, user_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
pending deactivation, if it isn't already running.

Returns:
None
"""
if not self._user_parter_running:
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

Returns:
None
"""
self._user_parter_running = True
logger.info("Starting user parter")
Expand All @@ -198,11 +194,8 @@ async def _user_parter_loop(self):
finally:
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

Returns:
None
"""
user = UserID.from_string(user_id)

Expand All @@ -224,3 +217,18 @@ async def _part_user(self, user_id):
user_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)
10 changes: 9 additions & 1 deletion synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@ async def on_PUT(self, request, user_id):
await self.deactivate_account_handler.deactivate_account(
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)
return 200, user
Expand All @@ -254,7 +263,6 @@ async def on_PUT(self, request, user_id):
admin = body.get("admin", None)
user_type = body.get("user_type", 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:
raise SynapseError(400, "Invalid user type")
Expand Down
47 changes: 47 additions & 0 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,53 @@ def test_deactivate_user(self):
self.assertEqual("@user:test", channel.json_body["name"])
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):
"""
Test setting the admin flag on a user.
Expand Down