This commit is contained in:
Andrew Morgan
2026-01-16 16:49:12 +00:00
parent d45fd845b3
commit 2aa16a8222
5 changed files with 58 additions and 44 deletions
+14 -14
View File
@@ -47,11 +47,11 @@ 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!
- `client_secret_filepath` - path to a plaintext file containing the client secret. If set, this is used instead of `client_secret`.
- `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!
- `client_secret_filepath` - path to a plaintext file containing the client secret. If set, this is used instead of `client_secret`.
Example configuration:
@@ -81,19 +81,19 @@ Then, add the following to your MAS config file:
```yaml
policy:
data:
admin_clients:
- 000000000000000000000G0EST
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
# 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
@@ -24,9 +24,9 @@ from synapse.module_api.errors import ConfigError
from synapse.types import UserID
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
from synapse_guest_module.mas_admin_client import MasAdminClient
logger = logging.getLogger("synapse.contrib." + __name__)
@@ -106,22 +106,30 @@ class GuestModule:
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")
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"
)
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")
raise ConfigError(
"Config option 'mas.client_id' is required and must be a string"
)
client_secret = mas_config.get("client_secret")
if client_secret is not None:
if not isinstance(client_secret, str) or len(client_secret.strip()) == 0:
if (
not isinstance(client_secret, str)
or len(client_secret.strip()) == 0
):
raise ConfigError(
"Config option 'mas.client_secret' must be a string"
)
@@ -129,7 +137,10 @@ class GuestModule:
client_secret_filepath = mas_config.get("client_secret_filepath")
if client_secret_filepath is not None:
if not isinstance(client_secret_filepath, str) or len(client_secret_filepath.strip()) == 0:
if (
not isinstance(client_secret_filepath, str)
or len(client_secret_filepath.strip()) == 0
):
raise ConfigError(
"Config option 'mas.client_secret_filepath' must be a string"
)
@@ -139,7 +150,7 @@ class GuestModule:
raise ConfigError(
"Config option 'mas.client_secret' or 'mas.client_secret_filepath' is required"
)
if client_secret is not None and client_secret_filepath is not None:
raise ConfigError(
"Config option 'mas.client_secret' and 'mas.client_secret_filepath' are mutually exclusive"
@@ -87,9 +87,7 @@ class GuestRegistrationServlet(DirectServeJsonResource):
localpart, displayname + self._config.display_name_suffix
)
device_id, access_token, _, _ = await self._api.register_device(
user_id
)
device_id, access_token, _, _ = await self._api.register_device(user_id)
else:
logger.info("Registering MAS guest user with username '%s'", localpart)
mas_user_id = await self._mas_admin_client.create_user(localpart)
@@ -112,10 +110,11 @@ class GuestRegistrationServlet(DirectServeJsonResource):
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
)
(
device_id,
access_token,
) = await self._mas_admin_client.create_personal_session(
mas_user_id, expires_in
)
logger.debug("Registered user '%s'", user_id)
@@ -145,6 +144,4 @@ class GuestRegistrationServlet(DirectServeJsonResource):
},
)
await self._api.run_db_interaction(
"guest_module_store_mas_user", store_user
)
await self._api.run_db_interaction("guest_module_store_mas_user", store_user)
@@ -106,6 +106,12 @@ class GuestUserReaper:
logger.error('Failed to delete user "%s": %s', user_id, e)
async def _deactivate_expired_mas_users(self) -> None:
"""Deactivate all MAS users that are older than the specified expiration
interval. This uses the MAS admin API to disable the user.
"""
assert self._mas_admin_client is not None
if self._mas_tables_ready is not None:
await self._mas_tables_ready.wait()
@@ -25,10 +25,10 @@ class MasAdminClient:
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.
"""
@@ -40,8 +40,8 @@ class MasAdminClient:
post_json={"username": username},
headers={"Authorization": [f"Bearer {token}"]},
)
mas_user_id = response.get("data", {}).get("id")
mas_user_id: str = 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")
@@ -109,7 +109,7 @@ class MasAdminClient:
if not isinstance(access_token, str) or len(access_token) == 0:
raise ValueError("MAS token response missing access_token")
return access_token
def _load_client_secret(self) -> str:
"""Source the MAS client secret from either configuration or a file."""
if self._config.client_secret_filepath is not None:
@@ -141,13 +141,13 @@ class MasAdminClient:
self, url: str, data: Dict[str, str], headers: Dict[str, Any]
) -> Any:
http_client = self._api.http_client
post_urlencoded = getattr(http_client, "post_urlencoded_get_json", None)
if callable(post_urlencoded):
post_urlencoded: Optional[Awaitable[Any]] = getattr(
http_client, "post_urlencoded_get_json", None
)
if post_urlencoded is not None and callable(post_urlencoded):
return await post_urlencoded(url, data, headers=headers)
logger.debug(
"MAS client falling back to post_json_get_json for %s", url
)
logger.debug("MAS client falling back to post_json_get_json for %s", url)
return await http_client.post_json_get_json(
uri=url, post_json=data, headers=headers
)