From c99d2e35aa73f64556d0d4ef582ce6fe39d2ebcb Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 2 Jul 2026 13:04:22 +0200 Subject: [PATCH] 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. --- crates/config/src/sections/account.rs | 54 ++- crates/config/src/sections/branding.rs | 14 +- crates/config/src/sections/captcha.rs | 23 +- crates/config/src/sections/clients.rs | 46 ++- crates/config/src/sections/database.rs | 63 +++- crates/config/src/sections/email.rs | 34 +- crates/config/src/sections/experimental.rs | 62 ++-- crates/config/src/sections/http.rs | 160 +++++---- crates/config/src/sections/matrix.rs | 37 +- crates/config/src/sections/mod.rs | 17 +- crates/config/src/sections/oauth.rs | 4 +- crates/config/src/sections/passwords.rs | 65 ++-- crates/config/src/sections/policy.rs | 126 ++++++- crates/config/src/sections/rate_limiting.rs | 27 +- crates/config/src/sections/secrets.rs | 94 ++++- crates/config/src/sections/telemetry.rs | 79 +++-- crates/config/src/sections/templates.rs | 23 +- crates/config/src/sections/upstream_oauth2.rs | 329 ++++++++++-------- 18 files changed, 860 insertions(+), 397 deletions(-) diff --git a/crates/config/src/sections/account.rs b/crates/config/src/sections/account.rs index fb87ee069..6720613cd 100644 --- a/crates/config/src/sections/account.rs +++ b/crates/config/src/sections/account.rs @@ -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, } diff --git a/crates/config/src/sections/branding.rs b/crates/config/src/sections/branding.rs index 750238bfa..5c85fdba5 100644 --- a/crates/config/src/sections/branding.rs +++ b/crates/config/src/sections/branding.rs @@ -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, /// 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, /// 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, - /// 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, } diff --git a/crates/config/src/sections/captcha.rs b/crates/config/src/sections/captcha.rs index 962d1f342..a9047a380 100644 --- a/crates/config/src/sections/captcha.rs +++ b/crates/config/src/sections/captcha.rs @@ -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::::None)] pub service: Option, - /// 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, - /// 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, } diff --git a/crates/config/src/sections/clients.rs b/crates/config/src/sections/clients.rs index 1520d5315..d5bc4282a 100644 --- a/crates/config/src/sections/clients.rs +++ b/crates/config/src/sections/clients.rs @@ -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, /// 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": "" + }] + }))] + #[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))] pub jwks: Option, /// 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, /// List of allowed redirect URIs #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[schemars(example = &["https://example.com/callback"])] pub redirect_uris: Vec, } @@ -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). +/// +/// +/// +/// **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::")] Vec); diff --git a/crates/config/src/sections/database.rs b/crates/config/src/sections/database.rs index 9820953ee..d8ea2e488 100644 --- a/crates/config/src/sections/database.rs +++ b/crates/config/src/sections/database.rs @@ -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 + /// /// /// 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, - /// 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::")] + #[schemars(with = "Option::", example = &"hostname")] + #[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))] pub host: Option, /// 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, /// 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")] + #[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))] pub socket: Option, /// 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, /// 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, /// 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, - /// 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, /// 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, /// 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")] + #[schemars(with = "Option", example = &"/path/to/ca.pem")] pub ssl_ca_file: Option, - /// 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, /// 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")] + #[schemars(with = "Option", example = &"/path/to/cert.pem")] pub ssl_certificate_file: Option, /// 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, /// 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")] + #[schemars(with = "Option", example = &"/path/to/key.pem")] pub ssl_key_file: Option, /// 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")] pub connect_timeout: Duration, - /// Set a maximum idle duration for individual connections - #[schemars(with = "Option")] + /// Set a maximum idle duration for individual connections, in seconds + #[schemars(with = "Option", example = &600u64)] #[serde( default = "default_idle_timeout", skip_serializing_if = "Option::is_none" @@ -220,8 +247,8 @@ pub struct DatabaseConfig { #[serde_as(as = "Option>")] pub idle_timeout: Option, - /// 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" diff --git a/crates/config/src/sections/email.rs b/crates/config/src/sections/email.rs index 17e5b9ded..716996604 100644 --- a/crates/config/src/sections/email.rs +++ b/crates/config/src/sections/email.rs @@ -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 { 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" "#)] 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" "#)] 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, /// SMTP transport: Hostname to connect to #[serde(skip_serializing_if = "Option::is_none")] #[schemars(with = "Option")] + #[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))] hostname: Option, /// 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, /// 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, /// 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, /// 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, } diff --git a/crates/config/src/sections/experimental.rs b/crates/config/src/sections/experimental.rs index ea4ac6716..ad1dacca5 100644 --- a/crates/config/src/sections/experimental.rs +++ b/crates/config/src/sections/experimental.rs @@ -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")] 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")] + #[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")] + #[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, /// 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, /// 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, } @@ -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, /// 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, } diff --git a/crates/config/src/sections/http.rs b/crates/config/src/sections/http.rs index 053b47006..031490d2e 100644 --- a/crates/config/src/sections/http.rs +++ b/crates/config/src/sections/http.rs @@ -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, - /// 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, }, - /// 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 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 = &"", extend("x-doc" = serde_json::json!({ "commented": true })))] pub certificate: Option, - /// 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")] + #[schemars(with = "Option", example = &"/path/to/cert.pem")] pub certificate_file: Option, - /// 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 = &"", extend("x-doc" = serde_json::json!({ "commented": true })))] pub key: Option, - /// 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")] + #[schemars(with = "Option", example = &"/path/to/key.pem")] pub key_file: Option, - /// 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 = &"", extend("x-doc" = serde_json::json!({ "commented": true })))] pub password: Option, - /// 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")] + #[schemars(with = "Option", example = &"/path/to/password.txt", extend("x-doc" = serde_json::json!({ "commented": true })))] pub password_file: Option, } @@ -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, - /// List of resources to mount + /// List of resources to serve pub resources: Vec, - /// 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, - /// List of sockets to bind + /// List of addresses and ports to listen to pub binds: Vec, - /// 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, } -/// 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. + /// + /// + /// + /// 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, - /// 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", inner(ip))] + #[schemars( + with = "Vec", + 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, - /// 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, } diff --git a/crates/config/src/sections/matrix.rs b/crates/config/src/sections/matrix.rs index 25c43620c..90eb644b9 100644 --- a/crates/config/src/sections/matrix.rs +++ b/crates/config/src/sections/matrix.rs @@ -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")] + /// 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", example = &"/path/to/secret/file")] #[serde(skip_serializing_if = "Option::is_none")] secret_file: Option, + + /// 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, } @@ -95,16 +100,19 @@ impl From 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, } diff --git a/crates/config/src/sections/mod.rs b/crates/config/src/sections/mod.rs index 04519e220..4bf70b558 100644 --- a/crates/config/src/sections/mod.rs +++ b/crates/config/src/sections/mod.rs @@ -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). + /// + /// + /// + /// **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")] + #[schemars(with = "Option", example = &"secret")] #[serde(skip_serializing_if = "Option::is_none")] client_secret_file: Option, /// 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, } diff --git a/crates/config/src/sections/oauth.rs b/crates/config/src/sections/oauth.rs index 315fd36df..af9467047 100644 --- a/crates/config/src/sections/oauth.rs +++ b/crates/config/src/sections/oauth.rs @@ -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, } diff --git a/crates/config/src/sections/passwords.rs b/crates/config/src/sections/passwords.rs index 8cd40e1f5..f46b39724 100644 --- a/crates/config/src/sections/passwords.rs +++ b/crates/config/src/sections/passwords.rs @@ -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, - /// 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 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, - /// 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 = &"")] + #[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))] pub secret: Option, /// 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")] + #[schemars(with = "Option", example = &"/path/to/secret")] + #[schemars(extend("x-doc" = serde_json::json!({ "commented": true })))] pub secret_file: Option, } @@ -218,13 +231,13 @@ fn default_bcrypt_cost() -> Option { #[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, } diff --git a/crates/config/src/sections/policy.rs b/crates/config/src/sections/policy.rs index 3b816b713..c753f471d 100644 --- a/crates/config/src/sections/policy.rs +++ b/crates/config/src/sections/policy.rs @@ -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(), } } diff --git a/crates/config/src/sections/rate_limiting.rs b/crates/config/src/sections/rate_limiting.rs index 0b7c95dbc..112795b8c 100644 --- a/crates/config/src/sections/rate_limiting.rs +++ b/crates/config/src/sections/rate_limiting.rs @@ -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, } diff --git a/crates/config/src/sections/secrets.rs b/crates/config/src/sections/secrets.rs index 31d44124d..fb276762e 100644 --- a/crates/config/src/sections/secrets.rs +++ b/crates/config/src/sections/secrets.rs @@ -199,14 +199,17 @@ pub enum Encryption { struct EncryptionRaw { /// File containing the encryption key for secure cookies. #[schemars(with = "Option")] + #[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))] #[serde(skip_serializing_if = "Option::is_none")] encryption_file: Option, - /// 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", regex(pattern = r"[0-9a-fA-F]{64}"), - example = &"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" + example = &"c7e42fb8baba8f228b2e169fdf4c8216dffd5d33ad18bafd8b928c09ca46c718" )] #[serde_as(as = "Option")] #[serde(skip_serializing_if = "Option::is_none")] @@ -258,7 +261,82 @@ async fn key_configs_from_path(path: &Utf8PathBuf) -> anyhow::Result +/// +/// ### `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 don’t 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 key’s `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]: #[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>, /// Directory of private keys to use for signing and encrypting payloads. #[schemars(with = "Option")] + #[schemars(extend("x-doc" = serde_json::json!({ "skip": true })))] #[serde(skip_serializing_if = "Option::is_none")] keys_dir: Option, } diff --git a/crates/config/src/sections/telemetry.rs b/crates/config/src/sections/telemetry.rs index c0f9489ef..0621c1b23 100644 --- a/crates/config/src/sections/telemetry.rs +++ b/crates/config/src/sections/telemetry.rs @@ -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, + + /// 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, - /// List of propagation formats to use for incoming and outgoing requests - #[serde(default)] - pub propagators: Vec, - - /// 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, } @@ -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, } @@ -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, - /// 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, - /// 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, - /// 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, } @@ -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 diff --git a/crates/config/src/sections/templates.rs b/crates/config/src/sections/templates.rs index 5656de44b..ad860c1d7 100644 --- a/crates/config/src/sections/templates.rs +++ b/crates/config/src/sections/templates.rs @@ -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")] + #[schemars(with = "Option", 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")] + #[schemars(with = "Option", 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")] + #[schemars(with = "Option", example = &"/to/translations")] pub translations_path: Utf8PathBuf, } diff --git a/crates/config/src/sections/upstream_oauth2.rs b/crates/config/src/sections/upstream_oauth2.rs index 0c81b9196..162333f2c 100644 --- a/crates/config/src/sections/upstream_oauth2.rs +++ b/crates/config/src/sections/upstream_oauth2.rs @@ -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, } @@ -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 - /// - /// + /// 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, } @@ -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, - /// 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, } @@ -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, } @@ -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, } @@ -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")] + #[schemars(with = "Option", example = &"/path/to/private.key")] pub private_key_file: Option, /// 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, /// The Team ID of the Apple Developer Portal + #[schemars(example = &"")] pub team_id: String, /// The key ID of the Apple Developer Portal + #[schemars(example = &"")] 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-`; 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, - /// 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 + /// `/.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, - /// 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, - /// 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, - /// 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, /// 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, /// 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, /// 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, - /// 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, - - /// 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, - /// 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, + + /// 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, - /// 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, /// 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, - /// 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, /// 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 {