Merge pull request #5002 from matrix-org/erikj/delete_group
Add delete group admin API
This commit is contained in:
commit
616e6a10bd
|
@ -0,0 +1 @@
|
||||||
|
Add a delete group admin API.
|
|
@ -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.api.errors import SynapseError
|
||||||
from synapse.types import GroupID, RoomID, UserID, get_domain_from_id
|
from synapse.types import GroupID, RoomID, UserID, get_domain_from_id
|
||||||
|
from synapse.util.async_helpers import concurrently_execute
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -896,6 +897,78 @@ class GroupsServerHandler(object):
|
||||||
"group_id": group_id,
|
"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):
|
def _parse_join_policy_from_contents(content):
|
||||||
"""Given a content for a request, return the specified join policy or None
|
"""Given a content for a request, return the specified join policy or None
|
||||||
|
|
|
@ -784,6 +784,31 @@ class SearchUsersRestServlet(ClientV1RestServlet):
|
||||||
defer.returnValue((200, ret))
|
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):
|
def register_servlets(hs, http_server):
|
||||||
WhoisRestServlet(hs).register(http_server)
|
WhoisRestServlet(hs).register(http_server)
|
||||||
PurgeMediaCacheRestServlet(hs).register(http_server)
|
PurgeMediaCacheRestServlet(hs).register(http_server)
|
||||||
|
@ -799,3 +824,4 @@ def register_servlets(hs, http_server):
|
||||||
ListMediaInRoom(hs).register(http_server)
|
ListMediaInRoom(hs).register(http_server)
|
||||||
UserRegisterServlet(hs).register(http_server)
|
UserRegisterServlet(hs).register(http_server)
|
||||||
VersionServlet(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):
|
def get_group_stream_token(self):
|
||||||
return self._group_updates_id_gen.get_current_token()
|
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.api.constants import UserTypes
|
||||||
from synapse.rest.client.v1 import admin, events, login, room
|
from synapse.rest.client.v1 import admin, events, login, room
|
||||||
|
from synapse.rest.client.v2_alpha import groups
|
||||||
|
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
|
|
||||||
|
@ -490,3 +491,126 @@ class ShutdownRoomTestCase(unittest.HomeserverTestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
expect_code, int(channel.result["code"]), msg=channel.result["body"],
|
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 New Issue