From cf3deec2325f2142d5ca7534f8548f920c09184f Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sun, 13 Sep 2020 12:32:00 +0200 Subject: [PATCH 1/4] Admin API for querying rooms where a user is a member Add a new admin API `GET /_synapse/admin/v1/users//rooms` to list all rooms where a user is a member. Fixes: #8299 --- docs/admin_api/user_admin_api.rst | 37 ++++++++++++ synapse/rest/admin/__init__.py | 2 + synapse/rest/admin/users.py | 27 +++++++++ tests/rest/admin/test_user.py | 96 ++++++++++++++++++++++++++++++- 4 files changed, 160 insertions(+), 2 deletions(-) diff --git a/docs/admin_api/user_admin_api.rst b/docs/admin_api/user_admin_api.rst index e21c78a9c62b..3f41b74e527f 100644 --- a/docs/admin_api/user_admin_api.rst +++ b/docs/admin_api/user_admin_api.rst @@ -304,6 +304,43 @@ To use it, you will need to authenticate by providing an ``access_token`` for a server admin: see `README.rst `_. +List room memberships of an user +================================ +Gets a list of all ``room_id`` that a specific ``user_id`` is member. + +The API is:: + + GET /_synapse/admin/v1/users//rooms + +To use it, you will need to authenticate by providing an ``access_token`` for a +server admin: see `README.rst `_. + +A response body like the following is returned: + +.. code:: json + + { + "rooms": [ + "!DuGcnbhHGaSZQoNQR:matrix.org", + "!ZtSaPCawyWtxfWiIy:matrix.org" + ], + "total": 2 + } + +**Parameters** + +The following parameters should be set in the URL: + +- ``user_id`` - fully qualified: for example, ``@user:server.com``. + +**Response** + +The following fields are returned in the JSON response body: + +- ``rooms`` - An array of ``room_id``. +- ``total`` - Number of rooms. + + User devices ============ diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 1c88c93f3836..fce67a15fb62 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -49,6 +49,7 @@ ResetPasswordRestServlet, SearchUsersRestServlet, UserAdminServlet, + UserMembershipRestServlet, UserRegisterServlet, UserRestServletV2, UsersRestServlet, @@ -209,6 +210,7 @@ def register_servlets(hs, http_server): SendServerNoticeServlet(hs).register(http_server) VersionServlet(hs).register(http_server) UserAdminServlet(hs).register(http_server) + UserMembershipRestServlet(hs).register(http_server) UserRestServletV2(hs).register(http_server) UsersRestServletV2(hs).register(http_server) DeviceRestServlet(hs).register(http_server) diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index f3e77da850c8..9ee37b60dcf6 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -29,6 +29,7 @@ parse_string, ) from synapse.rest.admin._base import ( + admin_patterns, assert_requester_is_admin, assert_user_is_admin, historical_admin_path_patterns, @@ -683,3 +684,29 @@ async def on_PUT(self, request, user_id): await self.store.set_server_admin(target_user, set_admin_to) return 200, {} + + +class UserMembershipRestServlet(RestServlet): + """ + Get room list of an user. + """ + + PATTERNS = admin_patterns("/users/(?P[^/]+)/rooms$") + + def __init__(self, hs): + self.hs = hs + self.auth = hs.get_auth() + self.store = hs.get_datastore() + + async def on_GET(self, request, user_id): + await assert_requester_is_admin(self.auth, request) + + if not self.hs.is_mine(UserID.from_string(user_id)): + raise SynapseError(400, "Can only lookup local users") + + room_ids = await self.store.get_rooms_for_user(user_id) + if not room_ids: + raise NotFoundError("User not found") + + ret = {"rooms": list(room_ids), "total": len(room_ids)} + return 200, ret diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index b8b7758d2447..f5c241203f39 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -22,8 +22,8 @@ import synapse.rest.admin from synapse.api.constants import UserTypes -from synapse.api.errors import HttpResponseException, ResourceLimitError -from synapse.rest.client.v1 import login +from synapse.api.errors import Codes, HttpResponseException, ResourceLimitError +from synapse.rest.client.v1 import login, room from synapse.rest.client.v2_alpha import sync from tests import unittest @@ -995,3 +995,95 @@ def test_accidental_deactivation_prevention(self): # Ensure they're still alive self.assertEqual(0, channel.json_body["deactivated"]) + + +class UserMembershipRestTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + sync.register_servlets, + room.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + 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.url = "/_synapse/admin/v1/users/%s/rooms" % urllib.parse.quote( + self.other_user + ) + + def test_no_auth(self): + """ + Try to list rooms of an user without authentication. + """ + request, channel = self.make_request("GET", self.url, b"{}") + self.render(request) + + self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) + + def test_requester_is_no_admin(self): + """ + If the user is not a server admin, an error is returned. + """ + other_user_token = self.login("user", "pass") + + request, channel = self.make_request( + "GET", self.url, access_token=other_user_token, + ) + self.render(request) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + def test_user_does_not_exist(self): + """ + Tests that a lookup for a user that does not exist returns a 404 + """ + url = "/_synapse/admin/v1/users/@unknown_person:test/rooms" + request, channel = self.make_request( + "GET", url, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(404, channel.code, msg=channel.json_body) + self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"]) + + def test_user_is_not_local(self): + """ + Tests that a lookup for a user that is not a local returns a 400 + """ + url = "/_synapse/admin/v1/users/@unknown_person:unknown_domain/rooms" + + request, channel = self.make_request( + "GET", url, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual("Can only lookup local users", channel.json_body["error"]) + + def test_get_rooms(self): + """ + Tests that a normal lookup for rooms is successfully + """ + # Create rooms and join + other_user_tok = self.login("user", "pass") + number_rooms = 5 + for n in range(number_rooms): + self.helper.create_room_as(self.other_user, tok=other_user_tok) + + # Get rooms + request, channel = self.make_request( + "GET", self.url, access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual(number_rooms, channel.json_body["total"]) + self.assertEqual(number_rooms, len(channel.json_body["rooms"])) From 5648239ab39a8dad31213f32ef4809ea4582e3e6 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sun, 13 Sep 2020 12:37:47 +0200 Subject: [PATCH 2/4] add changelog --- changelog.d/8306.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8306.feature diff --git a/changelog.d/8306.feature b/changelog.d/8306.feature new file mode 100644 index 000000000000..051213f6c202 --- /dev/null +++ b/changelog.d/8306.feature @@ -0,0 +1 @@ +Admin API for querying rooms where a user is a member. Contributed by @dklimpel. \ No newline at end of file From 66eb160d1b1fd1ea7e5d3cca0e8b46bafa112296 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Fri, 18 Sep 2020 14:28:20 +0200 Subject: [PATCH 3/4] Change API to `/joined_rooms` --- changelog.d/8306.feature | 2 +- docs/admin_api/user_admin_api.rst | 6 +++--- synapse/rest/admin/users.py | 6 +++--- tests/rest/admin/test_user.py | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/changelog.d/8306.feature b/changelog.d/8306.feature index 051213f6c202..5c23da4030ff 100644 --- a/changelog.d/8306.feature +++ b/changelog.d/8306.feature @@ -1 +1 @@ -Admin API for querying rooms where a user is a member. Contributed by @dklimpel. \ No newline at end of file +Add an admin API for querying rooms where a user is a member. Contributed by @dklimpel. \ No newline at end of file diff --git a/docs/admin_api/user_admin_api.rst b/docs/admin_api/user_admin_api.rst index 3f41b74e527f..7ca902faba25 100644 --- a/docs/admin_api/user_admin_api.rst +++ b/docs/admin_api/user_admin_api.rst @@ -310,7 +310,7 @@ Gets a list of all ``room_id`` that a specific ``user_id`` is member. The API is:: - GET /_synapse/admin/v1/users//rooms + GET /_synapse/admin/v1/users//joined_rooms To use it, you will need to authenticate by providing an ``access_token`` for a server admin: see `README.rst `_. @@ -320,7 +320,7 @@ A response body like the following is returned: .. code:: json { - "rooms": [ + "joined_rooms": [ "!DuGcnbhHGaSZQoNQR:matrix.org", "!ZtSaPCawyWtxfWiIy:matrix.org" ], @@ -337,7 +337,7 @@ The following parameters should be set in the URL: The following fields are returned in the JSON response body: -- ``rooms`` - An array of ``room_id``. +- ``joined_rooms`` - An array of ``room_id``. - ``total`` - Number of rooms. diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 9ee37b60dcf6..730ef536898b 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -691,17 +691,17 @@ class UserMembershipRestServlet(RestServlet): Get room list of an user. """ - PATTERNS = admin_patterns("/users/(?P[^/]+)/rooms$") + PATTERNS = admin_patterns("/users/(?P[^/]+)/joined_rooms$") def __init__(self, hs): - self.hs = hs + self.is_mine = hs.is_mine self.auth = hs.get_auth() self.store = hs.get_datastore() async def on_GET(self, request, user_id): await assert_requester_is_admin(self.auth, request) - if not self.hs.is_mine(UserID.from_string(user_id)): + if not self.is_mine(UserID.from_string(user_id)): raise SynapseError(400, "Can only lookup local users") room_ids = await self.store.get_rooms_for_user(user_id) diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index f5c241203f39..4568138ea998 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -1013,7 +1013,7 @@ def prepare(self, reactor, clock, hs): self.admin_user_tok = self.login("admin", "pass") self.other_user = self.register_user("user", "pass") - self.url = "/_synapse/admin/v1/users/%s/rooms" % urllib.parse.quote( + self.url = "/_synapse/admin/v1/users/%s/joined_rooms" % urllib.parse.quote( self.other_user ) @@ -1045,7 +1045,7 @@ def test_user_does_not_exist(self): """ Tests that a lookup for a user that does not exist returns a 404 """ - url = "/_synapse/admin/v1/users/@unknown_person:test/rooms" + url = "/_synapse/admin/v1/users/@unknown_person:test/joined_rooms" request, channel = self.make_request( "GET", url, access_token=self.admin_user_tok, ) @@ -1058,7 +1058,7 @@ def test_user_is_not_local(self): """ Tests that a lookup for a user that is not a local returns a 400 """ - url = "/_synapse/admin/v1/users/@unknown_person:unknown_domain/rooms" + url = "/_synapse/admin/v1/users/@unknown_person:unknown_domain/joined_rooms" request, channel = self.make_request( "GET", url, access_token=self.admin_user_tok, From 9c83471e8685c6e5f97fd7ce621ff4a80126031d Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Fri, 18 Sep 2020 14:34:00 +0200 Subject: [PATCH 4/4] small typo: `rooms` -> `joined_rooms` --- synapse/rest/admin/users.py | 2 +- tests/rest/admin/test_user.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 730ef536898b..0a3d0688ea2b 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -708,5 +708,5 @@ async def on_GET(self, request, user_id): if not room_ids: raise NotFoundError("User not found") - ret = {"rooms": list(room_ids), "total": len(room_ids)} + ret = {"joined_rooms": list(room_ids), "total": len(room_ids)} return 200, ret diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 4568138ea998..f96011fc1c8f 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -1086,4 +1086,4 @@ def test_get_rooms(self): self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(number_rooms, channel.json_body["total"]) - self.assertEqual(number_rooms, len(channel.json_body["rooms"])) + self.assertEqual(number_rooms, len(channel.json_body["joined_rooms"]))