From 1cb888e4e726a34cbbcb5d8c9c5c38461ae4902a Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 14 Jan 2026 15:45:46 +0000 Subject: [PATCH] Configuration options for MAS In order to create and manage users with MAS enabled, we'll need to reach out to the MAS admin API. We can do so automatically by requesting an admin-enabled token, assuming a matching client has been configured on the MAS side. Add some config options for the guest module (OAuth2 client) side. --- modules/restricted-guests/synapse/README.md | 42 ++++++++++++++++++ .../synapse/synapse_guest_module/config.py | 12 +++++ .../synapse_guest_module/guest_module.py | 44 +++++++++++++++++-- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/modules/restricted-guests/synapse/README.md b/modules/restricted-guests/synapse/README.md index 11fbc70afe..e3ed2ccac8 100644 --- a/modules/restricted-guests/synapse/README.md +++ b/modules/restricted-guests/synapse/README.md @@ -41,6 +41,17 @@ The module provides (optional) configuration options: - `enable_user_reaper` - if true, the module disables all users that are older than the configured expiration time. Default: `true`. - `user_expiration_seconds` - the expiration time in seconds when a guest user expires after their creation. Default: `86400` (=24 hours). +If matrix-authentication-service (MAS) is configured, the module will need to +interface with it in order to register/deactivate users. Provide the below +options in order to give the module access to [MAS' Admin +API](https://element-hq.github.io/matrix-authentication-service/topics/admin-api.html). + +- `mas` - optional configuration for Matrix Authentication Service (MAS). When set, the module creates users via MAS' admin API. + - `admin_api_base_url` - Base URL for MAS' admin API (e.g. `https://mas.example.org`). Trailing slashes will be automatically stripped. + - `oauth_base_url` - Base URL for MAS' OAuth endpoints (defaults to `admin_api_base_url` if not set). Trailing slashes will be automatically stripped. + - `client_id` - client ID for the automated tool. Must be a valid [ULID](https://github.com/ulid/spec). Generate one [here](https://ulidtools.com/). + - `client_secret` - client secret for the automated tool. Ideally long and cryptographically secure. Keep it a secret! + Example configuration: ```yaml @@ -49,6 +60,37 @@ modules: config: # Use a german suffix display_name_suffix: " (Gast)" + # The below is required if using MAS + mas: + admin_api_base_url: https://mas.example.org + oauth_base_url: https://mas.example.org + # The `client_id` must be a valid ULID: + # https://github.com/ulid/spec + # Generate ULID's easily at: + # https://ulidtools.com/ + client_id: 000000000000000000000G0EST + client_secret: your-client-secret +``` + +Enable [the Admin API on a MAS +listener](https://element-hq.github.io/matrix-authentication-service/topics/admin-api.html#enabling-the-api). +Then, add the following to your MAS config file: + +```yaml +policy: + data: + admin_clients: + - 000000000000000000000G0EST + +# ... + +clients: + # The `client_id` must be a valid ULID https://github.com/ulid/spec + # Generate ULID's easily at: https://ulidtools.com/ + - client_id: 000000000000000000000G0EST + # The guest module uses the client_secret_basic authentication method. + client_auth_method: client_secret_basic + client_secret: your-client-secret ``` ## Production installation diff --git a/modules/restricted-guests/synapse/synapse_guest_module/config.py b/modules/restricted-guests/synapse/synapse_guest_module/config.py index a0ddbe569a..5e54f2f642 100644 --- a/modules/restricted-guests/synapse/synapse_guest_module/config.py +++ b/modules/restricted-guests/synapse/synapse_guest_module/config.py @@ -7,12 +7,24 @@ # Originally licensed under the Apache License, Version 2.0: # . +from typing import Optional + import attr +@attr.s(frozen=True, auto_attribs=True) +class MasConfig: + admin_api_base_url: str + oauth_base_url: str + client_id: str + # TODO: Add a filepath option for the secret as well. + client_secret: str + + @attr.s(frozen=True, auto_attribs=True) class GuestModuleConfig: user_id_prefix: str display_name_suffix: str enable_user_reaper: bool user_expiration_seconds: int + mas: Optional[MasConfig] = None diff --git a/modules/restricted-guests/synapse/synapse_guest_module/guest_module.py b/modules/restricted-guests/synapse/synapse_guest_module/guest_module.py index 156012c35d..174c3474f6 100644 --- a/modules/restricted-guests/synapse/synapse_guest_module/guest_module.py +++ b/modules/restricted-guests/synapse/synapse_guest_module/guest_module.py @@ -8,7 +8,7 @@ # . import logging -from typing import Any, Dict, Literal, Tuple, Union +from typing import Any, Dict, Literal, Optional, Tuple, Union from synapse.module_api import ( NOT_SPAM, @@ -21,7 +21,8 @@ from synapse.module_api import ( from synapse.module_api.errors import ConfigError from synapse.types import UserID -from synapse_guest_module.config import GuestModuleConfig +from synapse_guest_module.config import GuestModuleConfig, MasConfig +from synapse_guest_module.mas_admin_client import MasAdminClient from synapse_guest_module.guest_registration_servlet import GuestRegistrationServlet from synapse_guest_module.guest_user_reaper import GuestUserReaper @@ -33,7 +34,12 @@ class GuestModule: self._api = api self._config = config - self.registration_servlet = GuestRegistrationServlet(config, api) + mas_admin_client = ( + MasAdminClient(api, config.mas) if config.mas is not None else None + ) + self.registration_servlet = GuestRegistrationServlet( + config, api, mas_admin_client + ) self._api.register_web_resource( "/_synapse/client/register_guest", self.registration_servlet ) @@ -81,11 +87,43 @@ class GuestModule: "Config option 'user_expiration_seconds' must be a number" ) + mas_config = config.get("mas") + mas: Optional[MasConfig] = None + if mas_config is not None: + if not isinstance(mas_config, dict): + raise ConfigError("Config option 'mas' must be an object") + + admin_api_base_url = mas_config.get("admin_api_base_url") + if not isinstance(admin_api_base_url, str) or len(admin_api_base_url.strip()) == 0: + raise ConfigError("Config option 'mas.admin_api_base_url' is required and must be a string") + + oauth_base_url = mas_config.get("oauth_base_url", admin_api_base_url) + if not isinstance(oauth_base_url, str) or len(oauth_base_url.strip()) == 0: + raise ConfigError( + "Config option 'mas.oauth_base_url' must be a string" + ) + + client_id = mas_config.get("client_id") + if not isinstance(client_id, str) or len(client_id.strip()) == 0: + raise ConfigError("Config option 'mas.client_id' is required and must be a string") + + client_secret = mas_config.get("client_secret") + if not isinstance(client_secret, str) or len(client_secret.strip()) == 0: + raise ConfigError("Config option 'mas.client_secret' is required and must be a string") + + mas = MasConfig( + admin_api_base_url.strip(), + oauth_base_url.strip(), + client_id.strip(), + client_secret.strip(), + ) + return GuestModuleConfig( user_id_prefix, display_name_suffix, enable_user_reaper, user_expiration_seconds, + mas, ) async def profile_update(