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

Add device management to admin API #7481

Merged
merged 6 commits into from
Jun 5, 2020
Merged
Show file tree
Hide file tree
Changes from 4 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/7481.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow server admins to list users's devices and logout specific devices.
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
211 changes: 211 additions & 0 deletions docs/admin_api/user_admin_api.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. contents::

Create or modify Account
========================

Expand Down Expand Up @@ -244,3 +246,212 @@ with a body of:
}

including an ``access_token`` of a server admin.


User devices
============

List all devices
----------------
Gets information about all devices for a specific ``user_id``.

**Parameters**
Copy link
Member

Choose a reason for hiding this comment

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

I'd suggest saying what the endpoint is called before listing the parameters, in the same way as https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/user_admin_api.rst#create-or-modify-account and similar.


The following query parameters are available:

- ``user_id`` - fully qualified: for example, ``@user:server.com``.

The following fields are possible in the JSON response body:

- ``devices`` - An array of objects, each containing information about a device.
Devices objects contain the following fields:

- ``device_id`` - Identifier of device.
- ``display_name`` - Display name set by the user for this device.
Absent if no name has been set.
- ``last_seen_ip`` - The IP address where this device was last seen.
(May be a few minutes out of date, for efficiency reasons).
- ``last_seen_ts`` - The timestamp (in milliseconds since the unix epoch) when this
devices was last seen. (May be a few minutes out of date, for efficiency reasons).
- ``user_id`` - Owner of device.

**Usage**

A standard request for query devices of an user:
richvdh marked this conversation as resolved.
Show resolved Hide resolved

::

GET /_synapse/admin/v2/users/<user_id>/devices

{}


Response:

.. code:: json

{
"devices": [
{
"device_id": "QBUAZIFURK",
"display_name": "android",
"last_seen_ip": "1.2.3.4",
"last_seen_ts": 1474491775024,
"user_id": "<user_id>"
},
{
"device_id": "AUIECTSRND",
"display_name": "ios",
"last_seen_ip": "1.2.3.5",
"last_seen_ts": 1474491775025,
"user_id": "<user_id>"
}
]
}

Delete multiple devices
------------------
Deletes the given devices for a specific ``user_id``, and invalidates
any access token associated with them.

**Parameters**

The following query parameters are available:

- ``user_id`` - fully qualified: for example, ``@user:server.com``.

The following fields are required in the JSON request body:

- ``devices`` - The list of device IDs to delete.

**Usage**

A standard request for delete devices:
richvdh marked this conversation as resolved.
Show resolved Hide resolved

::

POST /_synapse/admin/v2/users/<user_id>/delete_devices

{
"devices": [
"QBUAZIFURK",
"AUIECTSRND"
],
}


Response:

.. code:: json

{}

Show a device
---------------
Gets information on a single device, by ``device_id`` for a specific ``user_id``.

**Parameters**

The following query parameters are available:

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

The following fields are possible in the JSON response body:

- ``device_id`` - Identifier of device.
- ``display_name`` - Display name set by the user for this device.
Absent if no name has been set.
- ``last_seen_ip`` - The IP address where this device was last seen.
(May be a few minutes out of date, for efficiency reasons).
- ``last_seen_ts`` - The timestamp (in milliseconds since the unix epoch) when this
devices was last seen. (May be a few minutes out of date, for efficiency reasons).
- ``user_id`` - Owner of device.


**Usage**

A standard request for get a device:
richvdh marked this conversation as resolved.
Show resolved Hide resolved

::

GET /_synapse/admin/v2/users/<user_id>/devices/<device_id>

{}


Response:

.. code:: json

{
"device_id": "<device_id>",
"display_name": "android",
"last_seen_ip": "1.2.3.4",
"last_seen_ts": 1474491775024,
"user_id": "<user_id>"
}

Update a device
---------------
Updates the metadata on the given ``device_id`` for a specific ``user_id``.

**Parameters**

The following query parameters are available:

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

The following fields are required in the JSON request body:

- ``display_name`` - The new display name for this device. If not given,
the display name is unchanged.

**Usage**

A standard request for update a device:
richvdh marked this conversation as resolved.
Show resolved Hide resolved

::

PUT /_synapse/admin/v2/users/<user_id>/devices/<device_id>

{
"display_name": "My other phone"
}


Response:

.. code:: json

{}

Delete a device
---------------
Deletes the given ``device_id`` for a specific ``user_id``,
and invalidates any access token associated with it.

**Parameters**

