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

Commit

Permalink
Merge pull request #5002 from matrix-org/erikj/delete_group
Browse files Browse the repository at this point in the history
Add delete group admin API
  • Loading branch information
erikjohnston committed Apr 4, 2019
2 parents db265f0 + bd3435e commit 616e6a1
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 0 deletions.
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>[^/]*)")

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"]

0 comments on commit 616e6a1

Please sign in to comment.