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

Add delete group admin API #5002

Merged
merged 4 commits into from
Apr 4, 2019
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/5002.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a delete group admin API.
14 changes: 14 additions & 0 deletions docs/admin_api/delete_group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Delete a local group

This API lets a server admin delete a local group. Doing so will kick all
users out of the group so that their clients will correctly handle the group
being deleted.


The API is:

```
POST /_matrix/client/r0/admin/delete_group/<group_id>
```

including an `access_token` of a server admin.
73 changes: 73 additions & 0 deletions synapse/groups/groups_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from synapse.api.errors import SynapseError
from synapse.types import GroupID, RoomID, UserID, get_domain_from_id
from synapse.util.async_helpers import concurrently_execute

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -896,6 +897,78 @@ def create_group(self, group_id, requester_user_id, content):
"group_id": group_id,
})

@defer.inlineCallbacks
def delete_group(self, group_id, requester_user_id):
"""Deletes a group, kicking out all current members.

Only group admins or server admins can call this request

Args:
group_id (str)
request_user_id (str)

Returns:
Deferred
"""

yield self.check_group_is_ours(
group_id, requester_user_id,
and_exists=True,
)

# Only server admins or group admins can delete groups.

is_admin = yield self.store.is_user_admin_in_group(
group_id, requester_user_id
)

if not is_admin:
is_admin = yield self.auth.is_server_admin(
UserID.from_string(requester_user_id),
)

if not is_admin:
raise SynapseError(403, "User is not an admin")

# Before deleting the group lets kick everyone out of it
users = yield self.store.get_users_in_group(
group_id, include_private=True,
)

@defer.inlineCallbacks
def _kick_user_from_group(user_id):
if self.hs.is_mine_id(user_id):
groups_local = self.hs.get_groups_local_handler()
yield groups_local.user_removed_from_group(group_id, user_id, {})
else:
yield self.transport_client.remove_user_from_group_notification(
get_domain_from_id(user_id), group_id, user_id, {}
)
yield self.store.maybe_delete_remote_profile_cache(user_id)

# We kick users out in the order of:
# 1. Non-admins
# 2. Other admins
# 3. The requester
#
# This is so that if the deletion fails for some reason other admins or
# the requester still has auth to retry.
non_admins = []
admins = []
for u in users:
if u["user_id"] == requester_user_id:
continue
if u["is_admin"]:
admins.append(u["user_id"])
else:
non_admins.append(u["user_id"])

yield concurrently_execute(_kick_user_from_group, non_admins, 10)
yield concurrently_execute(_kick_user_from_group, admins, 10)
yield _kick_user_from_group(requester_user_id)

yield self.store.delete_group(group_id)


def _parse_join_policy_from_contents(content):
"""Given a content for a request, return the specified join policy or None
Expand Down
26 changes: 26 additions & 0 deletions synapse/rest/client/v1/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,31 @@ def on_GET(self, request, target_user_id):
defer.returnValue((200, ret))


class DeleteGroupAdminRestServlet(ClientV1RestServlet):
"""Allows deleting of local groups
"""
PATTERNS = client_path_patterns("/admin/delete_group/(?P<group_id>[^/]*)")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given #4850, it would be good to consider putting this elsewhere.

OTOH there's something to be said for first adding an alternative path for all the existing admin APIs, and only then start adding new ones on the new path.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Point. Though I don't really know what exactly we want to do there


def __init__(self, hs):
super(DeleteGroupAdminRestServlet, self).__init__(hs)
self.group_server = hs.get_groups_server_handler()
self.is_mine_id = hs.is_mine_id

@defer.inlineCallbacks
def on_POST(self, request, group_id):
requester = yield self.auth.get_user_by_req(request)
is_admin = yield self.auth.is_server_admin(requester.user)

if not is_admin:
raise AuthError(403, "You are not a server admin")

if not self.is_mine_id(group_id):
raise SynapseError(400, "Can only delete local groups")

yield self.group_server.delete_group(group_id, requester.user.to_string())
defer.returnValue((200, {}))


def register_servlets(hs, http_server):
WhoisRestServlet(hs).register(http_server)
PurgeMediaCacheRestServlet(hs).register(http_server)
Expand All @@ -799,3 +824,4 @@ def register_servlets(hs, http_server):
ListMediaInRoom(hs).register(http_server)
UserRegisterServlet(hs).register(http_server)
VersionServlet(hs).register(http_server)
DeleteGroupAdminRestServlet(hs).register(http_server)
37 changes: 37 additions & 0 deletions synapse/storage/group_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1150,3 +1150,40 @@ def _get_all_groups_changes_txn(txn):

def get_group_stream_token(self):
return self._group_updates_id_gen.get_current_token()

def delete_group(self, group_id):
"""Deletes a group fully from the database.

