config: align section docs and examples with the configuration reference

Enriches every config section's doc comments, schemars examples and
x-doc rendering hints so that the generated reference matches (and in
places improves on) the hand-written document: previously-undocumented
fields are now documented, rustdoc-only idioms that leaked into the
schema are gone, and a handful of typos are fixed. Metadata only - no
runtime behavior change.
This commit is contained in:
Quentin Gliech
2026-07-29 18:29:31 +02:00
parent 837614fd6c
commit c99d2e35aa
18 changed files with 860 additions and 397 deletions
+36 -18
View File
@@ -1,3 +1,4 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
@@ -27,71 +28,88 @@ const fn is_default_false(value: &bool) -> bool {
*value == default_false()
}
/// Configuration section to configure features related to account management
/// Configuration related to account management
#[expect(clippy::struct_excessive_bools)]
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
pub struct AccountConfig {
/// Whether users are allowed to change their email addresses. Defaults to
/// `true`.
/// Whether users are allowed to change their email addresses.
///
/// Defaults to `true`.
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
#[schemars(example = &true)]
pub email_change_allowed: bool,
/// Whether users are allowed to change their display names. Defaults to
/// `true`.
/// Whether users are allowed to change their display names.
///
/// Defaults to `true`.
/// This should be in sync with the policy in the homeserver configuration.
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
#[schemars(example = &true)]
pub displayname_change_allowed: bool,
/// Whether to enable self-service password registration. Defaults to
/// `false` if password authentication is enabled.
/// Whether to enable self-service password registration.
///
/// Defaults to `false`.
/// This has no effect if password login is disabled.
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
#[schemars(example = &false)]
pub password_registration_enabled: bool,
/// Whether self-service password registrations require a valid email.
/// Defaults to `true`.
/// Whether self-service registrations require a valid email.
///
/// Defaults to `true`.
/// This has no effect if password registration is disabled.
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
#[schemars(example = &true)]
pub password_registration_email_required: bool,
/// Whether users are allowed to change their passwords. Defaults to `true`.
/// Whether users are allowed to change their passwords.
///
/// Defaults to `true`.
/// This has no effect if password login is disabled.
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
#[schemars(example = &true)]
pub password_change_allowed: bool,
/// Whether email-based password recovery is enabled. Defaults to `false`.
/// Whether email-based password recovery is enabled.
///
/// Defaults to `false`.
/// This has no effect if password login is disabled.
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
#[schemars(example = &false)]
pub password_recovery_enabled: bool,
/// Whether registration tokens are required for password registrations.
///
/// Defaults to `false`.
///
/// When enabled, users must provide a valid registration token during
/// password registration. This has no effect if password registration
/// is disabled.
/// password registration. This has no effect if password registration is
/// disabled.
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
#[schemars(example = &false)]
pub password_registration_token_required: bool,
/// Whether users are allowed to delete their own account. Defaults to
/// `true`.
/// Whether users are allowed to delete their own account.
///
/// Defaults to `true`.
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
#[schemars(example = &true)]
pub account_deactivation_allowed: bool,
/// Whether users can log in with their email address. Defaults to `false`.
/// Whether users can log in with their email address.
///
/// Defaults to `false`.
/// This has no effect if password login is disabled.
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
#[schemars(example = &false)]
pub login_with_email_allowed: bool,
/// Whether registration tokens are required for password registrations
/// This is deprecated in favor of `password_registration_token_required`
/// Whether registration tokens are required for password registrations.
///
/// Deprecated: use `password_registration_token_required` instead.
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
#[schemars(example = &false)]
pub registration_token_required: bool,
}
+11 -3
View File
@@ -11,28 +11,36 @@ use url::Url;
use crate::ConfigurationSection;
/// Configuration section for tweaking the branding of the service
/// Configuration section for tweaking the branding of the service.
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Default)]
pub struct BrandingConfig {
/// A human-readable name. Defaults to the server's address.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub service_name: Option<String>,
/// Link to a privacy policy, displayed in the footer of web pages and
/// emails. It is also advertised to clients through the `op_policy_uri`
/// OIDC provider metadata.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub policy_uri: Option<Url>,
/// Link to a terms of service document, displayed in the footer of web
/// pages and emails. It is also advertised to clients through the
/// `op_tos_uri` OIDC provider metadata.
///
/// This also adds a mandatory checkbox during registration. The value of
/// this config item will be stored in the `user_terms` table to indicate
/// which `ToS` document the user accepted. Note that currently changing
/// this value will not force existing users to re-accept terms.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub tos_uri: Option<Url>,
/// Legal imprint, displayed in the footer in the footer of web pages and
/// emails.
/// Legal imprint, displayed in the footer of web pages and emails.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub imprint: Option<String>,
}
+17 -6
View File
@@ -1,3 +1,4 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
@@ -12,7 +13,7 @@ use crate::ConfigurationSection;
/// Which service should be used for CAPTCHA protection
#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, Serialize)]
pub enum CaptchaServiceKind {
/// Use Google's reCAPTCHA v2 API
/// Use Google's `reCAPTCHA` v2 API
#[serde(rename = "recaptcha_v2")]
RecaptchaV2,
@@ -20,24 +21,34 @@ pub enum CaptchaServiceKind {
#[serde(rename = "cloudflare_turnstile")]
CloudflareTurnstile,
/// Use ``HCaptcha``
/// Use `hCaptcha`
#[serde(rename = "hcaptcha")]
HCaptcha,
}
/// Configuration section to setup CAPTCHA protection on a few operations
/// Settings related to CAPTCHA protection
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Default)]
pub struct CaptchaConfig {
/// Which service should be used for CAPTCHA protection
/// Which service should be used for CAPTCHA protection. Set to `null` (or
/// `~`) to disable CAPTCHA protection
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &Option::<CaptchaServiceKind>::None)]
pub service: Option<CaptchaServiceKind>,
/// The site key to use
/// The site key to use.
///
/// The expected value depends on the chosen `service`.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub site_key: Option<String>,
/// The secret key to use
/// The secret key to use.
///
/// The expected value depends on the chosen `service`.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub secret_key: Option<String>,
}
+35 -11
View File
@@ -21,23 +21,21 @@ use super::{ClientSecret, ClientSecretRaw, ConfigurationSection};
#[derive(JsonSchema, Serialize, Deserialize, Copy, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ClientAuthMethodConfig {
/// `none`: No authentication
/// No authentication
None,
/// `client_secret_basic`: `client_id` and `client_secret` used as basic
/// authorization credentials
/// `client_id` and `client_secret` used as basic authorization credentials
ClientSecretBasic,
/// `client_secret_post`: `client_id` and `client_secret` sent in the
/// request body
/// `client_id` and `client_secret` sent in the request body
ClientSecretPost,
/// `client_secret_basic`: a `client_assertion` sent in the request body and
/// signed using the `client_secret`
/// A `client_assertion` sent in the request body and signed using the
/// `client_secret`
ClientSecretJwt,
/// `client_secret_basic`: a `client_assertion` sent in the request body and
/// signed by an asymmetric key
/// A `client_assertion` sent in the request body and signed by an
/// asymmetric key
PrivateKeyJwt,
}
@@ -61,15 +59,19 @@ pub struct ClientConfig {
#[schemars(
with = "String",
regex(pattern = r"^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$"),
description = "A ULID as per https://github.com/ulid/spec"
description = "A ULID as per https://github.com/ulid/spec",
example = &"000000000000000000000FIRST"
)]
pub client_id: Ulid,
/// Authentication method used for this client
#[schemars(example = &"client_secret_post")]
client_auth_method: ClientAuthMethodConfig,
/// Name of the `OAuth2` client
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"My Application")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub client_name: Option<String>,
/// Client URL for user-facing information about the client
@@ -86,15 +88,29 @@ pub struct ClientConfig {
/// The JSON Web Key Set (JWKS) used by the `private_key_jwt` authentication
/// method. Mutually exclusive with `jwks_uri`
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &serde_json::json!({
"keys": [{
"kid": "03e84aed4ef4431014e8617567864c4efaaaede9",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"e": "AQAB",
"n": "<base64url-encoded modulus>"
}]
}))]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub jwks: Option<PublicJsonWebKeySet>,
/// The URL of the JSON Web Key Set (JWKS) used by the `private_key_jwt`
/// authentication method. Mutually exclusive with `jwks`
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"https://example.com/.well-known/jwks.json")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub jwks_uri: Option<Url>,
/// List of allowed redirect URIs
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[schemars(example = &["https://example.com/callback"])]
pub redirect_uris: Vec<Url>,
}
@@ -209,7 +225,15 @@ impl ClientConfig {
}
}
/// List of OAuth 2.0/OIDC clients config
/// List of OAuth 2.0/OIDC clients and their keys/secrets. Each `client_id` must
/// be a [ULID](https://github.com/ulid/spec).
///
/// <!-- more -->
///
/// **Note:** any additions or modifications in this list are synced with the
/// database on server startup. Removed entries are only removed with the
/// [`config sync --prune`](./cli/config.md#config-sync---prune---dry-run)
/// command.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct ClientsConfig(#[schemars(with = "Vec::<ClientConfig>")] Vec<ClientConfig>);
+45 -18
View File
@@ -80,7 +80,7 @@ pub enum PgSslMode {
Prefer,
/// Only try an SSL connection. If a root CA file is present, verify the
/// connection in the same way as if `VerifyCa` was specified.
/// connection in the same way as if `verify-ca` was specified.
Require,
/// Only try an SSL connection, and verify that the server certificate is
@@ -93,30 +93,42 @@ pub enum PgSslMode {
VerifyFull,
}
/// Database connection configuration
/// Configure how to connect to the PostgreSQL database.
///
/// MAS must not be connected to a database pooler (such as pgBouncer or pgCat)
/// when it is configured in transaction pooling mode.
/// See [the relevant section of the database
/// page](./database.md#a-warning-about-database-pooling-software) for more
/// information.
#[serde_as]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DatabaseConfig {
/// Connection URI
/// Full connection string as per
/// <https://www.postgresql.org/docs/13/libpq-connect.html#id-1.7.3.8.3.6>
///
/// This must not be specified if `host`, `port`, `socket`, `username`,
/// `password`, or `database` are specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(url, default = "default_connection_string")]
#[schemars(url, default = "default_connection_string", example = &"postgresql://user:password@hostname:5432/database?sslmode=require")]
pub uri: Option<String>,
/// Name of host to connect to
/// Alternatively, the connection can be configured with separate
/// parameters.
///
/// Name of host to connect to.
///
/// This must not be specified if `uri` is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option::<schema::Hostname>")]
#[schemars(with = "Option::<schema::Hostname>", example = &"hostname")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub host: Option<String>,
/// Port number to connect at the server host
///
/// This must not be specified if `uri` is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1, max = 65535))]
#[schemars(range(min = 1, max = 65535), example = &5432u16)]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub port: Option<u16>,
/// Directory containing the UNIX socket to connect to
@@ -124,18 +136,23 @@ pub struct DatabaseConfig {
/// This must not be specified if `uri` is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<String>")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub socket: Option<Utf8PathBuf>,
/// PostgreSQL user name to connect as
///
/// This must not be specified if `uri` is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"user")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub username: Option<String>,
/// Password to be used if the server demands password authentication
///
/// This must not be specified if `uri` is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"password")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub password: Option<String>,
/// Path to the password to be used if the server demands password
@@ -151,68 +168,78 @@ pub struct DatabaseConfig {
///
/// This must not be specified if `uri` is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"database")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub database: Option<String>,
/// How to handle SSL connections
/// Whether to use SSL to connect to the database
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &PgSslMode::Require)]
pub ssl_mode: Option<PgSslMode>,
/// The PEM-encoded root certificate for SSL connections
///
/// This must not be specified if the `ssl_ca_file` option is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub ssl_ca: Option<String>,
/// Path to the root certificate for SSL connections
///
/// This must not be specified if the `ssl_ca` option is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"/path/to/ca.pem")]
pub ssl_ca_file: Option<Utf8PathBuf>,
/// The PEM-encoded client certificate for SSL connections
/// Client certificate to present to the server when SSL is enabled.
///
/// The PEM-encoded client certificate for SSL connections.
///
/// This must not be specified if the `ssl_certificate_file` option is
/// specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub ssl_certificate: Option<String>,
/// Path to the client certificate for SSL connections
///
/// This must not be specified if the `ssl_certificate` option is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"/path/to/cert.pem")]
pub ssl_certificate_file: Option<Utf8PathBuf>,
/// The PEM-encoded client key for SSL connections
///
/// This must not be specified if the `ssl_key_file` option is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub ssl_key: Option<String>,
/// Path to the client key for SSL connections
///
/// This must not be specified if the `ssl_key` option is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"/path/to/key.pem")]
pub ssl_key_file: Option<Utf8PathBuf>,
/// Set the maximum number of connections the pool should maintain
#[serde(default = "default_max_connections")]
#[schemars(example = &10u32)]
pub max_connections: NonZeroU32,
/// Set the minimum number of connections the pool should maintain
#[serde(default)]
#[schemars(example = &0u32)]
pub min_connections: u32,
/// Set the amount of time to attempt connecting to the database
#[schemars(with = "u64")]
/// Set the amount of time to attempt connecting to the database, in seconds
#[schemars(with = "u64", example = &30u64)]
#[serde(default = "default_connect_timeout")]
#[serde_as(as = "serde_with::DurationSeconds<u64>")]
pub connect_timeout: Duration,
/// Set a maximum idle duration for individual connections
#[schemars(with = "Option<u64>")]
/// Set a maximum idle duration for individual connections, in seconds
#[schemars(with = "Option<u64>", example = &600u64)]
#[serde(
default = "default_idle_timeout",
skip_serializing_if = "Option::is_none"
@@ -220,8 +247,8 @@ pub struct DatabaseConfig {
#[serde_as(as = "Option<serde_with::DurationSeconds<u64>>")]
pub idle_timeout: Option<Duration>,
/// Set the maximum lifetime of individual connections
#[schemars(with = "u64")]
/// Set the maximum lifetime of individual connections, in seconds
#[schemars(with = "u64", example = &1800u64)]
#[serde(
default = "default_max_lifetime",
skip_serializing_if = "Option::is_none"
+31 -3
View File
@@ -1,3 +1,4 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
//
@@ -53,35 +54,59 @@ fn default_sendmail_command() -> Option<String> {
Some("sendmail".to_owned())
}
/// Configuration related to sending emails
/// Hand-authored rendering of the transport selection for the documentation.
///
/// The schema models the transport as a flat `transport` enum plus sibling
/// optional fields, but the reference groups them per transport, so the whole
/// block is authored here and the sibling fields are skipped in the docs.
const TRANSPORT_DOC_YAML: &str = r"# Default transport: don't send any emails
transport: blackhole
# Send emails using SMTP
#transport: smtp
#mode: plain | tls | starttls
#hostname: localhost
#port: 587
#username: username
#password: password
# Send emails by calling a local sendmail binary
#transport: sendmail
#command: /usr/sbin/sendmail";
/// Settings related to sending emails
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct EmailConfig {
/// Email address to use as From when sending emails
#[serde(default = "default_email")]
#[schemars(email)]
#[schemars(email, example = &r#""The almighty auth service" <auth@example.com>"#)]
pub from: String,
/// Email address to use as Reply-To when sending emails
#[serde(default = "default_email")]
#[schemars(email)]
#[schemars(email, example = &r#""No reply" <no-reply@example.com>"#)]
pub reply_to: String,
/// What backend should be used when sending emails
#[schemars(extend("x-doc" = serde_json::json!({ "yaml": TRANSPORT_DOC_YAML })))]
transport: EmailTransportKind,
/// SMTP transport: Connection mode to the relay
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))]
mode: Option<EmailSmtpMode>,
/// SMTP transport: Hostname to connect to
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<crate::schema::Hostname>")]
#[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))]
hostname: Option<String>,
/// SMTP transport: Port to connect to. Default is 25 for plain, 465 for TLS
/// and 587 for `StartTLS`
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1, max = 65535))]
#[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))]
port: Option<NonZeroU16>,
/// SMTP transport: Username for use to authenticate when connecting to the
@@ -89,6 +114,7 @@ pub struct EmailConfig {
///
/// Must be set if the `password` or `password_file` field is set
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))]
username: Option<String>,
/// SMTP transport: Password for use to authenticate when connecting to the
@@ -96,6 +122,7 @@ pub struct EmailConfig {
///
/// Must be set if the `username` but not `password_file` field is set
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))]
password: Option<String>,
/// SMTP transport: Path to the password for use to authenticate when
@@ -109,6 +136,7 @@ pub struct EmailConfig {
/// Sendmail transport: Command to use to send emails
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(default = "default_sendmail_command")]
#[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))]
command: Option<String>,
}
+36 -26
View File
@@ -1,3 +1,4 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
@@ -33,59 +34,71 @@ fn is_default_token_ttl(value: &Duration) -> bool {
#[serde_as]
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
pub struct InactiveSessionExpirationConfig {
/// Time after which an inactive session is automatically finished
#[schemars(with = "u64", range(min = 600, max = 7_776_000))]
/// Time after which an inactive session is automatically finished, in
/// seconds
#[schemars(with = "u64", range(min = 600, max = 7_776_000), example = &32400)]
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
pub ttl: Duration,
/// Should compatibility sessions expire after inactivity
/// Should compatibility sessions expire after inactivity. Defaults to true.
#[serde(default = "default_true")]
#[schemars(example = &true)]
pub expire_compat_sessions: bool,
/// Should OAuth 2.0 sessions expire after inactivity
/// Should OAuth 2.0 sessions expire after inactivity. Defaults to true.
#[serde(default = "default_true")]
#[schemars(example = &true)]
pub expire_oauth_sessions: bool,
/// Should user sessions expire after inactivity
/// Should user sessions expire after inactivity. Defaults to true.
#[serde(default = "default_true")]
#[schemars(example = &true)]
pub expire_user_sessions: bool,
}
/// Configuration sections for experimental options
/// Settings that may change or be removed in future versions.
/// Some of which are in this section because they don't have a stable place
/// in the configuration yet.
///
/// Do not change these options unless you know what you are doing.
#[serde_as]
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
pub struct ExperimentalConfig {
/// Time-to-live of access tokens in seconds. Defaults to 5 minutes.
#[schemars(with = "u64", range(min = 60, max = 86400))]
/// Time-to-live of OAuth 2.0 access tokens in seconds. Defaults to 300, 5
/// minutes.
#[schemars(with = "u64", range(min = 60, max = 86400), example = &300)]
#[serde(
default = "default_token_ttl",
skip_serializing_if = "is_default_token_ttl"
)]
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub access_token_ttl: Duration,
/// Time-to-live of compatibility access tokens in seconds. Defaults to 5
/// minutes.
#[schemars(with = "u64", range(min = 60, max = 86400))]
/// Time-to-live of compatibility access tokens in seconds, when refresh
/// tokens are supported. Defaults to 300, 5 minutes.
#[schemars(with = "u64", range(min = 60, max = 86400), example = &300)]
#[serde(
default = "default_token_ttl",
skip_serializing_if = "is_default_token_ttl"
)]
#[serde_as(as = "serde_with::DurationSeconds<i64>")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub compat_token_ttl: Duration,
/// Experimetal feature to automatically expire inactive sessions
/// Experimental feature to automatically expire inactive sessions.
///
/// Disabled by default
/// Disabled by default.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub inactive_session_expiration: Option<InactiveSessionExpirationConfig>,
/// Experimental feature to show a plan management tab and iframe.
/// This value is passed through "as is" to the client without any
/// validation.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"https://example.com/plan")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub plan_management_iframe_uri: Option<String>,
/// Experimental feature to limit the number of application sessions per
@@ -93,6 +106,7 @@ pub struct ExperimentalConfig {
///
/// Disabled by default.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub session_limit: Option<SessionLimitConfig>,
}
@@ -150,20 +164,18 @@ pub struct SessionLimitConfig {
/// This is not enforced in non-interactive contexts (like
/// `m.login.password` login with the compatibility API) as there is no
/// opportunity for us to show some UI for people remove some sessions.
/// See [`hard_limit`] for enforcement on that side.
/// See `hard_limit` for enforcement on that side.
///
/// This is the limit that is displayed in the UI
///
/// [`hard_limit`]: Self::hard_limit
#[schemars(example = &10)]
pub soft_limit: NonZeroU64,
/// Upon login, when `dangerous_hard_limit_eviction: false`, will refuse the
/// new login (policy violation error), otherwise, see
/// [`dangerous_hard_limit_eviction`].
/// `dangerous_hard_limit_eviction`.
///
/// The hard limit is enforced in all contexts
/// (interactive/non-interactive).
///
/// [`dangerous_hard_limit_eviction`]: Self::dangerous_hard_limit_eviction
#[schemars(example = &50)]
pub hard_limit: NonZeroU64,
/// When set, only accounts with <= `max_session_threshold` sessions have
/// the session limits applied.
@@ -175,9 +187,10 @@ pub struct SessionLimitConfig {
/// and you want to avoid breaking their operation while maintaining some
/// level of sanity with the number of devices that people can have.
/// This will prevent anyone else from crossing the limit.
#[schemars(example = &100)]
pub max_session_threshold: Option<NonZeroU64>,
/// Whether we should automatically choose the least recently used devices
/// to remove when the [`Self::hard_limit`] is reached; in order to
/// to remove when the `hard_limit` is reached; in order to
/// allow the new login to continue.
///
/// Disabled by default
@@ -187,7 +200,7 @@ pub struct SessionLimitConfig {
/// be recovered if you have another verified active device or have a
/// recovery key setup.
///
/// When using [`dangerous_hard_limit_eviction`], the [`hard_limit`] must be
/// When using `dangerous_hard_limit_eviction`, the `hard_limit` must be
/// at least 2 to avoid catastrophically losing encrypted history and
/// digital identity in pathological cases. Keep in mind this is a bare
/// minimum restriction and you can still run into trouble.
@@ -201,13 +214,10 @@ pub struct SessionLimitConfig {
///
/// Removing devices is a non-trivial task for some homeservers to tackle
/// and can cause lots of device list changes, `/sync`, federation, and
/// replication traffic. Consider using [`max_session_threshold`] to
/// replication traffic. Consider using `max_session_threshold` to
/// limit the size of accounts that are acted upon.
///
/// [`hard_limit`]: Self::hard_limit
/// [`dangerous_hard_limit_eviction`]: Self::dangerous_hard_limit_eviction
/// [`max_session_threshold`]: Self::max_session_threshold
#[serde(default = "default_false")]
#[schemars(example = &false)]
pub dangerous_hard_limit_eviction: bool,
}
+94 -66
View File
@@ -1,3 +1,4 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
//
@@ -82,34 +83,34 @@ impl UnixOrTcp {
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
#[serde(untagged)]
pub enum BindConfig {
/// Listen on the specified host and port
/// Listen on the given host and port combination
Listen {
/// Host on which to listen.
///
/// Defaults to listening on all addresses
/// Host on which to listen, defaults to all addresses
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"localhost")]
host: Option<String>,
/// Port on which to listen.
/// Port on which to listen
#[schemars(example = &8081u16)]
port: u16,
},
/// Listen on the specified address
/// Listen on the given address
Address {
/// Host and port on which to listen
#[schemars(
example = &"[::1]:8080",
example = &"[::]:8080",
example = &"[::1]:8080",
example = &"127.0.0.1:8080",
example = &"0.0.0.0:8080",
)]
address: String,
},
/// Listen on a UNIX domain socket
/// Listen on the given UNIX socket
Unix {
/// Path to the socket
#[schemars(with = "String")]
#[schemars(with = "String", example = &"/tmp/mas.sock")]
socket: Utf8PathBuf,
/// Permissions to use for the socket. Defaults to the process's umask.
@@ -118,21 +119,24 @@ pub enum BindConfig {
mode: Option<String>,
},
/// Accept connections on file descriptors passed by the parent process.
/// Grab an already open file descriptor given by the parent process.
///
/// This is useful for grabbing sockets passed by systemd.
/// This is useful when using systemd socket activation.
///
/// The file descriptor index is offset by 3, to account for the standard
/// input, output and error streams, so a value of `0` grabs the file
/// descriptor `3`.
///
/// See <https://www.freedesktop.org/software/systemd/man/sd_listen_fds.html>
FileDescriptor {
/// Index of the file descriptor. Note that this is offseted by 3
/// because of the standard input/output sockets, so setting
/// here a value of `0` will grab the file descriptor `3`
/// Index of the file descriptor to grab
#[serde(default)]
#[schemars(example = &1usize)]
fd: usize,
/// Whether the socket is a TCP socket or a UNIX domain socket. Defaults
/// to TCP.
/// Kind of socket that was passed, defaults to tcp
#[serde(default = "UnixOrTcp::tcp")]
#[schemars(example = &UnixOrTcp::Tcp)]
kind: UnixOrTcp,
},
}
@@ -140,45 +144,36 @@ pub enum BindConfig {
/// Configuration related to TLS on a listener
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
pub struct TlsConfig {
/// PEM-encoded X509 certificate chain
///
/// Exactly one of `certificate` or `certificate_file` must be set.
/// Inline PEM-encoded X509 certificate chain (alternative to
/// `certificate_file`)
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"<inline PEM>", extend("x-doc" = serde_json::json!({ "commented": true })))]
pub certificate: Option<String>,
/// File containing the PEM-encoded X509 certificate chain
///
/// Exactly one of `certificate` or `certificate_file` must be set.
/// Path to a file containing the PEM-encoded X509 certificate chain
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"/path/to/cert.pem")]
pub certificate_file: Option<Utf8PathBuf>,
/// PEM-encoded private key
///
/// Exactly one of `key` or `key_file` must be set.
/// Inline PEM-encoded private key (alternative to `key_file`)
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"<inline PEM>", extend("x-doc" = serde_json::json!({ "commented": true })))]
pub key: Option<String>,
/// File containing a PEM or DER-encoded private key
///
/// Exactly one of `key` or `key_file` must be set.
/// Path to a file containing a PEM or DER-encoded private key
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"/path/to/key.pem")]
pub key_file: Option<Utf8PathBuf>,
/// Password used to decode the private key
///
/// One of `password` or `password_file` must be set if the key is
/// encrypted.
/// Inline password used to decrypt the private key, if it is encrypted
/// (alternative to `password_file`)
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"<password to decrypt the key>", extend("x-doc" = serde_json::json!({ "commented": true })))]
pub password: Option<String>,
/// Password file used to decode the private key
///
/// One of `password` or `password_file` must be set if the key is
/// encrypted.
/// Path to a file containing the password used to decrypt the private key
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"/path/to/password.txt", extend("x-doc" = serde_json::json!({ "commented": true })))]
pub password_file: Option<Utf8PathBuf>,
}
@@ -258,51 +253,55 @@ impl TlsConfig {
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
#[serde(tag = "name", rename_all = "lowercase")]
pub enum Resource {
/// Healthcheck endpoint (/health)
/// Serves the health check endpoint on `/health`
Health,
/// Prometheus metrics endpoint (/metrics)
/// Serves a Prometheus-compatible metrics endpoint on `/metrics`, if the
/// Prometheus exporter is enabled in `telemetry.metrics.exporter`
Prometheus,
/// OIDC discovery endpoints
/// Serves the `.well-known/openid-configuration` document
Discovery,
/// Pages destined to be viewed by humans
/// Serves the human-facing pages, such as the login page
Human,
/// GraphQL endpoint
/// Serves the GraphQL API used by the frontend, and optionally the GraphQL
/// playground
GraphQL {
/// Enabled the GraphQL playground
/// Enable the GraphQL playground
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[schemars(example = &true)]
playground: bool,
/// Allow access for OAuth 2.0 clients (undocumented)
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))]
undocumented_oauth2_access: bool,
},
/// OAuth-related APIs
/// Serves the OAuth 2.0/OIDC endpoints
OAuth,
/// Matrix compatibility API
/// Serves the Matrix C-S API compatibility endpoints
Compat,
/// Static files
/// Serves the given folder on the `/assets/` path
Assets {
/// Path to the directory to serve.
/// Path to the directory to serve
#[serde(
default = "http_listener_assets_path_default",
skip_serializing_if = "is_default_http_listener_assets_path"
)]
#[schemars(with = "String")]
#[schemars(with = "String", example = &"./share/assets/")]
path: Utf8PathBuf,
},
/// Admin API, served at `/api/admin/v1`
/// Serves the admin API on the `/api/admin/v1/` path. Disabled by default
AdminApi,
/// Mount a "/connection-info" handler which helps debugging informations on
/// the upstream connection
/// Mounts a `/connection-info` handler which shows debugging information
/// about the upstream connection
#[serde(rename = "connection-info")]
ConnectionInfo,
}
@@ -310,23 +309,25 @@ pub enum Resource {
/// Configuration of a listener
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
pub struct ListenerConfig {
/// A unique name for this listener which will be shown in traces and in
/// metrics labels
/// The name of the listener, used in logs and metrics
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"web")]
pub name: Option<String>,
/// List of resources to mount
/// List of resources to serve
pub resources: Vec<Resource>,
/// HTTP prefix to mount the resources on
/// Optional URL prefix to mount all the resources of this listener under
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"/auth", extend("x-doc" = serde_json::json!({ "commented": true })))]
pub prefix: Option<String>,
/// List of sockets to bind
/// List of addresses and ports to listen to
pub binds: Vec<BindConfig>,
/// Accept `HAProxy`'s Proxy Protocol V1
/// Whether to enable the PROXY protocol on the listener
#[serde(default)]
#[schemars(example = &false)]
pub proxy_protocol: bool,
/// If set, makes the listener use TLS with the provided certificate and key
@@ -334,24 +335,51 @@ pub struct ListenerConfig {
pub tls: Option<TlsConfig>,
}
/// Configuration related to the web server
/// Controls the web server.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct HttpConfig {
/// List of listeners to run
/// Each listener can serve multiple resources, and listen on multiple TCP
/// ports or UNIX sockets.
///
/// <!-- more -->
///
/// The following additional resources are available, although it is
/// recommended to serve them on a separate listener, not exposed to the
/// public internet:
///
/// - `name: prometheus`: serves a Prometheus-compatible metrics endpoint on
/// `/metrics`, if the Prometheus exporter is enabled in
/// `telemetry.metrics.exporter`.
/// - `name: health`: serves the health check endpoint on `/health`.
#[serde(default)]
pub listeners: Vec<ListenerConfig>,
/// List of trusted reverse proxies that can set the `X-Forwarded-For`
/// header
/// List of trusted reverse proxies that are allowed to set the
/// `X-Forwarded-For` header.
///
/// Defaults to the usual private IP ranges:
/// 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8, 127.0.0.0/8,
/// fd00::/8 and ::1/128
#[expect(
clippy::doc_markdown,
reason = "the IPv6 ranges are shown verbatim in the rendered config reference"
)]
#[serde(default = "default_trusted_proxies")]
#[schemars(with = "Vec<String>", inner(ip))]
#[schemars(
with = "Vec<String>",
inner(ip),
example = &["192.168.0.0/16", "172.16.0.0/12", "10.0.0.0/8", "127.0.0.0/8", "fd00::/8", "::1/128"],
extend("x-doc" = serde_json::json!({ "commented": true }))
)]
pub trusted_proxies: Vec<IpNetwork>,
/// Public URL base from where the authentication service is reachable
/// Public URL base used when building absolute public URLs
#[schemars(example = &"https://auth.example.com/")]
pub public_base: Url,
/// OIDC issuer URL. Defaults to `public_base` if not set.
/// OIDC issuer advertised by the service. Defaults to `public_base`
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"https://example.com/")]
pub issuer: Option<Url>,
}
+23 -14
View File
@@ -30,21 +30,19 @@ fn default_endpoint() -> Url {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum HomeserverKind {
/// Homeserver is Synapse, version 1.135.0 or newer
/// Synapse, version 1.135.0 or newer
#[default]
Synapse,
/// Homeserver is Synapse, version 1.135.0 or newer, in read-only mode
///
/// This is meant for testing rolling out Matrix Authentication Service with
/// no risk of writing data to the homeserver.
/// same as `synapse`, but in read-only mode. This is meant for testing
/// rolling out MAS with no risk of writing data to the homeserver.
SynapseReadOnly,
/// Homeserver is Synapse, using the legacy API
SynapseLegacy,
/// Homeserver is Synapse, with the modern API available (>= 1.135.0)
/// Synapse with the modern admin API available (>= 1.135.0)
SynapseModern,
/// Synapse using the legacy admin API
SynapseLegacy,
}
/// Shared secret between MAS and the homeserver.
@@ -60,9 +58,16 @@ pub enum Secret {
/// Secret fields as serialized in JSON.
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
struct SecretRaw {
#[schemars(with = "Option<String>")]
/// Shared secret used to authenticate the service to the homeserver.
/// This must be of high entropy, because leaking this secret would allow
/// anyone to perform admin actions on the homeserver.
#[schemars(with = "Option<String>", example = &"/path/to/secret/file")]
#[serde(skip_serializing_if = "Option::is_none")]
secret_file: Option<Utf8PathBuf>,
/// Alternatively, the shared secret can be passed inline.
#[schemars(example = &"SomeRandomSecret")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
#[serde(skip_serializing_if = "Option::is_none")]
secret: Option<String>,
}
@@ -95,16 +100,19 @@ impl From<Secret> for SecretRaw {
}
}
/// Configuration related to the Matrix homeserver
/// Settings related to the connection to the Matrix homeserver
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MatrixConfig {
/// The kind of homeserver it is.
/// The kind of homeserver it is. Defaults to `synapse`.
#[serde(default)]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub kind: HomeserverKind,
/// The server name of the homeserver.
/// The homeserver name, as per the `server_name` in the Synapse
/// configuration file.
#[serde(default = "default_homeserver")]
#[schemars(example = &"example.com")]
pub homeserver: String,
/// Shared secret to use for calls to the admin API
@@ -113,8 +121,9 @@ pub struct MatrixConfig {
#[serde(flatten)]
pub secret: Secret,
/// The base URL of the homeserver's client API
/// URL to which the homeserver is accessible from the service.
#[serde(default = "default_endpoint")]
#[schemars(example = &"http://localhost:8008")]
pub endpoint: Url,
}
+15 -2
View File
@@ -1,3 +1,4 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
//
@@ -68,7 +69,15 @@ use crate::util::ConfigurationSection;
/// Application configuration root
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct RootConfig {
/// List of OAuth 2.0/OIDC clients config
/// List of OAuth 2.0/OIDC clients and their keys/secrets. Each `client_id`
/// must be a [ULID](https://github.com/ulid/spec).
///
/// <!-- more -->
///
/// **Note:** any additions or modifications in this list are synced with
/// the database on server startup. Removed entries are only removed with
/// the [`config sync
/// --prune`](./cli/config.md#config-sync---prune---dry-run) command.
#[serde(default, skip_serializing_if = "ClientsConfig::is_default")]
pub clients: ClientsConfig,
@@ -338,12 +347,16 @@ pub struct ClientSecretRaw {
/// Path to the file containing the client secret. The client secret is used
/// by the `client_secret_basic`, `client_secret_post` and
/// `client_secret_jwt` authentication methods.
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"secret")]
#[serde(skip_serializing_if = "Option::is_none")]
client_secret_file: Option<Utf8PathBuf>,
/// Alternative to `client_secret_file`: Reads the client secret directly
/// from the config.
#[schemars(
example = &"f4f6bb68a0269264877e9cb23b1856ab",
extend("x-doc" = {"commented": true})
)]
#[serde(skip_serializing_if = "Option::is_none")]
client_secret: Option<String>,
}
+3 -1
View File
@@ -27,7 +27,7 @@ const fn is_default_false(value: &bool) -> bool {
*value == default_false()
}
/// Configuration section for OAuth 2.0 protocol options
/// Configuration section for OAuth 2.0 protocol options.
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
pub struct OAuthConfig {
/// Whether the Device Authorization Grant (RFC 8628) is enabled. Defaults
@@ -39,6 +39,7 @@ pub struct OAuthConfig {
/// `urn:ietf:params:oauth:grant-type:device_code` grant type will be
/// rejected.
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
#[schemars(example = &true)]
pub device_code_grant_enabled: bool,
/// Whether the device authorization endpoint advertises a
@@ -50,6 +51,7 @@ pub struct OAuthConfig {
/// `code` query parameter, forcing users to type their user code
/// manually.
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
#[schemars(example = &false)]
pub device_code_user_code_auto_fill_enabled: bool,
}
+39 -26
View File
@@ -1,3 +1,4 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
//
@@ -32,29 +33,28 @@ fn default_minimum_complexity() -> u8 {
3
}
/// User password hashing config
/// Settings related to the local password database
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PasswordsConfig {
/// Whether password-based authentication is enabled
/// Whether to enable the password database.
///
/// If disabled, users will only be able to log in using upstream OIDC
/// providers
#[serde(default = "default_enabled")]
pub enabled: bool,
/// The hashing schemes to use for hashing and validating passwords
/// List of password hashing schemes being used
///
/// The hashing scheme with the highest version number will be used for
/// hashing new passwords.
/// /!\ Only change this if you know what you're doing
#[serde(default = "default_schemes")]
pub schemes: Vec<HashingScheme>,
/// Score between 0 and 4 determining the minimum allowed password
/// complexity. Scores are based on the ESTIMATED number of guesses
/// needed to guess the password.
/// Minimum complexity required for passwords, estimated by the zxcvbn
/// algorithm
///
/// - 0: less than 10^2 (100)
/// - 1: less than 10^4 (10'000)
/// - 2: less than 10^6 (1'000'000)
/// - 3: less than 10^8 (100'000'000)
/// - 4: any more than that
/// Must be between 0 and 4, default is 3
///
/// See <https://github.com/dropbox/zxcvbn#usage> for more information
#[serde(default = "default_minimum_complexity")]
minimum_complexity: u8,
}
@@ -178,34 +178,47 @@ const fn is_default_false(value: &bool) -> bool {
/// Parameters for a password hashing scheme
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct HashingScheme {
/// The version of the hashing scheme. They must be unique, and the highest
/// version will be used for hashing new passwords.
/// The version of the hashing scheme. Must be unique; the highest version
/// is used for hashing new passwords, the others are kept to verify
/// existing passwords.
#[schemars(example = &1u16)]
pub version: u16,
/// The hashing algorithm to use
/// The hashing algorithm to use.
#[schemars(example = &Algorithm::Argon2id)]
pub algorithm: Algorithm,
/// Whether to apply Unicode normalization to the password before hashing
/// Whether to apply Unicode normalization to the password before hashing.
///
/// Defaults to `false`, and generally recommended to stay false. This is
/// although recommended when importing password hashs from Synapse, as it
/// applies an NFKC normalization to the password before hashing it.
/// recommended when importing password hashes from Synapse, which applies
/// an NFKC normalization to the password before hashing it.
#[serde(default, skip_serializing_if = "is_default_false")]
#[schemars(example = &false)]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub unicode_normalization: bool,
/// Cost for the bcrypt algorithm
/// Cost for the bcrypt algorithm. Defaults to `12`.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(default = "default_bcrypt_cost")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub cost: Option<u32>,
/// An optional secret to use when hashing passwords. This makes it harder
/// to brute-force the passwords in case of a database leak.
/// An optional secret ("pepper") to use when hashing passwords. This makes
/// it harder to brute-force the passwords in case of a database leak.
///
/// This must not be specified if `secret_file` is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"<secret>")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub secret: Option<String>,
/// Same as `secret`, but read from a file.
///
/// This must not be specified if `secret` is specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"/path/to/secret")]
#[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))]
pub secret_file: Option<Utf8PathBuf>,
}
@@ -218,13 +231,13 @@ fn default_bcrypt_cost() -> Option<u32> {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum Algorithm {
/// bcrypt
/// The bcrypt password hashing algorithm.
Bcrypt,
/// argon2id
/// The Argon2id password hashing algorithm. This is the default.
#[default]
Argon2id,
/// PBKDF2
/// The PBKDF2 password hashing algorithm.
Pbkdf2,
}
+116 -10
View File
@@ -1,3 +1,4 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
//
@@ -86,16 +87,21 @@ fn is_default_data(value: &serde_json::Value) -> bool {
*value == default_data()
}
/// Application secrets
/// Policy settings
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PolicyConfig {
/// Path to the WASM module
///
/// The default value depends on how MAS was built:
/// - Docker distribution: `/usr/local/share/mas-cli/policy.wasm`
/// - pre-built binaries: `./share/policy.wasm`
/// - locally-built binaries: `./policies/policy.wasm`
#[serde(
default = "default_policy_path",
skip_serializing_if = "is_default_policy_path"
)]
#[schemars(with = "String")]
#[schemars(with = "String", example = &"./policies/policy.wasm")]
pub wasm_module: Utf8PathBuf,
/// Entrypoint to use when evaluating client registrations
@@ -103,6 +109,7 @@ pub struct PolicyConfig {
default = "default_client_registration_entrypoint",
skip_serializing_if = "is_default_client_registration_entrypoint"
)]
#[schemars(example = &"client_registration/violation")]
pub client_registration_entrypoint: String,
/// Entrypoint to use when evaluating user registrations
@@ -110,6 +117,7 @@ pub struct PolicyConfig {
default = "default_register_entrypoint",
skip_serializing_if = "is_default_register_entrypoint"
)]
#[schemars(example = &"register/violation")]
pub register_entrypoint: String,
/// Entrypoint to use when evaluating authorization grants
@@ -117,20 +125,15 @@ pub struct PolicyConfig {
default = "default_authorization_grant_entrypoint",
skip_serializing_if = "is_default_authorization_grant_entrypoint"
)]
#[schemars(example = &"authorization_grant/violation")]
pub authorization_grant_entrypoint: String,
/// Entrypoint to use when evaluating compatibility logins
#[serde(
default = "default_compat_login_entrypoint",
skip_serializing_if = "is_default_compat_login_entrypoint"
)]
pub compat_login_entrypoint: String,
/// Entrypoint to use when changing password
#[serde(
default = "default_password_entrypoint",
skip_serializing_if = "is_default_password_entrypoint"
)]
#[schemars(example = &"password/violation")]
pub password_entrypoint: String,
/// Entrypoint to use when adding an email address
@@ -138,10 +141,113 @@ pub struct PolicyConfig {
default = "default_email_entrypoint",
skip_serializing_if = "is_default_email_entrypoint"
)]
#[schemars(example = &"email/violation")]
pub email_entrypoint: String,
/// Entrypoint to use when evaluating compatibility logins
#[serde(
default = "default_compat_login_entrypoint",
skip_serializing_if = "is_default_compat_login_entrypoint"
)]
#[schemars(example = &"compat_login/violation")]
pub compat_login_entrypoint: String,
/// Arbitrary data to pass to the policy
#[serde(default = "default_data", skip_serializing_if = "is_default_data")]
#[schemars(extend("x-doc" = {"yaml": r#"
# This data is being passed to the policy
data:
# Users which are allowed to ask for admin access. If possible, use the
# can_request_admin flag on users instead.
admin_users:
- person1
- person2
# Client IDs which are allowed to ask for admin access with a
# client_credentials grant
admin_clients:
- 01H8PKNWKKRPCBW4YGH1RWV279
- 01HWQCPA5KF10FNCETY9402WGF
# Dynamic Client Registration
client_registration:
# don't require URIs to be on the same host. default: false
allow_host_mismatch: false
# allow non-SSL and localhost URIs. default: false
allow_insecure_uris: false
# don't require clients to provide a client_uri. default: false
allow_missing_client_uri: false
# Restrictions on user registration
registration:
# If specified, the username (localpart) *must* match one of the allowed
# usernames. If unspecified, all usernames are allowed.
allowed_usernames:
# Exact usernames that are allowed
literals: ["alice", "bob"]
# Substrings that match allowed usernames
substrings: ["user"]
# Regular expressions that match allowed usernames
regexes: ["^[a-z]+$"]
# Prefixes that match allowed usernames
prefixes: ["user-"]
# Suffixes that match allowed usernames
suffixes: ["-corp"]
# If specified, the username (localpart) *must not* match one of the
# banned usernames. If unspecified, all usernames are allowed.
banned_usernames:
# Exact usernames that are banned
literals: ["admin", "root"]
# Substrings that match banned usernames
substrings: ["admin", "root"]
# Regular expressions that match banned usernames
regexes: ["^admin$", "^root$"]
# Prefixes that match banned usernames
prefixes: ["admin-", "root-"]
# Suffixes that match banned usernames
suffixes: ["-admin", "-root"]
# Restrict what email addresses can be added to a user
emails:
# If specified, the email address *must* match one of the allowed addresses.
# If unspecified, all email addresses are allowed.
allowed_addresses:
# Exact emails that are allowed
literals: ["alice@example.com", "bob@example.com"]
# Regular expressions that match allowed emails
regexes: ["@example\\.com$"]
# Suffixes that match allowed emails
suffixes: ["@example.com"]
# If specified, the email address *must not* match one of the banned addresses.
# If unspecified, all email addresses are allowed.
banned_addresses:
# Exact emails that are banned
literals: ["alice@evil.corp", "bob@evil.corp"]
# Emails that contains those substrings are banned
substrings: ["evil"]
# Regular expressions that match banned emails
regexes: ["@evil\\.corp$"]
# Suffixes that match banned emails
suffixes: ["@evil.corp"]
# Prefixes that match banned emails
prefixes: ["alice@"]
requester:
# List of IP addresses and CIDRs that are not allowed to register
banned_ips:
- 192.168.0.1
- 192.168.1.0/24
- fe80::/64
# User agent patterns that are not allowed to register
banned_user_agents:
literals: ["Pretend this is Real;"]
substrings: ["Chrome"]
regexes: ["Chrome 1.*;"]
prefixes: ["Mozilla/"]
suffixes: ["Safari/605.1.15"]
"#}))]
pub data: serde_json::Value,
}
@@ -152,9 +258,9 @@ impl Default for PolicyConfig {
client_registration_entrypoint: default_client_registration_entrypoint(),
register_entrypoint: default_register_entrypoint(),
authorization_grant_entrypoint: default_authorization_grant_entrypoint(),
compat_login_entrypoint: default_compat_login_entrypoint(),
password_entrypoint: default_password_entrypoint(),
email_entrypoint: default_email_entrypoint(),
compat_login_entrypoint: default_compat_login_entrypoint(),
data: default_data(),
}
}
+21 -6
View File
@@ -1,3 +1,4 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
@@ -12,23 +13,37 @@ use serde::{Deserialize, Serialize, de::Error as _};
use crate::ConfigurationSection;
/// Configuration related to sending emails
/// Settings for limiting the rate of user actions to prevent abuse.
///
/// Each rate limiter consists of two options:
/// - `burst`: a base amount of how many actions are allowed in one go.
/// - `per_second`: how many units of the allowance replenish per second.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct RateLimitingConfig {
/// Account Recovery-specific rate limits
/// Limits how many account recovery attempts are allowed.
/// These limits can protect against e-mail spam.
///
/// Note: these limit also apply to recovery e-mail re-sends.
#[serde(default)]
pub account_recovery: AccountRecoveryRateLimitingConfig,
/// Login-specific rate limits
/// Limits how many login attempts are allowed.
///
/// Note: these limit also applies to password checks when a user attempts
/// to change their own password.
#[serde(default)]
pub login: LoginRateLimitingConfig,
/// Controls how many registrations attempts are permitted
/// based on source address.
/// Limits how many registrations attempts are allowed,
/// based on source IP address.
/// This limit can protect against e-mail spam and against people
/// registering too many accounts.
#[serde(default = "default_registration")]
pub registration: RateLimiterConfiguration,
/// Email authentication-specific rate limits
/// Limits how many e-mail authentication attempts are allowed.
/// These limits can protect against e-mail spam and against brute-forcing
/// the verification code.
#[serde(default)]
pub email_authentication: EmailauthenticationRateLimitingConfig,
}
+91 -3
View File
@@ -199,14 +199,17 @@ pub enum Encryption {
struct EncryptionRaw {
/// File containing the encryption key for secure cookies.
#[schemars(with = "Option<String>")]
#[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))]
#[serde(skip_serializing_if = "Option::is_none")]
encryption_file: Option<Utf8PathBuf>,
/// Encryption key for secure cookies.
/// Encryption secret (used for encrypting cookies and database fields)
///
/// This must be a 32-byte long hex-encoded key
#[schemars(
with = "Option<String>",
regex(pattern = r"[0-9a-fA-F]{64}"),
example = &"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
example = &"c7e42fb8baba8f228b2e169fdf4c8216dffd5d33ad18bafd8b928c09ca46c718"
)]
#[serde_as(as = "Option<serde_with::hex::Hex>")]
#[serde(skip_serializing_if = "Option::is_none")]
@@ -258,7 +261,82 @@ async fn key_configs_from_path(path: &Utf8PathBuf) -> anyhow::Result<Vec<KeyConf
Ok(result)
}
/// Application secrets
/// Signing and encryption secrets
///
/// <!-- more -->
///
/// ### `secrets.encryption{_file}`
///
/// The encryption secret used for encrypting cookies and database fields. It
/// takes the form of a 32-bytes-long hex-encoded string. To provide the
/// encryption secret via file, set `secrets.encryption_file` to the file path;
/// alternatively use `secrets.encryption` for declaring the secret inline. The
/// options `secrets.encryption_file` and `secrets.encryption` are mutually
/// exclusive.
///
/// If given via file, the encryption secret is only read at application
/// startup. The secret is not updated when the content of the file changes.
///
/// > ⚠️ **Warning** Do not change the encryption secret after the initial
/// > start! Changing the encryption secret afterwards will lead to a loss of
/// > all encrypted information in the database.
///
/// ### Signing Keys
///
/// The service can use a number of key types for signing.
/// The following key types are supported:
///
/// - RSA
/// - ECDSA with the P-256 (`prime256v1`) curve
/// - ECDSA with the P-384 (`secp384r1`) curve
/// - ECDSA with the K-256 (`secp256k1`) curve
///
/// The following key formats are supported:
///
/// - PKCS#1 PEM or DER-encoded RSA private key
/// - PKCS#8 PEM or DER-encoded RSA or ECDSA private key, encrypted or not
/// - SEC1 PEM or DER-encoded ECDSA private key
///
/// The signing keys are used for:
/// - signing ID Tokens (as returned in the [Token Endpoint] at
/// `/oauth2/token`);
/// - signing the response of the [UserInfo Endpoint] at `/oauth2/userinfo` if
/// the client requests a signed response;
/// - (niche) signing a JWT for authenticating to an upstream OAuth provider
/// when the `private_key_jwt` client auth method is configured.
///
/// At a minimum, an RSA key must be configured in order to be compliant with
/// the [OpenID Connect Core specification][oidc-core-rs256] which specifies the
/// RS256 algorithm as mandatory to implement by servers for interoperability
/// reasons.
///
/// The keys can be given as a directory path via `secrets.keys_dir`
/// or, alternatively, as an inline configuration list via `secrets.keys`.
///
/// [Token Endpoint]: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
/// [UserInfo Endpoint]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
/// [oidc-core-rs256]: https://openid.net/specs/openid-connect-core-1_0.html#ServerMTI
///
/// #### `secrets.keys_dir`
///
/// Path to the directory containing MAS signing key files.
/// Only keys that dont require a password are supported.
///
/// #### `secrets.keys`
///
/// Each entry in the list corresponds to one signing key used by MAS.
/// The key can either be specified inline (with the `key` property),
/// or loaded from a file (with the `key_file` property).
///
/// A [JWK Key ID] is automatically derived from each key.
/// To override this default, set `kid` to a custom value.
/// The `kid` can be any case-sensitive string value as long as it is unique to
/// this list; a keys `kid` value must be stable across restarts.
///
/// For PKCS#8 encoded keys, the `password` or `password_file` properties can be
/// used to decrypt the key.
///
/// [JWK Key ID]: <https://datatracker.ietf.org/doc/html/rfc7517#section-4.5>
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SecretsConfig {
@@ -269,11 +347,21 @@ pub struct SecretsConfig {
encryption: Encryption,
/// List of private keys to use for signing and encrypting payloads.
///
/// At least one RSA key must be configured.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &serde_json::json!([
{ "key_file": "keys/rsa_key" },
{
"kid": "iv1aShae",
"key": "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIE8yeUh111Npqu2e5wXxjC/GA5lbGe0j0KVXqZP12vqioAcGBSuBBAAK\noUQDQgAESKfUtKaLqCfhK+p3z870W59yOYvd+kjGWe+tK16SmWzZJbRCgdHakHE5\nMC6tJRnvedsYoKTrYoDv/XZIBI9zlA==\n-----END EC PRIVATE KEY-----"
}
]))]
keys: Option<Vec<KeyConfig>>,
/// Directory of private keys to use for signing and encrypting payloads.
#[schemars(with = "Option<String>")]
#[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))]
#[serde(skip_serializing_if = "Option::is_none")]
keys_dir: Option<Utf8PathBuf>,
}
+54 -25
View File
@@ -1,3 +1,4 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
//
@@ -49,24 +50,37 @@ pub enum TracingExporterKind {
/// Configuration related to exporting traces
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct TracingConfig {
/// Exporter to use when exporting traces
/// List of propagators to use for extracting and injecting trace contexts
#[serde(default)]
pub propagators: Vec<Propagator>,
/// Exporter to use when exporting traces
///
/// Set to `otlp` to export traces to an OTLP-compatible endpoint (which
/// also requires setting `endpoint`), or to `stdout` to print traces to
/// the standard output. Defaults to `none`, which disables trace
/// exporting.
#[serde(default)]
#[schemars(extend("x-doc" = {"yaml": r"# The default: don't export traces
exporter: none
# Export traces to an OTLP-compatible endpoint
#exporter: otlp
#endpoint: https://localhost:4318
# Export traces to the standard output. Only useful for debugging
#exporter: stdout"}))]
pub exporter: TracingExporterKind,
/// OTLP exporter: OTLP over HTTP compatible endpoint
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(url, default = "otlp_endpoint_default")]
#[schemars(url, default = "otlp_endpoint_default", extend("x-doc" = {"skip": true}))]
pub endpoint: Option<Url>,
/// List of propagation formats to use for incoming and outgoing requests
#[serde(default)]
pub propagators: Vec<Propagator>,
/// Sample rate for traces
///
/// Defaults to `1.0` if not set.
/// Sample rate for traces, between 0.0 and 1.0. Defaults to `1.0` if not
/// set.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = 0.5, range(min = 0.0, max = 1.0))]
#[schemars(example = 0.5, range(min = 0.0, max = 1.0), extend("x-doc" = {"commented": true}))]
pub sample_rate: Option<f64>,
}
@@ -103,12 +117,30 @@ pub enum MetricsExporterKind {
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct MetricsConfig {
/// Exporter to use when exporting metrics
///
/// Set to `otlp` to export metrics to an OTLP-compatible endpoint (which
/// also requires setting `endpoint`), to `prometheus` to expose a
/// Prometheus endpoint, or to `stdout` to print metrics to the standard
/// output. Defaults to `none`, which disables metric exporting.
#[serde(default)]
#[schemars(extend("x-doc" = {"yaml": r"# The default: don't export metrics
exporter: none
# Export metrics to an OTLP-compatible endpoint
#exporter: otlp
#endpoint: https://localhost:4317
# Export metrics by exposing a Prometheus endpoint
# This requires mounting the `prometheus` resource to an HTTP listener
#exporter: prometheus
# Export metrics to the standard output. Only useful for debugging
#exporter: stdout"}))]
pub exporter: MetricsExporterKind,
/// OTLP exporter: OTLP over HTTP compatible endpoint
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(url, default = "otlp_endpoint_default")]
#[schemars(url, default = "otlp_endpoint_default", extend("x-doc" = {"skip": true}))]
pub endpoint: Option<Url>,
}
@@ -122,30 +154,27 @@ impl MetricsConfig {
/// Configuration related to the Sentry integration
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct SentryConfig {
/// Sentry DSN
/// DSN to use for sending errors and crashes to Sentry
#[schemars(url, example = &"https://public@host:port/1")]
#[serde(skip_serializing_if = "Option::is_none")]
pub dsn: Option<String>,
/// Environment to use when sending events to Sentry
///
/// Defaults to `production` if not set.
#[schemars(example = &"production")]
/// Environment to use when sending events to Sentry. Defaults to
/// `production`.
#[schemars(example = &"production", extend("x-doc" = {"commented": true}))]
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<String>,
/// Sample rate for event submissions
///
/// Defaults to `1.0` if not set.
/// Sample rate for event submissions, between 0.0 and 1.0. Defaults to
/// `1.0`.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = 0.5, range(min = 0.0, max = 1.0))]
#[schemars(example = 1.0, range(min = 0.0, max = 1.0), extend("x-doc" = {"commented": true}))]
pub sample_rate: Option<f32>,
/// Sample rate for tracing transactions
///
/// Defaults to `0.0` if not set.
/// Sample rate for tracing transactions, between 0.0 and 1.0. Defaults to
/// `0.0`.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = 0.5, range(min = 0.0, max = 1.0))]
#[schemars(example = 0.0, range(min = 0.0, max = 1.0), extend("x-doc" = {"commented": true}))]
pub traces_sample_rate: Option<f32>,
}
@@ -156,7 +185,7 @@ impl SentryConfig {
}
}
/// Configuration related to sending monitoring data
/// Settings related to metrics and traces
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct TelemetryConfig {
/// Configuration related to exporting traces
+16 -7
View File
@@ -1,3 +1,4 @@
// Copyright 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
//
@@ -67,28 +68,36 @@ fn is_default_translations_path(value: &Utf8PathBuf) -> bool {
*value == default_translations_path()
}
/// Configuration related to templates
/// Allows loading custom templates
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
pub struct TemplatesConfig {
/// Path to the folder which holds the templates
/// From where to load the templates
///
/// This is relative to the current working directory, *not* the config
/// file
#[serde(default = "default_path", skip_serializing_if = "is_default_path")]
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"/to/templates")]
pub path: Utf8PathBuf,
/// Path to the assets manifest
/// Path to the frontend assets manifest file
#[serde(
default = "default_assets_path",
skip_serializing_if = "is_default_assets_path"
)]
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"/to/manifest.json")]
pub assets_manifest: Utf8PathBuf,
/// Path to the translations
/// From where to load the translation files
///
/// The default depends on how the service is distributed:
/// - Docker distribution: `/usr/local/share/mas-cli/translations/`
/// - pre-built binaries: `./share/translations/`
/// - locally-built binaries: `./translations/`
#[serde(
default = "default_translations_path",
skip_serializing_if = "is_default_translations_path"
)]
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"/to/translations")]
pub translations_path: Utf8PathBuf,
}
+177 -152
View File
@@ -17,10 +17,17 @@ use url::Url;
use crate::{ClientSecret, ClientSecretRaw, ConfigurationSection};
/// Upstream OAuth 2.0 providers configuration
/// Settings related to upstream OAuth 2.0/OIDC providers.
/// Additions and modifications within this section are synced with the database
/// on server startup. Removed entries are only removed with the [`config sync
/// --prune`](./cli/config.md#config-sync---prune---dry-run) command.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct UpstreamOAuth2Config {
/// List of OAuth 2.0 providers
/// A list of upstream OAuth 2.0/OIDC providers to use to authenticate
/// users.
///
/// Sample configurations for popular providers can be found in the
/// [upstream provider setup](../setup/sso.md#sample-configurations) guide.
pub providers: Vec<Provider>,
}
@@ -160,14 +167,12 @@ impl ConfigurationSection for UpstreamOAuth2Config {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ResponseMode {
/// `query`: The provider will send the response as a query string in the
/// URL search parameters
/// The provider will send the response as a query string in the URL search
/// parameters. This is the default.
Query,
/// `form_post`: The provider will send the response as a POST request with
/// the response parameters in the request body
///
/// <https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html>
/// The provider will send the response as a POST request with the response
/// parameters in the request body
FormPost,
}
@@ -175,26 +180,24 @@ pub enum ResponseMode {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TokenAuthMethod {
/// `none`: No authentication
/// No authentication
None,
/// `client_secret_basic`: `client_id` and `client_secret` used as basic
/// authorization credentials
/// `client_id` and `client_secret` used as basic authorization credentials
ClientSecretBasic,
/// `client_secret_post`: `client_id` and `client_secret` sent in the
/// request body
/// `client_id` and `client_secret` sent in the request body
ClientSecretPost,
/// `client_secret_jwt`: a `client_assertion` sent in the request body and
/// signed using the `client_secret`
/// a `client_assertion` sent in the request body and signed using the
/// `client_secret`
ClientSecretJwt,
/// `private_key_jwt`: a `client_assertion` sent in the request body and
/// signed by an asymmetric key
/// a `client_assertion` sent in the request body and signed by an
/// asymmetric key, using the keys defined in the `secrets.keys` section
PrivateKeyJwt,
/// `sign_in_with_apple`: a special method for Signin with Apple
/// a special authentication method for Sign-in with Apple
SignInWithApple,
}
@@ -227,19 +230,20 @@ impl ImportAction {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum OnConflict {
/// Fails the upstream OAuth 2.0 login on conflict
/// Fails the upstream OAuth 2.0 login.
#[default]
Fail,
/// Adds the upstream OAuth 2.0 identity link, regardless of whether there
/// is an existing link or not
/// Adds the upstream account link to the existing user, regardless of
/// whether there is an existing link or not.
Add,
/// Replace any existing upstream OAuth 2.0 identity link
/// Replace any existing upstream OAuth 2.0 identity link for this provider
/// on the matching user.
Replace,
/// Adds the upstream OAuth 2.0 identity link *only* if there is no existing
/// link for this provider on the matching user
/// Adds the upstream account link *only* if there is no existing link for
/// this provider on the matching user.
Set,
}
@@ -257,6 +261,7 @@ pub struct SubjectImportPreference {
///
/// If not provided, the default template is `{{ user.sub }}`
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(example = &"{{ user.sub }}", extend("x-doc" = {"commented": true}))]
pub template: Option<String>,
}
@@ -271,16 +276,19 @@ impl SubjectImportPreference {
pub struct LocalpartImportPreference {
/// How to handle the attribute
#[serde(default, skip_serializing_if = "ImportAction::is_default")]
#[schemars(example = &ImportAction::Force, extend("x-doc" = {"commented": true}))]
pub action: ImportAction,
/// The Jinja2 template to use for the localpart attribute
///
/// If not provided, the default template is `{{ user.preferred_username }}`
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(example = &"{{ user.preferred_username }}", extend("x-doc" = {"commented": true}))]
pub template: Option<String>,
/// How to handle conflicts on the claim, default value is `Fail`
/// How to handle when localpart already exists.
#[serde(default, skip_serializing_if = "OnConflict::is_default")]
#[schemars(example = &OnConflict::Fail, extend("x-doc" = {"commented": true}))]
pub on_conflict: OnConflict,
}
@@ -295,12 +303,14 @@ impl LocalpartImportPreference {
pub struct DisplaynameImportPreference {
/// How to handle the attribute
#[serde(default, skip_serializing_if = "ImportAction::is_default")]
#[schemars(example = &ImportAction::Suggest, extend("x-doc" = {"commented": true}))]
pub action: ImportAction,
/// The Jinja2 template to use for the displayname attribute
///
/// If not provided, the default template is `{{ user.name }}`
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(example = &"{{ user.name }}", extend("x-doc" = {"commented": true}))]
pub template: Option<String>,
}
@@ -315,12 +325,14 @@ impl DisplaynameImportPreference {
pub struct EmailImportPreference {
/// How to handle the claim
#[serde(default, skip_serializing_if = "ImportAction::is_default")]
#[schemars(example = &ImportAction::Suggest, extend("x-doc" = {"commented": true}))]
pub action: ImportAction,
/// The Jinja2 template to use for the email address attribute
///
/// If not provided, the default template is `{{ user.email }}`
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(example = &"{{ user.email }}", extend("x-doc" = {"commented": true}))]
pub template: Option<String>,
}
@@ -338,6 +350,7 @@ pub struct AccountNameImportPreference {
///
/// If not provided, it will be ignored.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(example = &"@{{ user.preferred_username }}", extend("x-doc" = {"commented": true}))]
pub template: Option<String>,
}
@@ -350,33 +363,43 @@ impl AccountNameImportPreference {
/// How claims should be imported
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
pub struct ClaimsImports {
/// How to determine the subject of the user
/// The subject is an internal identifier used to link the user's provider
/// identity to local accounts.
/// By default it uses the `sub` claim as per the OIDC spec, which should
/// fit most use cases.
#[serde(default, skip_serializing_if = "SubjectImportPreference::is_default")]
pub subject: SubjectImportPreference,
/// Whether to skip the interactive screen prompting the user to confirm the
/// attributes that are being imported. This requires `localpart.action` to
/// be `require` and other attribute actions to be either `ignore`, `force`
/// or `require`
/// By default, new users will see a screen confirming the attributes they
/// are about to have on their account.
///
/// Setting this to `true` allows skipping this screen, but requires the
/// `localpart.action` to be set to `require` and the other attributes
/// actions to be set to `ignore`, `force` or `require`.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[schemars(example = &false, extend("x-doc" = {"commented": true}))]
pub skip_confirmation: bool,
/// Import the localpart of the MXID
/// The localpart is the local part of the user's Matrix ID.
/// For example, on the `example.com` server, if the localpart is `alice`,
/// the user's Matrix ID will be `@alice:example.com`.
#[serde(default, skip_serializing_if = "LocalpartImportPreference::is_default")]
pub localpart: LocalpartImportPreference,
/// Import the displayname of the user.
/// The display name is the user's display name.
#[serde(
default,
skip_serializing_if = "DisplaynameImportPreference::is_default"
)]
pub displayname: DisplaynameImportPreference,
/// Import the email address of the user
/// An email address to import.
#[serde(default, skip_serializing_if = "EmailImportPreference::is_default")]
pub email: EmailImportPreference,
/// Set a human-readable name for the upstream account for display purposes
/// An account name, for display purposes only.
///
/// This helps the end user identify what account they are using
#[serde(
default,
skip_serializing_if = "AccountNameImportPreference::is_default"
@@ -399,14 +422,15 @@ impl ClaimsImports {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum DiscoveryMode {
/// Use OIDC discovery with strict metadata verification
/// discover the provider through OIDC discovery, with strict metadata
/// validation (default)
#[default]
Oidc,
/// Use OIDC discovery with relaxed metadata verification
/// discover through OIDC discovery, but skip metadata validation
Insecure,
/// Use a static configuration
/// don't discover the provider and use the endpoints below
Disabled,
}
@@ -422,16 +446,15 @@ impl DiscoveryMode {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum PkceMethod {
/// Use PKCE if the provider supports it
///
/// Defaults to no PKCE if provider discovery is disabled
/// use PKCE if the provider supports it (default).
/// Determined through discovery, and disabled if discovery is disabled
#[default]
Auto,
/// Always use PKCE with the S256 challenge method
/// always use PKCE (with the S256 method)
Always,
/// Never use PKCE
/// never use PKCE
Never,
}
@@ -463,17 +486,20 @@ fn signed_response_alg_default() -> JsonWebSignatureAlg {
pub struct SignInWithApple {
/// The private key file used to sign the `id_token`
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<String>")]
#[schemars(with = "Option<String>", example = &"/path/to/private.key")]
pub private_key_file: Option<Utf8PathBuf>,
/// The private key used to sign the `id_token`
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----")]
pub private_key: Option<String>,
/// The Team ID of the Apple Developer Portal
#[schemars(example = &"<team-id>")]
pub team_id: String,
/// The key ID of the Apple Developer Portal
#[schemars(example = &"<key-id>")]
pub key_id: String,
}
@@ -489,7 +515,7 @@ fn is_default_scope(scope: &str) -> bool {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum OnBackchannelLogout {
/// Do nothing
/// do nothing, other than validating and logging the request
#[default]
DoNothing,
@@ -514,52 +540,48 @@ impl OnBackchannelLogout {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[expect(clippy::struct_excessive_bools)]
pub struct Provider {
/// Whether this provider is enabled.
/// A unique identifier for the provider.
///
/// Defaults to `true`
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
pub enabled: bool,
/// An internal unique identifier for this provider
/// Must be a valid ULID
#[schemars(
with = "String",
regex(pattern = r"^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$"),
description = "A ULID as per https://github.com/ulid/spec"
description = "A unique identifier for the provider.\n\nMust be a valid ULID",
example = &"01HFVBY12TMNTYTBV8W921M5FA"
)]
pub id: Ulid,
/// Whether this provider is enabled. Defaults to `true`.
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
#[schemars(example = &true, extend("x-doc" = {"commented": true}))]
pub enabled: bool,
/// The ID of the provider that was used by Synapse.
/// In order to perform a Synapse-to-MAS migration, this must be specified.
///
/// ## For providers that used OAuth 2.0 or OpenID Connect in Synapse
///
/// ### For `oidc_providers`:
/// This should be specified as `oidc-` followed by the ID that was
/// configured as `idp_id` in one of the `oidc_providers` in the Synapse
/// configuration.
/// For example, if Synapse's configuration contained `idp_id: wombat` for
/// this provider, then specify `oidc-wombat` here.
///
/// ### For `oidc_config` (legacy):
/// Specify `oidc` here.
/// Only required when performing a Synapse-to-MAS migration.
/// For Synapse's `oidc_providers`, this is `oidc-<idp_id>`; for the legacy
/// `oidc_config`, this is `oidc`.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"oidc-github", extend("x-doc" = {"commented": true}))]
pub synapse_idp_id: Option<String>,
/// The OIDC issuer URL
///
/// This is required if OIDC discovery is enabled (which is the default)
/// The issuer URL, which will be used to discover the provider's
/// configuration. If discovery is enabled, this *must* exactly match the
/// `issuer` field advertised in
/// `<issuer>/.well-known/openid-configuration`. It must be set if OIDC
/// discovery is enabled (which is the default).
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"https://example.com/", extend("x-doc" = {"commented": true}))]
pub issuer: Option<String>,
/// A human-readable name for the provider, that will be shown to users
/// A human-readable name for the provider, which will be displayed on the
/// login page
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"Example", extend("x-doc" = {"commented": true}))]
pub human_name: Option<String>,
/// A brand identifier used to customise the UI, e.g. `apple`, `google`,
/// `github`, etc.
///
/// Values supported by the default template are:
///
/// A brand identifier for the provider, which will be used to display a
/// logo on the login page. Values supported by the default template
/// are:
/// - `apple`
/// - `google`
/// - `facebook`
@@ -568,9 +590,11 @@ pub struct Provider {
/// - `twitter`
/// - `discord`
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"google", extend("x-doc" = {"commented": true}))]
pub brand_name: Option<String>,
/// The client ID to use when authenticating with the provider
/// The client ID to use to authenticate to the provider
#[schemars(example = &"mas-fb3f0c09c4c23de4")]
pub client_id: String,
/// The client secret to use when authenticating with the provider
@@ -583,10 +607,12 @@ pub struct Provider {
pub client_secret: Option<ClientSecret>,
/// The method to authenticate the client with the provider
#[schemars(example = &TokenAuthMethod::ClientSecretPost)]
pub token_endpoint_auth_method: TokenAuthMethod,
/// Additional parameters for the `sign_in_with_apple` method
/// Additional parameters for the `sign_in_with_apple` authentication method
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-doc" = {"commented": true}))]
pub sign_in_with_apple: Option<SignInWithApple>,
/// The JWS algorithm to use when authenticating the client with the
@@ -594,6 +620,7 @@ pub struct Provider {
///
/// Used by the `client_secret_jwt` and `private_key_jwt` methods
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &JsonWebSignatureAlg::Rs256, extend("x-doc" = {"commented": true}))]
pub token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
/// Expected signature for the JWT payload returned by the token
@@ -604,34 +631,32 @@ pub struct Provider {
default = "signed_response_alg_default",
skip_serializing_if = "is_signed_response_alg_default"
)]
#[schemars(example = &JsonWebSignatureAlg::Rs256, extend("x-doc" = {"commented": true}))]
pub id_token_signed_response_alg: JsonWebSignatureAlg,
/// The scopes to request from the provider
/// The scopes to request from the provider.
///
/// Defaults to `openid`.
/// In most cases, it should always include the `openid` scope
#[serde(default = "default_scope", skip_serializing_if = "is_default_scope")]
#[schemars(example = &"openid email profile")]
pub scope: String,
/// How to discover the provider's configuration
///
/// Defaults to `oidc`, which uses OIDC discovery with strict metadata
/// verification
/// How the provider configuration and endpoints should be discovered
#[serde(default, skip_serializing_if = "DiscoveryMode::is_default")]
#[schemars(example = &DiscoveryMode::Oidc, extend("x-doc" = {"commented": true}))]
pub discovery_mode: DiscoveryMode,
/// Whether to use proof key for code exchange (PKCE) when requesting and
/// exchanging the token.
///
/// Defaults to `auto`, which uses PKCE if the provider supports it.
/// Whether PKCE should be used during the authorization code flow.
#[serde(default, skip_serializing_if = "PkceMethod::is_default")]
#[schemars(example = &PkceMethod::Auto, extend("x-doc" = {"commented": true}))]
pub pkce_method: PkceMethod,
/// Whether to fetch the user profile from the userinfo endpoint,
/// or to rely on the data returned in the `id_token` from the
/// `token_endpoint`.
/// Whether to fetch user claims from the userinfo endpoint.
///
/// Defaults to `false`.
/// This is disabled by default, as most providers will return the necessary
/// claims in the `id_token`
#[serde(default)]
#[schemars(example = &true, extend("x-doc" = {"commented": true}))]
pub fetch_userinfo: bool,
/// Expected signature for the JWT payload returned by the userinfo
@@ -640,111 +665,111 @@ pub struct Provider {
/// If not specified, the response is expected to be an unsigned JSON
/// payload.
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &JsonWebSignatureAlg::Rs256, extend("x-doc" = {"commented": true}))]
pub userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
/// The URL to use for the provider's authorization endpoint
/// The userinfo endpoint.
///
/// Defaults to the `authorization_endpoint` provided through discovery
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization_endpoint: Option<Url>,
/// The URL to use for the provider's userinfo endpoint
///
/// Defaults to the `userinfo_endpoint` provided through discovery
/// This takes precedence over the discovery mechanism
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"https://example.com/oauth2/userinfo", extend("x-doc" = {"commented": true}))]
pub userinfo_endpoint: Option<Url>,
/// The URL to use for the provider's token endpoint
/// The provider authorization endpoint.
///
/// Defaults to the `token_endpoint` provided through discovery
/// This takes precedence over the discovery mechanism
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"https://example.com/oauth2/authorize", extend("x-doc" = {"commented": true}))]
pub authorization_endpoint: Option<Url>,
/// The provider token endpoint.
///
/// This takes precedence over the discovery mechanism
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"https://example.com/oauth2/token", extend("x-doc" = {"commented": true}))]
pub token_endpoint: Option<Url>,
/// The URL to use for getting the provider's public keys
/// The provider JWKS URI.
///
/// Defaults to the `jwks_uri` provided through discovery
/// This takes precedence over the discovery mechanism
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &"https://example.com/oauth2/keys", extend("x-doc" = {"commented": true}))]
pub jwks_uri: Option<Url>,
/// The response mode we ask the provider to use for the callback
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(example = &ResponseMode::Query, extend("x-doc" = {"commented": true}))]
pub response_mode: Option<ResponseMode>,
/// How claims should be imported from the `id_token` provided by the
/// provider
#[serde(default, skip_serializing_if = "ClaimsImports::is_default")]
pub claims_imports: ClaimsImports,
/// Additional parameters to include in the authorization request.
///
/// Each value is a [`MiniJinja`] template. The template context
/// exposes:
/// Values are Jinja2 templates. The template context exposes:
///
/// - `params`: a map containing the raw query parameters from the
/// downstream authorization request. The map is empty when the upstream
/// login was not initiated by a downstream OAuth/OIDC authorization
/// request (e.g. account linking, direct login from the login page).
/// - `params`: a map containing the raw query parameters of the downstream
/// authorization request (empty when the upstream login was not
/// triggered by a downstream authorization request, e.g. account linking
/// or direct login).
/// - `logged_out`: a boolean that is `true` when the browser recently
/// signed out of MAS and has no active session. This lets you force a
/// fresh prompt at the upstream provider after sign-out — otherwise a
/// fresh prompt at the upstream provider after sign-out — e.g.
/// `prompt: "{% if logged_out %}login{% endif %}"` — otherwise a
/// provider that still has a live session would silently sign the user
/// back in.
///
/// [`MiniJinja`]: https://docs.rs/minijinja
/// Templates that render to an empty string are dropped rather than
/// forwarded.
///
/// Templates that render to an empty string are dropped — so
/// referencing a downstream parameter that wasn't supplied (e.g.
/// `{{ params.login_hint }}`) results in no parameter being
/// forwarded, rather than an empty one.
///
/// Plain strings (without `{{ … }}`) are valid templates that render
/// to themselves.
///
/// Example:
///
/// ```yaml
/// additional_authorization_parameters:
/// login_hint: "{{ params.login_hint }}"
/// acr_values: "{{ params.acr_values }}"
/// kc_idp_hint: "saml"
/// # Force re-authentication at the upstream provider after sign-out.
/// # Keycloak only supports `prompt=login`, not `prompt=select_account`.
/// prompt: "{% if logged_out %}login{% endif %}"
/// ```
///
/// `params` exposes the entire raw query string of the downstream
/// request (including `client_id`, `state`, `code_challenge`, …).
/// Forward specific keys deliberately; don't blindly proxy the
/// whole map.
///
/// Order of keys is not preserved.
/// Plain strings without `{{ … }}` render to themselves, so static values
/// work as expected.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[schemars(example = &serde_json::json!({
"foo": "bar",
"login_hint": "{{ params.login_hint }}",
"acr_values": "{{ params.acr_values }}",
}), extend("x-doc" = {"commented": true}))]
pub additional_authorization_parameters: BTreeMap<String, String>,
/// Whether the `login_hint` should be forwarded to the provider in the
/// authorization request.
///
/// Defaults to `false`.
///
/// Deprecated: prefer adding
/// `login_hint: "{{ params.login_hint }}"` to
/// `additional_authorization_parameters` instead. When this flag is
/// set, a `login_hint` template entry is injected automatically if
/// one is not already present.
/// Deprecated: prefer adding `login_hint: "{{ params.login_hint }}"` to
/// `additional_authorization_parameters` instead. When this flag is set, a
/// `login_hint` template entry is injected automatically if one is not
/// already present.
#[serde(default)]
#[schemars(extend("x-doc" = {"commented": true}))]
pub forward_login_hint: bool,
/// What to do when receiving an OIDC Backchannel logout request.
///
/// Defaults to `do_nothing`.
#[serde(default, skip_serializing_if = "OnBackchannelLogout::is_default")]
#[schemars(example = &OnBackchannelLogout::DoNothing, extend("x-doc" = {"commented": true}))]
pub on_backchannel_logout: OnBackchannelLogout,
/// Whether or not to require a registration token on `OAuth2` auth
///
/// Defaults to `false`
/// Whether a registration token is required to register through this
/// provider. Defaults to `false`.
#[serde(default)]
#[schemars(extend("x-doc" = {"commented": true}))]
pub registration_token_required: bool,
/// How user attributes should be mapped
///
/// Most of those attributes have two main properties:
/// - `action`: what to do with the attribute. Possible values are:
/// - `ignore`: ignore the attribute
/// - `suggest`: suggest the attribute to the user, but let them opt
/// out
/// - `force`: always import the attribute, and don't fail if it's
/// missing
/// - `require`: always import the attribute, and fail if it's missing
/// - `template`: a Jinja2 template used to generate the value. In this
/// template, the `user` variable is available, which contains the
/// user's attributes retrieved from the `id_token` given by the
/// upstream provider and/or through the userinfo endpoint.
///
/// Each attribute has a default template which follows the well-known OIDC
/// claims.
#[serde(default, skip_serializing_if = "ClaimsImports::is_default")]
pub claims_imports: ClaimsImports,
}
impl Provider {