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

Commit 93f7955

Browse files
Admin API endpoint to delete a reported event (#15116)
* Admin api to delete event report * lint + tests * newsfile * Apply suggestions from code review Co-authored-by: David Robertson <[email protected]> * revert changes - move to WorkerStore * update unit test * Note that timestamp is in millseconds --------- Co-authored-by: David Robertson <[email protected]>
1 parent 1cd4fbc commit 93f7955

File tree

5 files changed

+224
-11
lines changed

5 files changed

+224
-11
lines changed

changelog.d/15116.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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).

docs/admin_api/event_reports.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,17 @@ The following fields are returned in the JSON response body:
169169
* `canonical_alias`: string - The canonical alias of the room. `null` if the room does not
170170
have a canonical alias set.
171171
* `event_json`: object - Details of the original event that was reported.
172+
173+
# Delete a specific event report
174+
175+
This API deletes a specific event report. If the request is successful, the response body
176+
will be an empty JSON object.
177+
178+
The api is:
179+
```
180+
DELETE /_synapse/admin/v1/event_reports/<report_id>
181+
```
182+
183+
**URL parameters:**
184+
185+
* `report_id`: string - The ID of the event report.

synapse/rest/admin/event_reports.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ class EventReportsRestServlet(RestServlet):
5353
PATTERNS = admin_patterns("/event_reports$")
5454

5555
def __init__(self, hs: "HomeServer"):
56-
self.auth = hs.get_auth()
57-
self.store = hs.get_datastores().main
56+
self._auth = hs.get_auth()
57+
self._store = hs.get_datastores().main
5858

5959
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
60-
await assert_requester_is_admin(self.auth, request)
60+
await assert_requester_is_admin(self._auth, request)
6161

6262
start = parse_integer(request, "from", default=0)
6363
limit = parse_integer(request, "limit", default=100)
@@ -79,7 +79,7 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
7979
errcode=Codes.INVALID_PARAM,
8080
)
8181

