Skip to main content

mas_config/sections/
mod.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8use anyhow::bail;
9use camino::Utf8PathBuf;
10use rand::RngExt;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14mod account;
15mod branding;
16mod captcha;
17mod clients;
18mod database;
19mod email;
20mod experimental;
21mod http;
22mod matrix;
23mod oauth;
24mod passwords;
25mod policy;
26mod rate_limiting;
27mod room_invites;
28mod secrets;
29mod telemetry;
30mod templates;
31mod upstream_oauth2;
32
33pub use self::{
34    account::AccountConfig,
35    branding::BrandingConfig,
36    captcha::{CaptchaConfig, CaptchaServiceKind},
37    clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
38    database::{DatabaseConfig, PgSslMode},
39    email::{EmailConfig, EmailSmtpMode, EmailTransportKind},
40    experimental::{ExperimentalConfig, SessionLimitConfig as ExperimentalSessionLimitConfig},
41    http::{
42        BindConfig as HttpBindConfig, HttpConfig, ListenerConfig as HttpListenerConfig,
43        Resource as HttpResource, TlsConfig as HttpTlsConfig, UnixOrTcp,
44    },
45    matrix::{HomeserverKind, MatrixConfig},
46    oauth::OAuthConfig,
47    passwords::{
48        Algorithm as PasswordAlgorithm, HashingScheme as PasswordHashingScheme, PasswordsConfig,
49    },
50    policy::PolicyConfig,
51    rate_limiting::RateLimitingConfig,
52    room_invites::RoomInvitesConfig,
53    secrets::SecretsConfig,
54    telemetry::{
55        MetricsConfig, MetricsExporterKind, Propagator, TelemetryConfig, TracingConfig,
56        TracingExporterKind,
57    },
58    templates::TemplatesConfig,
59    upstream_oauth2::{
60        ClaimsImports as UpstreamOAuth2ClaimsImports, DiscoveryMode as UpstreamOAuth2DiscoveryMode,
61        EmailImportPreference as UpstreamOAuth2EmailImportPreference,
62        ImportAction as UpstreamOAuth2ImportAction,
63        OnBackchannelLogout as UpstreamOAuth2OnBackchannelLogout,
64        OnConflict as UpstreamOAuth2OnConflict, PkceMethod as UpstreamOAuth2PkceMethod,
65        Provider as UpstreamOAuth2Provider, ResponseMode as UpstreamOAuth2ResponseMode,
66        TokenAuthMethod as UpstreamOAuth2TokenAuthMethod, UpstreamOAuth2Config,
67    },
68};
69use crate::util::ConfigurationSection;
70
71/// Application configuration root
72#[derive(Debug, Serialize, Deserialize, JsonSchema)]
73pub struct RootConfig {
74    /// List of OAuth 2.0/OIDC clients config
75    #[serde(default, skip_serializing_if = "ClientsConfig::is_default")]
76    pub clients: ClientsConfig,
77
78    /// Configuration of the HTTP server
79    #[serde(default)]
80    pub http: HttpConfig,
81
82    /// Database connection configuration
83    #[serde(default)]
84    pub database: DatabaseConfig,
85
86    /// Configuration related to sending monitoring data
87    #[serde(default, skip_serializing_if = "TelemetryConfig::is_default")]
88    pub telemetry: TelemetryConfig,
89
90    /// Configuration related to templates
91    #[serde(default, skip_serializing_if = "TemplatesConfig::is_default")]
92    pub templates: TemplatesConfig,
93
94    /// Configuration related to sending emails
95    #[serde(default)]
96    pub email: EmailConfig,
97
98    /// Application secrets
99    pub secrets: SecretsConfig,
100
101    /// Configuration related to user passwords
102    #[serde(default)]
103    pub passwords: PasswordsConfig,
104
105    /// Configuration related to the homeserver
106    pub matrix: MatrixConfig,
107
108    /// Configuration related to the OPA policies
109    #[serde(default, skip_serializing_if = "PolicyConfig::is_default")]
110    pub policy: PolicyConfig,
111
112    /// Configuration related to limiting the rate of user actions to prevent
113    /// abuse
114    #[serde(default, skip_serializing_if = "RateLimitingConfig::is_default")]
115    pub rate_limiting: RateLimitingConfig,
116
117    /// Configuration related to upstream OAuth providers
118    #[serde(default, skip_serializing_if = "UpstreamOAuth2Config::is_default")]
119    pub upstream_oauth2: UpstreamOAuth2Config,
120
121    /// Configuration section for tweaking the branding of the service
122    #[serde(default, skip_serializing_if = "BrandingConfig::is_default")]
123    pub branding: BrandingConfig,
124
125    /// Configuration section to setup CAPTCHA protection on a few operations
126    #[serde(default, skip_serializing_if = "CaptchaConfig::is_default")]
127    pub captcha: CaptchaConfig,
128
129    /// Configuration section to configure features related to account
130    /// management
131    #[serde(default, skip_serializing_if = "AccountConfig::is_default")]
132    pub account: AccountConfig,
133
134    /// Configuration section for OAuth 2.0 protocol options
135    #[serde(default, skip_serializing_if = "OAuthConfig::is_default")]
136    pub oauth: OAuthConfig,
137
138    /// Configuration section for inviting email addresses to rooms
139    #[serde(default, skip_serializing_if = "RoomInvitesConfig::is_default")]
140    pub room_invites: RoomInvitesConfig,
141
142    /// Experimental configuration options
143    #[serde(default, skip_serializing_if = "ExperimentalConfig::is_default")]
144    pub experimental: ExperimentalConfig,
145}
146
147impl ConfigurationSection for RootConfig {
148    fn validate(
149        &self,
150        figment: &figment::Figment,
151    ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
152        self.clients.validate(figment)?;
153        self.http.validate(figment)?;
154        self.database.validate(figment)?;
155        self.telemetry.validate(figment)?;
156        self.templates.validate(figment)?;
157        self.email.validate(figment)?;
158        self.passwords.validate(figment)?;
159        self.secrets.validate(figment)?;
160        self.matrix.validate(figment)?;
161        self.policy.validate(figment)?;
162        self.rate_limiting.validate(figment)?;
163        self.upstream_oauth2.validate(figment)?;
164        self.branding.validate(figment)?;
165        self.captcha.validate(figment)?;
166        self.account.validate(figment)?;
167        self.oauth.validate(figment)?;
168        self.room_invites.validate(figment)?;
169        self.experimental.validate(figment)?;
170
171        Ok(())
172    }
173}
174
175impl RootConfig {
176    /// Generate a new configuration with random secrets
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if the secrets could not be generated
181    pub async fn generate<R>(mut rng: R) -> anyhow::Result<Self>
182    where
183        R: RngExt + Send,
184    {
185        Ok(Self {
186            clients: ClientsConfig::default(),
187            http: HttpConfig::default(),
188            database: DatabaseConfig::default(),
189            telemetry: TelemetryConfig::default(),
190            templates: TemplatesConfig::default(),
191            email: EmailConfig::default(),
192            passwords: PasswordsConfig::default(),
193            secrets: SecretsConfig::generate(&mut rng).await?,
194            matrix: MatrixConfig::generate(&mut rng),
195            policy: PolicyConfig::default(),
196            rate_limiting: RateLimitingConfig::default(),
197            upstream_oauth2: UpstreamOAuth2Config::default(),
198            branding: BrandingConfig::default(),
199            captcha: CaptchaConfig::default(),
200            account: AccountConfig::default(),
201            oauth: OAuthConfig::default(),
202            room_invites: RoomInvitesConfig::default(),
203            experimental: ExperimentalConfig::default(),
204        })
205    }
206
207    /// Configuration used in tests
208    #[must_use]
209    pub fn test() -> Self {
210        Self {
211            clients: ClientsConfig::default(),
212            http: HttpConfig::default(),
213            database: DatabaseConfig::default(),
214            telemetry: TelemetryConfig::default(),
215            templates: TemplatesConfig::default(),
216            passwords: PasswordsConfig::default(),
217            email: EmailConfig::default(),
218            secrets: SecretsConfig::test(),
219            matrix: MatrixConfig::test(),
220            policy: PolicyConfig::default(),
221            rate_limiting: RateLimitingConfig::default(),
222            upstream_oauth2: UpstreamOAuth2Config::default(),
223            branding: BrandingConfig::default(),
224            captcha: CaptchaConfig::default(),
225            account: AccountConfig::default(),
226            oauth: OAuthConfig::default(),
227            room_invites: RoomInvitesConfig::default(),
228            experimental: ExperimentalConfig::default(),
229        }
230    }
231}
232
233/// Partial configuration actually used by the server
234#[expect(missing_docs)]
235#[derive(Debug, Deserialize)]
236pub struct AppConfig {
237    #[serde(default)]
238    pub http: HttpConfig,
239
240    #[serde(default)]
241    pub database: DatabaseConfig,
242
243    #[serde(default)]
244    pub templates: TemplatesConfig,
245
246    #[serde(default)]
247    pub email: EmailConfig,
248
249    pub secrets: SecretsConfig,
250
251    #[serde(default)]
252    pub passwords: PasswordsConfig,
253
254    pub matrix: MatrixConfig,
255
256    #[serde(default)]
257    pub policy: PolicyConfig,
258
259    #[serde(default)]
260    pub rate_limiting: RateLimitingConfig,
261
262    #[serde(default)]
263    pub branding: BrandingConfig,
264
265    #[serde(default)]
266    pub captcha: CaptchaConfig,
267
268    #[serde(default)]
269    pub account: AccountConfig,
270
271    #[serde(default)]
272    pub oauth: OAuthConfig,
273
274    #[serde(default)]
275    pub room_invites: RoomInvitesConfig,
276
277    #[serde(default)]
278    pub experimental: ExperimentalConfig,
279}
280
281impl ConfigurationSection for AppConfig {
282    fn validate(
283        &self,
284        figment: &figment::Figment,
285    ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
286        self.http.validate(figment)?;
287        self.database.validate(figment)?;
288        self.templates.validate(figment)?;
289        self.email.validate(figment)?;
290        self.passwords.validate(figment)?;
291        self.secrets.validate(figment)?;
292        self.matrix.validate(figment)?;
293        self.policy.validate(figment)?;
294        self.rate_limiting.validate(figment)?;
295        self.branding.validate(figment)?;
296        self.captcha.validate(figment)?;
297        self.account.validate(figment)?;
298        self.oauth.validate(figment)?;
299        self.room_invites.validate(figment)?;
300        self.experimental.validate(figment)?;
301
302        Ok(())
303    }
304}
305
306/// Partial config used by the `mas-cli config sync` command
307#[expect(missing_docs)]
308#[derive(Debug, Deserialize)]
309pub struct SyncConfig {
310    #[serde(default)]
311    pub database: DatabaseConfig,
312
313    pub secrets: SecretsConfig,
314
315    #[serde(default)]
316    pub clients: ClientsConfig,
317
318    #[serde(default)]
319    pub upstream_oauth2: UpstreamOAuth2Config,
320}
321
322impl ConfigurationSection for SyncConfig {
323    fn validate(
324        &self,
325        figment: &figment::Figment,
326    ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
327        self.database.validate(figment)?;
328        self.secrets.validate(figment)?;
329        self.clients.validate(figment)?;
330        self.upstream_oauth2.validate(figment)?;
331
332        Ok(())
333    }
334}
335
336/// Client secret config option.
337///
338/// It either holds the client secret value directly or references a file where
339/// the client secret is stored.
340#[derive(Clone, Debug)]
341pub enum ClientSecret {
342    /// Path to the file containing the client secret.
343    File(Utf8PathBuf),
344
345    /// Client secret value.
346    Value(String),
347}
348
349/// Client secret fields as serialized in JSON.
350#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
351pub struct ClientSecretRaw {
352    /// Path to the file containing the client secret. The client secret is used
353    /// by the `client_secret_basic`, `client_secret_post` and
354    /// `client_secret_jwt` authentication methods.
355    #[schemars(with = "Option<String>")]
356    #[serde(skip_serializing_if = "Option::is_none")]
357    client_secret_file: Option<Utf8PathBuf>,
358
359    /// Alternative to `client_secret_file`: Reads the client secret directly
360    /// from the config.
361    #[serde(skip_serializing_if = "Option::is_none")]
362    client_secret: Option<String>,
363}
364
365impl ClientSecret {
366    /// Returns the client secret.
367    ///
368    /// If `client_secret_file` was given, the secret is read from that file.
369    ///
370    /// # Errors
371    ///
372    /// Returns an error when the client secret could not be read from file.
373    pub async fn value(&self) -> anyhow::Result<String> {
374        Ok(match self {
375            ClientSecret::File(path) => tokio::fs::read_to_string(path).await?.trim().to_owned(),
376            ClientSecret::Value(client_secret) => client_secret.clone(),
377        })
378    }
379}
380
381impl TryFrom<ClientSecretRaw> for Option<ClientSecret> {
382    type Error = anyhow::Error;
383
384    fn try_from(value: ClientSecretRaw) -> Result<Self, Self::Error> {
385        match (value.client_secret, value.client_secret_file) {
386            (None, None) => Ok(None),
387            (None, Some(path)) => Ok(Some(ClientSecret::File(path))),
388            (Some(client_secret), None) => Ok(Some(ClientSecret::Value(client_secret))),
389            (Some(_), Some(_)) => {
390                bail!("Cannot specify both `client_secret` and `client_secret_file`")
391            }
392        }
393    }
394}
395
396impl From<Option<ClientSecret>> for ClientSecretRaw {
397    fn from(value: Option<ClientSecret>) -> Self {
398        match value {
399            Some(ClientSecret::File(path)) => ClientSecretRaw {
400                client_secret_file: Some(path),
401                client_secret: None,
402            },
403            Some(ClientSecret::Value(client_secret)) => ClientSecretRaw {
404                client_secret_file: None,
405                client_secret: Some(client_secret),
406            },
407            None => ClientSecretRaw {
408                client_secret_file: None,
409                client_secret: None,
410            },
411        }
412    }
413}