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

Commit 74f60ce

Browse files
authored
Add an admin API endpoint to find a user based on its external ID in an auth provider. (#13810)
1 parent f7a77ad commit 74f60ce

File tree

5 files changed

+155
-0
lines changed

5 files changed

+155
-0
lines changed

changelog.d/13810.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add an admin API endpoint to find a user based on its external ID in an auth provider.

docs/admin_api/user_admin_api.md

+38
Original file line numberDiff line numberDiff line change
@@ -1155,3 +1155,41 @@ GET /_synapse/admin/v1/username_available?username=$localpart
11551155

11561156
The request and response format is the same as the
11571157
[/_matrix/client/r0/register/available](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-register-available) API.
1158+
1159+
### Find a user based on their ID in an auth provider
1160+
1161+
The API is:
1162+
1163+
```
1164+
GET /_synapse/admin/v1/auth_providers/$provider/users/$external_id
1165+
```
1166+
1167+
When a user matched the given ID for the given provider, an HTTP code `200` with a response body like the following is returned:
1168+
1169+
```json
1170+
{
1171+
"user_id": "@hello:example.org"
1172+
}
1173+
```
1174+
1175+
**Parameters**
1176+
1177+
The following parameters should be set in the URL:
1178+
1179+
- `provider` - The ID of the authentication provider, as advertised by the [`GET /_matrix/client/v3/login`](https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3login) API in the `m.login.sso` authentication method.
1180+
- `external_id` - The user ID from the authentication provider. Usually corresponds to the `sub` claim for OIDC providers, or to the `uid` attestation for SAML2 providers.
1181+
1182+
The `external_id` may have characters that are not URL-safe (typically `/`, `:` or `@`), so it is advised to URL-encode those parameters.
1183+
1184+
**Errors**
1185+
1186+
Returns a `404` HTTP status code if no user was found, with a response body like this:
1187+
1188+
```json
1189+
{
1190+
"errcode":"M_NOT_FOUND",
1191+
"error":"User not found"
1192+
}
1193+
```
1194+
1195+
_Added in Synapse 1.68.0._

synapse/rest/admin/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
SearchUsersRestServlet,
8181
ShadowBanRestServlet,
8282
UserAdminServlet,
83+
UserByExternalId,
8384
UserMembershipRestServlet,
8485
UserRegisterServlet,
8586
UserRestServletV2,
@@ -275,6 +276,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
275276
ListDestinationsRestServlet(hs).register(http_server)
276277
RoomMessagesRestServlet(hs).register(http_server)
277278
RoomTimestampToEventRestServlet(hs).register(http_server)
279+
UserByExternalId(hs).register(http_server)
278280

279281
# Some servlets only get registered for the main process.
280282
if hs.config.worker.worker_app is None:

synapse/rest/admin/users.py

+27
Original file line numberDiff line numberDiff line change
@@ -1156,3 +1156,30 @@ async def on_GET(
11561156
"rooms": by_room_data,
11571157
},
11581158
}
1159+
1160+
1161+
class UserByExternalId(RestServlet):
1162+
"""Find a user based on an external ID from an auth provider"""
1163+
1164+
PATTERNS = admin_patterns(
1165+
"/auth_providers/(?P<provider>[^/]*)/users/(?P<external_id>[^/]*)"
1166+
)
1167+
1168+
def __init__(self, hs: "HomeServer"):
1169+
self._auth = hs.get_auth()
1170+
self._store = hs.get_datastores().main
1171+
1172+
async def on_GET(
1173+
self,
1174+
request: SynapseRequest,
1175+
provider: str,
1176+
external_id: str,
1177+
) -> Tuple[int, JsonDict]:
1178+
await assert_requester_is_admin(self._auth, request)
1179+
1180+
user_id = await self._store.get_user_by_external_id(provider, external_id)
1181+
1182+
if user_id is None:
1183+
raise NotFoundError("User not found")
1184+
1185+
return HTTPStatus.OK, {"user_id": user_id}

tests/rest/admin/test_user.py

+87
Original file line numberDiff line numberDiff line change
@@ -4140,3 +4140,90 @@ def test_success(self) -> None:
41404140
{"b": 2},
41414141
channel.json_body["account_data"]["rooms"]["test_room"]["m.per_room"],
41424142
)
4143+
4144+
4145+
class UsersByExternalIdTestCase(unittest.HomeserverTestCase):
4146+
4147+
servlets = [
4148+
synapse.rest.admin.register_servlets,
4149+
login.register_servlets,
4150+
]
4151+
4152+
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
4153+
self.store = hs.get_datastores().main
4154+
4155+
self.admin_user = self.register_user("admin", "pass", admin=True)
4156+
self.admin_user_tok = self.login("admin", "pass")
4157+
4158+
self.other_user = self.register_user("user", "pass")
4159+
self.get_success(
4160+
self.store.record_user_external_id(
4161+
"the-auth-provider", "the-external-id", self.other_user
4162+
)
4163+
)
4164+
self.get_success(
4165+
self.store.record_user_external_id(
4166+
"another-auth-provider", "a:complex@external/id", self.other_user
4167+
)
4168+
)
4169+
4170+
def test_no_auth(self) -> None:
4171+
"""Try to lookup a user without authentication."""
4172+
url = (
4173+
"/_synapse/admin/v1/auth_providers/the-auth-provider/users/the-external-id"
4174+
)
4175+
4176+
channel = self.make_request(
4177+
"GET",
4178+
url,
4179+
)
4180+
4181+
self.assertEqual(401, channel.code, msg=channel.json_body)
4182+
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
4183+
4184+
def test_binding_does_not_exist(self) -> None:
4185+
"""Tests that a lookup for an external ID that does not exist returns a 404"""
4186+
url = "/_synapse/admin/v1/auth_providers/the-auth-provider/users/unknown-id"
4187+
4188+
channel = self.make_request(
4189+
"GET",
4190+
url,
4191+
access_token=self.admin_user_tok,
4192+
)
4193+
4194+
self.assertEqual(404, channel.code, msg=channel.json_body)
4195+
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
4196+
4197+
def test_success(self) -> None:
4198+
"""Tests a successful external ID lookup"""
4199+
url = (
4200+
"/_synapse/admin/v1/auth_providers/the-auth-provider/users/the-external-id"
4201+
)
4202+
4203+
channel = self.make_request(
4204+
"GET",
4205+
url,
4206+
access_token=self.admin_user_tok,
4207+
)
4208+
4209+
self.assertEqual(200, channel.code, msg=channel.json_body)
4210+
self.assertEqual(
4211+
{"user_id": self.other_user},
4212+
channel.json_body,
4213+
)
4214+
4215+
def test_success_urlencoded(self) -> None:
4216+
"""Tests a successful external ID lookup with an url-encoded ID"""
4217+
url = "/_synapse/admin/v1/auth_providers/another-auth-provider/users/a%3Acomplex%40external%2Fid"
4218+
4219+
channel = self.make_request(
4220+
"GET",
4221+
url,
4222+
access_token=self.admin_user_tok,
4223+
)
4224+
4225+
self.assertEqual(200, channel.code, msg=channel.json_body)
4226+
self.assertEqual(
4227+
{"user_id": self.other_user},
4228+
channel.json_body,
4229+
)

0 commit comments

Comments
 (0)