82-
event_reports, total = await self.store.get_event_reports_paginate(
82+
event_reports, total = await self._store.get_event_reports_paginate(
8383
start, limit, direction, user_id, room_id
8484
)
8585
ret = {"event_reports": event_reports, "total": total}
@@ -108,13 +108,13 @@ class EventReportDetailRestServlet(RestServlet):
108108
PATTERNS = admin_patterns("/event_reports/(?P<report_id>[^/]*)$")
109109

110110
def __init__(self, hs: "HomeServer"):
111-
self.auth = hs.get_auth()
112-
self.store = hs.get_datastores().main
111+
self._auth = hs.get_auth()
112+
self._store = hs.get_datastores().main
113113

114114
async def on_GET(
115115
self, request: SynapseRequest, report_id: str
116116
) -> Tuple[int, JsonDict]:
117-
await assert_requester_is_admin(self.auth, request)
117+
await assert_requester_is_admin(self._auth, request)
118118

119119
message = (
120120
"The report_id parameter must be a string representing a positive integer."
@@ -131,8 +131,33 @@ async def on_GET(
131131
HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM
132132
)
133133

134-
ret = await self.store.get_event_report(resolved_report_id)
134+
ret = await self._store.get_event_report(resolved_report_id)
135135
if not ret:
136136
raise NotFoundError("Event report not found")
137137

138138
return HTTPStatus.OK, ret
139+
140+
async def on_DELETE(
141+
self, request: SynapseRequest, report_id: str
142+
) -> Tuple[int, JsonDict]:
143+
await assert_requester_is_admin(self._auth, request)
144+
145+
message = (
146+
"The report_id parameter must be a string representing a positive integer."
147+
)
148+
try:
149+
resolved_report_id = int(report_id)
150+
except ValueError:
151+
raise SynapseError(
152+
HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM
153+
)
154+
155+
if resolved_report_id < 0:
156+
raise SynapseError(
157+
HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM
158+
)
159+
160+
if await self._store.delete_event_report(resolved_report_id):
161+
return HTTPStatus.OK, {}
162+
163+
raise NotFoundError("Event report not found")

synapse/storage/databases/main/room.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,27 @@ def get_un_partial_stated_rooms_from_stream_txn(
14171417
get_un_partial_stated_rooms_from_stream_txn,
14181418
)
14191419

1420+
async def delete_event_report(self, report_id: int) -> bool:
1421+
"""Remove an event report from database.
1422+
1423+
Args:
1424+
report_id: Report to delete
1425+
1426+
Returns:
1427+
Whether the report was successfully deleted or not.
1428+
"""
1429+
try:
1430+
await self.db_pool.simple_delete_one(
1431+
table="event_reports",
1432+
keyvalues={"id": report_id},
1433+
desc="delete_event_report",
1434+
)
1435+
except StoreError:
1436+
# Deletion failed because report does not exist
1437+
return False
1438+
1439+
return True
1440+
14201441

14211442
class _BackgroundUpdates:
14221443
REMOVE_TOMESTONED_ROOMS_BG_UPDATE = "remove_tombstoned_rooms_from_directory"
@@ -2139,7 +2160,19 @@ async def add_event_report(
21392160
reason: Optional[str],
21402161
content: JsonDict,
21412162
received_ts: int,
2142-
) -> None:
2163+
) -> int:
2164+
"""Add an event report
2165+
2166+
Args:
2167+
room_id: Room that contains the reported event.
2168+
event_id: The reported event.
2169+
user_id: User who reports the event.
2170+
reason: Description that the user specifies.
2171+
content: Report request body (score and reason).
2172+
received_ts: Time when the user submitted the report (milliseconds).
2173+
Returns:
2174+
Id of the event report.
2175+
"""
21432176
next_id = self._event_reports_id_gen.get_next()
21442177
await self.db_pool.simple_insert(
21452178
table="event_reports",
@@ -2154,6 +2187,7 @@ async def add_event_report(
21542187
},
21552188
desc="add_event_report",
21562189
)
2190+
return next_id
21572191

21582192
async def get_event_report(self, report_id: int) -> Optional[Dict[str, Any]]:
21592193
"""Retrieve an event report

tests/rest/admin/test_event_reports.py

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def test_no_auth(self) -> None:
7878
"""
7979
Try to get an event report without authentication.
8080
"""
81-
channel = self.make_request("GET", self.url, b"{}")
81+
channel = self.make_request("GET", self.url, {})
8282

8383
self.assertEqual(401, channel.code, msg=channel.json_body)
8484
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
@@ -473,7 +473,7 @@ def test_no_auth(self) -> None:
473473
"""
474474
Try to get event report without authentication.
475475
"""
476-
channel = self.make_request("GET", self.url, b"{}")
476+
channel = self.make_request("GET", self.url, {})
477477

478478
self.assertEqual(401, channel.code, msg=channel.json_body)
479479
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
@@ -599,3 +599,142 @@ def _check_fields(self, content: JsonDict) -> None:
599599
self.assertIn("room_id", content["event_json"])
600600
self.assertIn("sender", content["event_json"])
601601
self.assertIn("content", content["event_json"])
602+
603+
604+
class DeleteEventReportTestCase(unittest.HomeserverTestCase):
605+
servlets = [
606+
synapse.rest.admin.register_servlets,
607+
login.register_servlets,
608+
]
609+
610+
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
611+
self._store = hs.get_datastores().main
612+
613+
self.admin_user = self.register_user("admin", "pass", admin=True)
614+
self.admin_user_tok = self.login("admin", "pass")
615+
616+
self.other_user = self.register_user("user", "pass")
617+
self.other_user_tok = self.login("user", "pass")
618+
619+
# create report
620+
event_id = self.get_success(
621+
self._store.add_event_report(
622+
"room_id",
623+
"event_id",
624+
self.other_user,
625+
"this makes me sad",
626+
{},
627+
self.clock.time_msec(),
628+
)
629+
)
630+
631+
self.url = f"/_synapse/admin/v1/event_reports/{event_id}"
632+
633+
def test_no_auth(self) -> None:
634+
"""
635+
Try to delete event report without authentication.
636+
"""
637+
channel = self.make_request("DELETE", self.url)
638+
639+
self.assertEqual(401, channel.code, msg=channel.json_body)
640+
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
641+
642+
def test_requester_is_no_admin(self) -> None:
643+
"""
644+
If the user is not a server admin, an error 403 is returned.
645+
"""
646+
647+
channel = self.make_request(
648+
"DELETE",
649+
self.url,
650+
access_token=self.other_user_tok,
651+
)
652+
653+
self.assertEqual(403, channel.code, msg=channel.json_body)
654+
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
655+
656+
def test_delete_success(self) -> None:
657+
"""
658+
Testing delete a report.
659+
"""
660+
661+
channel = self.make_request(
662+
"DELETE",
663+
self.url,
664+
access_token=self.admin_user_tok,
665+
)
666+
667+
self.assertEqual(200, channel.code, msg=channel.json_body)
668+
self.assertEqual({}, channel.json_body)
669+
670+
channel = self.make_request(
671+
"GET",
672+
self.url,
673+
access_token=self.admin_user_tok,
674+
)
675+
676+
# check that report was deleted
677+
self.assertEqual(404, channel.code, msg=channel.json_body)
678+
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
679+
680+
def test_invalid_report_id(self) -> None:
681+
"""
682+
Testing that an invalid `report_id` returns a 400.
683+
"""
684+
685+
# `report_id` is negative
686+
channel = self.make_request(
687+
"DELETE",
688+
"/_synapse/admin/v1/event_reports/-123",
689+
access_token=self.admin_user_tok,
690+
)
691+
692+
self.assertEqual(400, channel.code, msg=channel.json_body)
693+
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
694+
self.assertEqual(
695+
"The report_id parameter must be a string representing a positive integer.",
696+
channel.json_body["error"],
697+
)
698+
699+
# `report_id` is a non-numerical string
700+
channel = self.make_request(
701+
"DELETE",
702+
"/_synapse/admin/v1/event_reports/abcdef",
703+
access_token=self.admin_user_tok,
704+
)
705+
706+
self.assertEqual(400, channel.code, msg=channel.json_body)
707+
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
708+
self.assertEqual(
709+
"The report_id parameter must be a string representing a positive integer.",
710+
channel.json_body["error"],
711+
)
712+
713+
# `report_id` is undefined
714+
channel = self.make_request(
715+
"DELETE",
716+
"/_synapse/admin/v1/event_reports/",
717+
access_token=self.admin_user_tok,
718+
)
719+
720+
self.assertEqual(400, channel.code, msg=channel.json_body)
721+
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
722+
self.assertEqual(
723+
"The report_id parameter must be a string representing a positive integer.",
724+
channel.json_body["error"],
725+
)
726+
727+
def test_report_id_not_found(self) -> None:
728+
"""
729+
Testing that a not existing `report_id` returns a 404.
730+
"""
731+
732+
channel = self.make_request(
733+
"DELETE",
734+
"/_synapse/admin/v1/event_reports/123",
735+
access_token=self.admin_user_tok,
736+
)
737+
738+
self.assertEqual(404, channel.code, msg=channel.json_body)
739+
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
740+
self.assertEqual("Event report not found", channel.json_body["error"])

0 commit comments

Comments
 (0)