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

Commit

Permalink
Add an admin APIs to allow server admins to list users' pushers (#8610)
Browse files Browse the repository at this point in the history
Add an admin API `GET /_synapse/admin/v1/users/<user_id>/pushers` like https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushers
  • Loading branch information
dklimpel authored Oct 28, 2020
1 parent 29ce6d4 commit 2239813
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 1 deletion.
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":"[email protected]"
}
],
"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):
"""
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="[email protected]",
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

0 comments on commit 2239813

Please sign in to comment.