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

Add an Admin API endpoint for looking up users based on 3PID #14405

Merged
merged 5 commits into from
Nov 11, 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/14405.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an [Admin API](https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/index.html) endpoint for user lookup based on third-party ID (3PID). Contributed by @ashfame.
39 changes: 39 additions & 0 deletions docs/admin_api/user_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1197,3 +1197,42 @@ Returns a `404` HTTP status code if no user was found, with a response body like
```

_Added in Synapse 1.68.0._


### Find a user based on their Third Party ID (ThreePID or 3PID)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is only going to find local users, i.e. we won't contact an identity server to lookup a 3PID bound to another homeserver? If so, it'd be good to say so in a short sentence.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that was my intention. I am not sure if its supposed to do remote user lookups too?

Copy link
Contributor

Choose a reason for hiding this comment

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

It shouldn't. The function that gets called is

async def get_user_id_by_threepid(self, medium: str, address: str) -> Optional[str]:
"""Returns user id from threepid
Args:
medium: threepid medium e.g. email
address: threepid address e.g. me@example.com. This must already be
in canonical form.
Returns:
The user ID or None if no user id/threepid mapping exists
"""
user_id = await self.db_pool.runInteraction(
"get_user_id_by_threepid", self.get_user_id_by_threepid_txn, medium, address
)
return user_id
def get_user_id_by_threepid_txn(
self, txn: LoggingTransaction, medium: str, address: str
) -> Optional[str]:
"""Returns user id from threepid
Args:
txn (cursor):
medium: threepid medium e.g. email
address: threepid address e.g. me@example.com
Returns:
user id, or None if no user id/threepid mapping exists
"""
ret = self.db_pool.simple_select_one_txn(
txn,
"user_threepids",
{"medium": medium, "address": address},
["user_id"],
True,
)
if ret:
return ret["user_id"]
return None

which amounts to SELECT user_id FROM user_threepids WHERE medium = ? AND address = ? LIMIT 1. AFAIK that table only contains threepids for local users.


The API is:

```
GET /_synapse/admin/v1/threepid/$medium/users/$address
```
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved

When a user matched the given address for the given medium, an HTTP code `200` with a response body like the following is returned:

```json
{
"user_id": "@hello:example.org"
}
```

**Parameters**

The following parameters should be set in the URL:

- `medium` - Kind of third-party ID, either `email` or `msisdn`.
- `address` - Value of the third-party ID.

The `address` may have characters that are not URL-safe, so it is advised to URL-encode those parameters.

**Errors**

Returns a `404` HTTP status code if no user was found, with a response body like this:

```json
{
"errcode":"M_NOT_FOUND",
"error":"User not found"
}
```

_Added in Synapse 1.72.0._
2 changes: 2 additions & 0 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
ShadowBanRestServlet,
UserAdminServlet,
UserByExternalId,
UserByThreePid,
UserMembershipRestServlet,
UserRegisterServlet,
UserRestServletV2,
Expand Down Expand Up @@ -277,6 +278,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
RoomMessagesRestServlet(hs).register(http_server)
RoomTimestampToEventRestServlet(hs).register(http_server)
UserByExternalId(hs).register(http_server)
UserByThreePid(hs).register(http_server)

# Some servlets only get registered for the main process.
if hs.config.worker.worker_app is None:
Expand Down
25 changes: 25 additions & 0 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -1224,3 +1224,28 @@ async def on_GET(
raise NotFoundError("User not found")

return HTTPStatus.OK, {"user_id": user_id}


class UserByThreePid(RestServlet):
"""Find a user based on 3PID of a particular medium"""

PATTERNS = admin_patterns("/threepid/(?P<medium>[^/]*)/users/(?P<address>[^/]*)")

def __init__(self, hs: "HomeServer"):
self._auth = hs.get_auth()
self._store = hs.get_datastores().main

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

user_id = await self._store.get_user_id_by_threepid(medium, address)

if user_id is None:
raise NotFoundError("User not found")

return HTTPStatus.OK, {"user_id": user_id}
107 changes: 94 additions & 13 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,12 @@


class UserRegisterTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
profile.register_servlets,
]

def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:

self.url = "/_synapse/admin/v1/register"

self.registration_handler = Mock()
Expand Down Expand Up @@ -446,7 +444,6 @@ def test_register_mau_limit_reached(self) -> None:


class UsersListTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
Expand Down Expand Up @@ -1108,7 +1105,6 @@ def _validate_attributes_of_device_response(self, response: JsonDict) -> None:


class DeactivateAccountTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
Expand Down Expand Up @@ -1382,7 +1378,6 @@ def _is_erased(self, user_id: str, expect: bool) -> None:


class UserRestTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
Expand Down Expand Up @@ -2803,7 +2798,6 @@ def _check_fields(self, content: JsonDict) -> None:


class UserMembershipRestTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
Expand Down Expand Up @@ -2960,7 +2954,6 @@ def test_get_rooms_with_nonlocal_user(self) -> None:


class PushersRestTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
Expand Down Expand Up @@ -3089,7 +3082,6 @@ def test_get_pushers(self) -> None:


class UserMediaRestTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
Expand Down Expand Up @@ -3881,7 +3873,6 @@ def test_mau_limit(self) -> None:
],
)
class WhoisRestTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
Expand Down Expand Up @@ -3961,7 +3952,6 @@ def test_get_whois_user(self) -> None:


class ShadowBanRestTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
Expand Down Expand Up @@ -4042,7 +4032,6 @@ def test_success(self) -> None:


class RateLimitTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
Expand Down Expand Up @@ -4268,7 +4257,6 @@ def test_success(self) -> None:


class AccountDataTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
Expand Down Expand Up @@ -4358,7 +4346,6 @@ def test_success(self) -> None:


class UsersByExternalIdTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
Expand Down Expand Up @@ -4442,3 +4429,97 @@ def test_success_urlencoded(self) -> None:
{"user_id": self.other_user},
channel.json_body,
)


class UsersByThreePidTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
]

def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.store = hs.get_datastores().main

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.get_success(
self.store.user_add_threepid(
self.other_user, "email", "user@email.com", 1, 1
)
)
self.get_success(
self.store.user_add_threepid(self.other_user, "msidn", "+1-12345678", 1, 1)
)

def test_no_auth(self) -> None:
"""Try to look up a user without authentication."""
url = "/_synapse/admin/v1/threepid/email/users/user%40email.com"

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

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

def test_medium_does_not_exist(self) -> None:
"""Tests that both a lookup for a medium that does not exist and a user that
doesn't exist with that third party ID returns a 404"""
# test for unknown medium
url = "/_synapse/admin/v1/threepid/publickey/users/unknown-key"

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

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

# test for unknown user with a known medium
url = "/_synapse/admin/v1/threepid/email/users/unknown"

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

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

def test_success(self) -> None:
"""Tests a successful medium + address lookup"""
# test for email medium with encoded value of user@email.com
url = "/_synapse/admin/v1/threepid/email/users/user%40email.com"

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

self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(
{"user_id": self.other_user},
channel.json_body,
)

# test for msidn medium with encoded value of +1-12345678
url = "/_synapse/admin/v1/threepid/msidn/users/%2B1-12345678"

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

self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual(
{"user_id": self.other_user},
channel.json_body,
)