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

Admin API for querying rooms where a user is a member #8306

Merged
merged 4 commits into from
Sep 18, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8306.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Admin API for querying rooms where a user is a member. Contributed by @dklimpel.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Admin API for querying rooms where a user is a member. Contributed by @dklimpel.
Add an admin API for querying rooms where a user is a member. Contributed by @dklimpel.

37 changes: 37 additions & 0 deletions docs/admin_api/user_admin_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,43 @@ To use it, you will need to authenticate by providing an ``access_token`` for a
server admin: see `README.rst <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/<user_id>/rooms

To use it, you will need to authenticate by providing an ``access_token`` for a
server admin: see `README.rst <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
============

Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
ResetPasswordRestServlet,
SearchUsersRestServlet,
UserAdminServlet,
UserMembershipRestServlet,
UserRegisterServlet,
UserRestServletV2,
UsersRestServlet,
Expand Down Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<user_id>[^/]+)/rooms$")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be /joined_rooms to match the CS API one. I also prefer it as it more clearly states what its doing


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)):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a preference for trying to avoid storing self.hs, as it can lead to circular dependencies not caught during start up due to the dependency injections. This is technically fine to do so here, and I know we do it elsewhere, but could we change this to specifically storing the function rather than hs? i.e.:

    def __init__(self, hs):
        self.is_mine = hs.is_min
        ...

    async def on_GET(self, request, 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)
if not room_ids:
raise NotFoundError("User not found")

ret = {"rooms": list(room_ids), "total": len(room_ids)}
return 200, ret
96 changes: 94 additions & 2 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]))