mirror of
https://mau.dev/maunium/synapse.git
synced 2024-12-14 12:43:50 +01:00
Merge pull request #5002 from matrix-org/erikj/delete_group
Add delete group admin API
This commit is contained in:
commit
616e6a10bd
6 changed files with 275 additions and 0 deletions
1
changelog.d/5002.feature
Normal file
1
changelog.d/5002.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add a delete group admin API.
|
14
docs/admin_api/delete_group.md
Normal file
14
docs/admin_api/delete_group.md
Normal file
|
@ -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.
|
|
@ -22,6 +22,7 @@ from twisted.internet import defer
|
|||
|
||||
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__)
|
||||
|
||||
|
@ -896,6 +897,78 @@ class GroupsServerHandler(object):
|
|||
"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
|
||||
|
|
|
@ -784,6 +784,31 @@ class SearchUsersRestServlet(ClientV1RestServlet):
|
|||
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)
|
||||
|
@ -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)
|
||||
|
|
|
@ -1150,3 +1150,40 @@ class GroupServerStore(SQLBaseStore):
|
|||
|
||||
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
|
||||
)
|
||||
|
|
|
@ -21,6 +21,7 @@ from mock import Mock
|
|||
|
||||
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
|
||||
|
||||
|
@ -490,3 +491,126 @@ class ShutdownRoomTestCase(unittest.HomeserverTestCase):
|
|||
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"]
|
||||
|
|
Loading…
Reference in a new issue