mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-05-04 10:34:52 -04:00
Add delete room admin endpoint (#7613)
The Delete Room admin API allows server admins to remove rooms from server
and block these rooms.
`DELETE /_synapse/admin/v1/rooms/<room_id>`
It is a combination and improvement of "[Shutdown room](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/shutdown_room.md)" and "[Purge room](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/purge_room.md)" API.
Fixes: #6425
It also fixes a bug in [synapse/storage/data_stores/main/room.py](synapse/storage/data_stores/main/room.py) in ` get_room_with_stats`.
It should return `None` if the room is unknown. But it returns an `IndexError`.
901b1fa561/synapse/storage/data_stores/main/room.py (L99-L105)
Related to:
- #5575
- https://github.com/Awesome-Technologies/synapse-admin/issues/17
Signed-off-by: Dirk Klimpel dirk@klimpel.org
This commit is contained in:
parent
77d2c05410
commit
491f0dab1b
12 changed files with 782 additions and 120 deletions
|
@ -151,6 +151,401 @@ class ShutdownRoomTestCase(unittest.HomeserverTestCase):
|
|||
)
|
||||
|
||||
|
||||
class DeleteRoomTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets,
|
||||
login.register_servlets,
|
||||
events.register_servlets,
|
||||
room.register_servlets,
|
||||
room.register_deprecated_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
hs.config.user_consent_version = "1"
|
||||
|
||||
consent_uri_builder = Mock()
|
||||
consent_uri_builder.build_user_consent_uri.return_value = "http://example.com"
|
||||
self.event_creation_handler._consent_uri_builder = consent_uri_builder
|
||||
|
||||
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_tok = self.login("user", "pass")
|
||||
|
||||
# Mark the admin user as having consented
|
||||
self.get_success(self.store.user_set_consent_version(self.admin_user, "1"))
|
||||
|
||||
self.room_id = self.helper.create_room_as(
|
||||
self.other_user, tok=self.other_user_tok
|
||||
)
|
||||
self.url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id
|
||||
|
||||
def test_requester_is_no_admin(self):
|
||||
"""
|
||||
If the user is not a server admin, an error 403 is returned.
|
||||
"""
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST", self.url, json.dumps({}), access_token=self.other_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
||||
|
||||
def test_room_does_not_exist(self):
|
||||
"""
|
||||
Check that unknown rooms/server return error 404.
|
||||
"""
|
||||
url = "/_synapse/admin/v1/rooms/!unknown:test/delete"
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST", url, json.dumps({}), access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(404, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
|
||||
|
||||
def test_room_is_not_valid(self):
|
||||
"""
|
||||
Check that invalid room names, return an error 400.
|
||||
"""
|
||||
url = "/_synapse/admin/v1/rooms/invalidroom/delete"
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST", url, json.dumps({}), access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(
|
||||
"invalidroom is not a legal room ID", channel.json_body["error"],
|
||||
)
|
||||
|
||||
def test_new_room_user_does_not_exist(self):
|
||||
"""
|
||||
Tests that the user ID must be from local server but it does not have to exist.
|
||||
"""
|
||||
body = json.dumps({"new_room_user_id": "@unknown:test"})
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
content=body.encode(encoding="utf_8"),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertIn("new_room_id", channel.json_body)
|
||||
self.assertIn("kicked_users", channel.json_body)
|
||||
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||
self.assertIn("local_aliases", channel.json_body)
|
||||
|
||||
def test_new_room_user_is_not_local(self):
|
||||
"""
|
||||
Check that only local users can create new room to move members.
|
||||
"""
|
||||
body = json.dumps({"new_room_user_id": "@not:exist.bla"})
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
content=body.encode(encoding="utf_8"),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(
|
||||
"User must be our own: @not:exist.bla", channel.json_body["error"],
|
||||
)
|
||||
|
||||
def test_block_is_not_bool(self):
|
||||
"""
|
||||
If parameter `block` is not boolean, return an error
|
||||
"""
|
||||
body = json.dumps({"block": "NotBool"})
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
self.url,
|
||||
content=body.encode(encoding="utf_8"),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(Codes.BAD_JSON, channel.json_body["errcode"])
|
||||
|
||||
def test_purge_room_and_block(self):
|
||||
"""Test to purge a room and block it.
|
||||
Members will not be moved to a new room and will not receive a message.
|
||||
"""
|
||||
# Test that room is not purged
|
||||
with self.assertRaises(AssertionError):
|
||||
self._is_purged(self.room_id)
|
||||
|
||||
# Test that room is not blocked
|
||||
self._is_blocked(self.room_id, expect=False)
|
||||
|
||||
# Assert one user in room
|
||||
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||
|
||||
body = json.dumps({"block": True})
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
self.url.encode("ascii"),
|
||||
content=body.encode(encoding="utf_8"),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(None, channel.json_body["new_room_id"])
|
||||
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||
self.assertIn("local_aliases", channel.json_body)
|
||||
|
||||
self._is_purged(self.room_id)
|
||||
self._is_blocked(self.room_id, expect=True)
|
||||
self._has_no_members(self.room_id)
|
||||
|
||||
def test_purge_room_and_not_block(self):
|
||||
"""Test to purge a room and do not block it.
|
||||
Members will not be moved to a new room and will not receive a message.
|
||||
"""
|
||||
# Test that room is not purged
|
||||
with self.assertRaises(AssertionError):
|
||||
self._is_purged(self.room_id)
|
||||
|
||||
# Test that room is not blocked
|
||||
self._is_blocked(self.room_id, expect=False)
|
||||
|
||||
# Assert one user in room
|
||||
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||
|
||||
body = json.dumps({"block": False})
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
self.url.encode("ascii"),
|
||||
content=body.encode(encoding="utf_8"),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(None, channel.json_body["new_room_id"])
|
||||
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||
self.assertIn("local_aliases", channel.json_body)
|
||||
|
||||
self._is_purged(self.room_id)
|
||||
self._is_blocked(self.room_id, expect=False)
|
||||
self._has_no_members(self.room_id)
|
||||
|
||||
def test_shutdown_room_consent(self):
|
||||
"""Test that we can shutdown rooms with local users who have not
|
||||
yet accepted the privacy policy. This used to fail when we tried to
|
||||
force part the user from the old room.
|
||||
Members will be moved to a new room and will receive a message.
|
||||
"""
|
||||
self.event_creation_handler._block_events_without_consent_error = None
|
||||
|
||||
# Assert one user in room
|
||||
users_in_room = self.get_success(self.store.get_users_in_room(self.room_id))
|
||||
self.assertEqual([self.other_user], users_in_room)
|
||||
|
||||
# Enable require consent to send events
|
||||
self.event_creation_handler._block_events_without_consent_error = "Error"
|
||||
|
||||
# Assert that the user is getting consent error
|
||||
self.helper.send(
|
||||
self.room_id, body="foo", tok=self.other_user_tok, expect_code=403
|
||||
)
|
||||
|
||||
# Test that room is not purged
|
||||
with self.assertRaises(AssertionError):
|
||||
self._is_purged(self.room_id)
|
||||
|
||||
# Assert one user in room
|
||||
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||
|
||||
# Test that the admin can still send shutdown
|
||||
url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
url.encode("ascii"),
|
||||
json.dumps({"new_room_user_id": self.admin_user}),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||
self.assertIn("new_room_id", channel.json_body)
|
||||
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||
self.assertIn("local_aliases", channel.json_body)
|
||||
|
||||
# Test that member has moved to new room
|
||||
self._is_member(
|
||||
room_id=channel.json_body["new_room_id"], user_id=self.other_user
|
||||
)
|
||||
|
||||
self._is_purged(self.room_id)
|
||||
self._has_no_members(self.room_id)
|
||||
|
||||
def test_shutdown_room_block_peek(self):
|
||||
"""Test that a world_readable room can no longer be peeked into after
|
||||
it has been shut down.
|
||||
Members will be moved to a new room and will receive a message.
|
||||
"""
|
||||
self.event_creation_handler._block_events_without_consent_error = None
|
||||
|
||||
# Enable world readable
|
||||
url = "rooms/%s/state/m.room.history_visibility" % (self.room_id,)
|
||||
request, channel = self.make_request(
|
||||
"PUT",
|
||||
url.encode("ascii"),
|
||||
json.dumps({"history_visibility": "world_readable"}),
|
||||
access_token=self.other_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
|
||||
# Test that room is not purged
|
||||
with self.assertRaises(AssertionError):
|
||||
self._is_purged(self.room_id)
|
||||
|
||||
# Assert one user in room
|
||||
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||
|
||||
# Test that the admin can still send shutdown
|
||||
url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
url.encode("ascii"),
|
||||
json.dumps({"new_room_user_id": self.admin_user}),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||
self.assertIn("new_room_id", channel.json_body)
|
||||
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||
self.assertIn("local_aliases", channel.json_body)
|
||||
|
||||
# Test that member has moved to new room
|
||||
self._is_member(
|
||||
room_id=channel.json_body["new_room_id"], user_id=self.other_user
|
||||
)
|
||||
|
||||
self._is_purged(self.room_id)
|
||||
self._has_no_members(self.room_id)
|
||||
|
||||
# Assert we can no longer peek into the room
|
||||
self._assert_peek(self.room_id, expect_code=403)
|
||||
|
||||
def _is_blocked(self, room_id, expect=True):
|
||||
"""Assert that the room is blocked or not
|
||||
"""
|
||||
d = self.store.is_room_blocked(room_id)
|
||||
if expect:
|
||||
self.assertTrue(self.get_success(d))
|
||||
else:
|
||||
self.assertIsNone(self.get_success(d))
|
||||
|
||||
def _has_no_members(self, room_id):
|
||||
"""Assert there is now no longer anyone in the room
|
||||
"""
|
||||
users_in_room = self.get_success(self.store.get_users_in_room(room_id))
|
||||
self.assertEqual([], users_in_room)
|
||||
|
||||
def _is_member(self, room_id, user_id):
|
||||
"""Test that user is member of the room
|
||||
"""
|
||||
users_in_room = self.get_success(self.store.get_users_in_room(room_id))
|
||||
self.assertIn(user_id, users_in_room)
|
||||
|
||||
def _is_purged(self, room_id):
|
||||
"""Test that the following tables have been purged of all rows related to the room.
|
||||
"""
|
||||
for table in (
|
||||
"current_state_events",
|
||||
"event_backward_extremities",
|
||||
"event_forward_extremities",
|
||||
"event_json",
|
||||
"event_push_actions",
|
||||
"event_search",
|
||||
"events",
|
||||
"group_rooms",
|
||||
"public_room_list_stream",
|
||||
"receipts_graph",
|
||||
"receipts_linearized",
|
||||
"room_aliases",
|
||||
"room_depth",
|
||||
"room_memberships",
|
||||
"room_stats_state",
|
||||
"room_stats_current",
|
||||
"room_stats_historical",
|
||||
"room_stats_earliest_token",
|
||||
"rooms",
|
||||
"stream_ordering_to_exterm",
|
||||
"users_in_public_rooms",
|
||||
"users_who_share_private_rooms",
|
||||
"appservice_room_list",
|
||||
"e2e_room_keys",
|
||||
"event_push_summary",
|
||||
"pusher_throttle",
|
||||
"group_summary_rooms",
|
||||
"local_invites",
|
||||
"room_account_data",
|
||||
"room_tags",
|
||||
# "state_groups", # Current impl leaves orphaned state groups around.
|
||||
"state_groups_state",
|
||||
):
|
||||
count = self.get_success(
|
||||
self.store.db.simple_select_one_onecol(
|
||||
table=table,
|
||||
keyvalues={"room_id": room_id},
|
||||
retcol="COUNT(*)",
|
||||
desc="test_purge_room",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(count, 0, msg="Rows not purged in {}".format(table))
|
||||
|
||||
def _assert_peek(self, room_id, expect_code):
|
||||
"""Assert that the admin user can (or cannot) peek into the room.
|
||||
"""
|
||||
|
||||
url = "rooms/%s/initialSync" % (room_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"]
|
||||
)
|
||||
|
||||
url = "events?timeout=0&room_id=" + room_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"]
|
||||
)
|
||||
|
||||
|
||||
class PurgeRoomTestCase(unittest.HomeserverTestCase):
|
||||
"""Test /purge_room admin API.
|
||||
"""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue