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

Admin API endpoint to delete a reported event #15116

Merged
merged 8 commits into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/15116.feature
Original file line number Diff line number Diff line change
@@ -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).
14 changes: 14 additions & 0 deletions docs/admin_api/event_reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
41 changes: 33 additions & 8 deletions synapse/rest/admin/event_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -79,7 +79,7 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
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}
Expand Down Expand Up @@ -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."
Expand All @@ -131,8 +131,33 @@ async def on_GET(
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")
36 changes: 35 additions & 1 deletion synapse/storage/databases/main/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,27 @@ def get_un_partial_stated_rooms_from_stream_txn(
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"
Expand Down Expand Up @@ -2139,7 +2160,19 @@ async def add_event_report(
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",
Expand All @@ -2154,6 +2187,7 @@ async def add_event_report(
},
desc="add_event_report",
)
return next_id

async def get_event_report(self, report_id: int) -> Optional[Dict[str, Any]]:
"""Retrieve an event report
Expand Down
143 changes: 141 additions & 2 deletions tests/rest/admin/test_event_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def test_no_auth(self) -> None:
"""
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"])
Expand Down Expand Up @@ -473,7 +473,7 @@ def test_no_auth(self) -> None:
"""
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"])
Expand Down Expand Up @@ -599,3 +599,142 @@ def _check_fields(self, content: JsonDict) -> None:
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)
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved

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