The following query parameters are available:

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

**Usage**

A standard request for delete a device:

::

DELETE /_synapse/admin/v2/users/<user_id>/devices/<device_id>

{}


Response:

.. code:: json

{}
6 changes: 6 additions & 0 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
from synapse.rest.admin.users import (
AccountValidityRenewServlet,
DeactivateAccountRestServlet,
DeleteDevicesRestServlet,
DeviceRestServlet,
DevicesRestServlet,
ResetPasswordRestServlet,
SearchUsersRestServlet,
UserAdminServlet,
Expand Down Expand Up @@ -202,6 +205,9 @@ def register_servlets(hs, http_server):
UserAdminServlet(hs).register(http_server)
UserRestServletV2(hs).register(http_server)
UsersRestServletV2(hs).register(http_server)
DeviceRestServlet(hs).register(http_server)
DevicesRestServlet(hs).register(http_server)
DeleteDevicesRestServlet(hs).register(http_server)


def register_servlets_for_client_rest_resource(hs, http_server):
Expand Down
138 changes: 138 additions & 0 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,144 @@ async def on_PUT(self, request, user_id):
return 201, ret


class DeviceRestServlet(RestServlet):
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
PATTERNS = (
re.compile(
"^/_synapse/admin/v2/users/(?P<user_id>[^/]*)/devices/(?P<device_id>[^/]*)$"
),
)

"""
Get, update or delete the given user's device
"""
dklimpel marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, hs):
"""
Args:
hs (synapse.server.HomeServer): server
Copy link
Member

Choose a reason for hiding this comment

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

putting the type in the docstring isn't something we should do for new code. instead, we should have type annotations on the parameters.

"""
super(DeviceRestServlet, self).__init__()
self.hs = hs
self.auth = hs.get_auth()
self.device_handler = hs.get_device_handler()
self.store = hs.get_datastore()

async def on_GET(self, request, user_id, device_id):
await assert_requester_is_admin(self.auth, request)

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

u = await self.store.get_user_by_id(target_user.to_string())
if u is None:
raise NotFoundError("Unknown user")

device = await self.device_handler.get_device(
target_user.to_string(), device_id
)
return 200, device

async def on_DELETE(self, request, user_id, device_id):
await assert_requester_is_admin(self.auth, request)

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

u = await self.store.get_user_by_id(target_user.to_string())
if u is None:
raise NotFoundError("Unknown user")

await self.device_handler.delete_device(target_user.to_string(), device_id)
return 200, {}

async def on_PUT(self, request, user_id, device_id):
await assert_requester_is_admin(self.auth, request)

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

u = await self.store.get_user_by_id(target_user.to_string())
if u is None:
raise NotFoundError("Unknown user")

body = parse_json_object_from_request(request, allow_empty_body=True)
await self.device_handler.update_device(
target_user.to_string(), device_id, body
)
return 200, {}


class DevicesRestServlet(RestServlet):
PATTERNS = (re.compile("^/_synapse/admin/v2/users/(?P<user_id>[^/]*)/devices$"),)

"""
Retrieve the given user's devices
"""

def __init__(self, hs):
"""
Args:
hs (synapse.server.HomeServer): server
"""
self.hs = hs
self.auth = hs.get_auth()
self.device_handler = hs.get_device_handler()
self.store = hs.get_datastore()

async def on_GET(self, request, user_id):
await assert_requester_is_admin(self.auth, request)

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

u = await self.store.get_user_by_id(target_user.to_string())
if u is None:
raise NotFoundError("Unknown user")

devices = await self.device_handler.get_devices_by_user(target_user.to_string())
return 200, {"devices": devices}


class DeleteDevicesRestServlet(RestServlet):
"""
API for bulk deletion of devices. Accepts a JSON object with a devices
key which lists the device_ids to delete.
"""

PATTERNS = (
re.compile("^/_synapse/admin/v2/users/(?P<user_id>[^/]*)/delete_devices$"),
)

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

async def on_POST(self, request, user_id):
await assert_requester_is_admin(self.auth, request)

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

u = await self.store.get_user_by_id(target_user.to_string())
if u is None:
raise NotFoundError("Unknown user")

body = parse_json_object_from_request(request, allow_empty_body=False)
assert_params_in_dict(body, ["devices"])

await self.device_handler.delete_devices(
target_user.to_string(), body["devices"]
)
return 200, {}


class UserRegisterServlet(RestServlet):
"""
Attributes:
Expand Down
Loading