From 801d28f3ca4a581966510e4d343f0c9f19704d6e Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 14 Jan 2026 16:38:35 +0000 Subject: [PATCH] Create a personal session on MAS In order to get an access token for a user, one needs to create a personal session on MAS. We now do so, and extract the access token and device ID from the response. TODO: We're handing back the MAS user ID as the device ID. Is that correct? --- .../guest_registration_servlet.py | 26 ++++++++-- .../synapse_guest_module/mas_admin_client.py | 50 ++++++++++++++++++- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/modules/restricted-guests/synapse/synapse_guest_module/guest_registration_servlet.py b/modules/restricted-guests/synapse/synapse_guest_module/guest_registration_servlet.py index f60ce87b1f..41e2be9ed6 100644 --- a/modules/restricted-guests/synapse/synapse_guest_module/guest_registration_servlet.py +++ b/modules/restricted-guests/synapse/synapse_guest_module/guest_registration_servlet.py @@ -74,13 +74,20 @@ class GuestRegistrationServlet(DirectServeJsonResource): continue if self._mas_admin_client is None: - logger.info("Registering local Synapse guest user with localpart '%s'", localpart) + logger.info( + "Registering local Synapse guest user with localpart '%s'", + localpart, + ) user_id = await self._api.register_user( localpart, displayname + self._config.display_name_suffix ) + + device_id, access_token, _, _ = await self._api.register_device( + user_id + ) else: logger.info("Registering MAS guest user with username '%s'", localpart) - await self._mas_admin_client.create_user(localpart) + mas_user_id = await self._mas_admin_client.create_user(localpart) user_id = self._api.get_qualified_user_id(localpart) @@ -89,7 +96,20 @@ class GuestRegistrationServlet(DirectServeJsonResource): displayname + self._config.display_name_suffix, ) - device_id, access_token, _, _ = await self._api.register_device(user_id) + # Determine how long to keep the access token valid for. + # + # If a user reaper is enabled, just have the token expire after + # the configured period. + expires_in = ( + self._config.user_expiration_seconds + if self._config.enable_user_reaper + else 0 + ) + device_id, access_token = ( + await self._mas_admin_client.create_personal_session( + mas_user_id, expires_in + ) + ) logger.debug("Registered user '%s'", user_id) diff --git a/modules/restricted-guests/synapse/synapse_guest_module/mas_admin_client.py b/modules/restricted-guests/synapse/synapse_guest_module/mas_admin_client.py index 50fd38dae5..286a2e2f12 100644 --- a/modules/restricted-guests/synapse/synapse_guest_module/mas_admin_client.py +++ b/modules/restricted-guests/synapse/synapse_guest_module/mas_admin_client.py @@ -22,15 +22,61 @@ class MasAdminClient: self._admin_api_base_url = config.admin_api_base_url.rstrip("/") self._oauth_base_url = config.oauth_base_url.rstrip("/") - async def create_user(self, username: str) -> None: + async def create_user(self, username: str) -> str: + """Creates a new user in MAS with the given username. + + Args: + username: The username (localpart) of the user to create. + + Returns: + The MAS ID of the created user. + """ token = await self.request_admin_token() url = self._build_admin_url("/api/admin/v1/users") - await self._api.http_client.post_json_get_json( + response = await self._api.http_client.post_json_get_json( uri=url, post_json={"username": username}, headers={"Authorization": [f"Bearer {token}"]}, ) + + mas_user_id = response.get("data", {}).get("id") + if mas_user_id is None or not isinstance(mas_user_id, str): + raise ValueError("MAS user creation response missing `data.id` field") + + return mas_user_id + + async def create_personal_session( + self, mas_user_id: str, expires_in: int + ) -> tuple[str, str]: + token = await self.request_admin_token() + url = self._build_admin_url("/api/admin/v1/personal-sessions") + + request_body = { + "actor_user_id": mas_user_id, + "expires_in": expires_in, + "scope": "openid urn:matrix:client:api:*", + "human_name": "guest user", + } + + response = await self._api.http_client.post_json_get_json( + uri=url, + post_json=request_body, + headers={"Authorization": [f"Bearer {token}"]}, + ) + + data = response.get("data", {}) + attributes = data.get("attributes", {}) if isinstance(data, dict) else {} + access_token = attributes.get("access_token") + # TODO: Is this the correct device ID? + device_id = data.get("id") if isinstance(data, dict) else None + + if not isinstance(access_token, str) or len(access_token) == 0: + raise ValueError("MAS session response missing `access_token` field") + if not isinstance(device_id, str) or len(device_id) == 0: + raise ValueError("MAS session response missing device id") + + return device_id, access_token async def request_admin_token(self) -> str: url = self._build_oauth_url("/oauth2/token")