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

Add admin API to get users' account data #11664

Merged
merged 4 commits into from
Jan 5, 2022
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/11664.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add admin API to get users' account data.
75 changes: 75 additions & 0 deletions docs/admin_api/user_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,81 @@ The following fields are returned in the JSON response body:
- `joined_rooms` - An array of `room_id`.
- `total` - Number of rooms.

## Account Data
Gets information about account data for a specific `user_id`.

The API is:

```
GET /_synapse/admin/v1/users/<user_id>/accountdata
```

A response body like the following is returned:

```json
{
"account_data": {
"global": {
"m.secret_storage.key.LmIGHTg5W": {
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
"iv": "fwjNZatxg==",
"mac": "eWh9kNnLWZUNOgnc="
},
"im.vector.hide_profile": {
"hide_profile": true
},
"org.matrix.preview_urls": {
"disable": false
},
"im.vector.riot.breadcrumb_rooms": {
"rooms": [
"!LxcBDAsDUVAfJDEo:matrix.org",
"!MAhRxqasbItjOqxu:matrix.org"
]
},
"m.accepted_terms": {
"accepted": [
"https://example.org/somewhere/privacy-1.2-en.html",
"https://example.org/somewhere/terms-2.0-en.html"
]
},
"im.vector.setting.breadcrumbs": {
"recent_rooms": [
"!MAhRxqasbItqxuEt:matrix.org",
"!ZtSaPCawyWtxiImy:matrix.org"
]
}
},
"rooms": {
"!GUdfZSHUJibpiVqHYd:matrix.org": {
"m.fully_read": {
"event_id": "$156334540fYIhZ:matrix.org"
}
},
"!tOZwOOiqwCYQkLhV:matrix.org": {
"m.fully_read": {
"event_id": "$xjsIyp4_NaVl2yPvIZs_k1Jl8tsC_Sp23wjqXPno"
}
}
}
}
}
```

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

- `account_data` - A map containing the account data for the user
- `global` - A map containing the global account data for the user
- `rooms` - A map containing the account data per room for the user

## User media

### List media uploaded by a user
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 @@ -69,6 +69,7 @@
from synapse.rest.admin.statistics import UserMediaStatisticsRestServlet
from synapse.rest.admin.username_available import UsernameAvailableRestServlet
from synapse.rest.admin.users import (
AccountDataRestServlet,
AccountValidityRenewServlet,
DeactivateAccountRestServlet,
PushersRestServlet,
Expand Down Expand Up @@ -255,6 +256,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
UserMediaStatisticsRestServlet(hs).register(http_server)
EventReportDetailRestServlet(hs).register(http_server)
EventReportsRestServlet(hs).register(http_server)
AccountDataRestServlet(hs).register(http_server)
PushersRestServlet(hs).register(http_server)
MakeRoomAdminRestServlet(hs).register(http_server)
ShadowBanRestServlet(hs).register(http_server)
Expand Down
30 changes: 30 additions & 0 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -1121,3 +1121,33 @@ async def on_DELETE(
await self.store.delete_ratelimit_for_user(user_id)

return HTTPStatus.OK, {}


class AccountDataRestServlet(RestServlet):
"""Retrieve the given user's account data"""

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

def __init__(self, hs: "HomeServer"):
self._auth = hs.get_auth()
self._store = hs.get_datastore()
self._is_mine_id = hs.is_mine_id

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_id(user_id):
raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only look up local users")

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

global_data, by_room_data = await self._store.get_account_data_for_user(user_id)
return HTTPStatus.OK, {
"account_data": {
"global": global_data,
"rooms": by_room_data,
},
}
90 changes: 90 additions & 0 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3883,3 +3883,93 @@ def test_success(self):
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
self.assertNotIn("messages_per_second", channel.json_body)
self.assertNotIn("burst_count", channel.json_body)


class AccountDataTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
]

def prepare(self, reactor, clock, hs) -> None:
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 = f"/_synapse/admin/v1/users/{self.other_user}/accountdata"

def test_no_auth(self) -> None:
"""Try to get information of a user without authentication."""
channel = self.make_request("GET", self.url, {})

self.assertEqual(HTTPStatus.UNAUTHORIZED, channel.code, msg=channel.json_body)
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])

def test_requester_is_no_admin(self) -> None:
"""If the user is not a server admin, an error is returned."""
other_user_token = self.login("user", "pass")

channel = self.make_request(
"GET",
self.url,
access_token=other_user_token,
)

self.assertEqual(HTTPStatus.FORBIDDEN, channel.code, msg=channel.json_body)
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])

def test_user_does_not_exist(self) -> None:
"""Tests that a lookup for a user that does not exist returns a 404"""
url = "/_synapse/admin/v1/users/@unknown_person:test/override_ratelimit"

channel = self.make_request(
"GET",
url,
access_token=self.admin_user_tok,
)

self.assertEqual(HTTPStatus.NOT_FOUND, channel.code, msg=channel.json_body)
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])

def test_user_is_not_local(self) -> None:
"""Tests that a lookup for a user that is not a local returns a 400"""
url = "/_synapse/admin/v1/users/@unknown_person:unknown_domain/accountdata"

channel = self.make_request(
"GET",
url,
access_token=self.admin_user_tok,
)

self.assertEqual(HTTPStatus.BAD_REQUEST, channel.code, msg=channel.json_body)
self.assertEqual("Can only look up local users", channel.json_body["error"])

def test_success(self) -> None:
"""Request account data should succeed for an admin."""

# add account data
self.get_success(
self.store.add_account_data_for_user(self.other_user, "m.global", {"a": 1})
)
self.get_success(
self.store.add_account_data_to_room(
self.other_user, "test_room", "m.per_room", {"b": 2}
)
)

channel = self.make_request(
"GET",
self.url,
access_token=self.admin_user_tok,
)
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
self.assertEqual(
{"a": 1}, channel.json_body["account_data"]["global"]["m.global"]
)
self.assertEqual(
{"b": 2},
channel.json_body["account_data"]["rooms"]["test_room"]["m.per_room"],
)