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

Add an admin APIs to allow server admins to list users' pushers #8610

Merged
merged 7 commits into from
Oct 28, 2020
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/8610.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an admin APIs to allow server admins to list users' pushers. Contributed by @dklimpel.
79 changes: 79 additions & 0 deletions docs/admin_api/user_admin_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -611,3 +611,82 @@ The following parameters should be set in the URL:

- ``user_id`` - fully qualified: for example, ``@user:server.com``.
- ``device_id`` - The device to delete.

List all pushers
================
Gets information about all pushers for a specific ``user_id``.

The API is::

GET /_synapse/admin/v1/users/<user_id>/pushers

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

{
"pushers": [
{
"app_display_name":"HTTP Push Notifications",
"app_id":"m.http",
"data": {
"url":"example.com"
},
"device_display_name":"pushy push",
"kind":"http",
"lang":"None",
"profile_tag":"",
"pushkey":"a@example.com"
}
],
"total": 1
}

**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:

- ``pushers`` - An array containing the current pushers for the user

- ``app_display_name`` - string - A string that will allow the user to identify
what application owns this pusher.

- ``app_id`` - string - This is a reverse-DNS style identifier for the application.
Max length, 64 chars.

- ``data`` - A dictionary of information for the pusher implementation itself.

- ``url`` - string - Required if ``kind`` is ``http``. The URL to use to send
notifications to.

- ``format`` - string - The format to use when sending notifications to the
Push Gateway.

- ``device_display_name`` - string - A string that will allow the user to identify
what device owns this pusher.

- ``profile_tag`` - string - This string determines which set of device specific rules
this pusher executes.

- ``kind`` - string - The kind of pusher. "http" is a pusher that sends HTTP pokes.
- ``lang`` - string - The preferred language for receiving notifications
(e.g. 'en' or 'en-US')

- ``profile_tag`` - string - This string determines which set of device specific rules
this pusher executes.

- ``pushkey`` - string - This is a unique identifier for this pusher.
Max length, 512 bytes.

- ``total`` - integer - Number of pushers.

See also `Client-Server API Spec <https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushers>`_
4 changes: 3 additions & 1 deletion synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from synapse.rest.admin.users import (
AccountValidityRenewServlet,
DeactivateAccountRestServlet,
PushersRestServlet,
ResetPasswordRestServlet,
SearchUsersRestServlet,
UserAdminServlet,
Expand Down Expand Up @@ -226,8 +227,9 @@ def register_servlets(hs, http_server):
DeviceRestServlet(hs).register(http_server)
DevicesRestServlet(hs).register(http_server)
DeleteDevicesRestServlet(hs).register(http_server)
EventReportsRestServlet(hs).register(http_server)
EventReportDetailRestServlet(hs).register(http_server)
EventReportsRestServlet(hs).register(http_server)
PushersRestServlet(hs).register(http_server)


def register_servlets_for_client_rest_resource(hs, http_server):
Expand Down
52 changes: 52 additions & 0 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@

logger = logging.getLogger(__name__)

_GET_PUSHERS_ALLOWED_KEYS = {
"app_display_name",
"app_id",
"data",
"device_display_name",
"kind",
"lang",
"profile_tag",
"pushkey",
}


class UsersRestServlet(RestServlet):
PATTERNS = historical_admin_path_patterns("/users/(?P<user_id>[^/]*)$")
Expand Down Expand Up @@ -713,6 +724,47 @@ async def on_GET(self, request, user_id):
return 200, ret


class PushersRestServlet(RestServlet):
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
"""
Gets information about all pushers for a specific `user_id`.

Example:
http://localhost:8008/_synapse/admin/v1/users/
@user:server/pushers

Returns:
pushers: Dictionary containing pushers information.
total: Number of pushers in dictonary `pushers`.
"""

PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/pushers$")

def __init__(self, hs):
self.is_mine = hs.is_mine
self.store = hs.get_datastore()
self.auth = hs.get_auth()

async def on_GET(
self, request: SynapseRequest, user_id: str
) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self.auth, request)

if not self.is_mine(UserID.from_string(user_id)):
raise SynapseError(400, "Can only lookup local users")

if not await self.store.get_user_by_id(user_id):
raise NotFoundError("User not found")

pushers = await self.store.get_pushers_by_user_id(user_id)

filtered_pushers = [
{k: v for k, v in p.items() if k in _GET_PUSHERS_ALLOWED_KEYS}
for p in pushers
]

return 200, {"pushers": filtered_pushers, "total": len(filtered_pushers)}


class UserMediaRestServlet(RestServlet):
"""
Gets information about all uploaded local media for a specific `user_id`.
Expand Down
124 changes: 124 additions & 0 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,130 @@ def test_get_rooms(self):
self.assertEqual(number_rooms, len(channel.json_body["joined_rooms"]))


class PushersRestTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.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/pushers" % urllib.parse.quote(
self.other_user
)

def test_no_auth(self):
"""
Try to list pushers 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/pushers"
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/pushers"

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_pushers(self):
"""
Tests that a normal lookup for pushers is successfully
"""

# Get pushers
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(0, channel.json_body["total"])

# Register the pusher
other_user_token = self.login("user", "pass")
user_tuple = self.get_success(
self.store.get_user_by_access_token(other_user_token)
)
token_id = user_tuple["token_id"]

self.get_success(
self.hs.get_pusherpool().add_pusher(
user_id=self.other_user,
access_token=token_id,
kind="http",
app_id="m.http",
app_display_name="HTTP Push Notifications",
device_display_name="pushy push",
pushkey="a@example.com",
lang=None,
data={"url": "example.com"},
)
)

# Get pushers
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(1, channel.json_body["total"])

for p in channel.json_body["pushers"]:
self.assertIn("pushkey", p)
self.assertIn("kind", p)
self.assertIn("app_id", p)
self.assertIn("app_display_name", p)
self.assertIn("device_display_name", p)
self.assertIn("profile_tag", p)
self.assertIn("lang", p)
self.assertIn("url", p["data"])


class UserMediaRestTestCase(unittest.HomeserverTestCase):

servlets = [
Expand Down