mirror of
https://github.com/element-hq/synapse.git
synced 2026-10-23 19:33:44 +00:00
Add Admin API endpoints to manage user reports (#19657)
Adds [Admin API](https://element-hq.github.io/synapse/latest/usage/administration/admin_api/index.html) endpoints to list, fetch and delete user reports from the homeserver. Follows on from #18120, which added the endpoints to report users.
This commit is contained in:
5 files changed
+951
No files matched your search
@@ -0,0 +1,2 @@
|
||||
Adds [Admin API](https://element-hq.github.io/synapse/latest/usage/administration/admin_api/index.html) endpoints to
|
||||
list, fetch and delete user reports.
|
||||
@@ -96,6 +96,10 @@ from synapse.rest.admin.statistics import (
|
||||
LargestRoomsStatistics,
|
||||
UserMediaStatisticsRestServlet,
|
||||
)
|
||||
from synapse.rest.admin.user_reports import (
|
||||
UserReportDetailRestServlet,
|
||||
UserReportsRestServlet,
|
||||
)
|
||||
from synapse.rest.admin.username_available import UsernameAvailableRestServlet
|
||||
from synapse.rest.admin.users import (
|
||||
AccountDataRestServlet,
|
||||
@@ -312,6 +316,8 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||
LargestRoomsStatistics(hs).register(http_server)
|
||||
EventReportDetailRestServlet(hs).register(http_server)
|
||||
EventReportsRestServlet(hs).register(http_server)
|
||||
UserReportsRestServlet(hs).register(http_server)
|
||||
UserReportDetailRestServlet(hs).register(http_server)
|
||||
AccountDataRestServlet(hs).register(http_server)
|
||||
PushersRestServlet(hs).register(http_server)
|
||||
MakeRoomAdminRestServlet(hs).register(http_server)
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2026 Element Creations, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
|
||||
|
||||
import logging
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from synapse.api.constants import Direction
|
||||
from synapse.api.errors import Codes, NotFoundError, SynapseError
|
||||
from synapse.http.servlet import RestServlet, parse_enum, parse_integer, parse_string
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.rest.admin._base import admin_patterns, assert_requester_is_admin
|
||||
from synapse.types import JsonDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserReportsRestServlet(RestServlet):
|
||||
"""
|
||||
List all reported users that are known to the homeserver. Results are returned
|
||||
in a dictionary containing report information. Supports pagination.
|
||||
The requester must have administrator access in Synapse.
|
||||
|
||||
GET /_synapse/admin/v1/user_reports
|
||||
returns:
|
||||
200 OK with list of reports if success otherwise an error.
|
||||
|
||||
Args:
|
||||
The parameters `from` and `limit` are required only for pagination.
|
||||
By default, a `limit` of 100 is used.
|
||||
The parameter `dir` can be used to define the order of results.
|
||||
The `user_id` query parameter filters by the user ID of the reporter of the target user.
|
||||
The `target_user_id` query parameter filters by user id of the target user.
|
||||
Returns:
|
||||
A list of user reprots and an integer representing the total number of user
|
||||
reports that exist given this query
|
||||
"""
|
||||
|
||||
PATTERNS = admin_patterns("/user_reports$")
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
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)
|
||||
|
||||
start = parse_integer(request, "from", default=0)
|
||||
limit = parse_integer(request, "limit", default=100)
|
||||
direction = parse_enum(request, "dir", Direction, Direction.BACKWARDS)
|
||||
user_id = parse_string(request, "user_id")
|
||||
target_user_id = parse_string(request, "target_user_id")
|
||||
|
||||
if start < 0:
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"The start parameter must be a positive integer.",
|
||||
errcode=Codes.INVALID_PARAM,
|
||||
)
|
||||
|
||||
if limit < 0:
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"The limit parameter must be a positive integer.",
|
||||
errcode=Codes.INVALID_PARAM,
|
||||
)
|
||||
|
||||
user_reports, total = await self._store.get_user_reports_paginate(
|
||||
start, limit, direction, user_id, target_user_id
|
||||
)
|
||||
ret = {"user_reports": user_reports, "total": total}
|
||||
if (start + limit) < total:
|
||||
ret["next_token"] = start + len(user_reports)
|
||||
|
||||
return HTTPStatus.OK, ret
|
||||
|
||||
|
||||
class UserReportDetailRestServlet(RestServlet):
|
||||
"""
|
||||
Get a specific user report that is known to the homeserver. Results are returned
|
||||
in a dictionary containing report information.
|
||||
The requester must have administrator access in Synapse.
|
||||
|
||||
GET /_synapse/admin/v1/user_reports/<report_id>
|
||||
returns:
|
||||
200 OK with details report if success otherwise an error.
|
||||
|
||||
Args:
|
||||
The parameter `report_id` is the ID of the user report in the database.
|
||||
Returns:
|
||||
JSON blob of information about the user report
|
||||
"""
|
||||
|
||||
PATTERNS = admin_patterns("/user_reports/(?P<report_id>[^/]*)$")
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
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)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
ret = await self._store.get_user_report(resolved_report_id)
|
||||
if not ret:
|
||||
raise NotFoundError("User report not found")
|
||||
|
||||
id, received_ts, target_user_id, user_id, reason = ret
|
||||
response = {
|
||||
"id": id,
|
||||
"received_ts": received_ts,
|
||||
"target_user_id": target_user_id,
|
||||
"user_id": user_id,
|
||||
"reason": reason,
|
||||
}
|
||||
|
||||
return HTTPStatus.OK, response
|
||||
|
||||
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_user_report(resolved_report_id):
|
||||
return HTTPStatus.OK, {}
|
||||
|
||||
raise NotFoundError("User report not found")
|
||||
@@ -2277,6 +2277,132 @@ class RoomWorkerStore(CacheInvalidationWorkerStore):
|
||||
|
||||
return True
|
||||
|
||||
async def get_user_report(
|
||||
self, report_id: int
|
||||
) -> tuple[int, int, str, str, str] | None:
|
||||
"""Retrieve a user report
|
||||
|
||||
Args:
|
||||
report_id: ID of user report in database
|
||||
Returns:
|
||||
JSON dict of information from a user report or None if the
|
||||
report does not exist.
|
||||
"""
|
||||
|
||||
return await self.db_pool.simple_select_one(
|
||||
table="user_reports",
|
||||
keyvalues={"id": report_id},
|
||||
retcols=("id", "received_ts", "target_user_id", "user_id", "reason"),
|
||||
allow_none=True,
|
||||
desc="get_user_report",
|
||||
)
|
||||
|
||||
async def get_user_reports_paginate(
|
||||
self,
|
||||
start: int,
|
||||
limit: int,
|
||||
direction: Direction = Direction.BACKWARDS,
|
||||
user_id: str | None = None,
|
||||
target_user_id: str | None = None,
|
||||
) -> tuple[list[JsonDict], int]:
|
||||
"""Retrieve a paginated list of user reports
|
||||
|
||||
Args:
|
||||
start: event offset to begin the query from
|
||||
limit: number of rows to retrieve
|
||||
direction: Whether to fetch the most recent first (backwards) or the
|
||||
oldest first (forwards)
|
||||
user_id: search for user_id of the reporter. Ignored if user_id is None
|
||||
target_user_id: search for user_id of the target. Ignored if target_user_id is None
|
||||
Returns:
|
||||
Tuple of:
|
||||
json list of user reports
|
||||
total number of user reports matching the filter criteria
|
||||
"""
|
||||
|
||||
def _get_user_reports_paginate_txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> tuple[list[dict[str, Any]], int]:
|
||||
filters = []
|
||||
args: list[object] = []
|
||||
|
||||
if user_id:
|
||||
filters.append("user_id LIKE ?")
|
||||
args.extend(["%" + user_id + "%"])
|
||||
if target_user_id:
|
||||
filters.append("target_user_id LIKE ?")
|
||||
args.extend(["%" + target_user_id + "%"])
|
||||
|
||||
if direction == Direction.BACKWARDS:
|
||||
order = "DESC"
|
||||
else:
|
||||
order = "ASC"
|
||||
|
||||
where_clause = "WHERE " + " AND ".join(filters) if len(filters) > 0 else ""
|
||||
|
||||
sql = f"""
|
||||
SELECT COUNT(*) as total_user_reports
|
||||
FROM user_reports {where_clause}
|
||||
"""
|
||||
txn.execute(sql, args)
|
||||
count = cast(tuple[int], txn.fetchone())[0]
|
||||
|
||||
sql = f"""
|
||||
SELECT
|
||||
id,
|
||||
received_ts,
|
||||
target_user_id,
|
||||
user_id,
|
||||
reason
|
||||
FROM user_reports
|
||||
{where_clause}
|
||||
ORDER BY received_ts {order}
|
||||
LIMIT ?
|
||||
OFFSET ?
|
||||
"""
|
||||
|
||||
args += [limit, start]
|
||||
txn.execute(sql, args)
|
||||
|
||||
user_reports = []
|
||||
for row in txn:
|
||||
user_reports.append(
|
||||
{
|
||||
"id": row[0],
|
||||
"received_ts": row[1],
|
||||
"target_user_id": row[2],
|
||||
"user_id": row[3],
|
||||
"reason": row[4],
|
||||
}
|
||||
)
|
||||
|
||||
return user_reports, count
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"get_user_reports_paginate", _get_user_reports_paginate_txn
|
||||
)
|
||||
|
||||
async def delete_user_report(self, report_id: int) -> bool:
|
||||
"""Remove a user 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="user_reports",
|
||||
keyvalues={"id": report_id},
|
||||
desc="delete_user_report",
|
||||
)
|
||||
except StoreError:
|
||||
# Deletion failed because report does not exist
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def set_room_is_public(self, room_id: str, is_public: bool) -> None:
|
||||
await self.db_pool.simple_update_one(
|
||||
table="rooms",
|
||||
|
||||
@@ -0,0 +1,644 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2026 Element Creations Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
|
||||
|
||||
from twisted.internet.testing import MemoryReactor
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.rest.client import login, reporting, room
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import JsonDict
|
||||
from synapse.util.clock import Clock
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class UserReportsTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
reporting.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||
self.admin_user = self.register_user("admin", "pass", admin=True)
|
||||
self.admin_user_tok = self.login("admin", "pass")
|
||||
|
||||
self.users = {}
|
||||
for i in range(10):
|
||||
self.users[i] = self.register_user(f"user{i}", "pass")
|
||||
|
||||
# users 1 and 2 report all other users
|
||||
reporter_1_tok = self.login(self.users[0], "pass")
|
||||
reporter_2_tok = self.login(self.users[1], "pass")
|
||||
for num, user in self.users.items():
|
||||
if num <= 1:
|
||||
continue
|
||||
if num % 2 == 0:
|
||||
self._report_user(user, reporter_1_tok)
|
||||
else:
|
||||
self._report_user(user, reporter_2_tok)
|
||||
|
||||
self.url = "/_synapse/admin/v1/user_reports"
|
||||
|
||||
def test_no_auth(self) -> None:
|
||||
"""
|
||||
Try to get a user report without authentication.
|
||||
"""
|
||||
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"])
|
||||
|
||||
def test_requester_is_no_admin(self) -> None:
|
||||
"""
|
||||
If the user is not a server admin, an error 403 is returned.
|
||||
"""
|
||||
rando_tok = self.login(self.users[4], "pass")
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url,
|
||||
access_token=rando_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(403, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
||||
|
||||
def test_default_success(self) -> None:
|
||||
"""
|
||||
Testing list of reported users
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 8)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 8)
|
||||
self.assertNotIn("next_token", channel.json_body)
|
||||
self._check_fields(channel.json_body["user_reports"])
|
||||
|
||||
def test_limit(self) -> None:
|
||||
"""
|
||||
Testing list of reported users with limit
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?limit=5",
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 8)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 5)
|
||||
self.assertEqual(channel.json_body["next_token"], 5)
|
||||
self._check_fields(channel.json_body["user_reports"])
|
||||
|
||||
def test_from(self) -> None:
|
||||
"""
|
||||
Testing list of reported users with a defined starting point (from)
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?from=5",
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 8)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 3)
|
||||
self.assertNotIn("next_token", channel.json_body)
|
||||
self._check_fields(channel.json_body["user_reports"])
|
||||
|
||||
def test_limit_and_from(self) -> None:
|
||||
"""
|
||||
Testing list of reported users with a defined starting point and limit
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?from=2&limit=5",
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 8)
|
||||
self.assertEqual(channel.json_body["next_token"], 7)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 5)
|
||||
self._check_fields(channel.json_body["user_reports"])
|
||||
|
||||
def test_filter_by_target_user_id(self) -> None:
|
||||
"""
|
||||
Testing list of reported users with a filter of target_user_id
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?target_user_id=%s" % self.users[3],
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 1)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 1)
|
||||
self.assertNotIn("next_token", channel.json_body)
|
||||
self._check_fields(channel.json_body["user_reports"])
|
||||
|
||||
for report in channel.json_body["user_reports"]:
|
||||
self.assertEqual(report["target_user_id"], self.users[3])
|
||||
|
||||
def test_filter_user(self) -> None:
|
||||
"""
|
||||
Testing list of reported users with a filter of reporting user
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?user_id=%s" % self.users[0],
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 4)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 4)
|
||||
self.assertNotIn("next_token", channel.json_body)
|
||||
self._check_fields(channel.json_body["user_reports"])
|
||||
|
||||
for report in channel.json_body["user_reports"]:
|
||||
self.assertEqual(report["user_id"], self.users[0])
|
||||
|
||||
def test_filter_user_and_target_user(self) -> None:
|
||||
"""
|
||||
Testing list of reported users with a filter of reporting user and target_user
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?user_id=%s&target_user_id=%s" % (self.users[1], self.users[7]),
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 1)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 1)
|
||||
self.assertNotIn("next_token", channel.json_body)
|
||||
self._check_fields(channel.json_body["user_reports"])
|
||||
|
||||
for report in channel.json_body["user_reports"]:
|
||||
self.assertEqual(report["user_id"], self.users[1])
|
||||
self.assertEqual(report["target_user_id"], self.users[7])
|
||||
|
||||
def test_valid_search_order(self) -> None:
|
||||
"""
|
||||
Testing search order. Order by timestamps.
|
||||
"""
|
||||
|
||||
# fetch the most recent first, largest timestamp
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?dir=b",
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 8)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 8)
|
||||
report = 1
|
||||
while report < len(channel.json_body["user_reports"]):
|
||||
self.assertGreaterEqual(
|
||||
channel.json_body["user_reports"][report - 1]["received_ts"],
|
||||
channel.json_body["user_reports"][report]["received_ts"],
|
||||
)
|
||||
report += 1
|
||||
|
||||
# fetch the oldest first, smallest timestamp
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?dir=f",
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 8)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 8)
|
||||
report = 1
|
||||
while report < len(channel.json_body["user_reports"]):
|
||||
self.assertLessEqual(
|
||||
channel.json_body["user_reports"][report - 1]["received_ts"],
|
||||
channel.json_body["user_reports"][report]["received_ts"],
|
||||
)
|
||||
report += 1
|
||||
|
||||
def test_invalid_search_order(self) -> None:
|
||||
"""
|
||||
Testing that a invalid search order returns a 400
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?dir=bar",
|
||||
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(
|
||||
"Query parameter 'dir' must be one of ['b', 'f']",
|
||||
channel.json_body["error"],
|
||||
)
|
||||
|
||||
def test_limit_is_negative(self) -> None:
|
||||
"""
|
||||
Testing that a negative limit parameter returns a 400
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?limit=-5",
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
|
||||
|
||||
def test_from_is_negative(self) -> None:
|
||||
"""
|
||||
Testing that a negative from parameter returns a 400
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?from=-5",
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
|
||||
|
||||
def test_next_token(self) -> None:
|
||||
"""
|
||||
Testing that `next_token` appears at the right place
|
||||
"""
|
||||
|
||||
# `next_token` does not appear
|
||||
# Number of results is the number of entries
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?limit=8",
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 8)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 8)
|
||||
self.assertNotIn("next_token", channel.json_body)
|
||||
|
||||
# `next_token` does not appear
|
||||
# Number of max results is larger than the number of entries
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?limit=10",
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 8)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 8)
|
||||
self.assertNotIn("next_token", channel.json_body)
|
||||
|
||||
# `next_token` does appear
|
||||
# Number of max results is smaller than the number of entries
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?limit=6",
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 8)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 6)
|
||||
self.assertEqual(channel.json_body["next_token"], 6)
|
||||
|
||||
# Check
|
||||
# Set `from` to value of `next_token` for request remaining entries
|
||||
# `next_token` does not appear
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url + "?from=6",
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(channel.json_body["total"], 8)
|
||||
self.assertEqual(len(channel.json_body["user_reports"]), 2)
|
||||
self.assertNotIn("next_token", channel.json_body)
|
||||
|
||||
def _report_user(self, target_user: str, reporter_tok: str) -> None:
|
||||
"""Report a user"""
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
"_matrix/client/v3/users/%s/report" % (target_user),
|
||||
{"reason": "stone-cold bummer"},
|
||||
access_token=reporter_tok,
|
||||
shorthand=False,
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
|
||||
def _check_fields(self, content: list[JsonDict]) -> None:
|
||||
"""Checks that all attributes are present in a user report"""
|
||||
for c in content:
|
||||
self.assertIn("id", c)
|
||||
self.assertIn("received_ts", c)
|
||||
self.assertIn("user_id", c)
|
||||
self.assertIn("target_user_id", c)
|
||||
self.assertIn("reason", c)
|
||||
|
||||
|
||||
class UserReportDetailTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
reporting.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||
self.admin_user = self.register_user("admin", "pass", admin=True)
|
||||
self.admin_user_tok = self.login("admin", "pass")
|
||||
|
||||
self.bad_user = self.register_user("user", "pass")
|
||||
self.bad_user_tok = self.login("user", "pass")
|
||||
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
"_matrix/client/v3/users/%s/report" % (self.bad_user),
|
||||
{"reason": "stone-cold bummer"},
|
||||
access_token=self.admin_user_tok,
|
||||
shorthand=False,
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
|
||||
# first created user report gets `id`=2
|
||||
self.url = "/_synapse/admin/v1/user_reports/2"
|
||||
|
||||
def test_no_auth(self) -> None:
|
||||
"""
|
||||
Try to get user report without authentication.
|
||||
"""
|
||||
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"])
|
||||
|
||||
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(
|
||||
"GET",
|
||||
self.url,
|
||||
access_token=self.bad_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(403, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
||||
|
||||
def test_default_success(self) -> None:
|
||||
"""
|
||||
Testing get a reported user
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
self.url,
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
self._check_fields(channel.json_body)
|
||||
|
||||
def test_invalid_report_id(self) -> None:
|
||||
"""
|
||||
Testing that an invalid `report_id` returns a 400.
|
||||
"""
|
||||
|
||||
# `report_id` is negative
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"/_synapse/admin/v1/user_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(
|
||||
"GET",
|
||||
"/_synapse/admin/v1/user_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(
|
||||
"GET",
|
||||
"/_synapse/admin/v1/user_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(
|
||||
"GET",
|
||||
"/_synapse/admin/v1/user_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("User report not found", channel.json_body["error"])
|
||||
|
||||
def _check_fields(self, content: JsonDict) -> None:
|
||||
"""Checks that all attributes are present in a user report"""
|
||||
self.assertIn("id", content)
|
||||
self.assertIn("received_ts", content)
|
||||
self.assertIn("target_user_id", content)
|
||||
self.assertIn("user_id", content)
|
||||
self.assertIn("reason", content)
|
||||
|
||||
|
||||
class DeleteUserReportTestCase(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.bad_user = self.register_user("user", "pass")
|
||||
self.other_user_tok = self.login("user", "pass")
|
||||
|
||||
# create report
|
||||
self.get_success(
|
||||
self._store.add_user_report(
|
||||
self.bad_user,
|
||||
self.admin_user,
|
||||
"super bummer",
|
||||
self.clock.time_msec(),
|
||||
)
|
||||
)
|
||||
|
||||
self.url = "/_synapse/admin/v1/user_reports/2"
|
||||
|
||||
def test_no_auth(self) -> None:
|
||||
"""
|
||||
Try to delete user 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/user_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/user_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/user_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/user_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("User report not found", channel.json_body["error"])
|
||||
Reference in new issue
Block a user