Args:
group_id (str)

Returns:
Deferred
"""

def _delete_group_txn(txn):
tables = [
"groups",
"group_users",
"group_invites",
"group_rooms",
"group_summary_rooms",
"group_summary_room_categories",
"group_room_categories",
"group_summary_users",
"group_summary_roles",
"group_roles",
"group_attestations_renewals",
"group_attestations_remote",
]

for table in tables:
self._simple_delete_txn(
txn,
table=table,
keyvalues={"group_id": group_id},
)

return self.runInteraction(
"delete_group", _delete_group_txn
)
124 changes: 124 additions & 0 deletions tests/rest/client/v1/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from synapse.api.constants import UserTypes
from synapse.rest.client.v1 import admin, events, login, room
from synapse.rest.client.v2_alpha import groups

from tests import unittest

Expand Down Expand Up @@ -490,3 +491,126 @@ def _assert_peek(self, room_id, expect_code):
self.assertEqual(
expect_code, int(channel.result["code"]), msg=channel.result["body"],
)


class DeleteGroupTestCase(unittest.HomeserverTestCase):
servlets = [
admin.register_servlets,
login.register_servlets,
groups.register_servlets,
]

def prepare(self, reactor, clock, hs):
self.store = hs.get_datastore()

self.admin_user = self.register_user("admin", "pass", admin=True)
self.admin_user_tok = self.login("admin", "pass")

self.other_user = self.register_user("user", "pass")
self.other_user_token = self.login("user", "pass")

def test_delete_group(self):
# Create a new group
request, channel = self.make_request(
"POST",
"/create_group".encode('ascii'),
access_token=self.admin_user_tok,
content={
"localpart": "test",
}
)

self.render(request)
self.assertEqual(
200, int(channel.result["code"]), msg=channel.result["body"],
)

group_id = channel.json_body["group_id"]

self._check_group(group_id, expect_code=200)

# Invite/join another user

url = "/groups/%s/admin/users/invite/%s" % (group_id, self.other_user)
request, channel = self.make_request(
"PUT",
url.encode('ascii'),
access_token=self.admin_user_tok,
content={}
)
self.render(request)
self.assertEqual(
200, int(channel.result["code"]), msg=channel.result["body"],
)

url = "/groups/%s/self/accept_invite" % (group_id,)
request, channel = self.make_request(
"PUT",
url.encode('ascii'),
access_token=self.other_user_token,
content={}
)
self.render(request)
self.assertEqual(
200, int(channel.result["code"]), msg=channel.result["body"],
)

# Check other user knows they're in the group
self.assertIn(group_id, self._get_groups_user_is_in(self.admin_user_tok))
self.assertIn(group_id, self._get_groups_user_is_in(self.other_user_token))

# Now delete the group
url = "/admin/delete_group/" + group_id
request, channel = self.make_request(
"POST",
url.encode('ascii'),
access_token=self.admin_user_tok,
content={
"localpart": "test",
}
)

self.render(request)
self.assertEqual(
200, int(channel.result["code"]), msg=channel.result["body"],
)

# Check group returns 404
self._check_group(group_id, expect_code=404)

# Check users don't think they're in the group
self.assertNotIn(group_id, self._get_groups_user_is_in(self.admin_user_tok))
self.assertNotIn(group_id, self._get_groups_user_is_in(self.other_user_token))

def _check_group(self, group_id, expect_code):
"""Assert that trying to fetch the given group results in the given
HTTP status code
"""

url = "/groups/%s/profile" % (group_id,)
request, channel = self.make_request(
"GET",
url.encode('ascii'),
access_token=self.admin_user_tok,
)

self.render(request)
self.assertEqual(
expect_code, int(channel.result["code"]), msg=channel.result["body"],
)

def _get_groups_user_is_in(self, access_token):
"""Returns the list of groups the user is in (given their access token)
"""
request, channel = self.make_request(
"GET",
"/joined_groups".encode('ascii'),
access_token=access_token,
)

self.render(request)
self.assertEqual(
200, int(channel.result["code"]), msg=channel.result["body"],
)

return channel.json_body["groups"]