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