mas_config/sections/
mod.rs

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