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 to find a user based on its external ID in an auth provider. #13810

Merged
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/13810.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an admin API endpoint to find a user based on its external ID in an auth provider.
38 changes: 38 additions & 0 deletions docs/admin_api/user_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1155,3 +1155,41 @@ GET /_synapse/admin/v1/username_available?username=$localpart

The request and response format is the same as the
[/_matrix/client/r0/register/available](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-register-available) API.

### Find a user based on their ID in an auth provider

The API is:

```
GET /_synapse/admin/v1/auth_providers/$provider/users/$external_id
```

When a user matched the given ID for the given provider, 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:

- `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.
- `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.
sandhose marked this conversation as resolved.
Show resolved Hide resolved

The `external_id` may have characters that are not URL-safe (typically `/`, `:` or `@`), 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"
}
```
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved

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

# Some servlets only get registered for the main process.
if hs.config.worker.worker_app is None:
Expand Down
27 changes: 27 additions & 0 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -1156,3 +1156,30 @@ async def on_GET(
"rooms": by_room_data,
},
}


class UserByExternalId(RestServlet):
"""Find a user based on an external ID from an auth provider"""

PATTERNS = admin_patterns(
"/auth_providers/(?P<provider>[^/]*)/users/(?P<external_id>[^/]*)"
)

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

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

user_id = await self._store.get_user_by_external_id(provider, external_id)

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

return HTTPStatus.OK, {"user_id": user_id}
87 changes: 87 additions & 0 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4140,3 +4140,90 @@ def test_success(self) -> None:
{"b": 2},
channel.json_body["account_data"]["rooms"]["test_room"]["m.per_room"],
)


class UsersByExternalIdTestCase(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.record_user_external_id(
"the-auth-provider", "the-external-id", self.other_user
)
)
self.get_success(
self.store.record_user_external_id(
"another-auth-provider", "a:complex@external/id", self.other_user
)
)

def test_no_auth(self) -> None:
"""Try to lookup a user without authentication."""
url = (
"/_synapse/admin/v1/auth_providers/the-auth-provider/users/the-external-id"
)

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_binding_does_not_exist(self) -> None:
"""Tests that a lookup for an external ID that does not exist returns a 404"""
url = "/_synapse/admin/v1/auth_providers/the-auth-provider/users/unknown-id"

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 external ID lookup"""
url = (
"/_synapse/admin/v1/auth_providers/the-auth-provider/users/the-external-id"
)

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,
)

def test_success_urlencoded(self) -> None:
"""Tests a successful external ID lookup with an url-encoded ID"""
url = "/_synapse/admin/v1/auth_providers/another-auth-provider/users/a%3Acomplex%40external%2Fid"

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,
)