* Admin api to delete event report * lint + tests * newsfile * Apply suggestions from code review Co-authored-by: David Robertson <david.m.robertson1@gmail.com> * revert changes - move to WorkerStore * update unit test * Note that timestamp is in millseconds --------- Co-authored-by: David Robertson <david.m.robertson1@gmail.com>tags/v1.79.0rc1
@@ -0,0 +1 @@ | |||
Add an [admin API](https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/index.html) to delete a [specific event report](https://spec.matrix.org/v1.6/client-server-api/#reporting-content). |
@@ -169,3 +169,17 @@ The following fields are returned in the JSON response body: | |||
* `canonical_alias`: string - The canonical alias of the room. `null` if the room does not | |||
have a canonical alias set. | |||
* `event_json`: object - Details of the original event that was reported. | |||
# Delete a specific event report | |||
This API deletes a specific event report. If the request is successful, the response body | |||
will be an empty JSON object. | |||
The api is: | |||
``` | |||
DELETE /_synapse/admin/v1/event_reports/<report_id> | |||
``` | |||
**URL parameters:** | |||
* `report_id`: string - The ID of the event report. |
@@ -53,11 +53,11 @@ class EventReportsRestServlet(RestServlet): | |||
PATTERNS = admin_patterns("/event_reports$") | |||
def __init__(self, hs: "HomeServer"): | |||
self.auth = hs.get_auth() | |||
self.store = hs.get_datastores().main | |||
self._auth = hs.get_auth() | |||
self._store = hs.get_datastores().main | |||
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: | |||
await assert_requester_is_admin(self.auth, request) | |||
await assert_requester_is_admin(self._auth, request) | |||
start = parse_integer(request, "from", default=0) | |||
limit = parse_integer(request, "limit", default=100) | |||
@@ -79,7 +79,7 @@ class EventReportsRestServlet(RestServlet): | |||
errcode=Codes.INVALID_PARAM, | |||
) | |||
event_reports, total = await self.store.get_event_reports_paginate( | |||
event_reports, total = await self._store.get_event_reports_paginate( | |||
start, limit, direction, user_id, room_id | |||
) | |||
ret = {"event_reports": event_reports, "total": total} | |||
@@ -108,13 +108,13 @@ class EventReportDetailRestServlet(RestServlet): | |||
PATTERNS = admin_patterns("/event_reports/(?P<report_id>[^/]*)$") | |||
def __init__(self, hs: "HomeServer"): | |||
self.auth = hs.get_auth() | |||
self.store = hs.get_datastores().main | |||
self._auth = hs.get_auth() | |||
self._store = hs.get_datastores().main | |||
async def on_GET( | |||
self, request: SynapseRequest, report_id: str | |||
) -> Tuple[int, JsonDict]: | |||
await assert_requester_is_admin(self.auth, request) | |||
await assert_requester_is_admin(self._auth, request) | |||
message = ( | |||
"The report_id parameter must be a string representing a positive integer." | |||
@@ -131,8 +131,33 @@ class EventReportDetailRestServlet(RestServlet): | |||
HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM | |||
) | |||
ret = await self.store.get_event_report(resolved_report_id) | |||
ret = await self._store.get_event_report(resolved_report_id) | |||
if not ret: | |||
raise NotFoundError("Event report not found") | |||
return HTTPStatus.OK, ret | |||
async def on_DELETE( | |||
self, request: SynapseRequest, report_id: str | |||
) -> Tuple[int, JsonDict]: | |||
await assert_requester_is_admin(self._auth, request) | |||
message = ( | |||
"The report_id parameter must be a string representing a positive integer." | |||
) | |||
try: | |||
resolved_report_id = int(report_id) | |||
except ValueError: | |||
raise SynapseError( | |||
HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM | |||
) | |||
if resolved_report_id < 0: | |||
raise SynapseError( | |||
HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM | |||
) | |||
if await self._store.delete_event_report(resolved_report_id): | |||
return HTTPStatus.OK, {} | |||
raise NotFoundError("Event report not found") |
@@ -1417,6 +1417,27 @@ class RoomWorkerStore(CacheInvalidationWorkerStore): | |||
get_un_partial_stated_rooms_from_stream_txn, | |||
) | |||
async def delete_event_report(self, report_id: int) -> bool: | |||
"""Remove an event report from database. | |||
Args: | |||
report_id: Report to delete | |||
Returns: | |||
Whether the report was successfully deleted or not. | |||
""" | |||
try: | |||
await self.db_pool.simple_delete_one( | |||
table="event_reports", | |||
keyvalues={"id": report_id}, | |||
desc="delete_event_report", | |||
) | |||
except StoreError: | |||
# Deletion failed because report does not exist | |||
return False | |||
return True | |||
class _BackgroundUpdates: | |||
REMOVE_TOMESTONED_ROOMS_BG_UPDATE = "remove_tombstoned_rooms_from_directory" | |||
@@ -2139,7 +2160,19 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore): | |||
reason: Optional[str], | |||
content: JsonDict, | |||
received_ts: int, | |||
) -> None: | |||
) -> int: | |||
"""Add an event report | |||
Args: | |||
room_id: Room that contains the reported event. | |||
event_id: The reported event. | |||
user_id: User who reports the event. | |||
reason: Description that the user specifies. | |||
content: Report request body (score and reason). | |||
received_ts: Time when the user submitted the report (milliseconds). | |||
Returns: | |||
Id of the event report. | |||
""" | |||
next_id = self._event_reports_id_gen.get_next() | |||
await self.db_pool.simple_insert( | |||
table="event_reports", | |||
@@ -2154,6 +2187,7 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore): | |||
}, | |||
desc="add_event_report", | |||
) | |||
return next_id | |||
async def get_event_report(self, report_id: int) -> Optional[Dict[str, Any]]: | |||
"""Retrieve an event report | |||
@@ -78,7 +78,7 @@ class EventReportsTestCase(unittest.HomeserverTestCase): | |||
""" | |||
Try to get an event report without authentication. | |||
""" | |||
channel = self.make_request("GET", self.url, b"{}") | |||
channel = self.make_request("GET", self.url, {}) | |||
self.assertEqual(401, channel.code, msg=channel.json_body) | |||
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) | |||
@@ -473,7 +473,7 @@ class EventReportDetailTestCase(unittest.HomeserverTestCase): | |||
""" | |||
Try to get event report without authentication. | |||
""" | |||
channel = self.make_request("GET", self.url, b"{}") | |||
channel = self.make_request("GET", self.url, {}) | |||
self.assertEqual(401, channel.code, msg=channel.json_body) | |||
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) | |||
@@ -599,3 +599,142 @@ class EventReportDetailTestCase(unittest.HomeserverTestCase): | |||
self.assertIn("room_id", content["event_json"]) | |||
self.assertIn("sender", content["event_json"]) | |||
self.assertIn("content", content["event_json"]) | |||
class DeleteEventReportTestCase(unittest.HomeserverTestCase): | |||
servlets = [ | |||
synapse.rest.admin.register_servlets, | |||
login.register_servlets, | |||
] | |||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: | |||
self._store = hs.get_datastores().main | |||
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") | |||
# create report | |||
event_id = self.get_success( | |||
self._store.add_event_report( | |||
"room_id", | |||
"event_id", | |||
self.other_user, | |||
"this makes me sad", | |||
{}, | |||
self.clock.time_msec(), | |||
) | |||
) | |||
self.url = f"/_synapse/admin/v1/event_reports/{event_id}" | |||
def test_no_auth(self) -> None: | |||
""" | |||
Try to delete event report without authentication. | |||
""" | |||
channel = self.make_request("DELETE", self.url) | |||
self.assertEqual(401, channel.code, msg=channel.json_body) | |||
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) | |||
def test_requester_is_no_admin(self) -> None: | |||
""" | |||
If the user is not a server admin, an error 403 is returned. | |||
""" | |||
channel = self.make_request( | |||
"DELETE", | |||
self.url, | |||
access_token=self.other_user_tok, | |||
) | |||
self.assertEqual(403, channel.code, msg=channel.json_body) | |||
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) | |||
def test_delete_success(self) -> None: | |||
""" | |||
Testing delete a report. | |||
""" | |||
channel = self.make_request( | |||
"DELETE", | |||
self.url, | |||
access_token=self.admin_user_tok, | |||
) | |||
self.assertEqual(200, channel.code, msg=channel.json_body) | |||
self.assertEqual({}, channel.json_body) | |||
channel = self.make_request( | |||
"GET", | |||
self.url, | |||
access_token=self.admin_user_tok, | |||
) | |||
# check that report was deleted | |||
self.assertEqual(404, channel.code, msg=channel.json_body) | |||
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"]) | |||
def test_invalid_report_id(self) -> None: | |||
""" | |||
Testing that an invalid `report_id` returns a 400. | |||
""" | |||
# `report_id` is negative | |||
channel = self.make_request( | |||
"DELETE", | |||
"/_synapse/admin/v1/event_reports/-123", | |||
access_token=self.admin_user_tok, | |||
) | |||
self.assertEqual(400, channel.code, msg=channel.json_body) | |||
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) | |||
self.assertEqual( | |||
"The report_id parameter must be a string representing a positive integer.", | |||
channel.json_body["error"], | |||
) | |||
# `report_id` is a non-numerical string | |||
channel = self.make_request( | |||
"DELETE", | |||
"/_synapse/admin/v1/event_reports/abcdef", | |||
access_token=self.admin_user_tok, | |||
) | |||
self.assertEqual(400, channel.code, msg=channel.json_body) | |||
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) | |||
self.assertEqual( | |||
"The report_id parameter must be a string representing a positive integer.", | |||
channel.json_body["error"], | |||
) | |||
# `report_id` is undefined | |||
channel = self.make_request( | |||
"DELETE", | |||
"/_synapse/admin/v1/event_reports/", | |||
access_token=self.admin_user_tok, | |||
) | |||
self.assertEqual(400, channel.code, msg=channel.json_body) | |||
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) | |||
self.assertEqual( | |||
"The report_id parameter must be a string representing a positive integer.", | |||
channel.json_body["error"], | |||
) | |||
def test_report_id_not_found(self) -> None: | |||
""" | |||
Testing that a not existing `report_id` returns a 404. | |||
""" | |||
channel = self.make_request( | |||
"DELETE", | |||
"/_synapse/admin/v1/event_reports/123", | |||
access_token=self.admin_user_tok, | |||
) | |||
self.assertEqual(404, channel.code, msg=channel.json_body) | |||
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"]) | |||
self.assertEqual("Event report not found", channel.json_body["error"]) |